zotero/chrome/content/zotero/xpcom/itemTreeView.js
Dan Stillman 1a49018bdc Fix moving items between collections
`mozSourceNode` seems to no longer be set in `dataTransfer` objects
during drags, so we now store it in `Zotero.DragDrop`.
2017-02-03 00:07:16 -05:00

3255 lines
90 KiB
JavaScript

/*
***** 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 *****
*/
////////////////////////////////////////////////////////////////////////////////
///
/// ItemTreeView
/// -- handles the link between an individual tree and the data layer
/// -- displays only items (no collections, no hierarchy)
///
////////////////////////////////////////////////////////////////////////////////
/*
* Constructor for the ItemTreeView object
*/
Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) {
Zotero.LibraryTreeView.apply(this);
this.wrappedJSObject = this;
this.rowCount = 0;
this.collectionTreeRow = collectionTreeRow;
collectionTreeRow.itemTreeView = this;
this._skipKeypress = false;
this._sourcesOnly = sourcesOnly;
this._ownerDocument = null;
this._needsSort = false;
this._cellTextCache = {};
this._itemImages = {};
this._refreshPromise = Zotero.Promise.resolve();
this._unregisterID = Zotero.Notifier.registerObserver(
this,
['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search'],
'itemTreeView',
50
);
}
Zotero.ItemTreeView.prototype = Object.create(Zotero.LibraryTreeView.prototype);
Zotero.ItemTreeView.prototype.type = 'item';
/**
* Called by the tree itself
*/
Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (treebox) {
try {
Zotero.debug("Setting tree for " + this.collectionTreeRow.id + " items view " + this.id);
var start = Date.now();
// Try to set the window document if not yet set
if (treebox && !this._ownerDocument) {
try {
this._ownerDocument = treebox.treeBody.ownerDocument;
}
catch (e) {}
}
if (this._treebox) {
if (this._needsSort) {
this.sort();
}
return;
}
if (!treebox) {
Zotero.debug("Treebox not passed in setTree()", 2);
return;
}
if (!this._ownerDocument) {
Zotero.debug("No owner document in setTree()", 2);
return;
}
this._treebox = treebox;
this.setSortColumn();
if (this._ownerDocument.defaultView.ZoteroPane_Local) {
this._ownerDocument.defaultView.ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
}
if (Zotero.locked) {
Zotero.debug("Zotero is locked -- not loading items tree", 2);
if (this._ownerDocument.defaultView.ZoteroPane_Local) {
this._ownerDocument.defaultView.ZoteroPane_Local.clearItemsPaneMessage();
}
return;
}
yield this.refresh();
if (!this._treebox.treeBody) {
return;
}
// Add a keypress listener for expand/collapse
var tree = this._treebox.treeBody.parentNode;
var self = this;
var coloredTagsRE = new RegExp("^[1-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1}$");
var listener = function(event) {
if (self._skipKeyPress) {
self._skipKeyPress = false;
return;
}
// Handle arrow keys specially on multiple selection, since
// otherwise the tree just applies it to the last-selected row
if (event.keyCode == 39 || event.keyCode == 37) {
if (self._treebox.view.selection.count > 1) {
switch (event.keyCode) {
case 39:
self.expandSelectedRows();
break;
case 37:
self.collapseSelectedRows();
break;
}
event.preventDefault();
}
return;
}
var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
self.expandAllRows();
event.preventDefault();
return;
}
else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
self.collapseAllRows();
event.preventDefault();
return;
}
// Ignore other non-character keypresses
if (!event.charCode || event.shiftKey || event.ctrlKey ||
event.altKey || event.metaKey) {
return;
}
event.preventDefault();
Zotero.spawn(function* () {
if (coloredTagsRE.test(key)) {
let libraryID = self.collectionTreeRow.ref.libraryID;
let position = parseInt(key) - 1;
let colorData = Zotero.Tags.getColorByPosition(libraryID, position);
// If a color isn't assigned to this number or any
// other numbers, allow key navigation
if (!colorData) {
return !Zotero.Tags.getColors(libraryID).size;
}
var items = self.getSelectedItems();
yield Zotero.Tags.toggleItemsListTags(libraryID, items, colorData.name);
return;
}
// We have to disable key navigation on the tree in order to
// keep it from acting on the 1-9 keys used for colored tags.
// To allow navigation with other keys, we temporarily enable
// key navigation and recreate the keyboard event. Since
// that will trigger this listener again, we set a flag to
// ignore the event, and then clear the flag above when the
// event comes in. I see no way this could go wrong...
tree.disableKeyNavigation = false;
self._skipKeyPress = true;
var nsIDWU = Components.interfaces.nsIDOMWindowUtils;
var domWindowUtils = event.originalTarget.ownerDocument.defaultView
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(nsIDWU);
var modifiers = 0;
if (event.altKey) {
modifiers |= nsIDWU.MODIFIER_ALT;
}
if (event.ctrlKey) {
modifiers |= nsIDWU.MODIFIER_CONTROL;
}
if (event.shiftKey) {
modifiers |= nsIDWU.MODIFIER_SHIFT;
}
if (event.metaKey) {
modifiers |= nsIDWU.MODIFIER_META;
}
domWindowUtils.sendKeyEvent(
'keypress',
event.keyCode,
event.charCode,
modifiers
);
tree.disableKeyNavigation = true;
})
.catch(function (e) {
Zotero.logError(e);
})
}.bind(this);
// Store listener so we can call removeEventListener() in ItemTreeView.unregister()
this.listener = listener;
tree.addEventListener('keypress', listener);
// This seems to be the only way to prevent Enter/Return
// from toggle row open/close. The event is handled by
// handleKeyPress() in zoteroPane.js.
tree._handleEnter = function () {};
this.sort();
this.expandMatchParents();
if (this._ownerDocument.defaultView.ZoteroPane_Local) {
// For My Publications, show intro text in middle pane if no items
if (this.collectionTreeRow && this.collectionTreeRow.isPublications() && !this.rowCount) {
let doc = this._ownerDocument;
let ns = 'http://www.w3.org/1999/xhtml'
let div = doc.createElementNS(ns, 'div');
let p = doc.createElementNS(ns, 'p');
p.textContent = Zotero.getString('publications.intro.text1', ZOTERO_CONFIG.DOMAIN_NAME);
div.appendChild(p);
p = doc.createElementNS(ns, 'p');
p.textContent = Zotero.getString('publications.intro.text2');
div.appendChild(p);
p = doc.createElementNS(ns, 'p');
let html = Zotero.getString('publications.intro.text3');
// Convert <b> tags to placeholders
html = html.replace('<b>', ':b:').replace('</b>', ':/b:');
// Encode any other special chars, which shouldn't exist
html = Zotero.Utilities.htmlSpecialChars(html);
// Restore bold text
html = html.replace(':b:', '<strong>').replace(':/b:', '</strong>');
p.innerHTML = html; // AMO note: markup from hard-coded strings and filtered above
div.appendChild(p);
content = div;
doc.defaultView.ZoteroPane_Local.setItemsPaneMessage(content);
}
else {
this._ownerDocument.defaultView.ZoteroPane_Local.clearItemsPaneMessage();
}
}
if (this.collectionTreeRow && this.collectionTreeRow.itemToSelect) {
var item = this.collectionTreeRow.itemToSelect;
yield this.selectItem(item['id'], item['expand']);
this.collectionTreeRow.itemToSelect = null;
}
Zotero.debug("Set tree for items view " + this.id + " in " + (Date.now() - start) + " ms");
this._initialized = true;
yield this._runListeners('load');
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
if (this.onError) {
this.onError(e);
}
throw e;
}
});
Zotero.ItemTreeView.prototype.setSortColumn = function() {
var dir, col, currentCol, currentDir;
for (let i=0, len=this._treebox.columns.count; i<len; i++) {
let column = this._treebox.columns.getColumnAt(i);
if (column.element.getAttribute('sortActive')) {
currentCol = column;
currentDir = column.element.getAttribute('sortDirection');
column.element.removeAttribute('sortActive');
column.element.removeAttribute('sortDirection');
break;
}
}
let colID = Zotero.Prefs.get('itemTree.sortColumnID');
// Restore previous sort setting (feed -> non-feed)
if (! this.collectionTreeRow.isFeed() && colID) {
col = this._treebox.columns.getNamedColumn(colID);
dir = Zotero.Prefs.get('itemTree.sortDirection');
Zotero.Prefs.clear('itemTree.sortColumnID');
Zotero.Prefs.clear('itemTree.sortDirection');
// No previous sort setting stored, so store it (non-feed -> feed)
} else if (this.collectionTreeRow.isFeed() && !colID && currentCol) {
Zotero.Prefs.set('itemTree.sortColumnID', currentCol.id);
Zotero.Prefs.set('itemTree.sortDirection', currentDir);
// Retain current sort setting (non-feed -> non-feed)
} else {
col = currentCol;
dir = currentDir;
}
if (col) {
col.element.setAttribute('sortActive', true);
col.element.setAttribute('sortDirection', dir);
}
}
/**
* Reload the rows from the data access methods
* (doesn't call the tree.invalidate methods, etc.)
*/
Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(function* () {
Zotero.debug('Refreshing items list for ' + this.id);
// DEBUG: necessary?
try {
this._treebox.columns.count
}
// If treebox isn't ready, skip refresh
catch (e) {
return false;
}
var resolve, reject;
this._refreshPromise = new Zotero.Promise(function () {
resolve = arguments[0];
reject = arguments[1];
});
try {
Zotero.CollectionTreeCache.clear();
var newItems = yield this.collectionTreeRow.getItems();
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
}
var savedSelection = this.getSelectedItems(true);
var savedOpenState = this._saveOpenState();
var oldCount = this.rowCount;
var newSearchItemIDs = {};
var newSearchParentIDs = {};
var newCellTextCache = {};
var newSearchMode = this.collectionTreeRow.isSearchMode();
var newRows = [];
var added = 0;
for (let i=0, len=newItems.length; i < len; i++) {
let item = newItems[i];
// Only add regular items if sourcesOnly is set
if (this._sourcesOnly && !item.isRegularItem()) {
continue;
}
// Don't add child items directly (instead mark their parents for
// inclusion below)
let parentItemID = item.parentItemID;
if (parentItemID) {
newSearchParentIDs[parentItemID] = true;
}
// Add top-level items
else {
this._addRowToArray(
newRows,
new Zotero.ItemTreeRow(item, 0, false),
added++
);
}
newSearchItemIDs[item.id] = true;
}
// Add parents of matches if not matches themselves
for (let id in newSearchParentIDs) {
if (!newSearchItemIDs[id]) {
let item = Zotero.Items.get(id);
this._addRowToArray(
newRows,
new Zotero.ItemTreeRow(item, 0, false),
added++
);
}
}
this._rows = newRows;
this.rowCount = this._rows.length;
var diff = this.rowCount - oldCount;
if (diff != 0) {
this._treebox.rowCountChanged(0, diff);
}
this._refreshItemRowMap();
this._searchMode = newSearchMode;
this._searchItemIDs = newSearchItemIDs; // items matching the search
this._searchParentIDs = newSearchParentIDs;
this._cellTextCache = {};
this.rememberOpenState(savedOpenState);
this.rememberSelection(savedSelection);
this.expandMatchParents();
if (unsuppress) {
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
setTimeout(function () {
resolve();
});
}
catch (e) {
setTimeout(function () {
reject(e);
});
throw e;
}
}));
/*
* Called by Zotero.Notifier on any changes to items in the data layer
*/
Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData)
{
Zotero.debug("Yielding for refresh promise"); // TEMP
yield this._refreshPromise;
if (!this._treebox || !this._treebox.treeBody) {
Zotero.debug("Treebox didn't exist in itemTreeView.notify()");
return;
}
if (!this._rowMap) {
Zotero.debug("Item row map didn't exist in itemTreeView.notify()");
return;
}
if (type == 'search' && action == 'modify') {
// TODO: Only refresh on condition change (not currently available in extraData)
yield this.refresh();
this.sort();
this._treebox.invalidate();
return;
}
// Clear item type icon and tag colors when a tag is added to or removed from an item
if (type == 'item-tag') {
// TODO: Only update if colored tag changed?
ids.map(val => val.split("-")[0]).forEach(function (val) {
delete this._itemImages[val];
}.bind(this));
return;
}
var collectionTreeRow = this.collectionTreeRow;
if (collectionTreeRow.isFeed() && action == 'modify') {
for (let i=0; i<ids.length; i++) {
this._treebox.invalidateRow(this._rowMap[ids[i]]);
}
}
var madeChanges = false;
var refreshed = false;
var sort = false;
var savedSelection = this.getSelectedItems(true);
var previousFirstSelectedRow = this._rowMap[
// 'collection-item' ids are in the form <collectionID>-<itemID>
// 'item' events are just integers
type == 'collection-item' ? ids[0].split('-')[1] : ids[0]
];
// If there's not at least one new item to be selected, get a scroll position to restore later
var scrollPosition = false;
if (action != 'add' || ids.every(id => extraData[id] && extraData[id].skipSelect)) {
scrollPosition = this._saveScrollPosition();
}
// Redraw the tree (for tag color and progress changes)
if (action == 'redraw') {
// Redraw specific rows
if (type == 'item' && ids.length) {
// Redraw specific cells
if (extraData && extraData.column) {
var col = this._treebox.columns.getNamedColumn(
'zotero-items-column-' + extraData.column
);
for (let id of ids) {
if (extraData.column == 'title') {
delete this._itemImages[id];
}
this._treebox.invalidateCell(this._rowMap[id], col);
}
}
else {
for (let id of ids) {
delete this._itemImages[id];
this._treebox.invalidateRow(this._rowMap[id]);
}
}
}
// Redraw the whole tree
else {
this._itemImages = {};
this._treebox.invalidate();
}
return;
}
if (action == 'refresh') {
if (type == 'share-items') {
if (collectionTreeRow.isShare()) {
yield this.refresh();
refreshed = true;
}
}
else if (type == 'bucket') {
if (collectionTreeRow.isBucket()) {
yield this.refresh();
refreshed = true;
}
}
else if (type == 'publications') {
if (collectionTreeRow.isPublications()) {
yield this.refresh();
refreshed = true;
}
}
// If refreshing a single item, clear caches and then unselect and reselect row
else if (savedSelection.length == 1 && savedSelection[0] == ids[0]) {
let row = this._rowMap[ids[0]];
delete this._cellTextCache[row];
this.selection.clearSelection();
this.rememberSelection(savedSelection);
}
else {
this._cellTextCache = {};
}
return;
}
if (collectionTreeRow.isShare()) {
return;
}
// See if we're in the active window
var zp = Zotero.getActiveZoteroPane();
var activeWindow = zp && zp.itemsView == this;
var quicksearch = this._ownerDocument.getElementById('zotero-tb-search');
// 'collection-item' ids are in the form collectionID-itemID
if (type == 'collection-item') {
if (!collectionTreeRow.isCollection()) {
return;
}
var splitIDs = [];
for (let id of ids) {
var split = id.split('-');
// Skip if not an item in this collection
if (split[0] != collectionTreeRow.ref.id) {
continue;
}
splitIDs.push(split[1]);
}
ids = splitIDs;
// Select the last item even if there are no changes (e.g. if the tag
// selector is open and already refreshed the pane)
/*if (splitIDs.length > 0 && (action == 'add' || action == 'modify')) {
var selectItem = splitIDs[splitIDs.length - 1];
}*/
}
this.selection.selectEventsSuppressed = true;
//this._treebox.beginUpdateBatch();
if ((action == 'remove' && !collectionTreeRow.isLibrary(true))
|| action == 'delete' || action == 'trash'
|| (action == 'removeDuplicatesMaster' && collectionTreeRow.isDuplicates())) {
// Since a remove involves shifting of rows, we have to do it in order,
// so sort the ids by row
var rows = [];
let push = action == 'delete' || action == 'trash' || action == 'removeDuplicatesMaster';
for (var i=0, len=ids.length; i<len; i++) {
if (!push) {
push = !collectionTreeRow.ref.hasItem(ids[i]);
}
// Row might already be gone (e.g. if this is a child and
// 'modify' was sent to parent)
let row = this._rowMap[ids[i]];
if (push && row !== undefined) {
// Don't remove child items from collections, because it's handled by 'modify'
if (action == 'remove' && this.getParentIndex(row) != -1) {
continue;
}
rows.push(row);
// Remove child items of removed parents
if (this.isContainer(row) && this.isContainerOpen(row)) {
while (++row < this.rowCount && this.getLevel(row) > 0) {
rows.push(row);
}
}
}
}
if (rows.length > 0) {
// Child items might have been added more than once
rows = Zotero.Utilities.arrayUnique(rows);
rows.sort(function(a,b) { return a-b });
for (let i = rows.length - 1; i >= 0; i--) {
this._removeRow(rows[i]);
}
madeChanges = true;
sort = true;
}
}
else if (type == 'item' && action == 'modify')
{
// Clear row caches
var items = Zotero.Items.get(ids);
for (let i=0; i<items.length; i++) {
let id = items[i].id;
delete this._itemImages[id];
delete this._cellTextCache[id];
}
// If trash or saved search, just re-run search
if (collectionTreeRow.isTrash() || collectionTreeRow.isSearch())
{
yield this.refresh();
refreshed = true;
madeChanges = true;
sort = true;
}
else if (collectionTreeRow.isFeed()) {
this._ownerDocument.defaultView.ZoteroItemPane.setToggleReadLabel();
}
// If no quicksearch, process modifications manually
else if (!quicksearch || quicksearch.value == '')
{
var items = Zotero.Items.get(ids);
for (let i = 0; i < items.length; i++) {
let item = items[i];
let id = item.id;
let row = this._rowMap[id];
// Deleted items get a modify that we have to ignore when
// not viewing the trash
if (item.deleted) {
continue;
}
// Item already exists in this view
if (row !== undefined) {
let parentItemID = this.getRow(row).ref.parentItemID;
let parentIndex = this.getParentIndex(row);
// Top-level item
if (this.isContainer(row)) {
// If Unfiled Items and itm was added to a collection, remove from view
if (collectionTreeRow.isUnfiled() && item.getCollections().length) {
this._removeRow(row);
}
// Otherwise just resort
else {
sort = id;
}
}
// If item moved from top-level to under another item, remove the old row.
else if (parentIndex == -1 && parentItemID) {
this._removeRow(row);
}
// If moved from under another item to top level, remove old row and add new one
else if (parentIndex != -1 && !parentItemID) {
this._removeRow(row);
let beforeRow = this.rowCount;
this._addRow(new Zotero.ItemTreeRow(item, 0, false), beforeRow);
sort = id;
}
// If item was moved from one parent to another, remove from old parent
else if (parentItemID && parentIndex != -1 && this._rowMap[parentItemID] != parentIndex) {
this._removeRow(row);
}
// If not moved from under one item to another, just resort the row,
// which also invalidates it and refreshes it
else {
sort = id;
}
madeChanges = true;
}
// Otherwise, for a top-level item in a library root or a collection
// containing the item, the item has to be added
else if (item.isTopLevelItem()) {
// Root view
let add = collectionTreeRow.isLibrary(true)
&& collectionTreeRow.ref.libraryID == item.libraryID;
// Collection containing item
if (!add && collectionTreeRow.isCollection()) {
add = item.inCollection(collectionTreeRow.ref.id);
}
if (add) {
//most likely, the note or attachment's parent was removed.
let beforeRow = this.rowCount;
this._addRow(new Zotero.ItemTreeRow(item, 0, false), beforeRow);
madeChanges = true;
sort = id;
}
}
}
if (sort && ids.length != 1) {
sort = true;
}
}
// If quicksearch, re-run it, since the results may have changed
else
{
var allDeleted = true;
var isTrash = collectionTreeRow.isTrash();
var items = Zotero.Items.get(ids);
for (let item of items) {
// If not viewing trash and all items were deleted, ignore modify
if (allDeleted && !isTrash && !item.deleted) {
allDeleted = false;
}
}
if (!allDeleted) {
quicksearch.doCommand();
madeChanges = true;
sort = true;
}
}
}
else if(type == 'item' && action == 'add')
{
let items = Zotero.Items.get(ids);
// In some modes, just re-run search
if (collectionTreeRow.isSearch() || collectionTreeRow.isTrash() || collectionTreeRow.isUnfiled()) {
yield this.refresh();
refreshed = true;
madeChanges = true;
sort = true;
}
// If not a quicksearch, process new items manually
else if (!quicksearch || quicksearch.value == '')
{
for (let i=0; i<items.length; i++) {
let item = items[i];
// if the item belongs in this collection
if (((collectionTreeRow.isLibrary(true)
&& collectionTreeRow.ref.libraryID == item.libraryID)
|| (collectionTreeRow.isCollection() && item.inCollection(collectionTreeRow.ref.id)))
// if we haven't already added it to our hash map
&& this._rowMap[item.id] == null
// Regular item or standalone note/attachment
&& item.isTopLevelItem()) {
let beforeRow = this.rowCount;
this._addRow(new Zotero.ItemTreeRow(item, 0, false), beforeRow);
madeChanges = true;
}
}
if (madeChanges) {
sort = (items.length == 1) ? items[0].id : true;
}
}
// Otherwise re-run the quick search, which refreshes the item list
else
{
// For item adds, clear the quicksearch, unless all the new items have skipSelect or are
// child items
if (activeWindow && type == 'item') {
let clear = false;
for (let i=0; i<items.length; i++) {
if (!extraData[items[i].id].skipSelect && items[i].isTopLevelItem()) {
clear = true;
break;
}
}
if (clear) {
quicksearch.value = '';
}
}
quicksearch.doCommand();
madeChanges = true;
sort = true;
}
}
if(madeChanges)
{
// If we made individual changes, we have to clear the cache
if (!refreshed) {
Zotero.CollectionTreeCache.clear();
}
var singleSelect = false;
// If adding a single top-level item and this is the active window, select it
if (action == 'add' && activeWindow) {
if (ids.length == 1) {
singleSelect = ids[0];
}
// If there's only one parent item in the set of added items,
// mark that for selection in the UI
//
// Only bother checking for single parent item if 1-5 total items,
// since a translator is unlikely to save more than 4 child items
else if (ids.length <= 5) {
var items = Zotero.Items.get(ids);
if (items) {
var found = false;
for (let item of items) {
// Check for note and attachment type, since it's quicker
// than checking for parent item
if (item.itemTypeID == 1 || item.itemTypeID == 14) {
continue;
}
// We already found a top-level item, so cancel the
// single selection
if (found) {
singleSelect = false;
break;
}
found = true;
singleSelect = item.id;
}
}
}
}
if (sort) {
this.sort(typeof sort == 'number' ? sort : false);
}
else {
this._refreshItemRowMap();
}
if (singleSelect) {
if (!extraData[singleSelect] || !extraData[singleSelect].skipSelect) {
// Reset to Info tab
this._ownerDocument.getElementById('zotero-view-tabbox').selectedIndex = 0;
yield this.selectItem(singleSelect);
}
}
// If single item is selected and was modified
else if (action == 'modify' && ids.length == 1 &&
savedSelection.length == 1 && savedSelection[0] == ids[0]) {
// If the item no longer matches the search term, clear the search
// DEBUG: Still needed/wanted? (and search is async, so doesn't work anyway,
// here or above)
if (quicksearch && this._rowMap[ids[0]] == undefined) {
Zotero.debug('Selected item no longer matches quicksearch -- clearing');
quicksearch.value = '';
quicksearch.doCommand();
}
if (activeWindow) {
yield this.selectItem(ids[0]);
}
else {
this.rememberSelection(savedSelection);
}
}
// On removal of a row, select item at previous position
else if (savedSelection.length) {
if (action == 'remove' || action == 'trash' || action == 'delete') {
// In duplicates view, select the next set on delete
if (collectionTreeRow.isDuplicates()) {
if (this._rows[previousFirstSelectedRow]) {
// Mirror ZoteroPane.onTreeMouseDown behavior
var itemID = this._rows[previousFirstSelectedRow].ref.id;
var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID);
this.selectItems(setItemIDs);
}
}
else {
// If this was a child item and the next item at this
// position is a top-level item, move selection one row
// up to select a sibling or parent
if (ids.length == 1 && previousFirstSelectedRow > 0) {
let previousItem = Zotero.Items.get(ids[0]);
if (previousItem && !previousItem.isTopLevelItem()) {
if (this._rows[previousFirstSelectedRow]
&& this.getLevel(previousFirstSelectedRow) == 0) {
previousFirstSelectedRow--;
}
}
}
if (previousFirstSelectedRow !== undefined && this._rows[previousFirstSelectedRow]) {
this.selection.select(previousFirstSelectedRow);
}
// If no item at previous position, select last item in list
else if (this._rows[this._rows.length - 1]) {
this.selection.select(this._rows.length - 1);
}
}
}
else {
this.rememberSelection(savedSelection);
}
}
this._rememberScrollPosition(scrollPosition);
this._treebox.invalidate();
}
// For special case in which an item needs to be selected without changes
// necessarily having been made
// ('collection-item' add with tag selector open)
/*else if (selectItem) {
yield this.selectItem(selectItem);
}*/
//this._treebox.endUpdateBatch();
if (madeChanges) {
var deferred = Zotero.Promise.defer();
this.addEventListener('select', () => deferred.resolve());
}
this.selection.selectEventsSuppressed = false;
if (madeChanges) {
Zotero.debug("Yielding for select promise"); // TEMP
return deferred.promise;
}
});
/*
* Unregisters view from Zotero.Notifier (called on window close)
*/
Zotero.ItemTreeView.prototype.unregister = function()
{
Zotero.Notifier.unregisterObserver(this._unregisterID);
if (this.listener) {
if (!this._treebox.treeBody) {
Zotero.debug("No more tree body in Zotero.ItemTreeView::unregister()");
this.listener = null;
return;
}
let tree = this._treebox.treeBody.parentNode;
tree.removeEventListener('keypress', this.listener, false);
this.listener = null;
}
}
////////////////////////////////////////////////////////////////////////////////
///
/// nsITreeView functions
///
////////////////////////////////////////////////////////////////////////////////
Zotero.ItemTreeView.prototype.getCellText = function (row, column)
{
var obj = this.getRow(row);
var itemID = obj.id;
// If value is available, retrieve synchronously
if (this._cellTextCache[itemID] && this._cellTextCache[itemID][column.id] !== undefined) {
return this._cellTextCache[itemID][column.id];
}
if (!this._cellTextCache[itemID]) {
this._cellTextCache[itemID] = {}
}
var val;
// Image only
if (column.id === "zotero-items-column-hasAttachment") {
return;
}
else if(column.id == "zotero-items-column-itemType")
{
val = Zotero.ItemTypes.getLocalizedString(obj.ref.itemTypeID);
}
// Year column is just date field truncated
else if (column.id == "zotero-items-column-year") {
val = obj.getField('date', true).substr(0, 4)
}
else if (column.id === "zotero-items-column-numNotes") {
val = obj.numNotes();
}
else {
var col = column.id.substring(20);
if (col == 'title') {
val = obj.ref.getDisplayTitle();
}
else {
val = obj.getField(col);
}
}
switch (column.id) {
// Format dates as short dates in proper locale order and locale time
// (e.g. "4/4/07 14:27:23")
case 'zotero-items-column-dateAdded':
case 'zotero-items-column-dateModified':
case 'zotero-items-column-accessDate':
case 'zotero-items-column-date':
if (column.id == 'zotero-items-column-date' && !this.collectionTreeRow.isFeed()) {
break;
}
if (val) {
var order = Zotero.Date.getLocaleDateOrder();
if (order == 'mdy') {
order = 'mdy';
var join = '/';
}
else if (order == 'dmy') {
order = 'dmy';
var join = '/';
}
else if (order == 'ymd') {
order = 'YMD';
var join = '-';
}
var date = Zotero.Date.sqlToDate(val, true);
var parts = [];
for (var i=0; i<3; i++) {
switch (order[i]) {
case 'y':
parts.push(date.getFullYear().toString().substr(2));
break;
case 'Y':
parts.push(date.getFullYear());
break;
case 'm':
parts.push((date.getMonth() + 1));
break;
case 'M':
parts.push(Zotero.Utilities.lpad((date.getMonth() + 1).toString(), '0', 2));
break;
case 'd':
parts.push(date.getDate());
break;
case 'D':
parts.push(Zotero.Utilities.lpad(date.getDate().toString(), '0', 2));
break;
}
val = parts.join(join);
val += ' ' + date.toLocaleTimeString();
}
}
}
return this._cellTextCache[itemID][column.id] = val;
}
Zotero.ItemTreeView.prototype.getImageSrc = function(row, col)
{
if(col.id == 'zotero-items-column-title')
{
// Get item type icon and tag swatches
var item = this.getRow(row).ref;
var itemID = item.id;
if (this._itemImages[itemID]) {
return this._itemImages[itemID];
}
item.getImageSrcWithTags()
.then(function (uriWithTags) {
this._itemImages[itemID] = uriWithTags;
this._treebox.invalidateCell(row, col);
}.bind(this));
return item.getImageSrc();
}
else if (col.id == 'zotero-items-column-hasAttachment') {
if (this.collectionTreeRow.isTrash()) return false;
var treerow = this.getRow(row);
var item = treerow.ref;
if ((!this.isContainer(row) || !this.isContainerOpen(row))
&& Zotero.Sync.Storage.getItemDownloadImageNumber(item)) {
return '';
}
var itemID = item.id;
let suffix = Zotero.hiDPISuffix;
if (treerow.level === 0) {
if (item.isRegularItem()) {
let state = item.getBestAttachmentStateCached();
if (state !== null) {
switch (state) {
case 1:
return `chrome://zotero/skin/bullet_blue${suffix}.png`;
case -1:
return `chrome://zotero/skin/bullet_blue_empty${suffix}.png`;
default:
return "";
}
}
item.getBestAttachmentState()
// Refresh cell when promise is fulfilled
.then(function (state) {
this._treebox.invalidateCell(row, col);
}.bind(this))
.done();
}
}
if (item.isFileAttachment()) {
let exists = item.fileExistsCached();
if (exists !== null) {
return exists
? `chrome://zotero/skin/bullet_blue${suffix}.png`
: `chrome://zotero/skin/bullet_blue_empty${suffix}.png`;
}
item.fileExists()
// Refresh cell when promise is fulfilled
.then(function (exists) {
this._treebox.invalidateCell(row, col);
}.bind(this));
}
}
return "";
}
Zotero.ItemTreeView.prototype.isContainer = function(row)
{
return this.getRow(row).ref.isRegularItem();
}
Zotero.ItemTreeView.prototype.isContainerEmpty = function(row)
{
if (this._sourcesOnly) {
return true;
}
var item = this.getRow(row).ref;
if (!item.isRegularItem()) {
return false;
}
var includeTrashed = this.collectionTreeRow.isTrash();
return item.numNotes(includeTrashed) === 0 && item.numAttachments(includeTrashed) == 0;
}
// Gets the index of the row's container, or -1 if none (top-level)
Zotero.ItemTreeView.prototype.getParentIndex = function(row)
{
if (row==-1)
{
return -1;
}
var thisLevel = this.getLevel(row);
if(thisLevel == 0) return -1;
for(var i = row - 1; i >= 0; i--)
if(this.getLevel(i) < thisLevel)
return i;
return -1;
}
Zotero.ItemTreeView.prototype.hasNextSibling = function(row,afterIndex)
{
var thisLevel = this.getLevel(row);
for(var i = afterIndex + 1; i < this.rowCount; i++)
{
var nextLevel = this.getLevel(i);
if(nextLevel == thisLevel) return true;
else if(nextLevel < thisLevel) return false;
}
}
Zotero.ItemTreeView.prototype.toggleOpenState = function (row, skipRowMapRefresh) {
// Shouldn't happen but does if an item is dragged over a closed
// container until it opens and then released, since the container
// is no longer in the same place when the spring-load closes
if (!this.isContainer(row)) {
return;
}
if (this.isContainerOpen(row)) {
return this._closeContainer(row, skipRowMapRefresh);
}
var count = 0;
var level = this.getLevel(row);
//
// Open
//
var item = this.getRow(row).ref;
//Get children
var includeTrashed = this.collectionTreeRow.isTrash();
var attachments = item.getAttachments(includeTrashed);
var notes = item.getNotes(includeTrashed);
var newRows;
if (attachments.length && notes.length) {
newRows = notes.concat(attachments);
}
else if (attachments.length) {
newRows = attachments;
}
else if (notes.length) {
newRows = notes;
}
if (newRows) {
newRows = Zotero.Items.get(newRows);
for (let i = 0; i < newRows.length; i++) {
count++;
this._addRow(
new Zotero.ItemTreeRow(newRows[i], level + 1, false),
row + i + 1,
true
);
}
}
this._rows[row].isOpen = true;
if (count == 0) {
return;
}
this._treebox.invalidateRow(row);
if (!skipRowMapRefresh) {
Zotero.debug('Refreshing hash map');
this._refreshItemRowMap();
}
}
Zotero.ItemTreeView.prototype._closeContainer = function (row, skipRowMapRefresh) {
// isContainer == false shouldn't happen but does if an item is dragged over a closed
// container until it opens and then released, since the container is no longer in the same
// place when the spring-load closes
if (!this.isContainer(row)) return;
if (!this.isContainerOpen(row)) return;
var count = 0;
var level = this.getLevel(row);
// Remove child rows
while ((row + 1 < this._rows.length) && (this.getLevel(row + 1) > level)) {
// Skip the map update here and just refresh the whole map below,
// since we might be removing multiple rows
this._removeRow(row + 1, true);
count--;
}
this._rows[row].isOpen = false;
if (count == 0) {
return;
}
this._treebox.invalidateRow(row);
if (!skipRowMapRefresh) {
Zotero.debug('Refreshing hash map');
this._refreshItemRowMap();
}
}
Zotero.ItemTreeView.prototype.isSorted = function()
{
// We sort by the first column if none selected, so return true
return true;
}
Zotero.ItemTreeView.prototype.cycleHeader = Zotero.Promise.coroutine(function* (column) {
if (this.collectionTreeRow.isFeed()) {
return;
}
if (column.id == 'zotero-items-column-hasAttachment') {
Zotero.debug("Caching best attachment states");
if (!this._cachedBestAttachmentStates) {
let t = new Date();
for (let i = 0; i < this._rows.length; i++) {
let item = this.getRow(i).ref;
if (item.isRegularItem()) {
yield item.getBestAttachmentState();
}
}
Zotero.debug("Cached best attachment states in " + (new Date - t) + " ms");
this._cachedBestAttachmentStates = true;
}
}
for(var i=0, len=this._treebox.columns.count; i<len; i++)
{
col = this._treebox.columns.getColumnAt(i);
if(column != col)
{
col.element.removeAttribute('sortActive');
col.element.removeAttribute('sortDirection');
}
else
{
// If not yet selected, start with ascending
if (!col.element.getAttribute('sortActive')) {
col.element.setAttribute('sortDirection', 'ascending');
}
else {
col.element.setAttribute('sortDirection', col.element.getAttribute('sortDirection') == 'descending' ? 'ascending' : 'descending');
}
col.element.setAttribute('sortActive', true);
}
}
this.selection.selectEventsSuppressed = true;
var savedSelection = this.getSelectedItems(true);
if (savedSelection.length == 1) {
var pos = this._rowMap[savedSelection[0]] - this._treebox.getFirstVisibleRow();
}
this.sort();
this.rememberSelection(savedSelection);
// If single row was selected, try to keep it in the same place
if (savedSelection.length == 1) {
var newRow = this._rowMap[savedSelection[0]];
// Calculate the last row that would give us a full view
var fullTop = Math.max(0, this._rows.length - this._treebox.getPageLength());
// Calculate the row that would give us the same position
var consistentTop = Math.max(0, newRow - pos);
this._treebox.scrollToRow(Math.min(fullTop, consistentTop));
}
this._treebox.invalidate();
this.selection.selectEventsSuppressed = false;
});
/*
* Sort the items by the currently sorted column.
*/
Zotero.ItemTreeView.prototype.sort = function (itemID) {
var t = new Date;
// If Zotero pane is hidden, mark tree for sorting later in setTree()
if (!this._treebox.columns) {
this._needsSort = true;
return;
}
this._needsSort = false;
// Single child item sort -- just toggle parent closed and open
if (itemID && this._rowMap[itemID] &&
this.getRow(this._rowMap[itemID]).ref.parentKey) {
let parentIndex = this.getParentIndex(this._rowMap[itemID]);
this._closeContainer(parentIndex);
this.toggleOpenState(parentIndex);
return;
}
var primaryField = this.getSortField();
var sortFields = this.getSortFields();
var dir = this.getSortDirection();
var order = dir == 'descending' ? -1 : 1;
var collation = Zotero.getLocaleCollation();
var sortCreatorAsString = Zotero.Prefs.get('sortCreatorAsString');
Zotero.debug("Sorting items list by " + sortFields.join(", ") + " " + dir
+ (itemID ? " for 1 item" : ""));
// Set whether rows with empty values should be displayed last,
// which may be different for primary and secondary sorting.
var emptyFirst = {};
switch (primaryField) {
case 'title':
emptyFirst.title = true;
break;
// When sorting by title we want empty titles at the top, but if not
// sorting by title, empty titles should sort to the bottom so that new
// empty items don't get sorted to the middle of the items list.
default:
emptyFirst.title = false;
}
// Cache primary values while sorting, since base-field-mapped getField()
// calls are relatively expensive
var cache = {};
sortFields.forEach(x => cache[x] = {})
// Get the display field for a row (which might be a placeholder title)
function getField(field, row) {
var item = row.ref;
switch (field) {
case 'title':
return Zotero.Items.getSortTitle(item.getDisplayTitle());
case 'hasAttachment':
if (item.isFileAttachment()) {
var state = item.fileExistsCached() ? 1 : -1;
}
else if (item.isRegularItem()) {
var state = item.getBestAttachmentStateCached();
}
else {
return 0;
}
// Make sort order present, missing, empty when ascending
if (state === 1) {
state = 2;
}
else if (state === -1) {
state = 1;
}
return state;
case 'numNotes':
return row.numNotes(false, true) || 0;
// Use unformatted part of date strings (YYYY-MM-DD) for sorting
case 'date':
var val = row.ref.getField('date', true, true);
if (val) {
val = val.substr(0, 10);
if (val.indexOf('0000') == 0) {
val = "";
}
}
return val;
case 'year':
var val = row.ref.getField('date', true, true);
if (val) {
val = val.substr(0, 4);
if (val == '0000') {
val = "";
}
}
return val;
default:
return row.ref.getField(field, false, true);
}
}
var includeTrashed = this.collectionTreeRow.isTrash();
function fieldCompare(a, b, sortField) {
var aItemID = a.id;
var bItemID = b.id;
var fieldA = cache[sortField][aItemID];
var fieldB = cache[sortField][bItemID];
switch (sortField) {
case 'firstCreator':
return creatorSort(a, b);
case 'itemType':
var typeA = Zotero.ItemTypes.getLocalizedString(a.ref.itemTypeID);
var typeB = Zotero.ItemTypes.getLocalizedString(b.ref.itemTypeID);
return (typeA > typeB) ? 1 : (typeA < typeB) ? -1 : 0;
default:
if (fieldA === undefined) {
cache[sortField][aItemID] = fieldA = getField(sortField, a);
}
if (fieldB === undefined) {
cache[sortField][bItemID] = fieldB = getField(sortField, b);
}
// Display rows with empty values last
if (!emptyFirst[sortField]) {
if(fieldA === '' && fieldB !== '') return 1;
if(fieldA !== '' && fieldB === '') return -1;
}
if (sortField == 'hasAttachment') {
return fieldB - fieldA;
}
return collation.compareString(1, fieldA, fieldB);
}
}
var rowSort = function (a, b) {
var sortFields = Array.slice(arguments, 2);
var sortField;
while (sortField = sortFields.shift()) {
let cmp = fieldCompare(a, b, sortField);
if (cmp !== 0) {
return cmp;
}
}
return 0;
};
var creatorSortCache = {};
// Regexp to extract the whole string up to an optional "and" or "et al."
var andEtAlRegExp = new RegExp(
// Extract the beginning of the string in non-greedy mode
"^.+?"
// up to either the end of the string, "et al." at the end of string
+ "(?=(?: " + Zotero.getString('general.etAl').replace('.', '\.') + ")?$"
// or ' and '
+ "| " + Zotero.getString('general.and') + " "
+ ")"
);
function creatorSort(a, b) {
var itemA = a.ref;
var itemB = b.ref;
//
// Try sorting by the first name in the firstCreator field, since we already have it
//
// For sortCreatorAsString mode, just use the whole string
//
var aItemID = a.id,
bItemID = b.id,
fieldA = creatorSortCache[aItemID],
fieldB = creatorSortCache[bItemID];
var prop = sortCreatorAsString ? 'firstCreator' : 'sortCreator';
var sortStringA = itemA[prop];
var sortStringB = itemB[prop];
if (fieldA === undefined) {
let firstCreator = Zotero.Items.getSortTitle(sortStringA);
if (sortCreatorAsString) {
var fieldA = firstCreator;
}
else {
var matches = andEtAlRegExp.exec(firstCreator);
var fieldA = matches ? matches[0] : '';
}
creatorSortCache[aItemID] = fieldA;
}
if (fieldB === undefined) {
let firstCreator = Zotero.Items.getSortTitle(sortStringB);
if (sortCreatorAsString) {
var fieldB = firstCreator;
}
else {
var matches = andEtAlRegExp.exec(firstCreator);
var fieldB = matches ? matches[0] : '';
}
creatorSortCache[bItemID] = fieldB;
}
if (fieldA === "" && fieldB === "") {
return 0;
}
// Display rows with empty values last
if (fieldA === '' && fieldB !== '') return 1;
if (fieldA !== '' && fieldB === '') return -1;
return collation.compareString(1, fieldA, fieldB);
}
// Need to close all containers before sorting
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
}
var savedSelection = this.getSelectedItems(true);
var openItemIDs = this._saveOpenState(true);
// Single-row sort
if (itemID) {
let row = this._rowMap[itemID];
for (let i=0, len=this._rows.length; i<len; i++) {
if (i === row) {
continue;
}
let cmp = rowSort.apply(this, [this._rows[i], this._rows[row]].concat(sortFields)) * order;
// As soon as we find a value greater (or smaller if reverse sort),
// insert row at that position
if (cmp > 0) {
let rowItem = this._rows.splice(row, 1);
this._rows.splice(row < i ? i-1 : i, 0, rowItem[0]);
this._treebox.invalidate();
break;
}
// If greater than last row, move to end
if (i == len-1) {
let rowItem = this._rows.splice(row, 1);
this._rows.splice(i, 0, rowItem[0]);
this._treebox.invalidate();
}
}
}
// Full sort
else {
this._rows.sort(function (a, b) {
return rowSort.apply(this, [a, b].concat(sortFields)) * order;
}.bind(this));
Zotero.debug("Sorted items list without creators in " + (new Date - t) + " ms");
}
this._refreshItemRowMap();
this.rememberOpenState(openItemIDs);
this.rememberSelection(savedSelection);
if (unsuppress) {
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
Zotero.debug("Sorted items list in " + (new Date - t) + " ms");
};
////////////////////////////////////////////////////////////////////////////////
///
/// Additional functions for managing data in the tree
///
////////////////////////////////////////////////////////////////////////////////
/*
* Select an item
*/
Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (id, expand, noRecurse) {
// If no row map, we're probably in the process of switching collections,
// so store the item to select on the item group for later
if (!this._rowMap) {
if (this.collectionTreeRow) {
this.collectionTreeRow.itemToSelect = { id: id, expand: expand };
Zotero.debug("_rowMap not yet set; not selecting item");
return false;
}
Zotero.debug('Item group not found and no row map in ItemTreeView.selectItem() -- discarding select', 2);
return false;
}
var row = this._rowMap[id];
// Get the row of the parent, if there is one
var parentRow = null;
var item = yield Zotero.Items.getAsync(id);
// Can't select a deleted item if we're not in the trash
if (item.deleted && !this.collectionTreeRow.isTrash()) {
return false;
}
var parent = item.parentItemID;
if (parent && this._rowMap[parent] != undefined) {
parentRow = this._rowMap[parent];
}
var selected = this.getSelectedItems(true);
if (selected.length == 1 && selected[0] == id) {
Zotero.debug("Item " + id + " is already selected");
this.betterEnsureRowIsVisible(row, parentRow);
return true;
}
// If row with id not visible, check to see if it's hidden under a parent
if(row == undefined)
{
if (!parent || parentRow === null) {
// No parent -- it's not here
// Clear the quicksearch and tag selection and try again (once)
if (!noRecurse && this._ownerDocument.defaultView.ZoteroPane_Local) {
let cleared1 = yield this._ownerDocument.defaultView.ZoteroPane_Local.clearQuicksearch();
let cleared2 = this._ownerDocument.defaultView.ZoteroPane_Local.clearTagSelection();
if (cleared1 || cleared2) {
return this.selectItem(id, expand, true);
}
}
Zotero.debug("Could not find row for item; not selecting item");
return false;
}
// If parent is already open and we haven't found the item, the child
// hasn't yet been added to the view, so close parent to allow refresh
this._closeContainer(parentRow);
// Open the parent
this.toggleOpenState(parentRow);
row = this._rowMap[id];
}
// this.selection.select() triggers the <tree>'s 'onselect' attribute, which calls
// ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(), which refreshes the
// itembox. But since the 'onselect' doesn't handle promises, itemSelected() isn't waited for
// here, which means that 'yield selectItem(itemID)' continues before the itembox has been
// refreshed. To get around this, we wait for a select event that's triggered by
// itemSelected() when it's done.
if (this.selection.selectEventsSuppressed) {
this.selection.select(row);
}
else {
var deferred = Zotero.Promise.defer();
this.addEventListener('select', () => deferred.resolve());
this.selection.select(row);
}
// If |expand|, open row if container
if (expand && this.isContainer(row) && !this.isContainerOpen(row)) {
this.toggleOpenState(row);
}
this.selection.select(row);
if (deferred) {
yield deferred.promise;
}
this.betterEnsureRowIsVisible(row, parentRow);
return true;
});
/**
* Select multiple top-level items
*
* @param {Integer[]} ids An array of itemIDs
*/
Zotero.ItemTreeView.prototype.selectItems = function(ids) {
if (ids.length == 0) {
return;
}
var rows = [];
for (let id of ids) {
if(this._rowMap[id] !== undefined) rows.push(this._rowMap[id]);
}
rows.sort(function (a, b) {
return a - b;
});
this.selection.clearSelection();
this.selection.selectEventsSuppressed = true;
var lastStart = 0;
for (var i = 0, len = rows.length; i < len; i++) {
if (i == len - 1 || rows[i + 1] != rows[i] + 1) {
this.selection.rangedSelect(rows[lastStart], rows[i], true);
lastStart = i + 1;
}
}
this.selection.selectEventsSuppressed = false;
// TODO: This could probably be improved to try to focus more of the selected rows
this.betterEnsureRowIsVisible(rows[0]);
}
Zotero.ItemTreeView.prototype.betterEnsureRowIsVisible = function (row, parentRow = null) {
// We aim for a row 5 below the target row, since ensureRowIsVisible() does
// the bare minimum to get the row in view
for (let v = row + 5; v >= row; v--) {
if (this._rows[v]) {
this._treebox.ensureRowIsVisible(v);
if (this._treebox.getFirstVisibleRow() <= row) {
break;
}
}
}
// If the parent row isn't in view and we have enough room, make parent visible
if (parentRow !== null && this._treebox.getFirstVisibleRow() > parentRow) {
if ((row - parentRow) < this._treebox.getPageLength()) {
this._treebox.ensureRowIsVisible(parentRow);
}
}
};
/*
* Return an array of Item objects for selected items
*
* If asIDs is true, return an array of itemIDs instead
*/
Zotero.ItemTreeView.prototype.getSelectedItems = function(asIDs)
{
var items = [], start = {}, end = {};
for (var i=0, len = this.selection.getRangeCount(); i<len; i++)
{
this.selection.getRangeAt(i,start,end);
for (var j=start.value; j<=end.value; j++) {
if (asIDs) {
items.push(this.getRow(j).id);
}
else {
items.push(this.getRow(j).ref);
}
}
}
return items;
}
/**
* Delete the selection
*
* @param {Boolean} [force=false] Delete item even if removing from a collection
*/
Zotero.ItemTreeView.prototype.deleteSelection = Zotero.Promise.coroutine(function* (force)
{
if (arguments.length > 1) {
throw ("deleteSelection() no longer takes two parameters");
}
if (this.selection.count == 0) {
return;
}
//this._treebox.beginUpdateBatch();
// Collapse open items
for (var i=0; i<this.rowCount; i++) {
if (this.selection.isSelected(i) && this.isContainer(i)) {
this._closeContainer(i, true);
}
}
this._refreshItemRowMap();
// Create an array of selected items
var ids = [];
var start = {};
var end = {};
for (var i=0, len=this.selection.getRangeCount(); i<len; i++)
{
this.selection.getRangeAt(i,start,end);
for (var j=start.value; j<=end.value; j++)
ids.push(this.getRow(j).id);
}
var collectionTreeRow = this.collectionTreeRow;
if (collectionTreeRow.isBucket()) {
collectionTreeRow.ref.deleteItems(ids);
}
else if (collectionTreeRow.isTrash() || collectionTreeRow.isPublications()) {
yield Zotero.Items.eraseTx(ids);
}
else if (collectionTreeRow.isLibrary(true) || force) {
yield Zotero.Items.trashTx(ids);
}
else if (collectionTreeRow.isCollection()) {
yield Zotero.DB.executeTransaction(function* () {
yield collectionTreeRow.ref.removeItems(ids);
});
}
//this._treebox.endUpdateBatch();
});
/*
* Set the search/tags filter on the view
*/
Zotero.ItemTreeView.prototype.setFilter = Zotero.Promise.coroutine(function* (type, data) {
if (!this._treebox || !this._treebox.treeBody) {
Components.utils.reportError("Treebox didn't exist in itemTreeView.setFilter()");
return;
}
this.selection.selectEventsSuppressed = true;
//this._treebox.beginUpdateBatch();
switch (type) {
case 'search':
this.collectionTreeRow.setSearch(data);
break;
case 'tags':
this.collectionTreeRow.setTags(data);
break;
default:
throw ('Invalid filter type in setFilter');
}
var oldCount = this.rowCount;
yield this.refresh();
this.sort();
//this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
});
/*
* Create map of item ids to row indexes
*/
Zotero.ItemTreeView.prototype._refreshItemRowMap = function()
{
var rowMap = {};
for (var i=0, len=this.rowCount; i<len; i++) {
let row = this.getRow(i);
let id = row.ref.id;
if (rowMap[id] !== undefined) {
Zotero.debug("WARNING: Item row already found", 2);
}
rowMap[id] = i;
}
this._rowMap = rowMap;
}
Zotero.ItemTreeView.prototype.saveSelection = function () {
return this.getSelectedItems(true);
}
/*
* Sets the selection based on saved selection ids
*/
Zotero.ItemTreeView.prototype.rememberSelection = function (selection) {
if (!selection.length) {
return;
}
this.selection.clearSelection();
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
}
for(var i=0; i < selection.length; i++)
{
if (this._rowMap[selection[i]] != null) {
this.selection.toggleSelect(this._rowMap[selection[i]]);
}
// Try the parent
else {
var item = Zotero.Items.get(selection[i]);
if (!item) {
continue;
}
var parent = item.parentItemID;
if (!parent) {
continue;
}
if (this._rowMap[parent] != null) {
this._closeContainer(this._rowMap[parent]);
this.toggleOpenState(this._rowMap[parent]);
this.selection.toggleSelect(this._rowMap[selection[i]]);
}
}
}
if (unsuppress) {
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
}
Zotero.ItemTreeView.prototype.selectSearchMatches = function () {
if (this._searchMode) {
var ids = [];
for (var id in this._searchItemIDs) {
ids.push(id);
}
this.rememberSelection(ids);
}
else {
this.selection.clearSelection();
}
}
Zotero.ItemTreeView.prototype._saveOpenState = function (close) {
var itemIDs = [];
if (close) {
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
}
}
for (var i=0; i<this._rows.length; i++) {
if (this.isContainer(i) && this.isContainerOpen(i)) {
itemIDs.push(this.getRow(i).ref.id);
if (close) {
this._closeContainer(i, true);
}
}
}
if (close) {
this._refreshItemRowMap();
if (unsuppress) {
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
}
return itemIDs;
}
Zotero.ItemTreeView.prototype.rememberOpenState = function (itemIDs) {
var rowsToOpen = [];
for (let id of itemIDs) {
var row = this._rowMap[id];
// Item may not still exist
if (row == undefined) {
continue;
}
rowsToOpen.push(row);
}
rowsToOpen.sort(function (a, b) {
return a - b;
});
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
}
// Reopen from bottom up
for (var i=rowsToOpen.length-1; i>=0; i--) {
this.toggleOpenState(rowsToOpen[i], true);
}
this._refreshItemRowMap();
if (unsuppress) {
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
}
Zotero.ItemTreeView.prototype.expandMatchParents = function () {
var t = new Date();
var time = 0;
// Expand parents of child matches
if (!this._searchMode) {
return;
}
var parentIDs = new Set();
for (let id in this._searchParentIDs) {
parentIDs.add(parseInt(id));
}
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
}
for (var i=0; i<this.rowCount; i++) {
var id = this.getRow(i).ref.id;
if (parentIDs.has(id) && this.isContainer(i) && !this.isContainerOpen(i)) {
var t2 = new Date();
this.toggleOpenState(i, true);
time += (new Date() - t2);
}
}
this._refreshItemRowMap();
if (unsuppress) {
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
}
Zotero.ItemTreeView.prototype.expandAllRows = function () {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
for (var i=0; i<this.rowCount; i++) {
if (this.isContainer(i) && !this.isContainerOpen(i)) {
this.toggleOpenState(i, true);
}
}
this._refreshItemRowMap();
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
Zotero.ItemTreeView.prototype.collapseAllRows = function () {
var unsuppress = this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
for (var i=0; i<this.rowCount; i++) {
if (this.isContainer(i)) {
this._closeContainer(i, true);
}
}
this._refreshItemRowMap();
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
};
Zotero.ItemTreeView.prototype.expandSelectedRows = function () {
var start = {}, end = {};
this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
for (var i = 0, len = this.selection.getRangeCount(); i<len; i++) {
this.selection.getRangeAt(i, start, end);
for (var j = start.value; j <= end.value; j++) {
if (this.isContainer(j) && !this.isContainerOpen(j)) {
this.toggleOpenState(j, true);
}
}
}
this._refreshItemRowMap();
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
Zotero.ItemTreeView.prototype.collapseSelectedRows = function () {
var start = {}, end = {};
this.selection.selectEventsSuppressed = true;
this._treebox.beginUpdateBatch();
for (var i = 0, len = this.selection.getRangeCount(); i<len; i++) {
this.selection.getRangeAt(i, start, end);
for (var j = start.value; j <= end.value; j++) {
if (this.isContainer(j)) {
this._closeContainer(j, true);
}
}
}
this._refreshItemRowMap();
this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
Zotero.ItemTreeView.prototype.getVisibleFields = function() {
var columns = [];
for (var i=0, len=this._treebox.columns.count; i<len; i++) {
var col = this._treebox.columns.getColumnAt(i);
if (col.element.getAttribute('hidden') != 'true') {
columns.push(col.id.substring(20));
}
}
return columns;
}
/**
* Returns an array of items of visible items in current sort order
*
* @param bool asIDs Return itemIDs
* @return array An array of Zotero.Item objects or itemIDs
*/
Zotero.ItemTreeView.prototype.getSortedItems = function(asIDs) {
var items = [];
for (let item of this._rows) {
if (asIDs) {
items.push(item.ref.id);
}
else {
items.push(item.ref);
}
}
return items;
}
Zotero.ItemTreeView.prototype.getSortField = function() {
if (this.collectionTreeRow.isFeed()) {
return 'id';
}
var column = this._treebox.columns.getSortedColumn();
if (!column) {
column = this._treebox.columns.getFirstColumn();
}
// zotero-items-column-_________
return column.id.substring(20);
}
Zotero.ItemTreeView.prototype.getSortFields = function () {
var fields = [this.getSortField()];
var secondaryField = this.getSecondarySortField();
if (secondaryField) {
fields.push(secondaryField);
}
try {
var fallbackFields = Zotero.Prefs.get('fallbackSort')
.split(',')
.map((x) => x.trim())
.filter((x) => x !== '');
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
// This should match the default value for the fallbackSort pref
var fallbackFields = ['firstCreator', 'date', 'title', 'dateAdded'];
}
fields = Zotero.Utilities.arrayUnique(fields.concat(fallbackFields));
// If date appears after year, remove it, unless it's the explicit secondary sort
var yearPos = fields.indexOf('year');
if (yearPos != -1) {
let datePos = fields.indexOf('date');
if (datePos > yearPos && secondaryField != 'date') {
fields.splice(datePos, 1);
}
}
return fields;
}
/*
* Returns 'ascending' or 'descending'
*/
Zotero.ItemTreeView.prototype.getSortDirection = function() {
if (this.collectionTreeRow.isFeed()) {
return Zotero.Prefs.get('feeds.sortAscending') ? 'ascending' : 'descending';
}
var column = this._treebox.columns.getSortedColumn();
if (!column) {
return 'ascending';
}
return column.element.getAttribute('sortDirection');
}
Zotero.ItemTreeView.prototype.getSecondarySortField = function () {
var primaryField = this.getSortField();
var secondaryField = Zotero.Prefs.get('secondarySort.' + primaryField);
if (!secondaryField || secondaryField == primaryField) {
return false;
}
return secondaryField;
}
Zotero.ItemTreeView.prototype.setSecondarySortField = function (secondaryField) {
var primaryField = this.getSortField();
var currentSecondaryField = this.getSecondarySortField();
var sortFields = this.getSortFields();
if (primaryField == secondaryField) {
return false;
}
if (currentSecondaryField) {
// If same as the current explicit secondary sort, ignore
if (currentSecondaryField == secondaryField) {
return false;
}
// If not, but same as first implicit sort, remove current explicit sort
if (sortFields[2] && sortFields[2] == secondaryField) {
Zotero.Prefs.clear('secondarySort.' + primaryField);
return true;
}
}
// If same as current implicit secondary sort, ignore
else if (sortFields[1] && sortFields[1] == secondaryField) {
return false;
}
Zotero.Prefs.set('secondarySort.' + primaryField, secondaryField);
return true;
}
/**
* Build the More Columns and Secondary Sort submenus while the popup is opening
*/
Zotero.ItemTreeView.prototype.onColumnPickerShowing = function (event) {
var menupopup = event.originalTarget;
var ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
var prefix = 'zotero-column-header-';
var doc = menupopup.ownerDocument;
var anonid = menupopup.getAttribute('anonid');
if (anonid.indexOf(prefix) == 0) {
return;
}
var lastChild = menupopup.lastChild;
try {
// More Columns menu
let id = prefix + 'more-menu';
let moreMenu = doc.createElementNS(ns, 'menu');
moreMenu.setAttribute('label', Zotero.getString('pane.items.columnChooser.moreColumns'));
moreMenu.setAttribute('anonid', id);
let moreMenuPopup = doc.createElementNS(ns, 'menupopup');
moreMenuPopup.setAttribute('anonid', id + '-popup');
let treecols = menupopup.parentNode.parentNode;
let subs = Array.from(treecols.getElementsByAttribute('submenu', 'true'))
.map(x => x.getAttribute('label'));
var moreItems = [];
for (let i=0; i<menupopup.childNodes.length; i++) {
let elem = menupopup.childNodes[i];
if (elem.localName == 'menuseparator') {
break;
}
if (elem.localName == 'menuitem' && subs.indexOf(elem.getAttribute('label')) != -1) {
moreItems.push(elem);
}
}
// Disable certain fields for feeds
let labels = Array.from(treecols.getElementsByAttribute('disabled-in', '*'))
.filter(e => e.getAttribute('disabled-in').split(' ').indexOf(this.collectionTreeRow.type) != -1)
.map(e => e.getAttribute('label'));
for (let i = 0; i < menupopup.childNodes.length; i++) {
let elem = menupopup.childNodes[i];
elem.setAttribute('disabled', labels.indexOf(elem.getAttribute('label')) != -1);
}
// Sort fields and move to submenu
var collation = Zotero.getLocaleCollation();
moreItems.sort(function (a, b) {
return collation.compareString(1, a.getAttribute('label'), b.getAttribute('label'));
});
moreItems.forEach(function (elem) {
moreMenuPopup.appendChild(menupopup.removeChild(elem));
});
moreMenu.appendChild(moreMenuPopup);
menupopup.insertBefore(moreMenu, lastChild);
}
catch (e) {
Components.utils.reportError(e);
Zotero.debug(e, 1);
}
//
// Secondary Sort menu
//
if (!this.collectionTreeRow.isFeed()) {
try {
let id = prefix + 'sort-menu';
let primaryField = this.getSortField();
let sortFields = this.getSortFields();
let secondaryField = false;
if (sortFields[1]) {
secondaryField = sortFields[1];
}
// Get localized names from treecols, since the names are currently done via .dtd
let treecols = menupopup.parentNode.parentNode;
let primaryFieldLabel = treecols.getElementsByAttribute('id',
'zotero-items-column-' + primaryField)[0].getAttribute('label');
let sortMenu = doc.createElementNS(ns, 'menu');
sortMenu.setAttribute('label',
Zotero.getString('pane.items.columnChooser.secondarySort', primaryFieldLabel));
sortMenu.setAttribute('anonid', id);
let sortMenuPopup = doc.createElementNS(ns, 'menupopup');
sortMenuPopup.setAttribute('anonid', id + '-popup');
// Generate menuitems
let sortOptions = [
'title',
'firstCreator',
'itemType',
'date',
'year',
'publisher',
'publicationTitle',
'dateAdded',
'dateModified'
];
for (let i=0; i<sortOptions.length; i++) {
let field = sortOptions[i];
// Hide current primary field, and don't show Year for Date, since it would be a no-op
if (field == primaryField || (primaryField == 'date' && field == 'year')) {
continue;
}
let label = treecols.getElementsByAttribute('id',
'zotero-items-column-' + field)[0].getAttribute('label');
let sortMenuItem = doc.createElementNS(ns, 'menuitem');
sortMenuItem.setAttribute('fieldName', field);
sortMenuItem.setAttribute('label', label);
sortMenuItem.setAttribute('type', 'checkbox');
if (field == secondaryField) {
sortMenuItem.setAttribute('checked', 'true');
}
sortMenuItem.setAttribute('oncommand',
'var view = ZoteroPane.itemsView; '
+ 'if (view.setSecondarySortField(this.getAttribute("fieldName"))) { view.sort(); }');
sortMenuPopup.appendChild(sortMenuItem);
}
sortMenu.appendChild(sortMenuPopup);
menupopup.insertBefore(sortMenu, lastChild);
}
catch (e) {
Components.utils.reportError(e);
Zotero.debug(e, 1);
}
}
sep = doc.createElementNS(ns, 'menuseparator');
sep.setAttribute('anonid', prefix + 'sep');
menupopup.insertBefore(sep, lastChild);
}
Zotero.ItemTreeView.prototype.onColumnPickerHidden = function (event) {
var menupopup = event.originalTarget;
var prefix = 'zotero-column-header-';
for (let i=0; i<menupopup.childNodes.length; i++) {
let elem = menupopup.childNodes[i];
if (elem.getAttribute('anonid').indexOf(prefix) == 0) {
try {
menupopup.removeChild(elem);
}
catch (e) {
Zotero.debug(e, 1);
}
i--;
}
}
}
////////////////////////////////////////////////////////////////////////////////
///
/// Command Controller:
/// for Select All, etc.
///
////////////////////////////////////////////////////////////////////////////////
Zotero.ItemTreeCommandController = function(tree)
{
this.tree = tree;
}
Zotero.ItemTreeCommandController.prototype.supportsCommand = function(cmd)
{
return (cmd == 'cmd_selectAll');
}
Zotero.ItemTreeCommandController.prototype.isCommandEnabled = function(cmd)
{
return (cmd == 'cmd_selectAll');
}
Zotero.ItemTreeCommandController.prototype.doCommand = function (cmd) {
if (cmd == 'cmd_selectAll') {
if (this.tree.view.wrappedJSObject.collectionTreeRow.isSearchMode()) {
this.tree.view.wrappedJSObject.selectSearchMatches();
}
else {
this.tree.view.selection.selectAll();
}
}
}
Zotero.ItemTreeCommandController.prototype.onEvent = function(evt)
{
}
////////////////////////////////////////////////////////////////////////////////
///
/// Drag-and-drop functions
///
////////////////////////////////////////////////////////////////////////////////
/**
* Start a drag using HTML 5 Drag and Drop
*/
Zotero.ItemTreeView.prototype.onDragStart = function (event) {
// See note in LibraryTreeView::_setDropEffect()
if (Zotero.isWin) {
event.dataTransfer.effectAllowed = 'copyMove';
}
var itemIDs = this.getSelectedItems(true);
event.dataTransfer.setData("zotero/item", itemIDs);
// dataTransfer.mozSourceNode doesn't seem to be properly set anymore (tested in 50), so store
// target separately
if (!event.dataTransfer.mozSourceNode) {
Zotero.debug("mozSourceNode not set -- storing source node");
Zotero.DragDrop.currentSourceNode = event.target;
}
var items = Zotero.Items.get(itemIDs);
// If at least one file is a non-web-link attachment and can be found,
// enable dragging to file system
var files = items
.filter(item => item.isAttachment())
.map(item => item.getFilePath())
.filter(path => path);
if (files.length) {
// Advanced multi-file drag (with unique filenames, which otherwise happen automatically on
// Windows but not Linux) and auxiliary snapshot file copying on macOS
let dataProvider;
if (Zotero.isMac) {
dataProvider = new Zotero.ItemTreeView.fileDragDataProvider(itemIDs);
}
for (let i = 0; i < files.length; i++) {
let file = Zotero.File.pathToFile(files[i]);
if (dataProvider) {
Zotero.debug("Adding application/x-moz-file-promise");
event.dataTransfer.mozSetDataAt("application/x-moz-file-promise", dataProvider, i);
}
// Allow dragging to filesystem on Linux and Windows
let uri;
if (!Zotero.isMac) {
Zotero.debug("Adding text/x-moz-url " + i);
let fph = Components.classes["@mozilla.org/network/protocol;1?name=file"]
.createInstance(Components.interfaces.nsIFileProtocolHandler);
uri = fph.getURLSpecFromFile(file);
event.dataTransfer.mozSetDataAt("text/x-moz-url", uri + '\n' + file.leafName, i);
}
// Allow dragging to web targets (e.g., Gmail)
Zotero.debug("Adding application/x-moz-file " + i);
event.dataTransfer.mozSetDataAt("application/x-moz-file", file, i);
if (Zotero.isWin) {
event.dataTransfer.mozSetDataAt("application/x-moz-file-promise-url", uri, i);
}
else if (Zotero.isLinux) {
// Don't create a symlink for an unmodified drag
event.dataTransfer.effectAllowed = 'copy';
}
}
}
// Get Quick Copy format for current URL
var url = this._ownerDocument.defaultView.content && this._ownerDocument.defaultView.content.location ?
this._ownerDocument.defaultView.content.location.href : null;
var format = Zotero.QuickCopy.getFormatFromURL(url);
Zotero.debug("Dragging with format " + format);
var exportCallback = function(obj, worked) {
if (!worked) {
Zotero.log(Zotero.getString("fileInterface.exportError"), 'warning');
return;
}
var text = obj.string.replace(/\r\n/g, "\n");
event.dataTransfer.setData("text/plain", text);
}
format = Zotero.QuickCopy.unserializeSetting(format);
try {
if (format.mode == 'export') {
Zotero.QuickCopy.getContentFromItems(items, format, exportCallback);
}
else if (format.mode == 'bibliography') {
var content = Zotero.QuickCopy.getContentFromItems(items, format, null, event.shiftKey);
if (content) {
if (content.html) {
event.dataTransfer.setData("text/html", content.html);
}
event.dataTransfer.setData("text/plain", content.text);
}
}
else {
Components.utils.reportError("Invalid Quick Copy mode");
}
}
catch (e) {
Zotero.debug(e);
Components.utils.reportError(e + " with '" + format.id + "'");
}
};
Zotero.ItemTreeView.prototype.onDragEnd = function (event) {
setTimeout(function () {
Zotero.DragDrop.currentDragSource = null;
});
}
// Implements nsIFlavorDataProvider for dragging attachment files to OS
//
// Not used on Windows in Firefox 3 or higher
Zotero.ItemTreeView.fileDragDataProvider = function (itemIDs) {
this._itemIDs = itemIDs;
};
Zotero.ItemTreeView.fileDragDataProvider.prototype = {
QueryInterface : function(iid) {
if (iid.equals(Components.interfaces.nsIFlavorDataProvider) ||
iid.equals(Components.interfaces.nsISupports)) {
return this;
}
throw Components.results.NS_NOINTERFACE;
},
getFlavorData : function(transferable, flavor, data, dataLen) {
Zotero.debug("Getting flavor data for " + flavor);
if (flavor == "application/x-moz-file-promise") {
// On platforms other than OS X, the only directory we know of here
// is the system temp directory, and we pass the nsIFile of the file
// copied there in data.value below
var useTemp = !Zotero.isMac;
// Get the destination directory
var dirPrimitive = {};
var dataSize = {};
transferable.getTransferData("application/x-moz-file-promise-dir", dirPrimitive, dataSize);
var destDir = dirPrimitive.value.QueryInterface(Components.interfaces.nsILocalFile);
var draggedItems = Zotero.Items.get(this._itemIDs);
var items = [];
// Make sure files exist
var notFoundNames = [];
for (var i=0; i<draggedItems.length; i++) {
// TODO create URL?
if (!draggedItems[i].isAttachment() ||
draggedItems[i].getAttachmentLinkMode() == Zotero.Attachments.LINK_MODE_LINKED_URL) {
continue;
}
if (draggedItems[i].getFile()) {
items.push(draggedItems[i]);
}
else {
notFoundNames.push(draggedItems[i].getField('title'));
}
}
// If using the temp directory, create a directory to store multiple
// files, since we can (it seems) only pass one nsIFile in data.value
if (useTemp && items.length > 1) {
var tmpDirName = 'Zotero Dragged Files';
destDir.append(tmpDirName);
if (destDir.exists()) {
destDir.remove(true);
}
destDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
}
var copiedFiles = [];
var existingItems = [];
var existingFileNames = [];
for (var i=0; i<items.length; i++) {
// TODO create URL?
if (!items[i].isAttachment() ||
items[i].attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
continue;
}
var file = items[i].getFile();
// Determine if we need to copy multiple files for this item
// (web page snapshots)
if (items[i].attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
var parentDir = file.parent;
var files = parentDir.directoryEntries;
var numFiles = 0;
while (files.hasMoreElements()) {
var f = files.getNext();
f.QueryInterface(Components.interfaces.nsILocalFile);
if (f.leafName.indexOf('.') != 0) {
numFiles++;
}
}
}
// Create folder if multiple files
if (numFiles > 1) {
var dirName = Zotero.Attachments.getFileBaseNameFromItem(items[i]);
try {
if (useTemp) {
var copiedFile = destDir.clone();
copiedFile.append(dirName);
if (copiedFile.exists()) {
// If item directory already exists in the temp dir,
// delete it
if (items.length == 1) {
copiedFile.remove(true);
}
// If item directory exists in the container
// directory, it's a duplicate, so give this one
// a different name
else {
copiedFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0o644);
var newName = copiedFile.leafName;
copiedFile.remove(null);
}
}
}
parentDir.copyToFollowingLinks(destDir, newName ? newName : dirName);
// Store nsIFile
if (useTemp) {
copiedFiles.push(copiedFile);
}
}
catch (e) {
if (e.name == 'NS_ERROR_FILE_ALREADY_EXISTS') {
// Keep track of items that already existed
existingItems.push(items[i].id);
existingFileNames.push(dirName);
}
else {
throw (e);
}
}
}
// Otherwise just copy
else {
try {
if (useTemp) {
var copiedFile = destDir.clone();
copiedFile.append(file.leafName);
if (copiedFile.exists()) {
// If file exists in the temp directory,
// delete it
if (items.length == 1) {
copiedFile.remove(true);
}
// If file exists in the container directory,
// it's a duplicate, so give this one a different
// name
else {
copiedFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0o644);
var newName = copiedFile.leafName;
copiedFile.remove(null);
}
}
}
file.copyToFollowingLinks(destDir, newName ? newName : null);
// Store nsIFile
if (useTemp) {
copiedFiles.push(copiedFile);
}
}
catch (e) {
if (e.name == 'NS_ERROR_FILE_ALREADY_EXISTS') {
existingItems.push(items[i].id);
existingFileNames.push(items[i].getFile().leafName);
}
else {
throw (e);
}
}
}
}
// Files passed via data.value will be automatically moved
// from the temp directory to the destination directory
if (useTemp && copiedFiles.length) {
if (items.length > 1) {
data.value = destDir.QueryInterface(Components.interfaces.nsISupports);
}
else {
data.value = copiedFiles[0].QueryInterface(Components.interfaces.nsISupports);
}
dataLen.value = 4;
}
if (notFoundNames.length || existingItems.length) {
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
}
// Display alert if files were not found
if (notFoundNames.length > 0) {
// On platforms that use a temporary directory, an alert here
// would interrupt the dragging process, so we just log a
// warning to the console
if (useTemp) {
for (let name of notFoundNames) {
var msg = "Attachment file for dragged item '" + name + "' not found";
Zotero.log(msg, 'warning',
'chrome://zotero/content/xpcom/itemTreeView.js');
}
}
else {
promptService.alert(null, Zotero.getString('general.warning'),
Zotero.getString('dragAndDrop.filesNotFound') + "\n\n"
+ notFoundNames.join("\n"));
}
}
// Display alert if existing files were skipped
if (existingItems.length > 0) {
promptService.alert(null, Zotero.getString('general.warning'),
Zotero.getString('dragAndDrop.existingFiles') + "\n\n"
+ existingFileNames.join("\n"));
}
}
}
}
/**
* Called by treechildren.onDragOver() before setting the dropEffect,
* which is checked in libraryTreeView.canDrop()
*/
Zotero.ItemTreeView.prototype.canDropCheck = function (row, orient, dataTransfer) {
//Zotero.debug("Row is " + row + "; orient is " + orient);
var dragData = Zotero.DragDrop.getDataFromDataTransfer(dataTransfer);
if (!dragData) {
Zotero.debug("No drag data");
return false;
}
var dataType = dragData.dataType;
var data = dragData.data;
var collectionTreeRow = this.collectionTreeRow;
if (row != -1 && orient == 0) {
var rowItem = this.getRow(row).ref; // the item we are dragging over
}
if (dataType == 'zotero/item') {
let items = Zotero.Items.get(data);
// Directly on a row
if (rowItem) {
var canDrop = false;
for (let item of items) {
// If any regular items, disallow drop
if (item.isRegularItem()) {
return false;
}
// Disallow cross-library child drag
if (item.libraryID != collectionTreeRow.ref.libraryID) {
return false;
}
// Only allow dragging of notes and attachments
// that aren't already children of the item
if (item.parentItemID != rowItem.id) {
canDrop = true;
}
}
return canDrop;
}
// In library, allow children to be dragged out of parent
else if (collectionTreeRow.isLibrary(true) || collectionTreeRow.isCollection()) {
for (let item of items) {
// Don't allow drag if any top-level items
if (item.isTopLevelItem()) {
return false;
}
// Don't allow web attachments to be dragged out of parents,
// but do allow PDFs for now so they can be recognized
if (item.isWebAttachment() && item.attachmentContentType != 'application/pdf') {
return false;
}
// Don't allow children to be dragged within their own parents
var parentItemID = item.parentItemID;
var parentIndex = this._rowMap[parentItemID];
if (row != -1 && this.getLevel(row) > 0) {
if (this.getRow(this.getParentIndex(row)).ref.id == parentItemID) {
return false;
}
}
// Including immediately after the parent
if (orient == 1) {
if (row == parentIndex) {
return false;
}
}
// And immediately before the next parent
if (orient == -1) {
var nextParentIndex = null;
for (var i = parentIndex + 1; i < this.rowCount; i++) {
if (this.getLevel(i) == 0) {
nextParentIndex = i;
break;
}
}
if (row === nextParentIndex) {
return false;
}
}
// Disallow cross-library child drag
if (item.libraryID != collectionTreeRow.ref.libraryID) {
return false;
}
}
return true;
}
return false;
}
else if (dataType == "text/x-moz-url" || dataType == 'application/x-moz-file') {
// Disallow direct drop on a non-regular item (e.g. note)
if (rowItem) {
if (!rowItem.isRegularItem()) {
return false;
}
}
// Don't allow drop into searches
else if (collectionTreeRow.isSearch()) {
return false;
}
return true;
}
return false;
};
/*
* Called when something's been dropped on or next to a row
*/
Zotero.ItemTreeView.prototype.drop = Zotero.Promise.coroutine(function* (row, orient, dataTransfer) {
if (!this.canDrop(row, orient, dataTransfer)) {
return false;
}
var dragData = Zotero.DragDrop.getDataFromDataTransfer(dataTransfer);
if (!dragData) {
Zotero.debug("No drag data");
return false;
}
var dropEffect = dragData.dropEffect;
var dataType = dragData.dataType;
var data = dragData.data;
var sourceCollectionTreeRow = Zotero.DragDrop.getDragSource(dataTransfer);
var collectionTreeRow = this.collectionTreeRow;
var targetLibraryID = collectionTreeRow.ref.libraryID;
if (dataType == 'zotero/item') {
var ids = data;
var items = Zotero.Items.get(ids);
if (items.length < 1) {
return;
}
// TEMP: This is always false for now, since cross-library drag
// is disallowed in canDropCheck()
//
// TODO: support items coming from different sources?
if (items[0].libraryID == targetLibraryID) {
var sameLibrary = true;
}
else {
var sameLibrary = false;
}
var toMove = [];
// Dropped directly on a row
if (orient == 0) {
// Set drop target as the parent item for dragged items
//
// canDrop() limits this to child items
var rowItem = this.getRow(row).ref; // the item we are dragging over
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<items.length; i++) {
let item = items[i];
item.parentID = rowItem.id;
yield item.save();
}
});
}
// Dropped outside of a row
else
{
// Remove from parent and make top-level
if (collectionTreeRow.isLibrary(true)) {
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<items.length; i++) {
let item = items[i];
if (!item.isRegularItem()) {
item.parentID = false;
yield item.save()
}
}
});
}
// Add to collection
else
{
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<items.length; i++) {
let item = items[i];
var source = item.isRegularItem() ? false : item.parentItemID;
// Top-level item
if (source) {
item.parentID = false;
item.addToCollection(collectionTreeRow.ref.id);
yield item.save();
}
else {
item.addToCollection(collectionTreeRow.ref.id);
yield item.save();
}
toMove.push(item.id);
}
});
}
}
// If moving, remove items from source collection
if (dropEffect == 'move' && toMove.length) {
if (!sameLibrary) {
throw new Error("Cannot move items between libraries");
}
if (!sourceCollectionTreeRow || !sourceCollectionTreeRow.isCollection()) {
throw new Error("Drag source must be a collection");
}
if (collectionTreeRow.id != sourceCollectionTreeRow.id) {
yield Zotero.DB.executeTransaction(function* () {
yield collectionTreeRow.ref.removeItems(toMove);
}.bind(this));
}
}
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
// Disallow drop into read-only libraries
if (!collectionTreeRow.editable) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
win.ZoteroPane.displayCannotEditLibraryMessage();
return;
}
var targetLibraryID = collectionTreeRow.ref.libraryID;
var parentItemID = false;
var parentCollectionID = false;
if (orient == 0) {
let treerow = this.getRow(row);
parentItemID = treerow.ref.id
}
else if (collectionTreeRow.isCollection()) {
var parentCollectionID = collectionTreeRow.ref.id;
}
var notifierQueue = new Zotero.Notifier.Queue;
try {
for (var i=0; i<data.length; i++) {
var file = data[i];
if (dataType == 'text/x-moz-url') {
var url = data[i];
if (url.indexOf('file:///') == 0) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
// If dragging currently loaded page, only convert to
// file if not an HTML document
if (win.content.location.href != url ||
win.content.document.contentType != 'text/html') {
var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
.getService(Components.interfaces.nsIFileProtocolHandler);
try {
var file = nsIFPH.getFileFromURLSpec(url);
}
catch (e) {
Zotero.debug(e);
}
}
}
// Still string, so remote URL
if (typeof file == 'string') {
if (parentItemID) {
if (!collectionTreeRow.filesEditable) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
win.ZoteroPane.displayCannotEditLibraryFilesMessage();
return;
}
yield Zotero.Attachments.importFromURL({
libraryID: targetLibraryID,
url,
parentItemID,
saveOptions: {
notifierQueue
}
});
}
else {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
win.ZoteroPane.addItemFromURL(url, 'temporaryPDFHack'); // TODO: don't do this
}
continue;
}
// Otherwise file, so fall through
}
if (dropEffect == 'link') {
yield Zotero.Attachments.linkFromFile({
file,
parentItemID,
collections: parentCollectionID ? [parentCollectionID] : undefined,
saveOptions: {
notifierQueue
}
});
}
else {
if (file.leafName.endsWith(".lnk")) {
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
let win = wm.getMostRecentWindow("navigator:browser");
win.ZoteroPane.displayCannotAddShortcutMessage(file.path);
continue;
}
yield Zotero.Attachments.importFromFile({
file,
libraryID: targetLibraryID,
parentItemID,
collections: parentCollectionID ? [parentCollectionID] : undefined,
saveOptions: {
notifierQueue
}
});
// If moving, delete original file
if (dragData.dropEffect == 'move') {
try {
file.remove(false);
}
catch (e) {
Components.utils.reportError("Error deleting original file " + file.path + " after drag");
}
}
}
}
}
finally {
yield Zotero.Notifier.commit(notifierQueue);
}
}
});
////////////////////////////////////////////////////////////////////////////////
///
/// Functions for nsITreeView that we have to stub out.
///
////////////////////////////////////////////////////////////////////////////////
Zotero.ItemTreeView.prototype.isSeparator = function(row) { return false; }
Zotero.ItemTreeView.prototype.isSelectable = function (row, col) { return true; }
Zotero.ItemTreeView.prototype.getRowProperties = function(row, prop) {}
Zotero.ItemTreeView.prototype.getColumnProperties = function(col, prop) {}
Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) {
var treeRow = this.getRow(row);
var itemID = treeRow.ref.id;
var props = [];
// Mark items not matching search as context rows, displayed in gray
if (this._searchMode && !this._searchItemIDs[itemID]) {
props.push("contextRow");
}
// Mark hasAttachment column, which needs special image handling
if (col.id == 'zotero-items-column-hasAttachment') {
props.push("hasAttachment");
// Don't show pie for open parent items, since we show it for the
// child item
if (!this.isContainer(row) || !this.isContainerOpen(row)) {
var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref);
//var num = Math.round(new Date().getTime() % 10000 / 10000 * 64);
if (num !== false) props.push("pie", "pie" + num);
}
}
// Style unread items in feeds
if (treeRow.ref.isFeedItem && !treeRow.ref.isRead) props.push('unread');
return props.join(" ");
}
Zotero.ItemTreeRow = function(ref, level, isOpen)
{
this.ref = ref; //the item associated with this
this.level = level;
this.isOpen = isOpen;
this.id = ref.id;
}
Zotero.ItemTreeRow.prototype.getField = function(field, unformatted)
{
return this.ref.getField(field, unformatted, true);
}
Zotero.ItemTreeRow.prototype.numNotes = function() {
if (this.ref.isNote()) {
return '';
}
return this.ref.numNotes(false, true) || '';
}