mirror of
https://github.com/zotero/zotero.git
synced 2026-10-06 02:50:03 +00:00
select collection from long list via dialog
If the user has too many collections to comfortably navigate via context menu, do not render the collection hierarchy via menus, and instead have the user select the collection via dialog that has a full collectionTree. Two hidden prefs determine when the dialog will appear: - collectionsMenu.maxEntriesTotal (default 5000) - max collections in specified libraries - collectionsMenu.maxEntriesAtOneLevel (default 100) - max collections on any single level (e.g max top level collections) If there are more collections than maxEntriesTotal or one collection has more than maxEntriesAtOneLevel children, the dialog will be used. It also takes care of some performance issues caused by trying to render too many collections. Fixes: #5693
This commit is contained in:
parent
9a765dd0b9
commit
db71a75b13
16 changed files with 673 additions and 60 deletions
|
|
@ -1851,6 +1851,8 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
else if (dataType == 'zotero/collection') {
|
||||
// Can drop collections into trash to delete it
|
||||
if (treeRow.isTrash()) return true;
|
||||
let draggedCollectionID = data[0];
|
||||
let draggedCollection = Zotero.Collections.get(draggedCollectionID);
|
||||
let canMove = await draggedCollection.canMoveToTargetAsync(treeRow.ref, { debug: true });
|
||||
|
|
@ -2687,6 +2689,36 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
|
||||
clearCollectionSearch(searchFieldSelector) {
|
||||
let collectionsSearchField = document.querySelector(searchFieldSelector);
|
||||
// If empty filter - just focus the collectionTree
|
||||
if (collectionsSearchField.value.length == 0) {
|
||||
return document.getElementById("collection-tree");
|
||||
}
|
||||
// Clear the search field and focus collection tree
|
||||
if (collectionsSearchField.value.length) {
|
||||
collectionsSearchField.value = '';
|
||||
this.setFilter("", true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
focusCollectionTree(searchFieldSelector) {
|
||||
let collectionsSearchField = document.querySelector(searchFieldSelector);
|
||||
// Prevent Enter/Tab pressed before the filtering ran from doing anything
|
||||
if (!this.filterEquals(collectionsSearchField.value)) {
|
||||
return null;
|
||||
}
|
||||
// If the current row passes the filter, make sure it is visible and focus collectionTree
|
||||
if (this.focusedRowMatchesFilter()) {
|
||||
this.ensureRowIsVisible(this.selection.focused);
|
||||
return document.getElementById('collection-tree');
|
||||
}
|
||||
// Otherwise, focus the first row passing the filter
|
||||
this.focusFirstMatchingRow(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an extra hidden row to keep focus on it when a currently focused row does not match the filter.
|
||||
|
|
@ -2724,17 +2756,6 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
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.
|
||||
|
|
@ -2940,7 +2961,8 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
&& this._virtualCollectionLibraries.duplicates[libraryID] !== false;
|
||||
var showUnfiled = this.props.hideSources.indexOf('unfiled') == -1
|
||||
&& this._virtualCollectionLibraries.unfiled?.[libraryID] !== false;
|
||||
var showRecentlyRead = this._virtualCollectionLibraries.recentlyRead?.[libraryID] !== false;
|
||||
var showRecentlyRead = this.props.hideSources.indexOf('recentlyRead') == -1
|
||||
&& this._virtualCollectionLibraries.recentlyRead?.[libraryID] !== false;
|
||||
var showRetracted = this.props.hideSources.indexOf('retracted') == -1
|
||||
&& this._virtualCollectionLibraries.retracted?.[libraryID] !== false
|
||||
&& Zotero.Retractions.libraryHasRetractedItems(libraryID);
|
||||
|
|
@ -2948,6 +2970,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
&& this._virtualCollectionLibraries.publications?.[libraryID] !== false
|
||||
&& libraryID == Zotero.Libraries.userLibraryID;
|
||||
var showTrash = this.props.hideSources.indexOf('trash') == -1;
|
||||
var showSavedSearches = this.props.hideSources.indexOf('searches') == -1;
|
||||
}
|
||||
else {
|
||||
var savedSearches = [];
|
||||
|
|
@ -2957,6 +2980,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
var showRetracted = false;
|
||||
var showPublications = false;
|
||||
var showTrash = false;
|
||||
var showSavedSearches = false;
|
||||
}
|
||||
|
||||
// If not a manual open and either the library is set to be collapsed or this is a collection that isn't explicitly opened,
|
||||
|
|
@ -3013,14 +3037,16 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
// Add searches
|
||||
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 (showSavedSearches) {
|
||||
for (let i = 0; i < savedSearches.length; 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 && this._isFilterEmpty()) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
import CollectionTree from 'zotero/collectionTree';
|
||||
|
||||
var Zotero_New_Collection_Dialog = {
|
||||
_handleLoad() {
|
||||
let io = window.arguments[0];
|
||||
|
|
@ -34,7 +36,20 @@ var Zotero_New_Collection_Dialog = {
|
|||
|
||||
this._libraryID = io.libraryID;
|
||||
this._parentCollectionID = io.parentCollectionID;
|
||||
this._updateMenu();
|
||||
|
||||
// depending on how many collections there are, display the dropdown
|
||||
// or the entire collection tree
|
||||
if (Zotero.Collections.canListCollectionsInMenu(this._libraryID)) {
|
||||
document.getElementById("create-in").hidden = false;
|
||||
this._updateMenu();
|
||||
}
|
||||
else {
|
||||
document.getElementById("zotero-collections-tree-container").hidden = false;
|
||||
setTimeout(() => {
|
||||
window.moveToAlertPosition();
|
||||
this._initCollectionTree();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_handleAccept() {
|
||||
|
|
@ -45,6 +60,26 @@ var Zotero_New_Collection_Dialog = {
|
|||
};
|
||||
},
|
||||
|
||||
async _initCollectionTree() {
|
||||
this._collectionsView = await CollectionTree.init(document.getElementById('zotero-collections-tree'), {
|
||||
onSelectionChange: () => {
|
||||
let selectedCollection = this._collectionsView.getSelectedCollection();
|
||||
this._parentCollectionID = selectedCollection ? selectedCollection.id : null;
|
||||
},
|
||||
filterLibraryIDs: [this._libraryID],
|
||||
hideSources: ['duplicates', 'trash', 'feeds', 'unfiled', 'retracted', 'publications', 'searches', 'recentlyRead']
|
||||
});
|
||||
|
||||
await this._collectionsView.makeVisible();
|
||||
|
||||
if (this._parentCollectionID) {
|
||||
await this._collectionsView.selectByID('C' + this._parentCollectionID);
|
||||
}
|
||||
else {
|
||||
await this._collectionsView.selectByID('L' + this._libraryID);
|
||||
}
|
||||
},
|
||||
|
||||
_updateMenu() {
|
||||
let createInField = document.querySelector('#create-in');
|
||||
let menupopup = createInField.firstElementChild;
|
||||
|
|
|
|||
|
|
@ -65,9 +65,12 @@
|
|||
|
||||
<vbox>
|
||||
<label data-l10n-id="new-collection-create-in" control="create-in"/>
|
||||
<menulist id="create-in" native="true">
|
||||
<menulist id="create-in" native="true" hidden="true">
|
||||
<menupopup id="zotero-new-collection-menu" />
|
||||
</menulist>
|
||||
<vbox id="zotero-collections-tree-container" class="virtualized-table-container" hidden="true">
|
||||
<html:div id="zotero-collections-tree"/>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</dialog>
|
||||
</window>
|
||||
|
|
|
|||
120
chrome/content/zotero/selectCollectionDialog.js
Normal file
120
chrome/content/zotero/selectCollectionDialog.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
***** 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
import CollectionTree from 'zotero/collectionTree';
|
||||
|
||||
var collectionsView;
|
||||
var loaded;
|
||||
var io;
|
||||
|
||||
var doLoad = async function () {
|
||||
// Set font size from pref
|
||||
var sbc = document.getElementById('zotero-select-collection-container');
|
||||
Zotero.UIProperties.registerRoot(sbc);
|
||||
|
||||
io = window.arguments[0];
|
||||
if (io.wrappedJSObject) io = io.wrappedJSObject;
|
||||
|
||||
let filterLibraryIDs = io.filterLibraryIDs || false;
|
||||
collectionsView = await CollectionTree.init(document.getElementById('zotero-collections-tree'), {
|
||||
onSelectionChange: async () => {
|
||||
let selectedRow = collectionsView.getRow(collectionsView.selection.focused);
|
||||
let canSelect = await io.canSelect(selectedRow.ref);
|
||||
document.getElementById("select-collection-dialog").getButton("accept").disabled = !canSelect;
|
||||
},
|
||||
filterLibraryIDs,
|
||||
hideSources: ['duplicates', 'trash', 'feeds', 'unfiled', 'retracted', 'publications', 'searches', 'recentlyRead'],
|
||||
onActivate: () => {
|
||||
document.querySelector('dialog').acceptDialog();
|
||||
}
|
||||
});
|
||||
|
||||
await collectionsView.makeVisible();
|
||||
|
||||
if (io.currentCollectionID) {
|
||||
await collectionsView.selectByID('C' + io.currentCollectionID);
|
||||
}
|
||||
|
||||
document.addEventListener('dialogaccept', doAccept);
|
||||
|
||||
// Setup keyboard navigation
|
||||
let collectionTree = document.getElementById('collection-tree');
|
||||
let searchField = document.getElementById('zotero-collections-search');
|
||||
|
||||
searchField.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
_handleCollectionFilterEscape(event);
|
||||
}
|
||||
else if (event.key === 'Enter' || (event.key === 'Tab' && !event.shiftKey)) {
|
||||
_handleCollectionFilterEnter(event);
|
||||
}
|
||||
});
|
||||
|
||||
collectionTree.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
_handleCollectionFilterEscape(event);
|
||||
}
|
||||
});
|
||||
|
||||
// Used in tests
|
||||
loaded = true;
|
||||
};
|
||||
|
||||
function doUnload()
|
||||
{
|
||||
collectionsView.unregister();
|
||||
|
||||
io.deferred && io.deferred.resolve();
|
||||
}
|
||||
|
||||
function handleCollectionSearchInput() {
|
||||
let collectionsSearchField = document.getElementById("zotero-collections-search");
|
||||
collectionsView.setFilter(collectionsSearchField.value);
|
||||
}
|
||||
|
||||
function doAccept() {
|
||||
let selected = collectionsView.getRow(collectionsView.selection.focused);
|
||||
io.dataOut = selected.ref;
|
||||
}
|
||||
|
||||
function _handleCollectionFilterEnter(event) {
|
||||
let result = collectionsView.focusCollectionTree("#zotero-collections-search");
|
||||
if (result) {
|
||||
result.focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function _handleCollectionFilterEscape(event) {
|
||||
if (!document.getElementById("zotero-collections-search").value) return;
|
||||
|
||||
let result = collectionsView.clearCollectionSearch("#zotero-collections-search");
|
||||
if (result) {
|
||||
result.focus();
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
77
chrome/content/zotero/selectCollectionDialog.xhtml
Normal file
77
chrome/content/zotero/selectCollectionDialog.xhtml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
-->
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
|
||||
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
|
||||
|
||||
|
||||
<window
|
||||
id="zotero-select-collection-dialog"
|
||||
windowtype="zotero:collection-selector"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
onload="doLoad();"
|
||||
onunload="doUnload();"
|
||||
persist="screenX screenY width height"
|
||||
class="zotero-dialog"
|
||||
data-l10n-id="select-collection-window"
|
||||
drawintitlebar-platforms="mac,win"
|
||||
>
|
||||
<dialog
|
||||
id="select-collection-dialog"
|
||||
orient="vertical"
|
||||
buttons="cancel,accept"
|
||||
data-l10n-id="select-collection-dialog"
|
||||
data-l10n-attrs="buttonlabelaccept"
|
||||
>
|
||||
<script>
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/customElements.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/selectCollectionDialog.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/titlebar.js", this);
|
||||
</script>
|
||||
|
||||
<linkset>
|
||||
<html:link rel="localization" href="zotero.ftl" />
|
||||
</linkset>
|
||||
|
||||
<vbox id="zotero-select-collection-container" flex="1">
|
||||
<hbox id="search-toolbar">
|
||||
<search-textbox
|
||||
id="zotero-collections-search"
|
||||
data-l10n-id="zotero-collections-search"
|
||||
data-l10n-attrs="placeholder"
|
||||
oncommand="handleCollectionSearchInput()"/>
|
||||
</hbox>
|
||||
<vbox id="zotero-collections-tree-container" class="virtualized-table-container">
|
||||
<html:div id="zotero-collections-tree"/>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</dialog>
|
||||
</window>
|
||||
|
|
@ -1053,3 +1053,16 @@ Zotero.Collection.prototype.canMoveToTargetAsync = async function (target, optio
|
|||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Determine which libraries the collection can be copied to
|
||||
Zotero.Collection.prototype.getLibrariesForCopying = async function () {
|
||||
let libraries = Zotero.Libraries.getAll().filter(lib => !(lib instanceof Zotero.Feed));
|
||||
let librariesWhereOneCanCopy = [];
|
||||
for (let library of libraries) {
|
||||
let canCopy = await this.canMoveToTargetAsync(library);
|
||||
if (canCopy) {
|
||||
librariesWhereOneCanCopy.push(library);
|
||||
}
|
||||
}
|
||||
return librariesWhereOneCanCopy;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -85,6 +85,41 @@ Zotero.Collections = function () {
|
|||
return _getByContainer(null, parentCollectionID, recursive, includeTrashed);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has too many collections in a library to list them all in a menu.
|
||||
* @param {Integer[]} libraryIDs - ids of libraries whose collections will be counted.
|
||||
* If not provided, will check all libraries.
|
||||
* @returns {Boolean} true if the number of collections exceeds the limits set in preferences.
|
||||
*/
|
||||
this.canListCollectionsInMenu = function (libraryIDs) {
|
||||
if (!Array.isArray(libraryIDs)) {
|
||||
libraryIDs = [libraryIDs];
|
||||
}
|
||||
let maxEntriesTotal = Zotero.Prefs.get("collectionsMenu.maxEntriesTotal");
|
||||
let maxEntriesAtOneLevel = Zotero.Prefs.get("collectionsMenu.maxEntriesAtOneLevel");
|
||||
let totalCollectionsCount = 0;
|
||||
for (let libraryID of libraryIDs) {
|
||||
let allCollections = Zotero.Collections.getByLibrary(libraryID, true, false);
|
||||
totalCollectionsCount += allCollections.length;
|
||||
// Too many top-level collections in this library
|
||||
let topLevelCollections = Zotero.Collections.getByLibrary(libraryID);
|
||||
if (topLevelCollections.length > maxEntriesAtOneLevel) {
|
||||
return false;
|
||||
}
|
||||
// Any single collection has too many children
|
||||
for (let collection of allCollections) {
|
||||
if (collection.getChildCollections().length > maxEntriesAtOneLevel) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Too many total collections across all libraries
|
||||
if (totalCollectionsCount > maxEntriesTotal) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var _getByContainer = function (libraryID, parentID, recursive, includeTrashed) {
|
||||
let children = [];
|
||||
|
|
|
|||
|
|
@ -313,33 +313,15 @@ var ZoteroPane = new function () {
|
|||
moveFocus(actionsMap, event, true);
|
||||
});
|
||||
|
||||
let collectionsSearchField = document.getElementById("zotero-collections-search");
|
||||
let clearCollectionSearch = () => {
|
||||
// If empty filter - just focus the collectionTree
|
||||
if (collectionsSearchField.value.length == 0) {
|
||||
return document.getElementById("collection-tree");
|
||||
let result = ZoteroPane.collectionsView.clearCollectionSearch("#zotero-collections-search");
|
||||
if (result === null) {
|
||||
ZoteroPane.hideCollectionSearch();
|
||||
}
|
||||
// Clear the search field and focus collection tree
|
||||
if (collectionsSearchField.value.length) {
|
||||
collectionsSearchField.value = '';
|
||||
ZoteroPane.collectionsView.setFilter("", true);
|
||||
}
|
||||
ZoteroPane.hideCollectionSearch();
|
||||
return null;
|
||||
return result;
|
||||
};
|
||||
let focusCollectionTree = () => {
|
||||
// Prevent Enter/Tab 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;
|
||||
return ZoteroPane.collectionsView.focusCollectionTree("#zotero-collections-search");
|
||||
};
|
||||
collectionTreeToolbar.addEventListener("keydown", (event) => {
|
||||
let actionsMap = {
|
||||
|
|
@ -2664,6 +2646,26 @@ var ZoteroPane = new function () {
|
|||
await selected.saveTx();
|
||||
};
|
||||
|
||||
this.moveCollectionViaDialog = async function () {
|
||||
let selected = this.getSelectedCollection();
|
||||
if (!selected) return;
|
||||
|
||||
let io = {
|
||||
dataIn: null,
|
||||
dataOut: null,
|
||||
filterLibraryIDs: [selected.libraryID],
|
||||
currentCollectionID: selected.id,
|
||||
canSelect: async (target) => {
|
||||
return selected.canMoveToTargetAsync(target);
|
||||
}
|
||||
};
|
||||
window.openDialog('chrome://zotero/content/selectCollectionDialog.xhtml', '',
|
||||
'chrome,modal,centerscreen,resizable=no', io);
|
||||
|
||||
if (!io.dataOut) return;
|
||||
await this.moveCollection(io.dataOut);
|
||||
};
|
||||
|
||||
// Copy selected collection into another collection or library.
|
||||
// Partially, a replication of drag-drop mechanism from CollectionTree.onDrop.
|
||||
this.copyCollection = async (target) => {
|
||||
|
|
@ -2694,6 +2696,28 @@ var ZoteroPane = new function () {
|
|||
});
|
||||
};
|
||||
|
||||
this.copyCollectionViaDialog = async function () {
|
||||
let selected = this.getSelectedCollection();
|
||||
if (!selected) return;
|
||||
|
||||
let librariesWhereOneCanCopy = await selected.getLibrariesForCopying();
|
||||
|
||||
let io = {
|
||||
dataIn: null,
|
||||
dataOut: null,
|
||||
filterLibraryIDs: librariesWhereOneCanCopy.map(lib => lib.libraryID),
|
||||
currentCollectionID: selected.id,
|
||||
canSelect: async (collection) => {
|
||||
return selected.canMoveToTargetAsync(collection);
|
||||
}
|
||||
};
|
||||
window.openDialog('chrome://zotero/content/selectCollectionDialog.xhtml', '',
|
||||
'chrome,modal,centerscreen,resizable=no', io);
|
||||
|
||||
if (!io.dataOut) return;
|
||||
await this.copyCollection(io.dataOut);
|
||||
};
|
||||
|
||||
this.toggleSelectedItemsRead = async function () {
|
||||
await Zotero.FeedItems.toggleReadByID(this.getSelectedItems(true));
|
||||
};
|
||||
|
|
@ -3367,9 +3391,17 @@ var ZoteroPane = new function () {
|
|||
{
|
||||
id: "moveCollection",
|
||||
},
|
||||
{
|
||||
id: "moveCollectionDialog",
|
||||
oncommand: () => this.moveCollectionViaDialog()
|
||||
},
|
||||
{
|
||||
id: "copyCollection"
|
||||
},
|
||||
{
|
||||
id: "copyCollectionDialog",
|
||||
oncommand: () => this.copyCollectionViaDialog()
|
||||
},
|
||||
{
|
||||
id: "duplicate",
|
||||
oncommand: () => this.duplicateSelectedCollection()
|
||||
|
|
@ -3488,8 +3520,6 @@ var ZoteroPane = new function () {
|
|||
'newSubcollection',
|
||||
'sep2',
|
||||
'editSelectedCollection',
|
||||
'moveCollection',
|
||||
'copyCollection',
|
||||
'deleteCollection',
|
||||
'deleteCollectionAndItems',
|
||||
'sep3',
|
||||
|
|
@ -3497,6 +3527,16 @@ var ZoteroPane = new function () {
|
|||
'createBibCollection',
|
||||
'loadReport'
|
||||
];
|
||||
|
||||
// Decide if collections can be listed as menuitems for move/copy menus
|
||||
// or (if there are too many collections), collections will be selected in the dialog
|
||||
let librariesWhereOneCanCopy = await collectionTreeRow.ref.getLibrariesForCopying();
|
||||
if (Zotero.Collections.canListCollectionsInMenu(librariesWhereOneCanCopy.map(lib => lib.libraryID))) {
|
||||
show.push('moveCollection', 'copyCollection');
|
||||
}
|
||||
else {
|
||||
show.push('moveCollectionDialog', 'copyCollectionDialog');
|
||||
}
|
||||
|
||||
if (!this.itemsView.rowCount) {
|
||||
disable = ['createBibCollection', 'loadReport'];
|
||||
|
|
@ -3729,6 +3769,7 @@ var ZoteroPane = new function () {
|
|||
'toggleRead',
|
||||
'changeParentItem',
|
||||
'addToCollection',
|
||||
'addToCollectionDialog',
|
||||
'removeItems',
|
||||
'duplicateAndConvert',
|
||||
'duplicateItem',
|
||||
|
|
@ -4140,8 +4181,15 @@ var ZoteroPane = new function () {
|
|||
&& collectionTreeRow.editable
|
||||
&& Zotero.Items.keepTopLevel(items).every(item => item.isTopLevelItem())
|
||||
) {
|
||||
menu.childNodes[m.addToCollection].setAttribute('label', Zotero.getString('pane.items.menu.addToCollection'));
|
||||
show.add(m.addToCollection);
|
||||
// List collections if possible. If there are too many of them, collection
|
||||
// will be selected in the dialog.
|
||||
if (Zotero.Collections.canListCollectionsInMenu([items[0].libraryID])) {
|
||||
menu.childNodes[m.addToCollection].setAttribute('label', Zotero.getString('pane.items.menu.addToCollection'));
|
||||
show.add(m.addToCollection);
|
||||
}
|
||||
else {
|
||||
show.add(m.addToCollectionDialog);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from collection
|
||||
|
|
@ -4316,6 +4364,20 @@ var ZoteroPane = new function () {
|
|||
|
||||
// Build menus for all libraries (or collections)
|
||||
for (let obj of topLevelEntries) {
|
||||
// For libraries that cannot be copied to, just add a disabled stub
|
||||
if (obj instanceof Zotero.Library) {
|
||||
let canCopy = await selected.canMoveToTargetAsync(obj);
|
||||
if (!canCopy) {
|
||||
let menuItem = document.createXULElement("menu");
|
||||
menuItem.setAttribute("label", obj.name);
|
||||
menuItem.setAttribute("image", obj.treeViewImage);
|
||||
menuItem.setAttribute("value", obj.treeViewID);
|
||||
menuItem.classList.add('menu-iconic');
|
||||
menuItem.disabled = true;
|
||||
popup.append(menuItem);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let menuItem = Zotero.Utilities.Internal.createMenuForTarget(
|
||||
obj,
|
||||
popup,
|
||||
|
|
@ -4330,17 +4392,6 @@ var ZoteroPane = new function () {
|
|||
);
|
||||
popup.append(menuItem);
|
||||
}
|
||||
|
||||
// Disable libraries to which collection cannot be copied
|
||||
if (topLevelEntries[0] instanceof Zotero.Library) {
|
||||
for (let entry of topLevelEntries) {
|
||||
let canCopy = await selected.canMoveToTargetAsync(entry);
|
||||
if (!canCopy) {
|
||||
let menuitem = popup.querySelector(`[value="L${entry.libraryID}"]`);
|
||||
menuitem.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.buildAddItemToCollectionMenu = function (event, items = this.getSelectedItems()) {
|
||||
|
|
@ -4387,6 +4438,33 @@ var ZoteroPane = new function () {
|
|||
separator.hidden = !collections.length;
|
||||
};
|
||||
|
||||
this.addToCollectionViaDialog = async function (event, items = this.getSelectedItems()) {
|
||||
items = Zotero.Items.keepTopLevel(items);
|
||||
if (!items.length) return;
|
||||
|
||||
let libraryID = items[0].libraryID;
|
||||
if (items.some(item => item.libraryID !== libraryID)) return;
|
||||
|
||||
let selectedCollection = ZoteroPane.collectionsView.getSelectedCollection();
|
||||
let io = {
|
||||
dataIn: null,
|
||||
dataOut: null,
|
||||
filterLibraryIDs: [libraryID],
|
||||
currentCollectionID: selectedCollection?.id,
|
||||
canSelect: (collection) => {
|
||||
if (!(collection instanceof Zotero.Collection)) return false;
|
||||
if (selectedCollection && collection.id === selectedCollection.id) return false;
|
||||
if (items.every(item => collection.hasItem(item))) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
window.openDialog('chrome://zotero/content/selectCollectionDialog.xhtml', '',
|
||||
'chrome,modal,centerscreen,resizable=no', io);
|
||||
|
||||
if (!io.dataOut) return;
|
||||
await this.addItemsToCollection(items, io.dataOut);
|
||||
};
|
||||
|
||||
|
||||
this.addItemsToCollection = async function (items, collection, createNew = false) {
|
||||
items = Zotero.Items.keepTopLevel(items);
|
||||
|
|
|
|||
|
|
@ -947,10 +947,12 @@
|
|||
<menupopup id="zotero-move-collection-popup" onpopupshowing="ZoteroPane_Local.buildMoveCollectionMenu(event)">
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuitem class="zotero-menuitem-move-collection" data-l10n-id="collections-menu-move-collection-dialog"/>
|
||||
<menu class="menu-iconic zotero-menuitem-copy-collection">
|
||||
<menupopup id="zotero-copy-collection-popup" onpopupshowing="ZoteroPane_Local.buildCopyCollectionMenu(event)">
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuitem class="zotero-menuitem-copy-collection" data-l10n-id="collections-menu-copy-collection-dialog"/>
|
||||
<menuitem class="zotero-menuitem-duplicate-collection"/>
|
||||
<menuitem class="zotero-menuitem-mark-read-feed"/>
|
||||
<menuitem class="zotero-menuitem-edit-feed" label="&zotero.toolbar.feeds.edit;"/>
|
||||
|
|
@ -1008,6 +1010,7 @@
|
|||
<menu class="menu-iconic zotero-menuitem-add-to-collection">
|
||||
<menupopup id="zotero-add-to-collection-popup" onpopupshowing="ZoteroPane_Local.buildAddItemToCollectionMenu(event)"/>
|
||||
</menu>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-add-to-collection-dialog" data-l10n-id="item-menu-add-to-collection-dialog" oncommand="ZoteroPane_Local.addToCollectionViaDialog();"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-remove-items" oncommand="ZoteroPane_Local.deleteSelectedItems();"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-duplicate-and-convert" oncommand="ZoteroPane_Local.duplicateAndConvertSelectedItem();"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-duplicate-item" label="&zotero.items.menu.duplicateItem;" oncommand="ZoteroPane_Local.duplicateSelectedItem().done();"/>
|
||||
|
|
|
|||
|
|
@ -189,6 +189,10 @@ collections-menu-move-collection =
|
|||
.label = Move To
|
||||
collections-menu-copy-collection =
|
||||
.label = Copy To
|
||||
collections-menu-move-collection-dialog =
|
||||
.label = Move To…
|
||||
collections-menu-copy-collection-dialog =
|
||||
.label = Copy To…
|
||||
|
||||
item-creator-moveDown =
|
||||
.label = Move Down
|
||||
|
|
@ -230,6 +234,8 @@ item-menu-change-parent-item =
|
|||
.label = Change Parent Item…
|
||||
item-menu-relate-items =
|
||||
.label = Relate Items
|
||||
item-menu-add-to-collection-dialog =
|
||||
.label = Add to Collection…
|
||||
|
||||
view-online = View Online
|
||||
item-menu-option-view-online =
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ pref("extensions.zotero.hideContextAnnotationRows", true);
|
|||
pref("extensions.zotero.fontSize", "1.00");
|
||||
pref("extensions.zotero.layout", "standard");
|
||||
pref("extensions.zotero.recursiveCollections", false);
|
||||
pref("extensions.zotero.collectionsMenu.maxEntriesTotal", 5000);
|
||||
pref("extensions.zotero.collectionsMenu.maxEntriesAtOneLevel", 100);
|
||||
pref("extensions.zotero.autoRecognizeFiles", true);
|
||||
pref("extensions.zotero.autoRenameFiles", true);
|
||||
pref("extensions.zotero.autoRenameFiles.linked", false);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
@import "components/search";
|
||||
@import "components/select";
|
||||
@import "components/selectItemsDialog";
|
||||
@import "components/selectCollectionDialog";
|
||||
@import "components/spinner";
|
||||
@import "components/syncButtonTooltip";
|
||||
@import "components/tabBar";
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ $menu-icons: (
|
|||
rename-from-parent: "rename",
|
||||
create-note-from-annotations: "light-dark:note-annotation",
|
||||
add-to-collection: "new-collection",
|
||||
add-to-collection-dialog: "new-collection",
|
||||
new-tab: "new-tab",
|
||||
new-window: "new-window",
|
||||
pin: "pin",
|
||||
|
|
|
|||
|
|
@ -6,4 +6,24 @@
|
|||
menulist::part(icon) {
|
||||
margin-inline-end: 3px;
|
||||
}
|
||||
|
||||
[hidden="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#zotero-collections-tree-container {
|
||||
min-width: 300px;
|
||||
min-height: 300px;
|
||||
max-height: 400px;
|
||||
margin-top: 8px;
|
||||
|
||||
.virtualized-table-body {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
#zotero-collections-tree {
|
||||
border-radius: 5px;
|
||||
background: var(--material-sidepane);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
scss/components/_selectCollectionDialog.scss
Normal file
48
scss/components/_selectCollectionDialog.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#zotero-select-collection-dialog {
|
||||
display: flex;
|
||||
min-width: 400px;
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
#select-collection-dialog {
|
||||
appearance: none;
|
||||
background: var(--material-background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-moz-window-dragging: drag;
|
||||
height: 100%;
|
||||
|
||||
#zotero-select-collection-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
-moz-window-dragging: no-drag;
|
||||
margin: -15px -15px 0 -15px;
|
||||
|
||||
#search-toolbar {
|
||||
margin: 10px;
|
||||
@media (-moz-platform: windows) {
|
||||
margin: 20px 15px 10px 15px;
|
||||
}
|
||||
#zotero-collections-search {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#zotero-collections-tree-container {
|
||||
min-width: 400px;
|
||||
flex: 1;
|
||||
background: var(--material-sidepane);
|
||||
|
||||
.virtualized-table-body {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
#zotero-collections-tree {
|
||||
background: var(--material-sidepane);
|
||||
}
|
||||
}
|
||||
|
|
@ -1110,6 +1110,76 @@ describe("ZoteroPane", function () {
|
|||
var restoreMenuItem = menu.querySelector('.zotero-menuitem-restore-to-library');
|
||||
assert.isTrue(restoreMenuItem.disabled);
|
||||
});
|
||||
|
||||
it("should list collections when not too many", async function () {
|
||||
Zotero.Prefs.set('collectionsMenu.maxEntriesTotal', 1000);
|
||||
|
||||
let collection = await createDataObject('collection');
|
||||
let item = await createDataObject('item');
|
||||
|
||||
await zp.selectItems([item.id]);
|
||||
await zp.buildItemContextMenu();
|
||||
|
||||
let menu = win.document.getElementById('zotero-itemmenu');
|
||||
menu.openPopup();
|
||||
// The menu (not menuitem) variant should be shown
|
||||
let addToCollectionMenu = menu.querySelector('.zotero-menuitem-add-to-collection');
|
||||
assert.isFalse(addToCollectionMenu.hidden);
|
||||
// The dialog variant should be hidden
|
||||
let addToCollectionDialog = menu.querySelector('.zotero-menuitem-add-to-collection-dialog');
|
||||
assert.isTrue(addToCollectionDialog.hidden);
|
||||
|
||||
// Open the menupopup so it gets populated
|
||||
let popup = addToCollectionMenu.querySelector('menupopup');
|
||||
popup.openPopup();
|
||||
|
||||
// Find the menuitem for our collection and click it
|
||||
let collectionMenuItem = popup.querySelector(`[value="C${collection.id}"]`);
|
||||
assert.ok(collectionMenuItem);
|
||||
collectionMenuItem.doCommand();
|
||||
popup.hidePopup();
|
||||
await waitForNotifierEvent("modify", "item");
|
||||
|
||||
// Verify item was added to the collection
|
||||
assert.isTrue(collection.hasItem(item.id));
|
||||
});
|
||||
|
||||
it("should not list collections if too many", async function () {
|
||||
let collection = await createDataObject('collection');
|
||||
let item = await createDataObject('item');
|
||||
|
||||
Zotero.Prefs.set('collectionsMenu.maxEntriesTotal', 0);
|
||||
|
||||
await zp.selectItems([item.id]);
|
||||
await zp.buildItemContextMenu();
|
||||
|
||||
let menu = win.document.getElementById('zotero-itemmenu');
|
||||
// The menu variant should be hidden
|
||||
let addToCollectionMenu = menu.querySelector('.zotero-menuitem-add-to-collection');
|
||||
assert.isTrue(addToCollectionMenu.hidden);
|
||||
// The dialog menuitem should be shown
|
||||
let addToCollectionDialog = menu.querySelector('.zotero-menuitem-add-to-collection-dialog');
|
||||
assert.isFalse(addToCollectionDialog.hidden);
|
||||
|
||||
// Set up listener for the dialog before triggering it
|
||||
waitForWindow('chrome://zotero/content/selectCollectionDialog.xhtml', async (selectWin) => {
|
||||
// Wait for the dialog to finish loading
|
||||
do {
|
||||
await Zotero.Promise.delay(50);
|
||||
}
|
||||
while (!selectWin.loaded);
|
||||
await selectWin.collectionsView.makeVisible();
|
||||
// Select the target collection
|
||||
await selectWin.collectionsView.selectByID('C' + collection.id);
|
||||
selectWin.document.querySelector('dialog').acceptDialog();
|
||||
});
|
||||
await zp.addToCollectionViaDialog();
|
||||
|
||||
// Verify item was added to the collection
|
||||
assert.isTrue(collection.hasItem(item.id));
|
||||
|
||||
Zotero.Prefs.set('collectionsMenu.maxEntriesTotal', 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#restoreSelectedItems()", function () {
|
||||
|
|
@ -1913,6 +1983,81 @@ describe("ZoteroPane", function () {
|
|||
assert.equal(anotherNewCollection.libraryID, groupCollection.libraryID);
|
||||
});
|
||||
});
|
||||
describe("#buildCollectionContextMenu()", function () {
|
||||
it("should list collections for copy when not too many", async function () {
|
||||
Zotero.Prefs.set('collectionsMenu.maxEntriesTotal', 1000);
|
||||
|
||||
let collectionA = await createDataObject('collection');
|
||||
let collectionB = await createDataObject('collection');
|
||||
|
||||
await select(win, collectionA);
|
||||
await zp.buildCollectionContextMenu();
|
||||
|
||||
let menu = doc.getElementById('zotero-collectionmenu');
|
||||
menu.openPopup();
|
||||
|
||||
// The menu (not menuitem) variant for copy should be shown
|
||||
let copyCollectionMenu = menu.querySelector('#copyCollection');
|
||||
assert.isFalse(copyCollectionMenu.hidden);
|
||||
// The dialog variant should be hidden
|
||||
let copyCollectionDialog = menu.querySelector('#copyCollectionDialog');
|
||||
assert.isTrue(copyCollectionDialog.hidden);
|
||||
|
||||
// Open the menupopup so it gets populated
|
||||
let popup = doc.getElementById('zotero-copy-collection-popup');
|
||||
popup.openPopup();
|
||||
|
||||
// Find the menuitem for collection B and click it
|
||||
let collectionMenuItem = popup.querySelector(`[value="C${collectionB.id}"]`);
|
||||
assert.ok(collectionMenuItem);
|
||||
collectionMenuItem.doCommand();
|
||||
popup.hidePopup();
|
||||
await waitForNotifierEvent("add", "collection");
|
||||
|
||||
// Verify collection A was copied into collection B
|
||||
let childNames = collectionB.getDescendents(false, 'collection').map(col => col.name);
|
||||
assert.include(childNames, collectionA.name);
|
||||
});
|
||||
|
||||
it("should not list collections for copy if too many", async function () {
|
||||
let collectionA = await createDataObject('collection');
|
||||
let collectionB = await createDataObject('collection');
|
||||
|
||||
Zotero.Prefs.set('collectionsMenu.maxEntriesTotal', 0);
|
||||
|
||||
await select(win, collectionA);
|
||||
await zp.buildCollectionContextMenu();
|
||||
|
||||
let menu = doc.getElementById('zotero-collectionmenu');
|
||||
|
||||
// The menu variant for copy should be hidden
|
||||
let copyCollectionMenu = menu.querySelector('#copyCollection');
|
||||
assert.isTrue(copyCollectionMenu.hidden);
|
||||
// The dialog menuitem should be shown
|
||||
let copyCollectionDialog = menu.querySelector('#copyCollectionDialog');
|
||||
assert.isFalse(copyCollectionDialog.hidden);
|
||||
|
||||
// Set up listener for the dialog before triggering it
|
||||
waitForWindow('chrome://zotero/content/selectCollectionDialog.xhtml', async (selectWin) => {
|
||||
do {
|
||||
await Zotero.Promise.delay(50);
|
||||
}
|
||||
while (!selectWin.loaded);
|
||||
await selectWin.collectionsView.makeVisible();
|
||||
// Select the target collection
|
||||
await selectWin.collectionsView.selectByID('C' + collectionB.id);
|
||||
selectWin.document.querySelector('dialog').acceptDialog();
|
||||
});
|
||||
await zp.copyCollectionViaDialog();
|
||||
await waitForNotifierEvent("add", "collection");
|
||||
|
||||
// Verify collection A was copied into collection B
|
||||
let childNames = collectionB.getDescendents(false, 'collection').map(col => col.name);
|
||||
assert.include(childNames, collectionA.name);
|
||||
|
||||
Zotero.Prefs.set('collectionsMenu.maxEntriesTotal', 1000);
|
||||
});
|
||||
});
|
||||
describe("#moveCollection", function () {
|
||||
it("should move collection into another collection of the same library", async function () {
|
||||
let collection = await createDataObject('collection');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue