From ef78a94a6b31a5faa6ab459615167613e750041f Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 6 Mar 2026 15:24:57 -0800 Subject: [PATCH] restore open state of collections after filtering After filtering is complete, restore collapsed state of collections as much as possible even if the selected collection was changed. Collections that are ancestors of the newly selected collection are not collapsed. Fixes: #5825 --- chrome/content/zotero/collectionTree.jsx | 39 +++++++++++++----------- test/tests/collectionTreeTest.js | 18 +++++++++++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/chrome/content/zotero/collectionTree.jsx b/chrome/content/zotero/collectionTree.jsx index f3278f3933..b0ad7783c4 100644 --- a/chrome/content/zotero/collectionTree.jsx +++ b/chrome/content/zotero/collectionTree.jsx @@ -2684,34 +2684,39 @@ var CollectionTree = class CollectionTree extends LibraryTree { } } } - // If the filter has been cleared and the selection has not changed, restore the initial scroll position - if (shouldRestoreScrollPosition) { - // For the initial scroll position to make sense, collapse rows that were initially collapsed + // Filter has been cleared: restore collapsed state of collections and scroll position + if (willBeEmpty && !isEmpty) { + let selectedCollectionID = this.getSelectedCollection(true); for (let rowID of this._filterInitialCollapsedRows) { let index = this.getRowIndexByID(rowID); - if (index && this._rows[index].isOpen) { + let row = this._rows[index]; + if (!row || !row.isCollection()) continue; + // Do not collapse ancestory of selected collection + let descendents = new Set(Zotero.Collections.getByParent(row.ref.id, true, false).map(col => col.id)); + if (row.isOpen && !descendents.has(selectedCollectionID)) { this.toggleOpenState(index); } } this._filterInitialCollapsedRows = []; - collectionTable.scrollTop = this._filterInitialScrollPosition; - this._filterInitialScrollPosition = null; + // If collection tree was focused, scroll the selected row to the middle + if (this._treeWasFocused) { + let selectedRow = collectionTable.querySelector(".row.selected"); + let rowRect = selectedRow.getBoundingClientRect(); + let tableRect = collectionTable.getBoundingClientRect(); + let rowMiddle = rowRect.top + rowRect.height / 2; + let tableMiddle = tableRect.top + tableRect.height / 2; + collectionTable.scrollTop = collectionTable.scrollTop + rowMiddle - tableMiddle; + } + // Otherwise, restore the initial scroll position + else { + collectionTable.scrollTop = this._filterInitialScrollPosition; + this._filterInitialScrollPosition = null; + } } // During filtering, scroll to the very top else if (!willBeEmpty) { collectionTable.scrollTop = 0; } - // If the filtering is cleared and the selection has changed, scroll to have the - // newly selected row in the middle - else if (willBeEmpty && !isEmpty) { - let selectedRow = collectionTable.querySelector(".row.selected"); - let rowRect = selectedRow.getBoundingClientRect(); - let tableRect = collectionTable.getBoundingClientRect(); - let rowMiddle = rowRect.top + rowRect.height / 2; - let tableMiddle = tableRect.top + tableRect.height / 2; - let scrollPosition = collectionTable.scrollTop + rowMiddle - tableMiddle; - collectionTable.scrollTop = scrollPosition; - } // Focus the collection tree if (focusTree) { collectionTable.parentNode.focus(); diff --git a/test/tests/collectionTreeTest.js b/test/tests/collectionTreeTest.js index e1e9da7f59..f5025dcba7 100644 --- a/test/tests/collectionTreeTest.js +++ b/test/tests/collectionTreeTest.js @@ -1609,6 +1609,8 @@ describe("Zotero.CollectionTree", function () { }); it('should collapse collections collapsed before filtering', async function () { + // Select collection 1 + await cv.selectByID(`C${collection1.id}`); // Collapse top level collections 1 and 6 for (let c of [collection1, collection6]) { let index = cv.getRowIndexByID("C" + c.id); @@ -1637,6 +1639,22 @@ describe("Zotero.CollectionTree", function () { assert.isFalse(colOneRow.isOpen); let colSixRow = cv.getRow(cv.getRowIndexByID("C" + collection6.id)); assert.isFalse(colSixRow.isOpen); + + // Filter by 'collection' - this will expand all collections + await cv.setFilter('collection'); + assert.isTrue(cv.getRow(cv.getRowIndexByID("C" + collection1.id)).isOpen); + assert.isTrue(cv.getRow(cv.getRowIndexByID("C" + collection2.id)).isOpen); + assert.isTrue(cv.getRow(cv.getRowIndexByID("C" + collection6.id)).isOpen); + + // Select collection 8 and clear filter + await cv.selectByID(`C${collection8.id}`); + await cv.setFilter(""); + + // Collection 1 should be collapsed because it was collapsed before filtering + assert.isFalse(cv.getRow(cv.getRowIndexByID("C" + collection1.id)).isOpen); + // Collection 6 should be expanded because it's child (collection 8) is selected + assert.isTrue(cv.getRow(cv.getRowIndexByID("C" + collection6.id)).isOpen); + }); for (let type of ['collection', 'search']) {