/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see .
***** END LICENSE BLOCK *****
*/
////////////////////////////////////////////////////////////////////////////////
///
/// 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;
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'],
'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) {
yield 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;
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().done();
break;
case 37:
self.collapseSelectedRows().done();
break;
}
event.preventDefault();
}
return;
}
var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
self.expandAllRows().done();
event.preventDefault();
return;
}
else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
self.collapseAllRows().done();
event.preventDefault();
return;
}
// Ignore other non-character keypresses
if (!event.charCode || event.shiftKey || event.ctrlKey ||
event.altKey || event.metaKey) {
return;
}
event.preventDefault();
Zotero.Promise.try(function () {
if (coloredTagsRE.test(key)) {
let libraryID = self.collectionTreeRow.ref.libraryID;
let position = parseInt(key) - 1;
return Zotero.Tags.getColorByPosition(libraryID, position)
.then(function (colorData) {
// If a color isn't assigned to this number or any
// other numbers, allow key navigation
if (!colorData) {
return Zotero.Tags.getColors(libraryID)
.then(function (colors) {
return !Object.keys(colors).length;
});
}
var items = self.getSelectedItems();
return Zotero.Tags.toggleItemsListTags(libraryID, items, colorData.name)
.then(function () {
return false;
});
});
}
return true;
})
// We have to disable key navigation on the tree in order to
// keep it from acting on the 1-6 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...
.then(function (resend) {
if (!resend) {
return;
}
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.debug(e, 1);
Components.utils.reportError(e);
})
.done();
};
// 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 () {};
yield this.sort();
yield this.expandMatchParents();
if (this._ownerDocument.defaultView.ZoteroPane_Local) {
this._ownerDocument.defaultView.ZoteroPane_Local.clearItemsPaneMessage();
}
// Select a queued item from selectItem()
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;
}
});
/**
* 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);
//if(!Zotero.ItemTreeView._haveCachedFields) yield Zotero.Promise.resolve();
var cacheFields = ['title', 'date'];
// Cache the visible fields so they don't load individually
try {
var visibleFields = this.getVisibleFields();
}
// 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 {
for (let i=0; i 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') {
// On a delete in duplicates mode, just refresh rather than figuring
// out what to remove
if (collectionTreeRow.isDuplicates()) {
yield this.refresh();
refreshed = true;
madeChanges = true;
sort = true;
}
else {
// Since a remove involves shifting of rows, we have to do it in order,
// so sort the ids by row
var rows = [];
for (var i=0, len=ids.length; i 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 (action == 'modify')
{
// Clear row caches
var items = yield Zotero.Items.getAsync(ids);
for (let i=0; i 0) {
let previousItem = yield Zotero.Items.getAsync(ids[0]);
if (previousItem && !previousItem.isTopLevelItem()) {
if (this._rows[previousFirstRow] && this.getLevel(previousFirstRow) == 0) {
previousFirstRow--;
}
}
}
if (previousFirstRow !== undefined && this._rows[previousFirstRow]) {
this.selection.select(previousFirstRow);
}
// 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 {
yield this.rememberSelection(savedSelection);
}
}
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);
}*/
if (Zotero.suppressUIUpdates) {
yield this.rememberSelection(savedSelection);
}
//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) {
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':
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)
{
var self = this;
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;
if (treerow.level === 0) {
if (item.isRegularItem()) {
let state = item.getBestAttachmentStateCached();
if (state !== null) {
switch (state) {
case 1:
return "chrome://zotero/skin/bullet_blue.png";
case -1:
return "chrome://zotero/skin/bullet_blue_empty.png";
default:
return "";
}
}
item.getBestAttachmentState()
// Refresh cell when promise is fulfilled
.then(function (state) {
self._treebox.invalidateCell(row, col);
})
.done();
}
}
if (item.isFileAttachment()) {
let exists = item.fileExistsCached();
if (exists !== null) {
return exists
? "chrome://zotero/skin/bullet_blue.png"
: "chrome://zotero/skin/bullet_blue_empty.png";
}
item.fileExists()
// Refresh cell when promise is fulfilled
.then(function (exists) {
self._treebox.invalidateCell(row, col);
});
}
}
}
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 = Zotero.Promise.coroutine(function* (row, skipItemMapRefresh)
{
// 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, skipItemMapRefresh);
}
var count = 0;
var level = this.getLevel(row);
//
// Open
//
var item = this.getRow(row).ref;
yield item.loadChildItems();
//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 = yield Zotero.Items.getAsync(newRows);
for (let i = 0; i < newRows.length; i++) {
count++;
this._addRow(
new Zotero.ItemTreeRow(newRows[i], level + 1, false),
row + i + 1
);
}
}
this._rows[row].isOpen = true;
if (count == 0) {
return;
}
this._treebox.invalidateRow(row);
if (!skipItemMapRefresh) {
Zotero.debug('Refreshing hash map');
this._refreshItemRowMap();
}
});
Zotero.ItemTreeView.prototype._closeContainer = function (row, skipItemMapRefresh) {
// 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 (!skipItemMapRefresh) {
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)
{
for(var i=0, len=this._treebox.columns.count; i 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;
}
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 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();
yield this.rememberOpenState(openItemIDs);
yield this.rememberSelection(savedSelection);
if (unsuppress) {
this.selection.selectEventsSuppressed = false;
//this._treebox.endUpdateBatch();
}
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) {
// Don't change selection if UI updates are disabled (e.g., during sync)
if (Zotero.suppressUIUpdates) {
Zotero.debug("Sync is running; not selecting item");
return false;
}
// 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 selected = this.getSelectedItems(true);
if (selected.length == 1 && selected[0] == id) {
Zotero.debug("Item " + id + " is already selected");
return;
}
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];
}
// 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 = yield 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
yield this.toggleOpenState(parentRow);
row = this._rowMap[id];
}
// this.selection.select() triggers the '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)) {
yield this.toggleOpenState(row);
}
this.selection.select(row);
if (deferred) {
yield deferred.promise;
}
// We aim for a row 5 below the target row, since ensureRowIsVisible() does
// the bare minimum to get the row in view
for (var 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 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 each(var id in 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;
}
/*
* 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 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=0; i--) {
yield this.toggleOpenState(rowsToOpen[i], true);
}
this._refreshItemRowMap();
if (unsuppress) {
//this._treebox.endUpdateBatch();
this.selection.selectEventsSuppressed = false;
}
});
Zotero.ItemTreeView.prototype.expandMatchParents = Zotero.Promise.coroutine(function* () {
// Expand parents of child matches
if (!this._searchMode) {
return;
}
var hash = {};
for (var id in this._searchParentIDs) {
hash[id] = true;
}
if (!this.selection.selectEventsSuppressed) {
var unsuppress = this.selection.selectEventsSuppressed = true;
//this._treebox.beginUpdateBatch();
}
for (var i=0; i 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() {
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;
// More Columns menu
try {
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 = [x.getAttribute('label') for (x of treecols.getElementsByAttribute('submenu', 'true'))];
var moreItems = [];
for (let i=0; i 1) {
var tmpDirName = 'Zotero Dragged Files';
destDir.append(tmpDirName);
if (destDir.exists()) {
destDir.remove(true);
}
destDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
var copiedFiles = [];
var existingItems = [];
var existingFileNames = [];
for (var i=0; i 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, 0644);
var newName = copiedFile.leafName;
copiedFile.remove(null);
}
}
}
parentDir.copyTo(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, 0644);
var newName = copiedFile.leafName;
copiedFile.remove(null);
}
}
}
file.copyTo(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 each(var name in 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 each(var item in 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 each(var item in 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