Expand collectionTree containers on dragover (#5024)

- when something is dragged over a collapsed collection
or group, it will be expanded to reveal children
so one could drop the dragged item/collection/ into them
- when drag is over, the expanded collections will
collapse, except for the ancestors of the newly selected
collection
- trigger collapse of expanded collections on dragend
on the entire document (in case dragend happens not over
the collection tree), or from onDrop handler, from where
we will wait for selection to change before collapsing
rows

Fixes: #4952
This commit is contained in:
abaevbog 2025-04-21 21:17:56 -07:00 • committed by GitHub
parent 1c089bf448
commit e690b0b18e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 1 deletions

View file

@ -97,6 +97,8 @@ var CollectionTree = class CollectionTree extends LibraryTree {
this._editing = null;
this._editingInput = null;
this._dropRow = null;
this._dragoverRow = null;
this._flashingRow = null;
this._typingTimeout = null;
this._customRowHeights = [];
this._separatorHeight = 8;
@ -107,6 +109,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
this._filterInitialCollapsedRows = [];
this._treeWasFocused = false;
this._hiddenFocusedRow = null;
this._expandedRowsOnDrag = new Set();
this._expandRowOnHoverTimer = null;
this._collapseExpandedRowsTimer = null;
this.onLoad = this.createEventBinding('load', true, true);
}
@ -135,6 +140,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
componentDidMount() {
this.selection.select(0);
this.makeVisible();
if (this.props.dragAndDrop) {
this.domEl.addEventListener('dragleave', this.onDragLeaveFromTheTree);
}
}
componentDidUpdate() {
@ -304,6 +312,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
div.classList.toggle('selected', selection.isSelected(index));
div.classList.toggle('highlighted', this._highlightedRows.has(treeRow.id));
div.classList.toggle('drop', this._dropRow == index);
div.classList.toggle('flashing', this._flashingRow == index);
div.classList.toggle('unread', treeRow.ref && treeRow.ref.unreadCount > 0);
let { matchesFilter, hasChildMatchingFilter } = this._matchesFilter(treeRow.ref);
div.classList.toggle('context-row', !matchesFilter && hasChildMatchingFilter);
@ -1441,11 +1450,46 @@ var CollectionTree = class CollectionTree extends LibraryTree {
onDragOver(event, index) {
if (!event.currentTarget.classList.contains('row')) return;
const treeRow = this.getRow(index);
let previousDragoverRow = this._dragoverRow;
this._dragoverRow = index;
try {
// Prevent modifier keys from doing their normal things
event.preventDefault();
var previousOrientation = Zotero.DragDrop.currentOrientation;
Zotero.DragDrop.currentOrientation = getDragTargetOrient(event);
// Expand collapsed collections and groups when they are dragged over for 1 second
if (!this.isContainerEmpty(index) && !this.isContainerOpen(index)) {
// If dragged over row has changed, clear the timer
if (this._expandRowOnHoverTimer && previousDragoverRow !== index) {
clearTimeout(this._expandRowOnHoverTimer);
this._expandRowOnHoverTimer = null;
}
// set a new timer for currently hovered row if it does not yet exist
if (!this._expandRowOnHoverTimer) {
this._expandRowOnHoverTimer = setTimeout(async () => {
// if the dragged over row is still the same after delay, expand it
if (!this.isContainerOpen(index) && this._dragoverRow == index) {
this._flashingRow = index;
this.tree.invalidateRow(index);
// wait for the flashing to finish and then expand the container
await Zotero.Promise.delay(300); // 0.2s CSS animation length * 1.5 runs
this._flashingRow = null;
this._expandedRowsOnDrag.add(treeRow.id);
if (!this.isContainerOpen(index)) {
this.toggleOpenState(index);
}
}
this._expandRowOnHoverTimer = null;
}, 1000);
}
}
// If the expanded rows were to be collapsed after the mouse left collectionTree,
// don't do it because now the mouse is back in the collectionTree
if (this._collapseExpandedRowsTimer) {
clearTimeout(this._collapseExpandedRowsTimer);
this._collapseExpandedRowsTimer = null;
}
if (!this.canDropCheck(index, Zotero.DragDrop.currentOrientation, event.dataTransfer)) {
this.setDropEffect(event, "none");
@ -1554,9 +1598,24 @@ var CollectionTree = class CollectionTree extends LibraryTree {
if (!e.currentTarget.classList.contains('row')) return;
let dropRow = this._dropRow;
this._dropRow = null;
this._dragoverRow = null;
this.tree.invalidateRow(dropRow);
}
// when something is dragged over the container rows, they may be expanded.
// if the mouse then leaves the collectionTree, collapse all expanded rows after delay
// (unless onDragOver is called soon, which will clear the timeout)
onDragLeaveFromTheTree = (e) => {
if (!e.relatedTarget) return;
let fromOutOfTree = this.domEl.contains(e.target) && !this.domEl.contains(e.relatedTarget);
if (fromOutOfTree && !this._collapseExpandedRowsTimer) {
this._collapseExpandedRowsTimer = setTimeout(() => {
this.closeContainersExpandedOnDrag();
this._collapseExpandedRowsTimer = null;
}, 1000);
}
};
canDropCheck = (row, orient, dataTransfer) => {
const treeRow = this.getRow(row);
// TEMP
@ -2094,6 +2153,13 @@ var CollectionTree = class CollectionTree extends LibraryTree {
let row = this._rowMap[treeRow.id];
let dataTransfer = event.dataTransfer;
// Prevent potential container row opening and flashing started in onDragOver
clearTimeout(this._expandRowOnHoverTimer);
this._expandRowOnHoverTimer = null;
let oldFlashing = this._flashingRow;
this._flashingRow = null;
this.tree.invalidateRow(oldFlashing);
if (!dataTransfer.dropEffect || dataTransfer.dropEffect == "none"
|| !(await this.canDropCheckAsync(row, orient, dataTransfer))) {
return false;
@ -2378,6 +2444,30 @@ var CollectionTree = class CollectionTree extends LibraryTree {
// Automatically retrieve metadata for PDFs and ebooks
Zotero.RecognizeDocument.autoRecognizeItems(addedItems);
}
this.closeContainersExpandedOnDrag();
}
async closeContainersExpandedOnDrag() {
let expandedRows = [...this._expandedRowsOnDrag];
if (!expandedRows.length) return;
this._expandedRowsOnDrag.clear();
// Record the ancestors of the currently selected collection to not collapse them
let col = this.selectedTreeRow.ref;
let parentIDs = new Set();
parentIDs.add(Zotero.Libraries.get(col.libraryID).treeViewID);
while (col.parentID) {
col = Zotero.Collections.get(col.parentID);
parentIDs.add(col.treeViewID);
}
// Collapse all remaining rows that were expanded during drag-drop
for (let rowID of expandedRows) {
let index = this.getRowIndexByID(rowID);
let row = this._rows[index];
if (row && row.isOpen && !parentIDs.has(row.ref.treeViewID)) {
this.toggleOpenState(index);
}
}
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -54,6 +54,14 @@ $icons: (
}
}
}
.row.flashing:not(.drop) {
animation: row-flash 0.2s ease-in-out 2;
}
.row.flashing.drop {
animation: row-flash-selected 0.2s ease-in-out 2;
}
}
.virtualized-table-body {
@ -101,3 +109,15 @@ $icons: (
}
}
}
// flashing effect applied to the row container when it is about to be expanded on dragover
// two different background colors are used for containers with .drop class and without
@keyframes row-flash {
0%, 100% { background-color: transparent; }
50% { background-color: var(--accent-azure); }
}
@keyframes row-flash-selected {
0%, 100% { background-color: var(--fill-secondary); }
50% { background-color: var(--accent-blue50); }
}

View file

@ -107,7 +107,7 @@
box-sizing: border-box;
border-radius: 5px;
&.drop {
&.drop:not(.flashing) {
color: var(--material-background) !important;
background: var(--fill-secondary) !important;