collection search
This commit is contained in:
parent
7da00957ef
commit
f8a6b82c63
6 changed files with 742 additions and 89 deletions
|
@ -85,10 +85,13 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
this._editingInput = null;
|
||||
this._dropRow = null;
|
||||
this._typingTimeout = null;
|
||||
|
||||
this._customRowHeights = [];
|
||||
this._separatorHeight = 8;
|
||||
|
||||
this._filter = "";
|
||||
this._filterResultsCache = {};
|
||||
this._hiddenFocusedRow = null;
|
||||
|
||||
this.onLoad = this.createEventBinding('load', true, true);
|
||||
}
|
||||
|
||||
|
@ -150,6 +153,28 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
else if (event.key == "F2" && !Zotero.isMac && treeRow.isCollection()) {
|
||||
this.handleActivate(event, [this.selection.focused]);
|
||||
}
|
||||
else if (["ArrowDown", "ArrowUp"].includes(event.key)) {
|
||||
// Specific logic for keypress navigation during collection filtering
|
||||
// that skips context-rows
|
||||
if (!this._isFilterEmpty()) {
|
||||
this.focusNextMatchingRow(this.selection.focused, event.key == "ArrowUp", false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (["ArrowRight", "ArrowLeft"].includes(event.key)) {
|
||||
// No collapsing rows with arrows to avoid focusing on context rows
|
||||
if (!this._isFilterEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (event.key == "End" && !this._isFilterEmpty()) {
|
||||
this.focusLastMatchingRow();
|
||||
return false;
|
||||
}
|
||||
else if (event.key == "Home" && !this._isFilterEmpty()) {
|
||||
this.focusFirstMatchingRow(true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -162,6 +187,23 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
this.commitEditingName(this._editing);
|
||||
this._editing = null;
|
||||
}
|
||||
// If the filter is on, the last row can be a previously focused
|
||||
// row that does not match the filter. If the focus moves
|
||||
// away to another row, we can delete it.
|
||||
if (!this._isFilterEmpty() && this._hiddenFocusedRow && treeRow) {
|
||||
if (this._hiddenFocusedRow.isCollection()
|
||||
|| this._hiddenFocusedRow.isGroup()
|
||||
|| this._hiddenFocusedRow.isSearch()
|
||||
|| this._hiddenFocusedRow.isFeed()) {
|
||||
if (!this._includedInTree(this._hiddenFocusedRow.ref) && treeRow.id !== this._hiddenFocusedRow.id) {
|
||||
let indexToDelete = this.getRowIndexByID(this._hiddenFocusedRow.id);
|
||||
if (indexToDelete) {
|
||||
this._removeRow(indexToDelete);
|
||||
this._hiddenFocusedRow = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update aria-activedescendant on the tree
|
||||
this.forceUpdate();
|
||||
if (shouldDebounce) {
|
||||
|
@ -224,6 +266,12 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
// Div creation and content
|
||||
let div = oldDiv || document.createElement('div');
|
||||
div.innerHTML = "";
|
||||
// When a hidden focused row is added last during filtering, it
|
||||
// is removed on focus change, which can happen at the same time as rendering.
|
||||
// In this case, just return empty div.
|
||||
if (index >= this._rows.length) {
|
||||
return div;
|
||||
}
|
||||
|
||||
// Classes
|
||||
div.className = "row";
|
||||
|
@ -231,6 +279,16 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
div.classList.toggle('highlighted', this._highlightedRows.has(treeRow.id));
|
||||
div.classList.toggle('drop', this._dropRow == 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);
|
||||
// Hide currently focused but filtered out row to avoid confusing itemTree
|
||||
if (this._hiddenFocusedRow && this._hiddenFocusedRow.id == treeRow.id) {
|
||||
div.style.display = "none";
|
||||
}
|
||||
else if (div.style.display == "none") {
|
||||
// Make sure we unhide the div if the row matches filter conditions
|
||||
div.style.display = "";
|
||||
}
|
||||
|
||||
// Depth indent
|
||||
let depth = treeRow.level;
|
||||
|
@ -241,6 +299,10 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
&& treeRow.ref && treeRow.ref.libraryID != Zotero.Libraries.userLibraryID) {
|
||||
depth--;
|
||||
}
|
||||
// Ensures the feeds row has no padding
|
||||
if (treeRow.isFeeds()) {
|
||||
depth = 0;
|
||||
}
|
||||
div.style.paddingInlineStart = (CHILD_INDENT * depth) + 'px';
|
||||
|
||||
// Create a single-cell for the row (for the single-column layout)
|
||||
|
@ -249,7 +311,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
|
||||
// Twisty/spacer
|
||||
let twisty;
|
||||
if (this.isContainerEmpty(index)) {
|
||||
if (this.isContainerEmpty(index) || !hasChildMatchingFilter) {
|
||||
twisty = document.createElement('span');
|
||||
if (Zotero.isMac && treeRow.isHeader()) {
|
||||
twisty.classList.add("spacer-header");
|
||||
|
@ -385,7 +447,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
async refresh() {
|
||||
try {
|
||||
Zotero.debug("Refreshing collections pane");
|
||||
|
||||
|
||||
if (this.hideSources.indexOf('duplicates') == -1) {
|
||||
this._virtualCollectionLibraries.duplicates =
|
||||
Zotero.Prefs.getVirtualCollectionState('duplicates');
|
||||
|
@ -397,19 +459,26 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
|
||||
var newRows = [];
|
||||
var added = 0;
|
||||
|
||||
this._filterResultsCache = {};
|
||||
let libraryIncluded, groupsIncluded, feedsIncluded;
|
||||
//
|
||||
// Add "My Library"
|
||||
//
|
||||
newRows.splice(added++, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'library', { libraryID: Zotero.Libraries.userLibraryID }));
|
||||
newRows[0].isOpen = true;
|
||||
added += await this._expandRow(newRows, 0);
|
||||
libraryIncluded = this._includedInTree({ libraryID: Zotero.Libraries.userLibraryID });
|
||||
if (libraryIncluded) {
|
||||
newRows.splice(added++, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'library', { libraryID: Zotero.Libraries.userLibraryID }));
|
||||
newRows[0].isOpen = true;
|
||||
added += await this._expandRow(newRows, 0);
|
||||
}
|
||||
|
||||
// Add groups
|
||||
var groups = Zotero.Groups.getAll();
|
||||
if (groups.length) {
|
||||
newRows.splice(added++, 0, new Zotero.CollectionTreeRow(this, 'separator', false));
|
||||
groupsIncluded = groups.some(group => this._includedInTree(group));
|
||||
if (groups.length && groupsIncluded) {
|
||||
if (libraryIncluded) {
|
||||
newRows.splice(added++, 0, new Zotero.CollectionTreeRow(this, 'separator', false, 0));
|
||||
}
|
||||
let groupHeader = new Zotero.CollectionTreeRow(this, 'header', {
|
||||
id: "group-libraries-header",
|
||||
label: Zotero.getString('pane.collections.groupLibraries'),
|
||||
|
@ -417,6 +486,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
});
|
||||
newRows.splice(added++, 0, groupHeader);
|
||||
for (let group of groups) {
|
||||
if (!this._includedInTree(group)) continue;
|
||||
newRows.splice(added++, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'group', group, 1),
|
||||
);
|
||||
|
@ -424,29 +494,39 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
|
||||
// Add feeds
|
||||
if (this.hideSources.indexOf('feeds') == -1 && Zotero.Feeds.haveFeeds()) {
|
||||
let feeds = {
|
||||
get unreadCount() {
|
||||
return Zotero.Feeds.totalUnreadCount();
|
||||
},
|
||||
|
||||
async updateFeed() {
|
||||
for (let feed of Zotero.Feeds.getAll()) {
|
||||
await feed.updateFeed();
|
||||
}
|
||||
}
|
||||
};
|
||||
feedsIncluded = this._includedInTree(feeds);
|
||||
if (this.hideSources.indexOf('feeds') == -1 && Zotero.Feeds.haveFeeds() && feedsIncluded) {
|
||||
if (groupsIncluded || libraryIncluded) {
|
||||
newRows.splice(added++, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'separator', false),
|
||||
);
|
||||
}
|
||||
newRows.splice(added++, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'separator', false),
|
||||
);
|
||||
newRows.splice(added++, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'feeds', {
|
||||
get unreadCount() {
|
||||
return Zotero.Feeds.totalUnreadCount();
|
||||
},
|
||||
|
||||
async updateFeed() {
|
||||
for (let feed of Zotero.Feeds.getAll()) {
|
||||
await feed.updateFeed();
|
||||
}
|
||||
}
|
||||
})
|
||||
new Zotero.CollectionTreeRow(this, 'feeds', feeds)
|
||||
);
|
||||
added += await this._expandRow(newRows, added - 1);
|
||||
}
|
||||
|
||||
this.selection.selectEventsSuppressed = true;
|
||||
|
||||
// If the focused row does not match the filter, create a hidden dummy row at the bottom
|
||||
// of the tree to focus on to prevent itemTree from changing selection
|
||||
this._hiddenFocusedRow = this._createFocusedFilteredRow(newRows);
|
||||
if (this._hiddenFocusedRow) {
|
||||
newRows.splice(added++, 0,
|
||||
this._hiddenFocusedRow
|
||||
);
|
||||
}
|
||||
this._rows = newRows;
|
||||
this._refreshRowMap();
|
||||
} catch (e) {
|
||||
|
@ -465,7 +545,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
this.tree.invalidate();
|
||||
}
|
||||
|
||||
async selectByID(id) {
|
||||
async selectByID(id, ensureRowVisible = true) {
|
||||
var type = id[0];
|
||||
id = parseInt(('' + id).substr(1));
|
||||
|
||||
|
@ -490,10 +570,12 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
var row = this.getRowIndexByID(type + id);
|
||||
if (!row) {
|
||||
if (row === false) {
|
||||
return false;
|
||||
}
|
||||
this.ensureRowIsVisible(row);
|
||||
if (ensureRowVisible) {
|
||||
this.ensureRowIsVisible(row);
|
||||
}
|
||||
await this.selectWait(row);
|
||||
|
||||
return true;
|
||||
|
@ -664,6 +746,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
if (action == 'delete') {
|
||||
let selectedIndex = this.selection.focused;
|
||||
let feedDeleted = false;
|
||||
var offset = 0;
|
||||
|
||||
// Since a delete involves shifting of rows, we have to do it in reverse order
|
||||
let rows = [];
|
||||
|
@ -672,6 +755,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
switch (type) {
|
||||
case 'collection':
|
||||
if (this._rowMap['C' + id] !== undefined) {
|
||||
// During filtering, calculate by how many rows focus needs to be shifted.
|
||||
// e.g. Shift focus by 2 if a child is deleted and it's parent does not match the filter
|
||||
offset = Math.max(this._calculateOffsetForRowSelection('C' + id), offset);
|
||||
rows.push(this._rowMap['C' + id]);
|
||||
}
|
||||
break;
|
||||
|
@ -714,8 +800,12 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
this._removeRow(row - 1);
|
||||
}
|
||||
}
|
||||
|
||||
this._selectAfterRowRemoval(selectedIndex);
|
||||
// If there's an active filter, we can have a child matching filter be deleted
|
||||
// which means the non-matching parent needs to be removed, so the tree is rebuilt
|
||||
if (!this._isFilterEmpty()) {
|
||||
await this.reload();
|
||||
}
|
||||
this._selectAfterRowRemoval(selectedIndex - offset);
|
||||
}
|
||||
else if (action == 'modify') {
|
||||
let row;
|
||||
|
@ -723,6 +813,20 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
let rowID = "C" + id;
|
||||
let selectedIndex = this.selection.focused;
|
||||
|
||||
let handleFocusDuringSearch = async (type) => {
|
||||
let object = type == 'collection' ? Zotero.Collections.get(id) : Zotero.Searches.get(id);
|
||||
// If collections/searches are being filtered, some rows
|
||||
// need to be (un-)greyed out or removed, so reload.
|
||||
if (!this._isFilterEmpty()) {
|
||||
let offset = 0;
|
||||
if (!this._includedInTree(object, true)) {
|
||||
offset = this._calculateOffsetForRowSelection(type[0].toUpperCase() + id);
|
||||
}
|
||||
await this.reload();
|
||||
this._selectAfterRowRemoval(selectedIndex - offset);
|
||||
}
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'collection':
|
||||
let collection = Zotero.Collections.get(id);
|
||||
|
@ -765,6 +869,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
this.tree.invalidateRow(parentRow);
|
||||
}
|
||||
}
|
||||
await handleFocusDuringSearch('collection');
|
||||
break;
|
||||
|
||||
case 'search':
|
||||
|
@ -802,6 +907,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
}
|
||||
await handleFocusDuringSearch('search');
|
||||
break;
|
||||
|
||||
case 'feed':
|
||||
|
@ -854,6 +960,19 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
break;
|
||||
}
|
||||
}
|
||||
// After new collections were added, we need to update parents' info on if they have
|
||||
// a child matching the filter to show the arrow or not.
|
||||
if (!this._isFilterEmpty() && type == "collection") {
|
||||
for (let id of ids) {
|
||||
let rowIndex = this.getRowIndexByID("C" + id);
|
||||
let parentIndex = rowIndex ? this.getParentIndex(rowIndex) : -1;
|
||||
while (parentIndex > 0) {
|
||||
let parent = this.getRow(parentIndex);
|
||||
this._matchesFilter(parent.ref, true);
|
||||
parentIndex = this.getParentIndex(parentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (action == 'refresh' && type == 'trash') {
|
||||
// We need to update the trash's status (full or empty), and if empty,
|
||||
|
@ -892,8 +1011,10 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
var rows = [];
|
||||
for (let id of ids) {
|
||||
let row = this._rowMap[id];
|
||||
this._highlightedRows.add(id);
|
||||
rows.push(row);
|
||||
if (row) {
|
||||
this._highlightedRows.add(id);
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
rows.sort();
|
||||
// Select first collection
|
||||
|
@ -918,7 +1039,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
Zotero.debug("Cannot expand to nonexistent collection " + collectionID, 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._includedInTree(col)) {
|
||||
return false;
|
||||
}
|
||||
// Open library if closed
|
||||
var libraryRow = this._rowMap['L' + col.libraryID];
|
||||
if (!this.isContainerOpen(libraryRow)) {
|
||||
|
@ -1124,6 +1247,13 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
getRowString(index) {
|
||||
// During filtering, context rows return an empty string to not be selectable
|
||||
// with key-based navigation
|
||||
if (!this._isFilterEmpty()) {
|
||||
if (!this._matchesFilter(this.getRow(index).ref).matchesFilter) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return this.getRow(index).getName();
|
||||
}
|
||||
|
||||
|
@ -2199,6 +2329,275 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
Zotero.Prefs.set("sourceList.persist", JSON.stringify(state));
|
||||
})
|
||||
|
||||
// When collections are renamed or deleted during search, more than
|
||||
// a single row can be filtered out (if a child matching the filter is deleted,
|
||||
// its non-matching parent is deleted too). To select the right row
|
||||
// after such actions, the index should be offset by the total number of rows removed
|
||||
_calculateOffsetForRowSelection(id) {
|
||||
let rowIndex = this.getRowIndexByID(id);
|
||||
let offset = 0;
|
||||
let parentRowIndex = this.getParentIndex(rowIndex);
|
||||
while (parentRowIndex > 0) {
|
||||
let parentRow = this.getRow(parentRowIndex);
|
||||
if (parentRow.depth < 1 || this._matchesFilter(parentRow.ref).matchesFilter) {
|
||||
break;
|
||||
}
|
||||
parentRowIndex = this.getParentIndex(parentRowIndex);
|
||||
offset += 1;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set collection filter and refresh collectionTree to only include
|
||||
* rows that match the filter. Rows that do not match the filter but have children that do
|
||||
* are displayed as context rows. All relevant rows are toggled open. Selection is kept
|
||||
* on the currently selected row if any.
|
||||
* @param {String} filterText - Text that rows have to contain to match the filter
|
||||
* @param {Bool} scrollToSelected - Allow scrolling to the currently selected row.
|
||||
*/
|
||||
async setFilter(filterText, scrollToSelected) {
|
||||
this._filter = filterText.toLowerCase();
|
||||
let currentRow = this.getRow(this.selection.focused) || this._hiddenFocusedRow;
|
||||
let currentRowDisplayed = currentRow && this._includedInTree(currentRow.ref);
|
||||
// If current row does not match any filters, it'll be hidden, so clear selection
|
||||
if (!currentRowDisplayed) {
|
||||
this.selection.clearSelection();
|
||||
}
|
||||
await this.reload();
|
||||
if (currentRow) {
|
||||
// Special treatment for when there are no filter matches
|
||||
// Otherwise, selection.focused does not get updated by selectByID, which breaks ZoteroPane.
|
||||
if (this._rows.length == 1) {
|
||||
this.selection.select(0);
|
||||
}
|
||||
// Re-select previously selected row
|
||||
else {
|
||||
await this.selectByID(currentRow.id, scrollToSelected);
|
||||
}
|
||||
}
|
||||
|
||||
let promise = this.waitForSelect();
|
||||
this.selection.selectEventsSuppressed = false;
|
||||
await promise;
|
||||
|
||||
// Expand all container rows to see all search results
|
||||
if (!this._isFilterEmpty()) {
|
||||
for (let i = 0; i < this._rows.length; i++) {
|
||||
let row = this._rows[i];
|
||||
if (this.isContainer(i) && this._matchesFilter(row.ref).hasChildMatchingFilter && !row.isOpen) {
|
||||
await this.toggleOpenState(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an extra hidden row to keep focus on it when a currently focused row does not match the filter.
|
||||
* Required to avoid changes in itemTree during collection search.
|
||||
* @param {CollectionTreeRow[]} rows - Rows of collectionTree.
|
||||
* @return {CollectionTreeRow|null}
|
||||
*/
|
||||
_createFocusedFilteredRow(rows) {
|
||||
if (this._isFilterEmpty()) {
|
||||
return null;
|
||||
}
|
||||
let focused = this.getRow(this.selection.focused);
|
||||
// If row already exists - nothing to add
|
||||
let focusedRowAlreadyExists = rows.some(row => row.id == focused?.id);
|
||||
if (!focused || focusedRowAlreadyExists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Zotero.CollectionTreeRow(this, focused.type, focused.ref, 0, false);
|
||||
}
|
||||
|
||||
focusedRowMatchesFilter() {
|
||||
let row = this.getRow(this.selection.focused);
|
||||
return this._matchesFilter(row.ref).matchesFilter;
|
||||
}
|
||||
|
||||
filterEquals(filterValue) {
|
||||
return filterValue === this._filter;
|
||||
}
|
||||
|
||||
_isFilterEmpty() {
|
||||
return this._filter === "";
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
// Clear the search field
|
||||
if (collectionsSearchField.value.length) {
|
||||
collectionsSearchField.value = '';
|
||||
ZoteroPane.handleCollectionSearchInput();
|
||||
return null;
|
||||
}
|
||||
// If the search field is empty, focus the collection tree
|
||||
return document.getElementById('collection-tree');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select and focus the first row matching the collection filter. If it is a child of a collapsed
|
||||
* container(s), the container(s) on the way will be toggled open.
|
||||
* @param {Bool} scrollToLibrary - Scroll to the very top after selection
|
||||
*/
|
||||
async focusFirstMatchingRow(scrollToLibrary) {
|
||||
let index = 0;
|
||||
let row = this.getRow(index);
|
||||
while (index < this._rows.length && !this._matchesFilter(row.ref).matchesFilter) {
|
||||
row = this.getRow(index);
|
||||
if (!row.isOpen) {
|
||||
await this.toggleOpenState(index);
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
this.tree.focus();
|
||||
await this.selectByID(row.id);
|
||||
if (scrollToLibrary) {
|
||||
this.ensureRowIsVisible(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the last row matching collection filter. Opens any container rows on the way.
|
||||
*/
|
||||
async focusLastMatchingRow() {
|
||||
let loopCounter = 0;
|
||||
let offset = 1;
|
||||
if (this._hiddenFocusedRow) {
|
||||
offset = 2;
|
||||
}
|
||||
let lastRow = this.getRow(this._rows.length - offset);
|
||||
while (lastRow && !lastRow.isOpen && this._matchesFilter(lastRow.ref).hasChildMatchingFilter) {
|
||||
await this.toggleOpenState(this._rows.length - 1);
|
||||
lastRow = this.getRow(this._rows.length - offset);
|
||||
// Sanity check to make sure we are not stuck in an infinite loop if something goes wrong
|
||||
loopCounter++;
|
||||
if (loopCounter > 100) {
|
||||
Zotero.debug("Reasonable collections depth exceeded");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.selectByID(lastRow.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select and focus the next row after startIndex that matches the filter
|
||||
*
|
||||
* @param {Int} startIndex - Index of the row from which the search of the next matching row begins
|
||||
* @param {Bool} up - Move focus up the collection tree. Unless true, default direction is down.
|
||||
* @return {Bool} true if focus was shifted, false if selected row was not changed
|
||||
*/
|
||||
async focusNextMatchingRow(startIndex, up) {
|
||||
if (this._isFilterEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Increment or decrement the row index depending on direction
|
||||
let moveInDirection = (rowIndex) => {
|
||||
return up ? rowIndex - 1 : rowIndex + 1;
|
||||
};
|
||||
|
||||
let rowIndex = startIndex;
|
||||
while (rowIndex < this._rows.length && rowIndex >= 0) {
|
||||
let nextIndex = moveInDirection(rowIndex);
|
||||
let nextRow = this.getRow(nextIndex);
|
||||
|
||||
// If there is not next row or the next row is hidden (which should never happen), stop
|
||||
if (!nextRow || nextRow.id == this._hiddenFocusedRow?.id) {
|
||||
// If we stopped going up, make sure the library or group row is visible
|
||||
if (up && !this.tree.rowIsVisible(0)) {
|
||||
this.ensureRowIsVisible(0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Select the row if it's matching the filter unless it's a header or separator
|
||||
if (this._matchesFilter(nextRow.ref).matchesFilter
|
||||
&& !["separator", "header"].includes(nextRow.type)) {
|
||||
this.tree.focus();
|
||||
return this.selectByID(nextRow.id);
|
||||
}
|
||||
rowIndex = nextIndex;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object matches filter or has children that match the filter.
|
||||
*
|
||||
* @param {Collection|Search|Library|Group} object - Object to check
|
||||
* @param {Bool} resetCache - Ignore and reset existing cache value for that object
|
||||
* @return {Object} { matchesFilter: Bool, hasChildMatchingFilter: Bool }
|
||||
* matchesFilter = object itself matches the filter
|
||||
* hasChildMatchingFilter = object has children that match the filter
|
||||
*/
|
||||
_matchesFilter(object, resetCache = false) {
|
||||
// When the filter is empty, everything matches
|
||||
if (this._isFilterEmpty()) {
|
||||
return { matchesFilter: true, hasChildMatchingFilter: true };
|
||||
}
|
||||
// Handle separator or group headers
|
||||
if ((object.libraryID === undefined || object.libraryID === -1) && !object.updateFeed) {
|
||||
return { matchesFilter: true, hasChildMatchingFilter: false };
|
||||
}
|
||||
// Define objectID to be used in cache
|
||||
let objectID = "L" + object.libraryID;
|
||||
if (['Collection', 'Search', 'Feed'].includes(object._ObjectType)) {
|
||||
objectID = object._ObjectType[0] + object.id;
|
||||
}
|
||||
else if (object.updateFeed && !object.libraryID) {
|
||||
// Special ID for 'Feeds' parent row of all feeds
|
||||
objectID = 'feeds';
|
||||
}
|
||||
// If we found the filter status during previous recursions, return that
|
||||
if (this._filterResultsCache[objectID] && !resetCache) {
|
||||
return this._filterResultsCache[objectID];
|
||||
}
|
||||
// Filtering is case insensitive
|
||||
let objectName = (object.name || "").toLowerCase();
|
||||
// Special treatment to fetch the name for My Library or Feeds
|
||||
if (objectID[0] == 'L' && object._ObjectType !== "Group") {
|
||||
objectName = Zotero.getString('pane.collections.library').toLowerCase();
|
||||
}
|
||||
else if (objectID == 'feeds') {
|
||||
objectName = Zotero.getString('pane.collections.feedLibraries').toLowerCase();
|
||||
}
|
||||
let filterValue = this._filter;
|
||||
|
||||
let childrenToSearch = [];
|
||||
if (object._ObjectType == 'Collection') {
|
||||
let collection = Zotero.Collections.get(object.id);
|
||||
childrenToSearch = collection.getChildCollections();
|
||||
}
|
||||
else if (object.libraryID && !["Search", "Feeds"].includes(object._ObjectType)) {
|
||||
childrenToSearch = Zotero.Collections.getByLibrary(object.libraryID);
|
||||
childrenToSearch = childrenToSearch.concat(Zotero.Searches.getByLibrary(object.libraryID));
|
||||
}
|
||||
else if (objectID == 'feeds') {
|
||||
childrenToSearch = Zotero.Feeds.getAll();
|
||||
}
|
||||
let matchesFilter = objectName.includes(filterValue);
|
||||
// For libraries, groups and collections, recursively check if they have any children that match the filter
|
||||
let hasChildMatchingFilter = childrenToSearch.some((child) => {
|
||||
let { matchesFilter, hasChildMatchingFilter } = this._matchesFilter(child);
|
||||
return matchesFilter || hasChildMatchingFilter;
|
||||
});
|
||||
// Save filter status to cache
|
||||
this._filterResultsCache[objectID] = {
|
||||
matchesFilter: matchesFilter,
|
||||
hasChildMatchingFilter: hasChildMatchingFilter
|
||||
};
|
||||
return this._filterResultsCache[objectID];
|
||||
}
|
||||
|
||||
// A shortcut to call this._matchesFilter to check if a given object should be present
|
||||
// in collectionTree or not
|
||||
_includedInTree(object, resetCache) {
|
||||
let { matchesFilter, hasChildMatchingFilter } = this._matchesFilter(object, resetCache);
|
||||
return matchesFilter || hasChildMatchingFilter;
|
||||
}
|
||||
|
||||
async _expandRow(rows, row, forceOpen) {
|
||||
var treeRow = rows[row];
|
||||
var level = rows[row].level;
|
||||
|
@ -2260,7 +2659,8 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
for (var i = 0, len = collections.length; i < len; i++) {
|
||||
// Skip collections in trash
|
||||
if (collections[i].deleted) continue;
|
||||
|
||||
// Skip collections that do not match the filter and have no matching children
|
||||
if (!this._includedInTree(collections[i])) continue;
|
||||
let beforeRow = row + 1 + newRows;
|
||||
rows.splice(beforeRow, 0,
|
||||
new Zotero.CollectionTreeRow(this, isFeeds ? 'feed' : 'collection', collections[i], level + 1));
|
||||
|
@ -2277,13 +2677,14 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
for (var i = 0, len = savedSearches.length; i < len; i++) {
|
||||
// Skip searches in trash
|
||||
if (savedSearches[i].deleted) continue;
|
||||
|
||||
// Skip searches not matching the filter
|
||||
if (!this._includedInTree(savedSearches[i])) continue;
|
||||
rows.splice(row + 1 + newRows, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'search', savedSearches[i], level + 1));
|
||||
newRows++;
|
||||
}
|
||||
|
||||
if (showPublications) {
|
||||
if (showPublications && this._isFilterEmpty()) {
|
||||
// Add "My Publications"
|
||||
rows.splice(row + 1 + newRows, 0,
|
||||
new Zotero.CollectionTreeRow(this,
|
||||
|
@ -2299,7 +2700,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
// Duplicate items
|
||||
if (showDuplicates) {
|
||||
if (showDuplicates && this._isFilterEmpty()) {
|
||||
let d = new Zotero.Duplicates(libraryID);
|
||||
rows.splice(row + 1 + newRows, 0,
|
||||
new Zotero.CollectionTreeRow(this, 'duplicates', d, level + 1));
|
||||
|
@ -2307,7 +2708,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
// Unfiled items
|
||||
if (showUnfiled) {
|
||||
if (showUnfiled && this._isFilterEmpty()) {
|
||||
let s = new Zotero.Search;
|
||||
s.libraryID = libraryID;
|
||||
s.name = Zotero.getString('pane.collections.unfiled');
|
||||
|
@ -2319,7 +2720,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
// Retracted items
|
||||
if (showRetracted) {
|
||||
if (showRetracted && this._isFilterEmpty()) {
|
||||
let s = new Zotero.Search;
|
||||
s.libraryID = libraryID;
|
||||
s.name = Zotero.getString('pane.collections.retracted');
|
||||
|
@ -2330,7 +2731,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
newRows++;
|
||||
}
|
||||
|
||||
if (showTrash) {
|
||||
if (showTrash && this._isFilterEmpty()) {
|
||||
let deletedItems = await Zotero.Items.getDeleted(libraryID, true);
|
||||
if (deletedItems.length || Zotero.Prefs.get("showTrashWhenEmpty")) {
|
||||
var ref = {
|
||||
|
@ -2428,10 +2829,12 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
}
|
||||
this._addRow(
|
||||
new Zotero.CollectionTreeRow(this, 'collection', collection, level),
|
||||
beforeRow
|
||||
);
|
||||
if (this._includedInTree(collection)) {
|
||||
this._addRow(
|
||||
new Zotero.CollectionTreeRow(this, 'collection', collection, level),
|
||||
beforeRow
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (objectType == 'search') {
|
||||
let search = Zotero.Searches.get(id);
|
||||
|
@ -2467,10 +2870,12 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
}
|
||||
this._addRow(
|
||||
new Zotero.CollectionTreeRow(this, 'search', search, level),
|
||||
beforeRow
|
||||
);
|
||||
if (this._includedInTree(search)) {
|
||||
this._addRow(
|
||||
new Zotero.CollectionTreeRow(this, 'search', search, level),
|
||||
beforeRow
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return beforeRow;
|
||||
|
@ -2494,7 +2899,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
if (row >= this._rows.length) {
|
||||
row = this._rows.length - 1;
|
||||
};
|
||||
|
||||
|
||||
// Make sure the selection doesn't land on a separator (e.g. deleting last feed)
|
||||
while (row >= 0 && !this.isSelectable(row)) {
|
||||
// move up, since we got shifted down
|
||||
|
|
|
@ -259,17 +259,46 @@ var ZoteroPane = new function()
|
|||
moveFocus(actionsMap, event, true);
|
||||
});
|
||||
|
||||
let collectionsSearchField = document.getElementById("zotero-collections-search");
|
||||
let clearCollectionSearch = (removeFocus) => {
|
||||
// Clear the search field
|
||||
if (collectionsSearchField.value.length) {
|
||||
collectionsSearchField.value = '';
|
||||
ZoteroPane.collectionsView.setFilter("", true);
|
||||
if (!removeFocus) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
ZoteroPane.hideCollectionSearch();
|
||||
// If the search field is empty, focus the collection tree
|
||||
return document.getElementById('collection-tree');
|
||||
};
|
||||
collectionTreeToolbar.addEventListener("keydown", (event) => {
|
||||
let actionsMap = {
|
||||
'zotero-tb-collection-add': {
|
||||
ArrowRight: () => null,
|
||||
ArrowLeft: () => null,
|
||||
Tab: () => document.getElementById('zotero-tb-collection-search').click(),
|
||||
Tab: () => document.getElementById('zotero-tb-collections-search').click(),
|
||||
ShiftTab: () => document.getElementById('zotero-tb-sync')
|
||||
},
|
||||
'zotero-collections-search': {
|
||||
Tab: () => document.getElementById('zotero-tb-add'),
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collection-add')
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collection-add'),
|
||||
Enter: () => {
|
||||
// Prevent Enter pressed before the filtering ran from doing anything
|
||||
if (!ZoteroPane.collectionsView.filterEquals(collectionsSearchField.value)) {
|
||||
return null;
|
||||
}
|
||||
// If the current row passes the filter, make sure it is visible and focus collectionTree
|
||||
if (ZoteroPane.collectionsView.focusedRowMatchesFilter()) {
|
||||
ZoteroPane.collectionsView.ensureRowIsVisible(ZoteroPane.collectionsView.selection.focused);
|
||||
return document.getElementById('collection-tree');
|
||||
}
|
||||
// Otherwise, focus the first row passing the filter
|
||||
ZoteroPane.collectionsView.focusFirstMatchingRow(false);
|
||||
return null;
|
||||
},
|
||||
Escape: clearCollectionSearch
|
||||
},
|
||||
};
|
||||
moveFocus(actionsMap, event, true);
|
||||
|
@ -291,7 +320,7 @@ var ZoteroPane = new function()
|
|||
if (collectionsPane.getAttribute("collapsed")) {
|
||||
return document.getElementById('zotero-tb-sync');
|
||||
}
|
||||
document.getElementById('zotero-tb-collection-search').click();
|
||||
document.getElementById('zotero-tb-collections-search').click();
|
||||
return null;
|
||||
},
|
||||
' ': () => {
|
||||
|
@ -305,7 +334,7 @@ var ZoteroPane = new function()
|
|||
ArrowRight: () => document.getElementById("zotero-tb-attachment-add"),
|
||||
ArrowLeft: () => document.getElementById("zotero-tb-add"),
|
||||
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collection-search').click(),
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collections-search').click(),
|
||||
Enter: () => Zotero_Lookup.showPanel(event.target),
|
||||
' ': () => Zotero_Lookup.showPanel(event.target)
|
||||
},
|
||||
|
@ -313,13 +342,13 @@ var ZoteroPane = new function()
|
|||
ArrowRight: () => document.getElementById("zotero-tb-note-add"),
|
||||
ArrowLeft: () => document.getElementById("zotero-tb-lookup"),
|
||||
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collection-search').click()
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collections-search').click()
|
||||
},
|
||||
'zotero-tb-note-add': {
|
||||
ArrowRight: () => null,
|
||||
ArrowLeft: () => document.getElementById("zotero-tb-attachment-add"),
|
||||
Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(),
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collection-search').click()
|
||||
ShiftTab: () => document.getElementById('zotero-tb-collections-search').click()
|
||||
},
|
||||
'zotero-tb-search-textbox': {
|
||||
ShiftTab: () => {
|
||||
|
@ -348,6 +377,9 @@ var ZoteroPane = new function()
|
|||
// If tag selector is collapsed, go to itemTree, otherwise
|
||||
// default to focusing on tag selector
|
||||
return false;
|
||||
},
|
||||
Escape: () => {
|
||||
clearCollectionSearch(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -424,6 +456,7 @@ var ZoteroPane = new function()
|
|||
ZoteroContextPane.init();
|
||||
await ZoteroPane.initCollectionsTree();
|
||||
await ZoteroPane.initItemsTree();
|
||||
ZoteroPane.initCollectionTreeSearch();
|
||||
|
||||
// Add a default progress window
|
||||
ZoteroPane.progressWindow = new Zotero.ProgressWindow({ window });
|
||||
|
@ -1026,32 +1059,36 @@ var ZoteroPane = new function()
|
|||
ZoteroPane_Local.collectionsView.setHighlightedRows();
|
||||
}
|
||||
|
||||
this.revealCollectionSearch = function () {
|
||||
this.hideCollectionSearch = function () {
|
||||
let collectionSearchField = document.getElementById("zotero-collections-search");
|
||||
let collectionSearchButton = document.getElementById("zotero-tb-collection-search");
|
||||
var hideIfEmpty = function () {
|
||||
if (!collectionSearchField.value.length) {
|
||||
collectionSearchField.classList.remove("visible");
|
||||
collectionSearchButton.style.display = '';
|
||||
let collectionSearchButton = document.getElementById("zotero-tb-collections-search");
|
||||
if (!collectionSearchField.value.length && collectionSearchField.classList.contains("visible")) {
|
||||
collectionSearchField.classList.remove("visible");
|
||||
collectionSearchButton.style.display = '';
|
||||
collectionSearchField.setAttribute("disabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
this.initCollectionTreeSearch = function () {
|
||||
let collectionSearchField = document.getElementById("zotero-collections-search");
|
||||
let collectionSearchButton = document.getElementById("zotero-tb-collections-search");
|
||||
collectionSearchField.addEventListener("blur", ZoteroPane.hideCollectionSearch);
|
||||
collectionSearchButton.addEventListener("click", (_) => {
|
||||
if (!collectionSearchField.classList.contains("visible")) {
|
||||
collectionSearchButton.style.display = 'none';
|
||||
collectionSearchField.classList.add("visible");
|
||||
// Enable and focus the field only after it was revealed to prevent the cursor
|
||||
// from changing between 'text' and 'pointer' back and forth as the input field expands
|
||||
setTimeout(() => {
|
||||
collectionSearchField.removeAttribute("disabled");
|
||||
collectionSearchField.focus();
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if (!collectionSearchField.classList.contains("visible")) {
|
||||
collectionSearchButton.style.display = 'none';
|
||||
collectionSearchField.classList.add("visible");
|
||||
collectionSearchField.addEventListener('blur', hideIfEmpty);
|
||||
}
|
||||
collectionSearchField.focus();
|
||||
collectionSearchField.focus();
|
||||
});
|
||||
};
|
||||
|
||||
this.handleCollectionSearchKeypress = function (textbox, event) {
|
||||
if (event.keyCode == event.DOM_VK_ESCAPE) {
|
||||
textbox.value = '';
|
||||
textbox.blur();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.handleCollectionSearchInput = function (textbox, _) { };
|
||||
|
||||
function handleKeyUp(event) {
|
||||
if ((Zotero.isWin && event.keyCode == 17) ||
|
||||
|
@ -2721,6 +2758,13 @@ var ZoteroPane = new function()
|
|||
this.search(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.handleCollectionSearchInput = function () {
|
||||
let collectionsSearchField = document.getElementById("zotero-collections-search");
|
||||
// Set the filter without scrolling the selected row into the view
|
||||
this.collectionsView.setFilter(collectionsSearchField.value, false);
|
||||
}
|
||||
|
||||
|
||||
this.handleSearchInput = function (textbox, event) {
|
||||
|
|
|
@ -953,11 +953,10 @@
|
|||
<toolbarbutton id="zotero-tb-collection-add" tabindex="-1" class="zotero-tb-button" tooltiptext="&zotero.toolbar.newCollection.label;" command="cmd_zotero_newCollection"/>
|
||||
<spacer flex="1"></spacer>
|
||||
<html:div style="display: flex;flex-direction: row;">
|
||||
<toolbarbutton id="zotero-tb-collection-search" tabindex="-1" class="zotero-tb-button" tooltiptext="&zotero.toolbar.newCollection.label;" oncommand="ZoteroPane.revealCollectionSearch()"/>
|
||||
<toolbarbutton id="zotero-tb-collections-search" tabindex="-1" class="zotero-tb-button" tooltiptext="&zotero.toolbar.newCollection.label;"/>
|
||||
<search-textbox
|
||||
id="zotero-collections-search" class="hidden"
|
||||
onkeydown="ZoteroPane.handleCollectionSearchKeypress(this, event)"
|
||||
oncommand="ZoteroPane.handleCollectionSearchInput(this, event)"/>
|
||||
id="zotero-collections-search" class="hidden" disabled="true"
|
||||
oncommand="ZoteroPane.handleCollectionSearchInput()"/>
|
||||
</html:div>
|
||||
</hbox>
|
||||
</toolbar>
|
||||
|
|
|
@ -207,7 +207,6 @@
|
|||
}
|
||||
|
||||
#zotero-collections-toolbar {
|
||||
margin-inline-end: 10px; /* Set to width of splitter for visual aesthetics */
|
||||
padding-inline-start: 2px;
|
||||
}
|
||||
|
||||
|
@ -282,11 +281,8 @@
|
|||
}
|
||||
|
||||
#zotero-collections-search {
|
||||
height: 25px;
|
||||
max-width: 0;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.7s ease;
|
||||
transition: max-width 0.5s ease;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
|
@ -294,9 +290,14 @@
|
|||
}
|
||||
|
||||
#zotero-collections-search.visible {
|
||||
max-width: 200px;
|
||||
max-width: 180px;
|
||||
appearance: auto;
|
||||
visibility: visible;
|
||||
/* Avoids flickering between pointer and text cursor during transition */
|
||||
cursor: text;
|
||||
/* Bring back default margins and padding */
|
||||
padding: revert-layer;
|
||||
margin: revert-layer;
|
||||
}
|
||||
|
||||
/* Hide collection search on pane collapse, otherwise it still shows when focused */
|
||||
|
|
|
@ -1374,4 +1374,200 @@ describe("Zotero.CollectionTree", function() {
|
|||
assert.lengthOf(win.document.querySelectorAll('#zotero-collections-tree .row.unread'), 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setFilter()", function () {
|
||||
var collection1, collection2, collection3, collection4, collection5, collection6, collection7, collection8;
|
||||
var search1, search2, feed1, feed2;
|
||||
var allRows = [];
|
||||
let keyboardClick = (key) => {
|
||||
return new KeyboardEvent('keydown', {
|
||||
key: key,
|
||||
code: key,
|
||||
bubbles: true,
|
||||
});
|
||||
};
|
||||
before(async function () {
|
||||
// Delete all previously added collections, feeds, searches
|
||||
for (let col of Zotero.Collections.getByLibrary(userLibraryID)) {
|
||||
await col.eraseTx();
|
||||
}
|
||||
await clearFeeds();
|
||||
for (let s of Zotero.Searches.getByLibrary(userLibraryID)) {
|
||||
await s.eraseTx();
|
||||
}
|
||||
// Display the collection search bar
|
||||
win.document.getElementById("zotero-tb-collections-search").click();
|
||||
// Do not hide the search panel on blur
|
||||
win.document.getElementById("zotero-collections-search").removeEventListener('blur', zp.hideCollectionSearch);
|
||||
|
||||
feed1 = await createFeed({ name: "feed_1 " });
|
||||
feed2 = await createFeed({ name: "feed_2" });
|
||||
|
||||
collection1 = await createDataObject('collection', { name: "collection_level_one", libraryID: userLibraryID });
|
||||
collection2 = await createDataObject('collection', { name: "collection_level_two_1", parentID: collection1.id, libraryID: userLibraryID });
|
||||
collection3 = await createDataObject('collection', { name: "collection_level_two_2", parentID: collection1.id, libraryID: userLibraryID });
|
||||
collection4 = await createDataObject('collection', { name: "collection_level_three_1", parentID: collection2.id, libraryID: userLibraryID });
|
||||
collection5 = await createDataObject('collection', { name: "collection_level_three_11", parentID: collection2.id, libraryID: userLibraryID });
|
||||
collection6 = await createDataObject('collection', { name: "collection_level_one_1", libraryID: userLibraryID });
|
||||
collection7 = await createDataObject('collection', { name: "collection_level_two_21", parentID: collection6.id, libraryID: userLibraryID });
|
||||
collection8 = await createDataObject('collection', { name: "collection_level_two_22", parentID: collection6.id, libraryID: userLibraryID });
|
||||
search1 = await createDataObject('search', { name: "search_1", libraryID: userLibraryID });
|
||||
search2 = await createDataObject('search', { name: "search_2", libraryID: userLibraryID });
|
||||
allRows = [feed1, feed2, collection1, collection2, collection3, collection4, collection5, collection6, collection7, collection8, search1, search2];
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
// Empty filter and let it settle
|
||||
await cv.setFilter("");
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await cv.setFilter("");
|
||||
});
|
||||
|
||||
for (let type of ['collection', 'search', 'feed']) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should show only ${type} matching the filter`, async function () {
|
||||
await cv.setFilter(type);
|
||||
let displayedRowNames = cv._rows.filter(row => row.type == type).map(row => row.getName());
|
||||
let expectedRowNames = allRows.filter(row => row.name.includes(type)).map(row => row.name);
|
||||
assert.sameMembers(displayedRowNames, expectedRowNames);
|
||||
});
|
||||
}
|
||||
|
||||
it('should show non-passing entries whose children pass the filter', async function () {
|
||||
await cv.setFilter("three");
|
||||
let displayedRowNames = cv._rows.filter(row => row.type == "collection").map(row => row.ref.name);
|
||||
let expectedNames = [
|
||||
"collection_level_one",
|
||||
"collection_level_two_1",
|
||||
"collection_level_three_1",
|
||||
"collection_level_three_11"
|
||||
];
|
||||
assert.sameMembers(displayedRowNames, expectedNames);
|
||||
});
|
||||
|
||||
it('should not move focus from selected collection during filtering', async function () {
|
||||
await cv.selectByID("C" + collection5.id);
|
||||
await cv.setFilter("three");
|
||||
let focusedRow = cv.getRow(cv.selection.focused);
|
||||
assert.equal(focusedRow.id, "C" + collection5.id);
|
||||
await cv.setFilter("two");
|
||||
focusedRow = cv.getRow(cv.selection.focused);
|
||||
assert.equal(focusedRow.id, "C" + collection5.id);
|
||||
});
|
||||
|
||||
it('should only open relevant collections', async function () {
|
||||
// Collapse top level collections 1 and 6
|
||||
for (let c of [collection1, collection6]) {
|
||||
let index = cv.getRowIndexByID("C" + c.id);
|
||||
let row = cv.getRow(index);
|
||||
if (row.isOpen) {
|
||||
await cv.toggleOpenState(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Set filter and remove it
|
||||
await cv.setFilter(collection5.name);
|
||||
await cv.setFilter("");
|
||||
|
||||
// Collection 1 and 2 had a matching child, so they are opened
|
||||
let colOneRow = cv.getRow(cv.getRowIndexByID("C" + collection1.id));
|
||||
assert.isTrue(colOneRow.isOpen);
|
||||
let colTwoRow = cv.getRow(cv.getRowIndexByID("C" + collection2.id));
|
||||
assert.isTrue(colTwoRow.isOpen);
|
||||
// Collection 6 had no matches, it remains closed
|
||||
let colSixRow = cv.getRow(cv.getRowIndexByID("C" + collection6.id));
|
||||
assert.isFalse(colSixRow.isOpen);
|
||||
});
|
||||
|
||||
for (let type of ['collection', 'search']) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should only hide ${type} if it's renamed to not match the filter`, async function () {
|
||||
await cv.setFilter(type);
|
||||
let objectToSelect = type == 'collection' ? collection5 : search2;
|
||||
objectToSelect.name += "_updated";
|
||||
await objectToSelect.saveTx();
|
||||
let displayedRowNames = cv._rows.map(row => row.getName());
|
||||
assert.include(displayedRowNames, objectToSelect.name);
|
||||
|
||||
objectToSelect.name = "not_matching_filter";
|
||||
await objectToSelect.saveTx();
|
||||
displayedRowNames = cv._rows.map(row => row.getName());
|
||||
assert.notInclude(displayedRowNames, objectToSelect.name);
|
||||
});
|
||||
}
|
||||
|
||||
for (let type of ['collection', 'search']) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should only add ${type} if its name matches the filter`, async function () {
|
||||
await cv.setFilter(type);
|
||||
let newCollection = await createDataObject(type, { name: `new_${type}`, libraryID: userLibraryID });
|
||||
|
||||
let displayedRowNames = cv._rows.map(row => row.ref.name);
|
||||
assert.include(displayedRowNames, newCollection.name);
|
||||
|
||||
newCollection = await createDataObject(type, { name: `not_passing_${type.substring(1)}`, libraryID: userLibraryID });
|
||||
|
||||
displayedRowNames = cv._rows.map(row => row.ref.name);
|
||||
assert.notInclude(displayedRowNames, newCollection.name);
|
||||
});
|
||||
}
|
||||
|
||||
it(`should focus selected collection on Enter if it matches filter`, async function () {
|
||||
await cv.selectByID(`C${collection3.id}`);
|
||||
win.document.getElementById("zotero-collections-search").value = "_2";
|
||||
await cv.setFilter("_2");
|
||||
win.document.getElementById("zotero-collections-search").dispatchEvent(keyboardClick("Enter"));
|
||||
assert.equal(cv.getSelectedCollection(true), collection3.id);
|
||||
assert.equal(win.document.activeElement.id, 'collection-tree');
|
||||
});
|
||||
|
||||
it(`should focus first matching collection on Enter if selected collection does not match filter`, async function () {
|
||||
await cv.selectByID(`C${collection2.id}`);
|
||||
win.document.getElementById("zotero-collections-search").focus();
|
||||
win.document.getElementById("zotero-collections-search").value = "_2";
|
||||
await cv.setFilter("_2");
|
||||
win.document.getElementById("zotero-collections-search").dispatchEvent(keyboardClick("Enter"));
|
||||
// Wait for the selection to go through
|
||||
await Zotero.Promise.delay(100);
|
||||
assert.equal(cv.getSelectedCollection(true), collection3.id);
|
||||
assert.equal(win.document.activeElement.id, 'collection-tree');
|
||||
});
|
||||
|
||||
it(`should not move focus from collection filter on Enter if no rows pass the filter`, async function () {
|
||||
await cv.selectByID(`C${collection3.id}`);
|
||||
win.document.getElementById("zotero-collections-search").focus();
|
||||
win.document.getElementById("zotero-collections-search").value = "Not matching anything";
|
||||
await cv.setFilter("Not matching anything");
|
||||
win.document.getElementById("zotero-collections-search").dispatchEvent(keyboardClick("Enter"));
|
||||
assert.equal(win.document.activeElement.id, 'zotero-collections-search');
|
||||
});
|
||||
|
||||
it(`should skip context rows on arrow up/down`, async function () {
|
||||
await cv.selectByID(`C${collection2.id}`);
|
||||
await cv.setFilter("_2");
|
||||
await cv.focusFirstMatchingRow();
|
||||
// Skip collection6 that does not match on the way up and down
|
||||
for (let col of [collection3, collection7, collection8]) {
|
||||
assert.equal(cv.getSelectedCollection(true), col.id);
|
||||
await cv.focusNextMatchingRow(cv.selection.focused);
|
||||
}
|
||||
await cv.selectByID(`C${collection8.id}`);
|
||||
for (let col of [collection8, collection7, collection3]) {
|
||||
assert.equal(cv.getSelectedCollection(true), col.id);
|
||||
await cv.focusNextMatchingRow(cv.selection.focused, true);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should clear filter on Escape from collectionTree`, async function () {
|
||||
await cv.selectByID(`C${collection2.id}`);
|
||||
let colTree = win.document.getElementById('collection-tree');
|
||||
await cv.setFilter("_2");
|
||||
cv.focusFirstMatchingRow();
|
||||
colTree.dispatchEvent(keyboardClick("Escape"));
|
||||
assert.equal(cv._filter, "");
|
||||
assert.equal(cv.getSelectedCollection(true), collection2.id);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1531,6 +1531,10 @@ describe("ZoteroPane", function() {
|
|||
|
||||
for (let id of sequence) {
|
||||
doc.activeElement.dispatchEvent(shiftTab);
|
||||
// Wait for collection search to be revealed
|
||||
if (id === "zotero-collections-search") {
|
||||
await Zotero.Promise.delay(300);
|
||||
}
|
||||
assert.equal(doc.activeElement.id, id);
|
||||
}
|
||||
|
||||
|
@ -1555,6 +1559,10 @@ describe("ZoteroPane", function() {
|
|||
];
|
||||
for (let id of sequence) {
|
||||
doc.activeElement.dispatchEvent(tab);
|
||||
// Wait for collection search to be revealed
|
||||
if (id === "zotero-collections-search") {
|
||||
await Zotero.Promise.delay(300);
|
||||
}
|
||||
assert.equal(doc.activeElement.id, id);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue