diff --git a/chrome/content/zotero/advancedSearch.js b/chrome/content/zotero/advancedSearch.js deleted file mode 100644 index b28c441147..0000000000 --- a/chrome/content/zotero/advancedSearch.js +++ /dev/null @@ -1,194 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://zotero.org - - This file is part of Zotero. - - Zotero is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Zotero is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Zotero. If not, see . - - ***** END LICENSE BLOCK ***** -*/ - - -import CollectionViewItemTree from 'zotero/collectionViewItemTree'; -import { COLUMNS } from 'zotero/itemTreeColumns'; - - -var ZoteroAdvancedSearch = new function () { - this.onLoad = onLoad; - this.search = search; - this.clear = clear; - this.onItemActivate = onItemActivate; - - this.itemsView = false; - this._loadedDeferred = Zotero.Promise.defer(); - - var _searchBox; - var _libraryID; - var _searchCounter = 0; - - async function onLoad() { - _searchBox = document.getElementById('zotero-search-box'); - - // Set font size from pref - var sbc = document.getElementById('zotero-search-box-container'); - Zotero.UIProperties.registerRoot(sbc); - - _searchBox.onLibraryChange = this.onLibraryChange; - var io = window.arguments[0]; - - io.dataIn.search.loadPrimaryData() - .then(function () { - _searchBox.search = io.dataIn.search; - }); - - var elem = document.getElementById('zotero-items-tree'); - const columns = COLUMNS.map((column) => { - column = Object.assign({}, column); - column.hidden = !['title', 'firstCreator', 'year', 'hasAttachment'].includes(column.dataKey); - return column; - }); - this.itemsView = await CollectionViewItemTree.init(elem, { - id: "advanced-search", - dragAndDrop: true, - columnPicker: true, - onActivate: this.onItemActivate.bind(this), - columns, - }); - - await this.itemsView.changeCollectionTreeRow({ - id: 'advanced-search-' + _searchCounter++, - ref: _searchBox.search, - visibilityGroup: 'default', - isSearchMode: () => true, - getItems: async () => [], - isLibrary: () => false, - isCollection: () => false, - isSearch: () => true, - isPublications: () => false, - isDuplicates: () => false, - isFeed: () => false, - isFeeds: () => false, - isFeedsOrFeed: () => false, - isRecentlyRead: () => false, - isSortable: () => true, - isShare: () => false, - isTrash: () => false, - isSearch: () => true - }); - - // Focus the first field in the window - Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_FORWARD, 0); - this._loadedDeferred.resolve(); - } - - this.onUnload = function () { - this.itemsView.unregister(); - } - - function search() { - _searchBox.updateSearch(); - _searchBox.active = true; - - return this.itemsView.changeCollectionTreeRow({ - id: 'advanced-search-' + _searchCounter++, - ref: _searchBox.search, - visibilityGroup: 'default', - isSearchMode: () => true, - isSearch: () => true, - getItems: async function () { - await Zotero.Libraries.get(_libraryID).waitForDataLoad('item'); - - var search = _searchBox.search.clone(); - search.libraryID = _libraryID; - var ids = await search.search(); - return Zotero.Items.get(ids); - } - }); - } - - - function clear() { - this.itemsView.changeCollectionTreeRow(null); - - var s = new Zotero.Search(); - // Don't clear the selected library - s.libraryID = _searchBox.search.libraryID; - s.addCondition('title', 'contains', ''); - _searchBox.search = s; - _searchBox.active = false; - } - - - this.save = async function () { - _searchBox.updateSearch(); - - var promptService = Services.prompt; - - var libraryID = _searchBox.search.libraryID; - - var searches = await Zotero.Searches.getAll(libraryID); - var prefix = Zotero.getString('pane.collections.untitled'); - var name = Zotero.Utilities.Internal.getNextName( - prefix, - searches.map(s => s.name).filter(n => n.startsWith(prefix)) - ); - - name = { value: name }; - var result = promptService.prompt(window, - Zotero.getString('pane.collections.newSavedSeach'), - Zotero.getString('pane.collections.savedSearchName'), name, "", {}); - - if (!result) { - return; - } - - if (!name.value) { - name.value = 'untitled'; - } - - var s = _searchBox.search.clone(); - s.name = name.value; - await s.saveTx(); - - window.close(); - }; - - - this.onLibraryChange = function (libraryID) { - _libraryID = libraryID; - var library = Zotero.Libraries.get(libraryID); - var isEditable = library.editable && library.libraryType != 'publications'; - document.getElementById('zotero-search-save').disabled = !isEditable; - } - - - function onItemActivate(event, items) - { - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - - var lastWin = wm.getMostRecentWindow("navigator:browser"); - - if (!lastWin) { - return; - } - - lastWin.ZoteroPane.selectItems(items.map(item => item.id)); - lastWin.focus(); - } -} diff --git a/chrome/content/zotero/advancedSearch.xhtml b/chrome/content/zotero/advancedSearch.xhtml deleted file mode 100644 index ac90733b78..0000000000 --- a/chrome/content/zotero/advancedSearch.xhtml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - %zoteroDTD; - - %searchboxDTD; -]> - - - - - - - - - - - - - - - - - diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js index 38ded96839..86d3cb656f 100644 --- a/chrome/content/zotero/xpcom/collectionTreeRow.js +++ b/chrome/content/zotero/xpcom/collectionTreeRow.js @@ -464,7 +464,8 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = async function () { } s2.setScope(s, includeScopeChildren); - if (this.searchText) { + // Add Quick Search unless advanced search is enabled + if (this.searchText && !this.advancedSearch) { let cond = 'quicksearch-' + (this.searchMode || Zotero.Prefs.get('search.quicksearch-mode')); s2.addCondition(cond, 'contains', this.searchText); @@ -476,8 +477,37 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = async function () { } } - this._cachedSearch = s2; - return s2; + let s3; + if (this.advancedSearch) { + if (this.advancedSearch.libraryID === null) { + // A library-less search (Feeds pseudo-library) can't be clone()d + s3 = new Zotero.Search(); + s3.fromJSON(this.advancedSearch.toJSON()); + } + else { + s3 = this.advancedSearch.clone(); + } + // Show matches in the trash too. includeDeleted has to be set on the + // scope searches as well, since a search's scope defines the superset + // of possible results. Special condition - unaffected by joinMode. + if (!this.isTrash()) { + if (s == this.ref) { + // Don't modify a saved search object + s = s.clone(this.ref.libraryID); + s2.setScope(s, includeScopeChildren); + } + s.addCondition('includeDeleted', 'true'); + s2.addCondition('includeDeleted', 'true'); + } + s3.addCondition('includeDeleted', 'true'); + s3.setScope(s2, includeScopeChildren); + } + else { + s3 = s2; + } + + this._cachedSearch = s3; + return s3; }; Zotero.CollectionTreeRow.prototype.getChildTags = function () { @@ -537,6 +567,23 @@ Zotero.CollectionTreeRow.prototype.setSearch = function (searchText, mode = null return true; } +Zotero.CollectionTreeRow.prototype.setAdvancedSearch = function (advancedSearch) { + this.clearCache(); + if (!advancedSearch) { + this.advancedSearch = undefined; + } + else if (this.ref.libraryID === undefined) { + // Feeds pseudo-library -- leave the library unset so that the search + // spans all feed libraries + this.advancedSearch = new Zotero.Search(); + this.advancedSearch.fromJSON(advancedSearch.toJSON()); + } + else { + this.advancedSearch = advancedSearch.clone(this.ref.libraryID); + } + return true; +}; + Zotero.CollectionTreeRow.prototype.setTags = function (tags) { let oldTags = this.tags instanceof Set ? this.tags : new Set(this.tags || []); let newTags = tags instanceof Set ? new Set(tags) : new Set(tags || []); @@ -570,8 +617,8 @@ Zotero.CollectionTreeRow.prototype.isSearchMode = function () { return true; } - // Quicksearch - if (this.searchText != '') { + // Search filters + if (this.advancedSearch || this.searchText != '') { return true; } diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js index 8110841918..edde327cd7 100644 --- a/chrome/content/zotero/xpcom/data/search.js +++ b/chrome/content/zotero/xpcom/data/search.js @@ -597,8 +597,9 @@ Zotero.Search.prototype.search = async function (asTempTable) { // Run a subsearch to define the superset of possible results if (this._scope) { - // If subsearch has post-search filter, run and insert ids into temp table - if (this._scope.hasPostSearchFilter()) { + // If subsearch has post-search filter or a recursive scope, + // run and insert ids into temp table + if (this._scope.hasPostSearchFilter() || this._scope._scope) { var ids = await this._scope.search(); if (!ids) { return []; diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 943686cb0a..37d7f866fd 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -364,7 +364,7 @@ var ZoteroPane = new function () { 'zotero-tb-add': { ArrowNext: () => document.getElementById("zotero-tb-lookup"), ArrowPrevious: () => null, - Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(), + Tab: () => document.getElementById("zotero-tb-search").focus(), ShiftTab: () => { if (collectionsPane.getAttribute("collapsed")) { return document.getElementById('zotero-tb-sync'); @@ -378,7 +378,7 @@ var ZoteroPane = new function () { 'zotero-tb-lookup': { ArrowNext: () => document.getElementById("zotero-tb-attachment-add"), ArrowPrevious: () => document.getElementById("zotero-tb-add"), - Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(), + Tab: () => document.getElementById("zotero-tb-search").focus(), ShiftTab: () => document.getElementById('zotero-tb-collections-search').click(), Enter: () => Zotero_Lookup.showPanel(event.target), ' ': () => Zotero_Lookup.showPanel(event.target) @@ -386,13 +386,13 @@ var ZoteroPane = new function () { 'zotero-tb-attachment-add': { ArrowNext: () => document.getElementById("zotero-tb-note-add"), ArrowPrevious: () => document.getElementById("zotero-tb-lookup"), - Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(), + Tab: () => document.getElementById("zotero-tb-search").focus(), ShiftTab: () => document.getElementById('zotero-tb-collections-search').click() }, 'zotero-tb-note-add': { ArrowNext: () => null, ArrowPrevious: () => document.getElementById("zotero-tb-attachment-add"), - Tab: () => document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(), + Tab: () => document.getElementById("zotero-tb-search").focus(), ShiftTab: () => document.getElementById('zotero-tb-collections-search').click() }, 'zotero-tb-search-dropmarker': { @@ -403,9 +403,7 @@ var ZoteroPane = new function () { }, 'zotero-tb-search-textbox': { Tab: () => document.getElementById("zotero-tb-toggle-item-pane-stacked"), - ShiftTab: () => { - document.getElementById("zotero-tb-search")._searchModePopup.flattenedTreeParentNode.focus(); - } + ShiftTab: () => document.getElementById("zotero-tb-search").focus() }, 'zotero-tb-toggle-item-pane-stacked': { Tab: () => itemTree.querySelector(".virtualized-table"), @@ -1554,59 +1552,9 @@ var ZoteroPane = new function () { this.loadURI(Zotero.Groups.addGroupURL); } - - this.newSearch = async function () { - if (Zotero.DB.inTransaction()) { - await Zotero.DB.waitForTransaction(); - } - - var libraryID = this.getSelectedLibraryID(); - - var s = new Zotero.Search(); - s.libraryID = libraryID; - s.addCondition('title', 'contains', ''); - - var searches = await Zotero.Searches.getAll(libraryID) - var prefix = Zotero.getString('pane.collections.untitled'); - var name = Zotero.Utilities.Internal.getNextName( - prefix, - searches.map(s => s.name).filter(n => n.startsWith(prefix)) - ); - - var io = { dataIn: { search: s, name }, dataOut: null }; - window.openDialog('chrome://zotero/content/searchDialog.xhtml','','chrome,modal,centerscreen',io); - if (!io.dataOut) { - return false; - } - s.fromJSON(io.dataOut.json); - await s.saveTx(); - return s.id; - }; - this.setVirtual = function (libraryID, type, show, select) { return this.collectionsView.toggleVirtualCollection(libraryID, type, show, select); }; - - this.openAdvancedSearchWindow = function () { - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var enumerator = wm.getEnumerator('zotero:search'); - while (enumerator.hasMoreElements()) { - var win = enumerator.getNext(); - } - - if (win) { - win.focus(); - return; - } - - var s = new Zotero.Search(); - s.libraryID = this.getSelectedLibraryID(); - s.addCondition('title', 'contains', ''); - - var io = {dataIn: {search: s}, dataOut: null}; - window.openDialog('chrome://zotero/content/advancedSearch.xhtml', '', 'chrome,dialog=no,centerscreen', io); - }; this.initItemsTree = async function () { try { @@ -1688,7 +1636,7 @@ var ZoteroPane = new function () { this.tagSelector.handleResize(); } if (this.collectionsView) { - this.collectionsView.updateHeight(); + this.collectionsView.updateHeightDebounced(); } }, 100); @@ -1756,7 +1704,6 @@ var ZoteroPane = new function () { } }; - this.onCollectionSelected = Zotero.serial(async function () { var collectionTreeRow = this.getCollectionTreeRow(); if (!collectionTreeRow) { @@ -1772,13 +1719,43 @@ var ZoteroPane = new function () { return; } + let advancedSearchDeck = document.getElementById('zotero-advanced-search-pane-deck'); + if (this.itemsView.collectionTreeRow?.isSearch() + && advancedSearchDeck.state === 'open' + && advancedSearchDeck.selectedSearchType === 'saved') { + let result = Services.prompt.confirmEx(window, + Zotero.getString('saved-search-close-confirmation-title'), + Zotero.getString('saved-search-close-confirmation-body'), + Ci.nsIPromptService.BUTTON_POS_0_DEFAULT + | Ci.nsIPrompt.BUTTON_TITLE_SAVE * Ci.nsIPrompt.BUTTON_POS_0 + | Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1 + | Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE * Ci.nsIPrompt.BUTTON_POS_2, + null, null, null, + null, {}); + switch (result) { + case 0: + await advancedSearchDeck.pane.save(); + return; + case 1: + this.collectionsView.selection.selectEventsSuppressed = true; + try { + await this.collectionsView.selectByID(this.itemsView.collectionTreeRow.id); + } + finally { + this.collectionsView.selection.selectEventsSuppressed = false; + } + return; + case 2: + await advancedSearchDeck.pane.cancel(); + break; + } + } + // Rename tab Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName()); - let type = Zotero.Libraries.get(collectionTreeRow.ref.libraryID).libraryType; - // Clear quick search and tag selector when switching views - document.getElementById('zotero-tb-search-textbox').value = ""; + document.getElementById('zotero-tb-search').onCollectionSelected(); if (ZoteroPane.tagSelector) { ZoteroPane.tagSelector.clearTagSelection(); } @@ -1788,6 +1765,7 @@ var ZoteroPane = new function () { collectionTreeRow.setTags(ZoteroPane.tagSelector.getTagSelection()); } + this._refreshAdvancedSearchPane(collectionTreeRow); this._updateEnabledActionsForRow(collectionTreeRow); // If item data not yet loaded for library, load it now. @@ -1815,6 +1793,125 @@ var ZoteroPane = new function () { Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id); }); + + + /** + * @param {Zotero.CollectionTreeRow} [collectionTreeRow] - During collection selection, + * the newly selected row, which isn't in the items view yet + */ + this._refreshAdvancedSearchPane = function (collectionTreeRow) { + let deck = document.getElementById('zotero-advanced-search-pane-deck'); + + deck.pane.refresh(); + + let search = deck.state === 'closed' || deck.selectedSearchType !== 'temporary' || !deck.pane.active + ? null + : deck.pane.search; + if (collectionTreeRow) { + collectionTreeRow.setAdvancedSearch(search); + return undefined; + } + // Apply via the items view, whose row can be a different object from the + // collection tree's current row + return this.itemsView.setFilter('advanced-search', search); + }; + + + /** + * @param {'open' | 'collapsed' | 'closed'} state + */ + this.setAdvancedSearchState = async function (state) { + let deck = document.getElementById('zotero-advanced-search-pane-deck'); + let oldState = deck.state; + deck.selectedSearchType = 'temporary'; + deck.state = state; + + let advancedSearchPane = deck.pane; + + document.getElementById('zotero-tb-search').updateMode(); + let refreshPromise; + if (state === 'open' && oldState === 'collapsed' + || state === 'collapsed' && oldState === 'open') { + // State change only causes visual refresh - update the tree height + this.itemsView.updateHeight(); + } + else { + // State change changes displayed items - refresh the tree + refreshPromise = this._refreshAdvancedSearchPane(); + } + + // Update the pane state synchronously, so that a state change initiated + // while the refresh below is pending doesn't see a stale search + if (state === 'closed') { + advancedSearchPane.search = null; + } + else if (state === 'open') { + Zotero_Tabs.select('zotero-pane'); + advancedSearchPane.focus(); + } + + await refreshPromise; + }; + + + /** + * @param {'open' | 'collapsed' | 'closed'} state + */ + this.toggleAdvancedSearchState = async function (state) { + let deck = document.getElementById('zotero-advanced-search-pane-deck'); + if (state === deck.state && deck.selectedSearchType !== 'saved') { + // If we're trying to open the pane, and it's already open but not focused, + // focus it + if (state === 'open' && !deck.pane.matches(':focus-within')) { + deck.pane.focus(); + return; + } + + // Flip the state + switch (state) { + case 'open': + state = 'closed'; + break; + case 'collapsed': + case 'closed': + state = 'open'; + break; + } + } + await this.setAdvancedSearchState(state); + }; + + + /** + * @param {'open' | 'closed'} state + */ + this.setSavedSearchEditorState = async function (state) { + let collectionTreeRow = this.getCollectionTreeRow(); + if (state === 'open' && !collectionTreeRow.isSearch()) { + throw new Error('Cannot show saved search editor outside search row'); + } + + let deck = document.getElementById('zotero-advanced-search-pane-deck'); + deck.selectedSearchType = 'saved'; + deck.state = state; + + if (state === 'open') { + deck.pane.search = collectionTreeRow.ref; + } + + document.getElementById('zotero-tb-search').updateMode(); + let refreshPromise = this._refreshAdvancedSearchPane(); + if (state === 'open') { + deck.pane.focus(); + } + await refreshPromise; + }; + + + this.openAdvancedSearchWindow = function () { + Zotero.debug(`ZoteroPane.openAdvancedSearchWindow() is deprecated -- use ZoteroPane.toggleAdvancedSearchState() instead`); + this.toggleAdvancedSearchState('open'); + }; /** @@ -1828,7 +1925,6 @@ var ZoteroPane = new function () { "menu_noteAdd", "cmd_zotero_newCollection", - "cmd_zotero_newSavedSearch", "cmd_zotero_import", "cmd_zotero_importFromClipboard", @@ -2580,28 +2676,7 @@ var ZoteroPane = new function () { this.collectionsView.startEditing(row); } else { - let s = row.ref.clone(); - let groups = []; - // Promises don't work in the modal dialog, so get the group name here, if - // applicable, and pass it in. We only need the group that this search belongs - // to, if any, since the library drop-down is disabled for saved searches. - if (Zotero.Libraries.get(s.libraryID).libraryType == 'group') { - groups.push(Zotero.Groups.getByLibraryID(s.libraryID)); - } - var io = { - dataIn: { - search: s, - name: row.getName(), - groups: groups - }, - dataOut: null - }; - window.openDialog('chrome://zotero/content/searchDialog.xhtml','','chrome,modal,centerscreen',io); - if (io.dataOut) { - row.ref.fromJSON(io.dataOut.json); - await row.ref.saveTx(); - Zotero_Tabs.rename("zotero-pane", row.ref.name); - } + this.setSavedSearchEditorState('open'); } } }; @@ -3274,10 +3349,6 @@ var ZoteroPane = new function () { id: "newCollection", command: "cmd_zotero_newCollection" }, - { - id: "newSavedSearch", - command: "cmd_zotero_newSavedSearch" - }, { id: "newSubcollection", oncommand: () => { @@ -3564,8 +3635,7 @@ var ZoteroPane = new function () { show.push( 'sync', 'sep1', - 'newCollection', - 'newSavedSearch' + 'newCollection' ); } // Only show "Show Duplicates", "Show Unfiled Items", and "Show Retracted" if rows are hidden @@ -6784,7 +6854,7 @@ var ZoteroPane = new function () { if (ZoteroPane.itemsView) { // Need to immediately rerender the items here without any debouncing // since tree height will have changed - ZoteroPane.itemsView._updateHeight(); + ZoteroPane.itemsView.updateHeight(); } ZoteroContextPane.update(); Zotero_Tabs.updateSidebarLayout(); @@ -6970,7 +7040,7 @@ var ZoteroPane = new function () { var collectionsPaneWidth = collectionsPane.getBoundingClientRect().width; tagSelector.style.maxWidth = collectionsPaneWidth + 'px'; if (ZoteroPane.itemsView) { - ZoteroPane.itemsView.updateHeight(); + ZoteroPane.itemsView.updateHeightDebounced(); } this.handleTagSelectorResize(); diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml index a15a042f6e..9f4d1cf0c2 100644 --- a/chrome/content/zotero/zoteroPane.xhtml +++ b/chrome/content/zotero/zoteroPane.xhtml @@ -102,7 +102,7 @@ - + @@ -116,7 +116,6 @@ - @@ -428,7 +427,7 @@ key="key_find" accesskey="&findCmd.accesskey;" oncommand="ZoteroStandalone.currentReader.toggleFindPopup({ open: true });"/>