this.getRowIndexByID(id) === false)) {
+ // In duplicates view, select the next set on delete
+ if (collectionTreeRow.isDuplicates()) {
+ if (this._rows[previousFirstSelectedRow]) {
+ var itemID = this._rows[previousFirstSelectedRow].ref.id;
+ var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID);
+ this.selectItems(setItemIDs);
+ reselect = true;
+ }
+ }
+ 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 && previousFirstSelectedRow in this._rows) {
+ this.selection.select(previousFirstSelectedRow);
+ reselect = true;
+ }
+ // If no item at previous position, select last item in list
+ else if (this._rows.length > 0 && this._rows[this._rows.length - 1]) {
+ this.selection.select(this._rows.length - 1);
+ reselect = true;
+ }
+ }
+ }
+ else {
+ await this._restoreSelection(savedSelection);
+ reselect = true;
+ }
+ }
+
+ this._rememberScrollPosition(scrollPosition);
+ }
+
+ this._updateIntroText();
+
+ // If we made changes to the selection (including reselecting the same item, which will register as
+ // a selection when selectEventsSuppressed is set to false), wait for a select event on the tree
+ // view (e.g., as triggered by itemsView.runListeners('select') in ZoteroPane::itemSelected())
+ // before returning. This guarantees that changes are reflected in the middle and right-hand panes
+ // before returning from the save transaction.
+ //
+ // If no onselect handler is set on the tree element, as is the case in the Advanced Search window,
+ // the select listeners never get called, so don't wait.
+ if (reselect && this.props.onSelectionChange) {
+ var selectPromise = this.waitForSelect();
+ this.selection.selectEventsSuppressed = false;
+ Zotero.debug("Yielding for select promise"); // TEMP
+ return selectPromise;
+ }
+ else {
+ this.selection.selectEventsSuppressed = false;
+ }
+ }
+
+ handleTyping(char) {
+ this._typingString += char.toLowerCase();
+ let allSameChar = true;
+ for (let i = this._typingString.length - 1; i >= 0; i--) {
+ if (char != this._typingString[i]) {
+ allSameChar = false;
+ break;
+ }
+ }
+ if (allSameChar) {
+ for (let i = this.selection.pivot + 1, checked = 0; checked < this._rows.length; i++, checked++) {
+ i %= this._rows.length;
+ let row = this.getRow(i);
+ if (row.getField('title').toLowerCase().indexOf(char) == 0) {
+ if (i != this.selection.pivot) {
+ this.ensureRowIsVisible(i);
+ this.selectItem([row.ref.id]);
+ }
+ break;
+ }
+ }
+ }
+ else {
+ for (let i = 0; i < this._rows.length; i++) {
+ let row = this.getRow(i);
+ if (row.getField('title').toLowerCase().indexOf(this._typingString) == 0) {
+ if (i != this.selection.pivot) {
+ this.ensureRowIsVisible(i);
+ this.selectItem([row.ref.id]);
+ }
+ break;
+ }
+ }
+ }
+ clearTimeout(this._typingTimeout);
+ this._typingTimeout = setTimeout(() => {
+ this._typingString = "";
+ }, TYPING_TIMEOUT);
+ }
+
+ handleActivate = (event, indices) => {
+ // Ignore double-clicks in duplicates view on everything except attachments
+ let items = indices.map(index => this.getRow(index).ref);
+ if (event.button == 0 && this.collectionTreeRow.isDuplicates) {
+ if (items.length != 1 || !items[0].isAttachment()) {
+ return false;
+ }
+ }
+ this.props.onActivate(event, items);
+ }
+
+ /**
+ * @param event {InputEvent}
+ * @returns {boolean} false to prevent any handling by the virtualized-table
+ */
+ handleKeyDown = (event) => {
+ if (Zotero.locked) {
+ return false;
+ }
+
+ // Handle arrow keys specially on multiple selection, since
+ // otherwise the tree just applies it to the last-selected row
+ if (this.selection.count > 1 && ["ArrowLeft", "ArrowRight"].includes(event.key)) {
+ if (event.key == "ArrowRight") {
+ this.expandSelectedRows();
+ }
+ else {
+ this.collapseSelectedRows();
+ }
+ return false;
+ }
+ if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey && COLORED_TAGS_RE.test(event.key)) {
+ let libraryID = this.collectionTreeRow.ref.libraryID;
+ let position = parseInt(event.key) - 1;
+ // When 0 is pressed, remove all colored tags
+ if (position == -1) {
+ let items = this.getSelectedItems();
+ return Zotero.Tags.removeColoredTagsFromItems(items);
+ }
+ 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 = this.getSelectedItems();
+ // Async operation and we're not waiting for the promise
+ // since we need to return false below to prevent virtualized-table from handling the event
+ const _promise = Zotero.Tags.toggleItemsListTags(items, colorData.name);
+ return false;
+ }
+ else if (event.key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
+ this.expandAllRows();
+ return false;
+ }
+ else if (event.key == '-' && !(event.shiftKey || event.ctrlKey
+ || event.altKey || event.metaKey)) {
+ this.collapseAllRows();
+ return false;
+ }
+ else if (!event.ctrlKey && !event.metaKey && (event.key.length == 1 && (event.key != " " || this.selection.isSelected(this.selection.focused)))) {
+ this.handleTyping(event.key);
+ return false;
+ }
+ return true;
+ }
+
+ render() {
+ const itemsPaneMessageHTML = this._itemsPaneMessage || this.props.emptyMessage;
+ const showMessage = !this.collectionTreeRow || this._itemsPaneMessage;
+
+ const itemsPaneMessage = ( this.props.dragAndDrop && this.onDragOver(e, -1)}
+ onDrop={e => this.props.dragAndDrop && this.onDrop(e, -1)}
+ onClick={(e) => {
+ if (e.target.dataset.href) {
+ window.ZoteroPane.loadURI(e.target.dataset.href);
+ }
+ if (e.target.dataset.action == 'open-sync-prefs') {
+ Zotero.Utilities.Internal.openPreferences('zotero-prefpane-sync');
+ }
+ }}
+ className={"items-tree-message"}
+ style={{ display: showMessage ? "flex" : "none" }}
+ // Due to some collision between React and the XUL environment
+ // setting innerHTML on a cached React node triggers an XML
+ // parsing error god knows where. So on every refresh we set a new
+ // key for the element, forcing it to be recreated. This shouldn't
+ // be a major performance concern since we're not calling #forceUpdate()
+ // that often and even if we did it's just a single div here.
+ key={Date.now()}
+ dangerouslySetInnerHTML={{ __html: itemsPaneMessageHTML }}>
+
);
+
+ let virtualizedTable = (
);
+ if (this.collectionTreeRow) {
+ virtualizedTable = React.createElement(VirtualizedTable,
+ {
+ getRowCount: () => this._rows.length,
+ id: this.id,
+ ref: ref => this.tree = ref,
+ treeboxRef: ref => this._treebox = ref,
+ renderItem: this.renderItem,
+ hide: showMessage,
+ key: "virtualized-table",
+ label: Zotero.getString('pane.items.title'),
+ alternatingRowColors: Zotero.isMac ? ['-moz-OddTreeRow', '-moz-EvenTreeRow'] : null,
+
+ showHeader: true,
+ columns: this._getColumns(),
+ onColumnPickerMenu: this._displayColumnPickerMenu,
+ onColumnSort: this._handleColumnSort,
+ getColumnPrefs: this._getColumnPrefs,
+ storeColumnPrefs: this._storeColumnPrefs,
+ containerWidth: this.domEl.clientWidth,
+
+ multiSelect: true,
+
+ onSelectionChange: this._handleSelectionChange,
+ isSelectable: () => true,
+ getParentIndex: this.getParentIndex,
+ isContainer: this.isContainer,
+ isContainerEmpty: this.isContainerEmpty,
+ isContainerOpen: this.isContainerOpen,
+ toggleOpenState: this.toggleOpenState,
+
+ onDragOver: e => this.props.dragAndDrop && this.onDragOver(e, -1),
+ onDrop: e => this.props.dragAndDrop && this.onDrop(e, -1),
+ onKeyDown: this.handleKeyDown,
+ onActivate: this.handleActivate,
+
+ onItemContextMenu: e => this.props.onContextMenu(e),
+ }
+ );
+ }
+ Zotero.debug(`itemTree.render(). Displaying ${showMessage ? "Item Pane Message" : "Item Tree"}`);
+
+ return [
+ itemsPaneMessage,
+ virtualizedTable
+ ];
+ }
+
+ async changeCollectionTreeRow(collectionTreeRow) {
+ Zotero.debug(`itemTree.changeCollectionTreeRow(): ${collectionTreeRow.id}`);
+ this.selection.selectEventsSuppressed = true;
+ this.collectionTreeRow = collectionTreeRow;
+ this.id = "item-tree-" + this.props.id + "-" + this.collectionTreeRow.visibilityGroup;
+ if (!collectionTreeRow) {
+ this.tree = null;
+ this._treebox = null;
+ return this.clearItemsPaneMessage();
+ }
+ this._itemTreeLoadingDeferred = Zotero.Promise.defer();
+ this.collectionTreeRow.view.itemTreeView = this;
+ this.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
+ // Ensures that an up to date this._columns is set
+ this._getColumns();
+
+ this.selection.clearSelection();
+ await this.refresh();
+ if (Zotero.CollectionTreeCache.error) {
+ return this.setItemsPaneMessage(Zotero.getString('pane.items.loadError'));
+ }
+ else {
+ this.clearItemsPaneMessage();
+ }
+ this.forceUpdate(() => {
+ this.selection.selectEventsSuppressed = false;
+ this._updateIntroText();
+ this._itemTreeLoadingDeferred.resolve();
+ });
+ await this._itemTreeLoadingDeferred.promise;
+ }
+
+ async refreshAndMaintainSelection(clearItemsPaneMessage=true) {
+ if (this.selection) {
+ this.selection.selectEventsSuppressed = true;
+ }
+ const selection = this.getSelectedItems(true);
+ await this.refresh();
+ clearItemsPaneMessage && this.clearItemsPaneMessage();
+ await new Promise((resolve) => {
+ this.forceUpdate(() => {
+ if (this.tree) {
+ this.tree.invalidate();
+ this._restoreSelection(selection);
+ if (this.selection) {
+ this.selection.selectEventsSuppressed = false;
+ }
+ }
+ resolve();
+ });
+ });
+ }
+
+ async selectItem(id, noRecurse) {
+ return this.selectItems([id], noRecurse);
+ }
+
+ async selectItems(ids, noRecurse) {
+ if (!ids.length) return 0;
+
+ // If no row map, we're probably in the process of switching collections,
+ // so store the items to select on the item group for later
+ if (!this._rowMap) {
+ if (this.collectionTreeRow) {
+ this.collectionTreeRow.itemsToSelect = ids;
+ Zotero.debug("_rowMap not yet set; not selecting items");
+ return 0;
+ }
+
+ Zotero.debug('Item group not found and no row map in ItemTree.selectItem() -- discarding select', 2);
+ return 0;
+ }
+
+ var idsToSelect = [];
+ for (let id of ids) {
+ let row = this._rowMap[id];
+ let item = Zotero.Items.get(id);
+
+ // Can't select a deleted item if we're not in the trash
+ if (item.deleted && !this.collectionTreeRow.isTrash()) {
+ continue;
+ }
+
+ // Get the row of the parent, if there is one
+ let parent = item.parentItemID;
+ let parentRow = parent && this._rowMap[parent];
+
+ // If row with id isn't visible, check to see if it's hidden under a parent
+ if (row == undefined) {
+ if (!parent || parentRow === undefined) {
+ // No parent -- it's not here
+
+ // Clear the quick search and tag selection and try again (once)
+ if (!noRecurse && window.ZoteroPane) {
+ let cleared1 = await window.ZoteroPane.clearQuicksearch();
+ let cleared2 = window.ZoteroPane.tagSelector
+ && window.ZoteroPane.tagSelector.clearTagSelection();
+ if (cleared1 || cleared2) {
+ return this.selectItems(ids, true);
+ }
+ }
+
+ Zotero.debug(`Couldn't find row for item ${id} -- not selecting`);
+ continue;
+ }
+
+ // 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
+ await this._closeContainer(parentRow);
+
+ // Open the parent
+ await this.toggleOpenState(parentRow);
+ }
+
+ // Since we're opening containers, we still need to reference by id
+ idsToSelect.push(id);
+ }
+
+ // Now that all items have been expanded, get associated rows
+ var rowsToSelect = [];
+ for (let id of idsToSelect) {
+ let row = this._rowMap[id];
+ if (row === undefined) {
+ Zotero.debug(`Item ${id} not in row map -- skipping`);
+ continue;
+ }
+ rowsToSelect.push(row);
+ }
+
+ if (!rowsToSelect.length) {
+ return 0;
+ }
+
+ // If items are already selected, just scroll to the top-most one
+ var selectedRows = this.selection.selected;
+ if (rowsToSelect.length == selectedRows.size && rowsToSelect.every(row => selectedRows.has(row))) {
+ this.ensureRowsAreVisible(rowsToSelect);
+ return rowsToSelect.length;
+ }
+
+ // Single item
+ if (rowsToSelect.length == 1) {
+ // this.selection.select() triggers the tree onSelect handler 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.
+ let promise;
+ let nothingToSelect = false;
+ try {
+ if (!this.selection.selectEventsSuppressed) {
+ promise = this.waitForSelect();
+ }
+ nothingToSelect = !this.selection.select(rowsToSelect[0]);
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+
+ if (!nothingToSelect && promise) {
+ await promise;
+ }
+ }
+ // Multiple items
+ else {
+ this.selection.clearSelection();
+ this.selection.selectEventsSuppressed = true;
+
+ var lastStart = 0;
+ for (let i = 0, len = rowsToSelect.length; i < len; i++) {
+ if (i == len - 1 || rowsToSelect[i + 1] != rowsToSelect[i] + 1) {
+ this.selection.rangedSelect(rowsToSelect[lastStart], rowsToSelect[i], true);
+ lastStart = i + 1;
+ }
+ }
+
+ this.selection.selectEventsSuppressed = false;
+ }
+
+ this.ensureRowsAreVisible(rowsToSelect);
+
+ return rowsToSelect.length;
+ }
+
+ /*
+ * Sort the items by the currently sorted column.
+ */
+ async sort(itemIDs) {
+ var t = new Date;
+
+ // For child items, just close and reopen parents
+ if (itemIDs) {
+ let parentItemIDs = new Set();
+ let skipped = [];
+ for (let itemID of itemIDs) {
+ let row = this._rowMap[itemID];
+ let item = this.getRow(row).ref;
+ let parentItemID = item.parentItemID;
+ if (!parentItemID) {
+ skipped.push(itemID);
+ continue;
+ }
+ parentItemIDs.add(parentItemID);
+ }
+
+ let parentRows = [...parentItemIDs].map(itemID => this._rowMap[itemID]);
+ parentRows.sort();
+
+ for (let i = parentRows.length - 1; i >= 0; i--) {
+ let row = parentRows[i];
+ this._closeContainer(row, true, true);
+ this.toggleOpenState(row, true, true);
+ }
+ this._refreshRowMap();
+
+ let numSorted = itemIDs.length - skipped.length;
+ if (numSorted) {
+ Zotero.debug(`Sorted ${numSorted} child items by parent toggle`);
+ }
+ if (!skipped.length) {
+ return;
+ }
+ itemIDs = skipped;
+ if (numSorted) {
+ Zotero.debug(`${itemIDs.length} items left to sort`);
+ }
+ }
+
+ var primaryField = this._getSortField();
+ var sortFields = this._getSortFields();
+ var order = 1;
+ const columns = this._getColumns();
+ for (const field of sortFields) {
+ const col = columns.find(c => c.dataKey == primaryField);
+ if (col) {
+ order = col.sortDirection;
+ break;
+ }
+ }
+ var collation = Zotero.getLocaleCollation();
+ var sortCreatorAsString = Zotero.Prefs.get('sortCreatorAsString');
+
+ Zotero.debug(`Sorting items list by ${sortFields.join(", ")} ${order == 1 ? "ascending" : "descending"} `
+ + (itemIDs && itemIDs.length
+ ? `for ${itemIDs.length} ` + Zotero.Utilities.pluralize(itemIDs.length, ['item', 'items'])
+ : ""));
+
+ // 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) {
+ for (let i = 0; i < sortFields.length; i++) {
+ let cmp = fieldCompare(a, b, sortFields[i]);
+ 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);
+ fieldA = matches ? matches[0] : '';
+ }
+ creatorSortCache[aItemID] = fieldA;
+ }
+ if (fieldB === undefined) {
+ let firstCreator = Zotero.Items.getSortTitle(sortStringB);
+ if (sortCreatorAsString) {
+ var fieldB = firstCreator;
+ }
+ else {
+ matches = andEtAlRegExp.exec(firstCreator);
+ 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);
+ }
+
+ var savedSelection = this.getSelectedItems(true);
+
+ // Save open state and close containers before sorting
+ var openItemIDs = this._saveOpenState(true);
+
+ // Sort specific items
+ if (itemIDs) {
+ let idsToSort = new Set(itemIDs);
+ this._rows.sort((a, b) => {
+ // Don't re-sort existing items. This assumes a stable sort(), which is the case in Firefox
+ // but not Chrome/v8.
+ if (!idsToSort.has(a.ref.id) && !idsToSort.has(b.ref.id)) return 0;
+ return rowSort(a, b) * order;
+ });
+ }
+ // Full sort
+ else {
+ this._rows.sort((a, b) => rowSort(a, b) * order);
+ }
+
+ this.tree && this.tree.invalidate();
+
+ this._refreshRowMap();
+
+ this._rememberOpenState(openItemIDs);
+ this._restoreSelection(savedSelection);
+
+ var numSorted = itemIDs ? itemIDs.length : this._rows.length;
+ Zotero.debug(`Sorted ${numSorted} ${Zotero.Utilities.pluralize(numSorted, ['item', 'items'])} `
+ + `in ${new Date - t} ms`);
+ }
+
+ async setFilter(type, data) {
+ switch (type) {
+ case 'search':
+ this.collectionTreeRow.setSearch(data);
+ break;
+ case 'tags':
+ this.collectionTreeRow.setTags(data);
+ break;
+ default:
+ throw ('Invalid filter type in setFilter');
+ }
+ await this.refreshAndMaintainSelection()
+ };
+
+ ensureRowsAreVisible(indices) {
+ if (!this._treebox) return;
+ let itemHeight = 20; // px
+ if (Zotero.isLinux) {
+ itemHeight = 22;
+ }
+ itemHeight *= Zotero.Prefs.get('fontSize');
+
+ const pageLength = Math.floor(this._treebox.getWindowHeight() / itemHeight);
+ const maxBuffer = 5;
+
+ indices = Array.from(indices).filter(index => index < this._rows.length);
+ indices.sort((a, b) => a - b);
+
+ var indicesWithParents = [];
+ for (let row of indices) {
+ let parent = this.getParentIndex(row);
+ indicesWithParents.push(parent != -1 ? parent : row);
+ }
+
+ // If we can fit all parent indices in view, do that
+ for (let buffer = maxBuffer; buffer >= 0; buffer--) {
+ if (indicesWithParents[indicesWithParents.length - 1] - indicesWithParents[0] - buffer < pageLength) {
+ //Zotero.debug(`We can fit all parent indices with buffer ${buffer}`);
+ this.ensureRowIsVisible(indicesWithParents[0] - buffer);
+ this.ensureRowIsVisible(indicesWithParents[indicesWithParents.length-1] + buffer);
+ return;
+ }
+ }
+
+ // If we can fit all indices in view, do that
+ for (let buffer = maxBuffer; buffer >= 0; buffer--) {
+ if (indices[indices.length - 1] - indices[0] - buffer < pageLength) {
+ //Zotero.debug(`We can fit all indices with buffer ${buffer}`);
+ this.ensureRowIsVisible(indices[0] - buffer);
+ this.ensureRowIsVisible(indices[indices.length-1] + buffer);
+ return;
+ }
+ }
+
+ // If the first parent row isn't in view and we have enough room, make it visible, trying to
+ // put it five indices from the top
+ if (indices[0] != indicesWithParents[0]) {
+ for (let buffer = maxBuffer; buffer >= 0; buffer--) {
+ if (indices[0] - indicesWithParents[0] - buffer <= pageLength) {
+ //Zotero.debug(`Scrolling to first parent minus ${buffer}`);
+ this.ensureRowIsVisible(indicesWithParents[0] + buffer);
+ this.ensureRowIsVisible(indicesWithParents[0] - buffer);
+ return;
+ }
+ }
+ }
+
+ // Otherwise just put the first row at the top
+ //Zotero.debug("Scrolling to first row " + Math.max(indices[0] - maxBuffer, 0));
+ this.ensureRowIsVisible(indices[0] - maxBuffer);
+ this.ensureRowIsVisible(indices[0] + maxBuffer);
+ }
+
+ toggleOpenState = async (index, skipRowMapRefresh=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(index)) {
+ return;
+ }
+
+ if (this.isContainerOpen(index)) {
+ return this._closeContainer(index, skipRowMapRefresh);
+ }
+
+ var count = 0;
+ var level = this.getLevel(index);
+
+ //
+ // Open
+ //
+ var item = this.getRow(index).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 ItemTreeRow(newRows[i], level + 1, false),
+ index + i + 1,
+ true
+ );
+ }
+ }
+
+ this._rows[index].isOpen = true;
+
+ if (count == 0) {
+ return;
+ }
+
+ if (!skipRowMapRefresh) {
+ await this._refreshPromise;
+ this.tree.invalidate(index);
+
+ Zotero.debug('Refreshing item row map');
+ this._refreshRowMap();
+ }
+ }
+
+ expandMatchParents(searchParentIDs) {
+ // Expand parents of child matches
+ if (!this._searchMode) {
+ return;
+ }
+
+ var savedSelection = this.getSelectedItems(true);
+ for (var i=0; i b - a);
+ for (const index of indices) {
+ if (this.isContainer(index) && !this.isContainerOpen(index)) {
+ this.toggleOpenState(index, true);
+ }
+ }
+ this._refreshRowMap();
+ this._restoreSelection(selectedItems);
+ this.tree.invalidate();
+ this.selection.selectEventsSuppressed = false;
+ }
+
+
+ collapseSelectedRows() {
+ this.selection.selectEventsSuppressed = true;
+ const selectedItems = this.getSelectedItems(true);
+ // Reverse sort and so we don't mess up indices of subsequent
+ // items when collapsing
+ const indices = Array.from(this.selection.selected).sort((a, b) => b - a);
+ for (const index of indices) {
+ if (this.isContainer(index)) {
+ this._closeContainer(index, true);
+ }
+ }
+ this._refreshRowMap();
+ this._restoreSelection(selectedItems, false);
+ this.tree.invalidate();
+ this.selection.selectEventsSuppressed = false;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////
+ //
+ // Data access methods
+ //
+ // //////////////////////////////////////////////////////////////////////////////
+
+ getCellText(index, column) {
+ return this._getRowData(index)[column];
+ }
+
+ async deleteSelection(force) {
+ if (arguments.length > 1) {
+ throw new Error("ItemTree.deleteSelection() no longer takes two parameters");
+ }
+
+ if (this.selection.count == 0) {
+ return;
+ }
+
+ // Collapse open items
+ for (var i=0; i this.getRow(index).id);
+
+ var collectionTreeRow = this.collectionTreeRow;
+
+ if (collectionTreeRow.isBucket()) {
+ collectionTreeRow.ref.deleteItems(ids);
+ }
+ if (collectionTreeRow.isTrash()) {
+ await Zotero.Items.erase(ids);
+ }
+ else if (collectionTreeRow.isLibrary(true) || force) {
+ await Zotero.Items.trashTx(ids);
+ }
+ else if (collectionTreeRow.isCollection()) {
+ await Zotero.DB.executeTransaction(async () => {
+ await collectionTreeRow.ref.removeItems(ids);
+ });
+ }
+ else if (collectionTreeRow.isPublications()) {
+ await Zotero.Items.removeFromPublications(ids.map(id => Zotero.Items.get(id)));
+ }
+ }
+
+ getSelectedItems(asIDs) {
+ var items = this.selection ? Array.from(this.selection.selected) : [];
+ items = items.filter(index => index < this._rows.length);
+ try {
+ if (asIDs) return items.map(index => this.getRow(index).ref.id);
+ return items.map(index => this.getRow(index).ref);
+ } catch (e) {
+ Zotero.debug(items);
+ throw e;
+ }
+ }
+
+ isContainer = (index) => {
+ return this.getRow(index).ref.isRegularItem();
+ }
+
+ isContainerOpen = (index) => {
+ return this.getRow(index).isOpen;
+ }
+
+ isContainerEmpty = (index) => {
+ if (this.regularOnly) {
+ return true;
+ }
+
+ var item = this.getRow(index).ref;
+ if (!item.isRegularItem()) {
+ return true;
+ }
+ var includeTrashed = this.collectionTreeRow.isTrash();
+ return item.numNotes(includeTrashed) === 0 && item.numAttachments(includeTrashed) == 0;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ ///
+ /// Drag-and-drop methods
+ ///
+ ////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Start a drag using HTML 5 Drag and Drop
+ */
+ onDragStart = (event, index) => {
+ // See note in LibraryTreeView::setDropEffect()
+ if (Zotero.isWin || Zotero.isLinux) {
+ event.dataTransfer.effectAllowed = 'copyMove';
+ }
+
+ // Propagate selection before we set the drag image if dragging not one of the selected rows
+ if (!this.selection.isSelected(index)) {
+ this.selection.select(index);
+ }
+ // Set drag image
+ const dragElems = this.domEl.querySelectorAll('.selected');
+ for (let elem of dragElems) {
+ elem = elem.cloneNode(true);
+ elem.style.position = "initial";
+ this._dragImageContainer.appendChild(elem);
+ }
+ event.dataTransfer.setDragImage(this._dragImageContainer, 0, 0);
+
+ var itemIDs = this.getSelectedItems(true);
+ event.dataTransfer.setData("zotero/item", itemIDs);
+
+ var items = Zotero.Items.get(itemIDs);
+ Zotero.DragDrop.currentDragSource = this.collectionTreeRow;
+
+ // 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.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 = Cc["@mozilla.org/network/protocol;1?name=file"]
+ .createInstance(Ci.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 (set via /ping from connector)
+ var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
+
+ 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 {
+ Cu.reportError("Invalid Quick Copy mode");
+ }
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Cu.reportError(e + " with '" + format.id + "'");
+ }
+ }
+
+ /**
+ * We use this to set the drag action, which is used by view.canDrop(),
+ * based on the view's canDropCheck() and modifier keys.
+ */
+ onDragOver = (event, row) => {
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ Zotero.DragDrop.currentOrientation = getDragTargetOrient(event);
+ Zotero.debug(`Dragging over item ${row} with ${Zotero.DragDrop.currentOrientation}, drop row: ${this._dropRow}`);
+
+ var target = event.target;
+ if (target.classList.contains('items-tree-message')) {
+ let doc = target.ownerDocument;
+ // Consider a drop on the items pane message box (e.g., when showing the welcome text)
+ // a drop on the items tree
+ if (target.firstChild.hasAttribute('allowdrop')) {
+ target = doc.querySelector('#zotero-items-tree treechildren');
+ }
+ else {
+ this.setDropEffect(event, "none");
+ return false;
+ }
+ }
+
+ if (!this.canDropCheck(row, Zotero.DragDrop.currentOrientation, event.dataTransfer)) {
+ this.setDropEffect(event, "none");
+ return false;
+ }
+
+ if (event.dataTransfer.getData("zotero/item")) {
+ var sourceCollectionTreeRow = Zotero.DragDrop.getDragSource();
+ if (sourceCollectionTreeRow) {
+ var targetCollectionTreeRow = this.collectionTreeRow;
+
+ if (!targetCollectionTreeRow) {
+ this.setDropEffect(event, "none");
+ return false;
+ }
+
+ if (sourceCollectionTreeRow.id == targetCollectionTreeRow.id) {
+ // If dragging from the same source, do a move
+ this.setDropEffect(event, "move");
+ return false;
+ }
+ // If the source isn't a collection, the action has to be a copy
+ if (!sourceCollectionTreeRow.isCollection()) {
+ this.setDropEffect(event, "copy");
+ return false;
+ }
+ // For now, all cross-library drags are copies
+ if (sourceCollectionTreeRow.ref.libraryID != targetCollectionTreeRow.ref.libraryID) {
+ this.setDropEffect(event, "copy");
+ return false;
+ }
+ }
+
+ if ((Zotero.isMac && event.metaKey) || (!Zotero.isMac && event.shiftKey)) {
+ this.setDropEffect(event, "move");
+ }
+ else {
+ this.setDropEffect(event, "copy");
+ }
+ }
+ else if (event.dataTransfer.types.contains("application/x-moz-file")) {
+ // As of Aug. 2013 nightlies:
+ //
+ // - Setting the dropEffect only works on Linux and OS X.
+ //
+ // - Modifier keys don't show up in the drag event on OS X until the
+ // drop (https://bugzilla.mozilla.org/show_bug.cgi?id=911918),
+ // so since we can't show a correct effect, we leave it at
+ // the default 'move', the least misleading option, and set it
+ // below in onDrop().
+ //
+ // - The cursor effect gets set by the system on Windows 7 and can't
+ // be overridden.
+ if (!Zotero.isMac) {
+ if (event.shiftKey) {
+ if (event.ctrlKey) {
+ event.dataTransfer.dropEffect = "link";
+ }
+ else {
+ event.dataTransfer.dropEffect = "move";
+ }
+ }
+ else {
+ event.dataTransfer.dropEffect = "copy";
+ }
+ }
+ }
+ return false;
+ } finally {
+ let prevDropRow = this._dropRow;
+ if (event.dataTransfer.dropEffect != 'none') {
+ this._dropRow = row;
+ } else {
+ this._dropRow = null;
+ }
+ if (prevDropRow != this._dropRow) {
+ typeof prevDropRow == 'number' && this.tree.invalidateRow(prevDropRow);
+ this.tree.invalidateRow(row);
+ }
+ }
+ }
+
+ onDragEnd = () => {
+ this._dragImageContainer.innerHTML = "";
+ this._dropRow = null;
+ this.tree.invalidate();
+ }
+
+ onDragLeave = () => {
+ let dropRow = this._dropRow;
+ this._dropRow = null;
+ this.tree.invalidateRow(dropRow);
+ }
+
+ /**
+ * Called by treeRow.onDragOver() before setting the dropEffect
+ */
+ canDropCheck = (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
+ // Cannot drop anything on attachments/notes
+ if (!rowItem.isRegularItem()) {
+ return false;
+ }
+ }
+
+ 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 or publications
+ else if (collectionTreeRow.isSearch() || collectionTreeRow.isPublications()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ };
+
+ /*
+ * Called when something's been dropped on or next to a row
+ */
+ onDrop = async (event, row) => {
+ const dataTransfer = event.dataTransfer;
+ var orient = Zotero.DragDrop.currentOrientation;
+ if (row == -1) {
+ row = 0;
+ orient = -1;
+ }
+ this._dropRow = null;
+ Zotero.DragDrop.currentDragSource = null;
+ if (!dataTransfer.dropEffect || dataTransfer.dropEffect == "none") {
+ 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
+ await Zotero.DB.executeTransaction(async function () {
+ for (let i=0; i {
+ let selection = this.selection;
+ // Update aria-activedescendant on the tree
+ if (this.collectionTreeRow.isDuplicates() && selection.count == 1) {
+ var itemID = this.getRow(selection.focused).ref.id;
+ var setItemIDs = this.collectionTreeRow.ref.getSetItemsByItemID(itemID);
+
+ // We are modifying the selection object directly here
+ // which won't trigger item updates
+ for (let id of setItemIDs) {
+ selection.selected.add(this._rowMap[id]);
+ }
+ }
+ if (shouldDebounce) {
+ this._onSelectionChangeDebounced();
+ }
+ else {
+ this._onSelectionChange();
+ }
+ }
+
+ async _closeContainer(index, 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(index)) return;
+ if (!this.isContainerOpen(index)) return;
+
+ var count = 0;
+ var level = this.getLevel(index);
+
+ // Remove child rows
+ while ((index + 1 < this._rows.length) && (this.getLevel(index + 1) > level)) {
+ // Skip the map update here and just refresh the whole map below,
+ // since we might be removing multiple rows
+ this._removeRow(index + 1, true);
+ count++;
+ }
+
+ this._rows[index].isOpen = false;
+
+ if (count == 0) {
+ return;
+ }
+
+ if (!skipRowMapRefresh) {
+ await this._refreshPromise;
+ this.tree.invalidate(index);
+
+ Zotero.debug('Refreshing item row map');
+ this._refreshRowMap();
+ }
+ }
+
+ _getRowData = (index) => {
+ var treeRow = this.getRow(index);
+ if (!treeRow) {
+ throw new Error(`Attempting to get row data for a non-existant tree row ${index}`);
+ }
+ var itemID = treeRow.id;
+
+ // If value is available, retrieve synchronously
+ if (this._rowCache[itemID]) {
+ return this._rowCache[itemID];
+ }
+
+ let row = {};
+
+ // Mark items not matching search as context rows, displayed in gray
+ if (this._searchMode && !this._searchItemIDs.has(itemID)) {
+ row.contextRow = true;
+ }
+
+ row.hasAttachment = "";
+ // Don't show pie for open parent items, since we show it for the
+ // child item
+ if (!this.isContainer(index) || !this.isContainerOpen(index)) {
+ var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref);
+ row.hasAttachment = num === false ? "pie" : "pie" + num;
+ }
+
+ // Style unread items in feeds
+ if (treeRow.ref.isFeedItem && !treeRow.ref.isRead) {
+ row.unread = true;
+ }
+
+
+ row.itemType = Zotero.ItemTypes.getLocalizedString(treeRow.ref.itemTypeID);
+ // Year column is just date field truncated
+ row.year = treeRow.getField('date', true).substr(0, 4);
+ if (row.year) {
+ // Don't show anything for unparsed year
+ if (row.year === "0000") {
+ row.year = "";
+ }
+ // Show pre-1000 year without leading zeros
+ else if (row.year < 1000) {
+ row.year = parseInt(row.year);
+ }
+ }
+ row.numNotes = treeRow.numNotes() || "";
+ row.title = treeRow.ref.getDisplayTitle();
+
+ for (let col of this.props.columns) {
+ let key = col.dataKey;
+ let val = row[key];
+ if (val === undefined) {
+ val = treeRow.getField(key);
+ }
+
+ switch (key) {
+ // Format dates as short dates in proper locale order and locale time
+ // (e.g. "4/4/07 14:27:23")
+ case 'dateAdded':
+ case 'dateModified':
+ case 'accessDate':
+ case 'date':
+ if (key == 'date' && !this.collectionTreeRow.isFeed()) {
+ break;
+ }
+ if (val) {
+ let date = Zotero.Date.sqlToDate(val, true);
+ if (date) {
+ // If no time, interpret as local, not UTC
+ if (Zotero.Date.isSQLDate(val)) {
+ date = Zotero.Date.sqlToDate(val);
+ val = date.toLocaleDateString();
+ }
+ else {
+ val = date.toLocaleString();
+ }
+ }
+ else {
+ val = '';
+ }
+ }
+ }
+ row[key] = val;
+ }
+
+ return this._rowCache[itemID] = row;
+ }
+
+ _getColumnPrefs = () => {
+ if (!this.props.persistColumns) return {};
+ if (this._columnPrefs) return this._columnPrefs;
+
+ const persistSettings = JSON.parse(Zotero.Prefs.get('pane.persist') || "{}");
+ this._columnPrefs = persistSettings[this._columnsId];
+ return this._columnPrefs || {};
+ }
+
+ // N.B. We are banging the prefs with this new implementation somewhat more:
+ // column resize, hiding and order changes require pref reads and sets
+ // but we do not have the magic of xul itemtree to handle this for us.
+ // We should try to avoid calling this function as much as possible since it writes
+ // to disk and might introduce undesirable performance costs on HDDs (which
+ // will not be obvious on SSDs)
+ _storeColumnPrefs = (prefs) => {
+ if (!this.props.persistColumns) return;
+ Zotero.debug(`Storing itemTree ${this._columnsId} column prefs`, 2);
+ this._columnPrefs = prefs;
+ let persistSettings = JSON.parse(Zotero.Prefs.get('pane.persist') || "{}");
+ persistSettings[this._columnsId] = prefs;
+ this._columns = this._columns.map(column => Object.assign(column, prefs[column.dataKey]))
+ .sort((a, b) => a.ordinal - b.ordinal);
+ Zotero.Prefs.set('pane.persist', JSON.stringify(persistSettings));
+ }
+
+ _setLegacyColumnSettings(column) {
+ let persistSettings = JSON.parse(Zotero.Prefs.get('pane.persist') || "{}");
+ const legacyDataKey = "zotero-items-column-" + column.dataKey;
+ const legacyPersistSetting = persistSettings[legacyDataKey];
+ if (legacyPersistSetting) {
+ // Remove legacy pref
+ // TODO: uncomment once xul item tree fully phased out
+ // delete persistSettings[legacyDataKey];
+ for (const key in legacyPersistSetting) {
+ if (typeof legacyPersistSetting[key] == "string") {
+ if (key == 'sortDirection') {
+ legacyPersistSetting[key] = legacyPersistSetting[key] == 'ascending' ? 1 : -1;
+ }
+ else {
+ try {
+ legacyPersistSetting[key] = JSON.parse(legacyPersistSetting[key]);
+ } catch (e) {}
+ }
+ }
+ if (key == 'ordinal') {
+ legacyPersistSetting[key] /= 2;
+ }
+ }
+ Zotero.Prefs.set('pane.persist', JSON.stringify(persistSettings));
+ }
+ return Object.assign({}, column, legacyPersistSetting || {});
+ }
+
+ _getColumns() {
+ if (!this.collectionTreeRow) {
+ return [];
+ }
+
+ const visibilityGroup = this.collectionTreeRow.visibilityGroup;
+ const prefKey = this.id + "-" + visibilityGroup;
+ if (this._columnsId == prefKey) {
+ return this._columns;
+ }
+
+ this._columnsId = prefKey;
+ this._columns = [];
+
+ let columnsSettings = this._getColumnPrefs();
+
+ let hasDefaultIn = this.props.columns.some(column => 'defaultIn' in column);
+ for (let column of this.props.columns) {
+ if (this.props.persistColumns) {
+ if (column.disabledIn && column.disabledIn.includes(visibilityGroup)) continue;
+ const columnSettings = columnsSettings[column.dataKey];
+ if (!columnSettings) {
+ column = this._setLegacyColumnSettings(column);
+ }
+
+ // Also includes a `hidden` pref and overrides the above if available
+ column = Object.assign({}, column, columnSettings || {});
+
+ if (column.sortDirection) {
+ this._sortedColumn = column;
+ }
+ // If column does not have an "ordinal" field it means it
+ // is newly added
+ if (!("ordinal" in column)) {
+ column.ordinal = this.props.columns.findIndex(c => c.dataKey == column.dataKey);
+ }
+ }
+ else {
+ column = Object.assign({}, column);
+ }
+ // Initial hidden value
+ if (!("hidden" in column)) {
+ if (hasDefaultIn) {
+ column.hidden = !(column.defaultIn && column.defaultIn.has(visibilityGroup));
+ }
+ else {
+ column.hidden = false;
+ }
+ }
+ this._columns.push(column);
+ }
+
+ return this._columns;
+ }
+
+ _getColumn(index) {
+ return this._getColumns()[index];
+ }
+
+ _updateIntroText() {
+ if (!window.ZoteroPane) {
+ return;
+ }
+
+ if (this.collectionTreeRow && !this.rowCount) {
+ let doc = this._ownerDocument;
+ let ns = 'http://www.w3.org/1999/xhtml';
+ let div;
+
+ // My Library and no groups
+ if (this.collectionTreeRow.isLibrary() && !Zotero.Groups.getAll().length) {
+ div = doc.createElementNS(ns, 'div');
+ let p = doc.createElementNS(ns, 'p');
+ let html = Zotero.getString(
+ 'pane.items.intro.text1',
+ [
+ Zotero.clientName
+ ]
+ );
+ // Encode special chars, which shouldn't exist
+ html = Zotero.Utilities.htmlSpecialChars(html);
+ html = `${html} `;
+ p.innerHTML = html;
+ div.appendChild(p);
+
+ p = doc.createElementNS(ns, 'p');
+ html = Zotero.getString(
+ 'pane.items.intro.text2',
+ [
+ Zotero.getString('connector.name', Zotero.clientName),
+ Zotero.clientName
+ ]
+ );
+ // Encode special chars, which shouldn't exist
+ html = Zotero.Utilities.htmlSpecialChars(html);
+ html = html.replace(
+ /\[([^\]]+)](.+)\[([^\]]+)]/,
+ `$1 `
+ + '$2'
+ + `$3 `
+ );
+ p.innerHTML = html;
+ div.appendChild(p);
+
+ p = doc.createElementNS(ns, 'p');
+ html = Zotero.getString('pane.items.intro.text3', [Zotero.clientName]);
+ // Encode special chars, which shouldn't exist
+ html = Zotero.Utilities.htmlSpecialChars(html);
+ html = html.replace(
+ /\[([^\]]+)]/,
+ '$1 '
+ );
+ p.innerHTML = html;
+ div.appendChild(p);
+
+ // Activate text links
+ for (let span of div.getElementsByTagName('span')) {
+ if (span.classList.contains('text-link')) {
+ if (span.hasAttribute('data-href')) {
+ span.onclick = function () {
+ doc.defaultView.ZoteroPane.loadURI(this.getAttribute('data-href'));
+ };
+ }
+ else if (span.hasAttribute('data-action')) {
+ if (span.getAttribute('data-action') == 'open-sync-prefs') {
+ span.onclick = () => {
+ Zotero.Utilities.Internal.openPreferences('zotero-prefpane-sync');
+ };
+ }
+ }
+ }
+ }
+
+ div.setAttribute('allowdrop', true);
+ }
+ // My Publications
+ else if (this.collectionTreeRow.isPublications()) {
+ div = doc.createElementNS(ns, 'div');
+ div.className = 'publications';
+ let p = doc.createElementNS(ns, 'p');
+ p.textContent = Zotero.getString('publications.intro.text1', window.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 tags to placeholders
+ html = html.replace('', ':b:').replace(' ', ':/b:');
+ // Encode any other special chars, which shouldn't exist
+ html = Zotero.Utilities.htmlSpecialChars(html);
+ // Restore bold text
+ html = html.replace(':b:', '').replace(':/b:', ' ');
+ p.innerHTML = html; // AMO note: markup from hard-coded strings and filtered above
+ div.appendChild(p);
+ }
+ if (div) {
+ this._introText = true;
+ doc.defaultView.ZoteroPane_Local.setItemsPaneMessage(div);
+ return;
+ }
+ this._introText = null;
+ }
+
+ if (this._introText || this._introText === null) {
+ window.ZoteroPane.clearItemsPaneMessage();
+ this._introText = false;
+ }
+ }
+
+ /**
+ * Restore a scroll position returned from _saveScrollPosition()
+ */
+ _rememberScrollPosition(scrollPosition) {
+ if (!scrollPosition || !scrollPosition.id || !this._treebox) {
+ return;
+ }
+ var row = this.getRowIndexByID(scrollPosition.id);
+ if (row === false) {
+ return;
+ }
+ this._treebox.scrollToRow(Math.max(row - scrollPosition.offset, 0));
+ }
+
+ /**
+ * Return an object describing the current scroll position to restore after changes
+ *
+ * @return {Object|Boolean} - Object with .id (a treeViewID) and .offset, or false if no rows
+ */
+ _saveScrollPosition() {
+ if (!this._treebox) return false;
+ var treebox = this._treebox;
+ var first = treebox.getFirstVisibleRow();
+ if (!first) {
+ return false;
+ }
+ var last = treebox.getLastVisibleRow();
+ var firstSelected = null;
+ for (let i = first; i <= last; i++) {
+ // If an object is selected, keep the first selected one in position
+ if (this.selection.isSelected(i)) {
+ let row = this.getRow(i);
+ if (!row) return false;
+ return {
+ id: row.ref.treeViewID,
+ offset: i - first
+ };
+ }
+ }
+
+ // Otherwise keep the first visible row in position
+ let row = this.getRow(first);
+ if (!row) return false;
+ return {
+ id: row.ref.treeViewID,
+ offset: 0
+ };
+ }
+
+ _saveOpenState(close) {
+ if (!this.tree) return [];
+ var itemIDs = [];
+ if (close) {
+ if (!this.selection.selectEventsSuppressed) {
+ var unsuppress = this.selection.selectEventsSuppressed = true;
+ }
+ }
+ for (var i=0; i=0; i--) {
+ this.toggleOpenState(rowsToOpen[i], true);
+ }
+ this._refreshRowMap();
+ if (unsuppress) {
+ this.selection.selectEventsSuppressed = false;
+ }
+ }
+
+ /**
+ *
+ * @param selection
+ * @param {Boolean} expandCollapsedParents - if an item to select is in a collapsed parent
+ * will expand the parent, otherwise the item is ignored
+ * @private
+ */
+ async _restoreSelection(selection, expandCollapsedParents=true) {
+ if (!selection.length || !this._treebox) {
+ return;
+ }
+
+ this.selection.clearSelection();
+
+ if (!this.selection.selectEventsSuppressed) {
+ var unsuppress = this.selection.selectEventsSuppressed = true;
+ }
+
+ try {
+ for (let i = 0; i < selection.length; i++) {
+ if (this._rowMap[selection[i]] != null) {
+ this.selection.toggleSelect(this._rowMap[selection[i]]);
+ }
+ // Try the parent
+ else if (expandCollapsedParents) {
+ var item = Zotero.Items.get(selection[i]);
+ if (!item) {
+ continue;
+ }
+
+ var parent = item.parentItemID;
+ if (!parent) {
+ continue;
+ }
+
+ if (this._rowMap[parent] != null) {
+ await this._closeContainer(this._rowMap[parent]);
+ await this.toggleOpenState(this._rowMap[parent]);
+ this.selection.toggleSelect(this._rowMap[selection[i]]);
+ }
+ }
+ }
+ }
+ // Ignore NS_ERROR_UNEXPECTED from nsITreeSelection::toggleSelect(), apparently when the tree
+ // disappears before it's called (though I can't reproduce it):
+ //
+ // https://forums.zotero.org/discussion/69226/papers-become-invisible-in-the-middle-pane
+ catch (e) {
+ Zotero.logError(e);
+ }
+
+ this.ensureRowsAreVisible(Array.from(this.selection.selected.keys()));
+
+ if (unsuppress) {
+ this.selection.selectEventsSuppressed = false;
+ }
+ }
+
+ _handleColumnSort = async (index, sortDirection) => {
+ let columnSettings = this._getColumnPrefs();
+ let column = this._getColumn(index);
+ if (this.collectionTreeRow.isFeed()) {
+ return;
+ }
+ if (column.dataKey == '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()) {
+ await item.getBestAttachmentState();
+ }
+ }
+ Zotero.debug("Cached best attachment states in " + (new Date - t) + " ms");
+ this._cachedBestAttachmentStates = true;
+ }
+ }
+ if (this._sortedColumn && this._sortedColumn.dataKey == column.dataKey) {
+ this._sortedColumn.sortDirection = sortDirection;
+ if (columnSettings[column.dataKey]) {
+ columnSettings[column.dataKey].sortDirection = this._sortedColumn.sortDirection;
+ }
+ }
+ else {
+ if (this._sortedColumn) {
+ delete this._sortedColumn.sortDirection;
+ if (columnSettings[column.dataKey]) {
+ delete columnSettings[this._sortedColumn.dataKey].sortDirection;
+ }
+ }
+ this._sortedColumn = column;
+ this._sortedColumn.sortDirection = sortDirection;
+ if (columnSettings[column.dataKey]) {
+ columnSettings[column.dataKey].sortDirection = this._sortedColumn.sortDirection;
+ }
+ }
+
+ this._storeColumnPrefs(columnSettings);
+ await this._refreshPromise;
+ this.selection.selectEventsSuppressed = true;
+ await this.sort();
+ this.forceUpdate(() => {
+ this.tree.invalidate();
+ this.selection.selectEventsSuppressed = false;
+ })
+ }
+
+ _displayColumnPickerMenu = (event) => {
+ if (!this.props.columnPicker) return;
+ const ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+ const prefix = 'zotero-column-picker-';
+ const doc = document;
+
+ const menupopup = doc.createElementNS(ns, 'menupopup');
+ menupopup.id = 'zotero-column-picker';
+ menupopup.addEventListener('popuphiding', (event) => {
+ if (event.target.id == menupopup.id) {
+ document.children[0].removeChild(menupopup);
+ }
+ });
+
+ const columns = this._getColumns();
+ for (let i = 0; i < columns.length; i++) {
+ const column = columns[i];
+ let menuitem = doc.createElementNS(ns, 'menuitem');
+ menuitem.setAttribute('type', 'checkbox');
+ menuitem.setAttribute('label', Zotero.Intl.strings[column.label]);
+ menuitem.setAttribute('colindex', i);
+ menuitem.addEventListener('command', () => this.tree._columns.toggleHidden(i));
+ if (!column.hidden) {
+ menuitem.setAttribute('checked', true);
+ }
+ if (column.disabledIn && column.disabledIn.includes(this.collectionTreeRow.visibilityGroup)) {
+ menuitem.setAttribute('disabled', true);
+ }
+ menupopup.appendChild(menuitem);
+ }
+
+ 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 moreItems = [];
+ for (let i = 0; i < columns.length; i++) {
+ const column = columns[i];
+ if (column.submenu) {
+ moreItems.push(menupopup.children[i]);
+ }
+ }
+
+ // 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));
+ });
+
+ let sep = doc.createElementNS(ns, 'menuseparator');
+ menupopup.appendChild(sep);
+ moreMenu.appendChild(moreMenuPopup);
+ menupopup.appendChild(moreMenu);
+ }
+ catch (e) {
+ Cu.reportError(e);
+ Zotero.debug(e, 1);
+ }
+
+ //
+ // Secondary Sort menu
+ //
+ if (!this.collectionTreeRow.isFeed()) {
+ try {
+ const id = prefix + 'sort-menu';
+ const primaryField = this._getSortField();
+ const sortFields = this._getSortFields();
+ let secondaryField = false;
+ if (sortFields[1]) {
+ secondaryField = sortFields[1];
+ }
+
+ const primaryFieldLabel = Zotero.Intl.strings[columns.find(c => c.dataKey == primaryField).label];
+
+ const sortMenu = doc.createElementNS(ns, 'menu');
+ sortMenu.setAttribute('label',
+ Zotero.getString('pane.items.columnChooser.secondarySort', primaryFieldLabel));
+ sortMenu.setAttribute('anonid', id);
+
+ const sortMenuPopup = doc.createElementNS(ns, 'menupopup');
+ sortMenuPopup.setAttribute('anonid', id + '-popup');
+
+ // Generate menuitems
+ const sortOptions = [
+ 'title',
+ 'firstCreator',
+ 'itemType',
+ 'date',
+ 'year',
+ 'publisher',
+ 'publicationTitle',
+ 'dateAdded',
+ 'dateModified'
+ ];
+ for (let field of sortOptions) {
+ // 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 = Zotero.Intl.strings[columns.find(c => c.dataKey == field).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.addEventListener('command', async () => {
+ if (this._setSecondarySortField(field)) {
+ await this.sort();
+ }
+ })
+ sortMenuPopup.appendChild(sortMenuItem);
+ }
+
+ sortMenu.appendChild(sortMenuPopup);
+ menupopup.appendChild(sortMenu);
+ }
+ catch (e) {
+ Cu.reportError(e);
+ Zotero.debug(e, 1);
+ }
+ }
+
+ let sep = doc.createElementNS(ns, 'menuseparator');
+ sep.setAttribute('anonid', prefix + 'sep');
+ menupopup.appendChild(sep);
+
+ // TODO: RESTORE DEFAULT ORDER option
+ //
+
+ document.children[0].appendChild(menupopup);
+ menupopup.openPopup(null, null, event.clientX + 2, event.clientY + 2);
+ }
+
+ _getSortField() {
+ if (this.collectionTreeRow.isFeed()) {
+ return 'id';
+ }
+ var column = this._sortedColumn;
+ if (!column) {
+ column = this._getColumns().find(col => !col.hidden);
+ }
+ // zotero-items-column-_________
+ return column.dataKey;
+ }
+
+
+ _getSortFields() {
+ 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);
+ Cu.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;
+ }
+
+ _getSecondarySortField() {
+ var primaryField = this._getSortField();
+ var secondaryField = Zotero.Prefs.get('secondarySort.' + primaryField);
+ if (!secondaryField || secondaryField == primaryField) {
+ return false;
+ }
+ return secondaryField;
+ }
+
+ _setSecondarySortField(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;
+ }
+
+ _getIcon(index) {
+ var item = this.getRow(index).ref;
+ var itemType = Zotero.ItemTypes.getName(item.itemTypeID);
+ if (itemType == 'attachment') {
+ var linkMode = item.attachmentLinkMode;
+
+ if (item.attachmentContentType == 'application/pdf' && item.isFileAttachment()) {
+ if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ itemType += 'PdfLink';
+ }
+ else {
+ itemType += 'Pdf';
+ }
+ }
+ else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
+ itemType += "File";
+ }
+ else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ itemType += "Link";
+ }
+ else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) {
+ itemType += "Snapshot";
+ }
+ else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
+ itemType += "WebLink";
+ }
+ }
+ let iconClsName = "IconTreeitem" + Zotero.Utilities.capitalize(itemType);
+ if (!Icons[iconClsName]) {
+ iconClsName = "IconTreeitem";
+ }
+ var icon = getDomElement(iconClsName);
+ if (!icon) {
+ Zotero.debug('Could not find tree icon for "' + itemType + '"');
+ return document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ }
+ return icon;
+ }
+
+ _getTagSwatch(color) {
+ let span = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ span.className = 'tag-swatch';
+ span.style.backgroundColor = color;
+ return span;
+ }
+};
+
+var ItemTreeRow = function(ref, level, isOpen)
+{
+ this.ref = ref; //the item associated with this
+ this.level = level;
+ this.isOpen = isOpen;
+ this.id = ref.id;
+}
+
+ItemTreeRow.prototype.getField = function(field, unformatted)
+{
+ return this.ref.getField(field, unformatted, true);
+}
+
+ItemTreeRow.prototype.numNotes = function() {
+ if (this.ref.isNote()) {
+ return 0;
+ }
+ if (this.ref.isAttachment()) {
+ return this.ref.note !== '' ? 1 : 0;
+ }
+ return this.ref.numNotes(false, true) || 0;
+}
+
+Zotero.Utilities.Internal.makeClassEventDispatcher(ItemTree);
+
+module.exports = ItemTree;
diff --git a/chrome/content/zotero/containers/itemTreeColumns.jsx b/chrome/content/zotero/containers/itemTreeColumns.jsx
new file mode 100644
index 0000000000..7f77c7725f
--- /dev/null
+++ b/chrome/content/zotero/containers/itemTreeColumns.jsx
@@ -0,0 +1,289 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 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 *****
+*/
+
+(function() {
+const React = require('react');
+const Icons = require('components/icons');
+
+const COLUMNS = [
+ {
+ dataKey: "title",
+ primary: true,
+ defaultIn: new Set(["default", "feed"]),
+ label: "zotero.items.title_column",
+ ignoreInColumnPicker: "true",
+ flex: 4,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "firstCreator",
+ defaultIn: new Set(["default", "feed"]),
+ label: "zotero.items.creator_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "itemType",
+ label: "zotero.items.type_column",
+ width: "40",
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "date",
+ defaultIn: new Set(["feed"]),
+ label: "zotero.items.date_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "year",
+ disabledIn: "feed",
+ label: "zotero.items.year_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "publisher",
+ label: "zotero.items.publisher_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "publicationTitle",
+ disabledIn: "feed",
+ label: "zotero.items.publication_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "journalAbbreviation",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.journalAbbr_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "language",
+ submenu: true,
+ label: "zotero.items.language_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "accessDate",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.accessDate_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "libraryCatalog",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.libraryCatalog_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "callNumber",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.callNumber_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "rights",
+ submenu: true,
+ label: "zotero.items.rights_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "dateAdded",
+ disabledIn: "feed",
+ label: "zotero.items.dateAdded_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "dateModified",
+ disabledIn: "feed",
+ label: "zotero.items.dateModified_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "archive",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.archive_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "archiveLocation",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.archiveLocation_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "place",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.place_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "volume",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.volume_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "edition",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.edition_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "pages",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.pages_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "issue",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.issue_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "series",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.series_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "seriesTitle",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.seriesTitle_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "court",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.court_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "medium",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.medium_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "genre",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.genre_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "system",
+ disabledIn: "feed",
+ submenu: true,
+ label: "zotero.items.system_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "extra",
+ disabledIn: "feed",
+ label: "zotero.items.extra_column",
+ flex: 1,
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ },
+ {
+ dataKey: "hasAttachment",
+ defaultIn: new Set(["default"]),
+ disabledIn: "feed",
+ label: "zotero.tabs.attachments.label",
+ iconLabel: ,
+ fixedWidth: true,
+ width: "14",
+ zoteroPersist: new Set(["hidden", "sortDirection"])
+ },
+ {
+ dataKey: "numNotes",
+ disabledIn: "feed",
+ label: "zotero.tabs.notes.label",
+ iconLabel: ,
+ width: "14",
+ zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ }
+];
+let DATA_KEY_TO_COLUMN = {};
+for (const column of COLUMNS) {
+ DATA_KEY_TO_COLUMN[column.dataKey] = column;
+}
+
+function getDefaultColumnByDataKey(dataKey) {
+ return Object.assign({}, DATA_KEY_TO_COLUMN[dataKey], {hidden: false});
+}
+
+function getDefaultColumnsByDataKeys(dataKeys) {
+ return COLUMNS.filter(column => dataKeys.includes(column.dataKey)).map(column => Object.assign({}, column, {hidden: false}));
+}
+
+module.exports = {
+ COLUMNS,
+ getDefaultColumnByDataKey,
+ getDefaultColumnsByDataKeys,
+};
+
+})();
diff --git a/chrome/content/zotero/containers/libraryTree.js b/chrome/content/zotero/containers/libraryTree.js
new file mode 100644
index 0000000000..9cd0a1ec7e
--- /dev/null
+++ b/chrome/content/zotero/containers/libraryTree.js
@@ -0,0 +1,253 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 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 *****
+*/
+
+const { TreeSelectionStub } = require('components/virtualized-table');
+const React = require('react');
+
+/**
+ * Common methods for Zotero.ItemTree and Zotero.CollectionTree
+ * @type {Zotero.LibraryTree}
+ */
+var LibraryTree = class LibraryTree extends React.Component {
+ constructor(props) {
+ super(props);
+ this._rows = [];
+ this._rowMap = {};
+
+ this.domEl = props.domEl;
+ this._ownerDocument = props.domEl.ownerDocument;
+
+ this.onSelect = this._createEventBinding('select');
+ this.onRefresh = this._createEventBinding('refresh');
+ }
+
+ get window() {
+ return this._ownerDocument.defaultView;
+ }
+
+ get selection() {
+ return this.tree ? this.tree.selection : TreeSelectionStub;
+ }
+
+ get rowCount() {
+ return this._rows.length;
+ }
+
+ waitForSelect() {
+ return this._waitForEvent('select');
+ }
+
+ componentDidCatch(error, info) {
+ // Async operations might attempt to update the react components
+ // after window close in tests, which will cause unnecessary crashing
+ // so we set an unintialized flag that we check in select functions
+ // like #notify
+ if (this._uninitialized) return;
+ Zotero.debug("ItemTree: React threw an error");
+ Zotero.logError(error);
+ Zotero.debug(info);
+ if (this.type == 'item') Zotero.Prefs.clear('lastViewedFolder');
+ Zotero.crash();
+ }
+
+ getParentIndex = (index) => {
+ var thisLevel = this.getLevel(index);
+ if (thisLevel == 0) return -1;
+ for (var i = index - 1; i >= 0; i--) {
+ if (this.getLevel(i) < thisLevel) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ getLevel(index) {
+ return this._rows[index].level;
+ }
+
+ /**
+ * Return a reference to the tree row at a given row
+ *
+ * @return {TreeRow}
+ */
+ getRow(index) {
+ return this._rows[index];
+ }
+
+ /**
+ * Return the index of the row with a given ID (e.g., "C123" for collection 123)
+ *
+ * @param {String} - Row id
+ * @return {Integer|false}
+ */
+ getRowIndexByID(id) {
+ if (!(id in this._rowMap)) {
+ Zotero.debug(`${this.name}: Trying to access a row with invalid ID ${id}`)
+ return false;
+ }
+ return this._rowMap[id];
+ }
+
+ /**
+ * Add a tree row to the main array, update the row count, tell the treebox that the row
+ * count changed, and update the row map
+ *
+ * @param {TreeRow} treeRow
+ * @param {Number} [beforeRow] - Row index to insert new row before
+ */
+ _addRow(treeRow, beforeRow, skipRowMapRefresh) {
+ this._rows.splice(beforeRow, 0, treeRow);
+ if (!skipRowMapRefresh) {
+ // Increment all rows in map at or above insertion point
+ for (let i in this._rowMap) {
+ if (this._rowMap[i] >= beforeRow) {
+ this._rowMap[i]++;
+ }
+ }
+ // Add new row to map
+ this._rowMap[treeRow.id] = beforeRow;
+ }
+ }
+
+ _removeRows(rows) {
+ rows = Zotero.Utilities.arrayUnique(rows);
+ rows.sort((a, b) => a - b);
+ for (let i = rows.length - 1; i >= 0; i--) {
+ this._removeRow(rows[i], true);
+ }
+ this._refreshRowMap();
+ }
+
+ /**
+ * Remove a row from the main array and parent row children arrays,
+ * delete the row from the map, and optionally update all rows above it in the map
+ */
+ _removeRow(index, skipMapUpdate) {
+ var id = this.getRow(index).id;
+ let level = this.getLevel(index);
+
+ let moveSelect = index - 1;
+ if (index <= this.selection.pivot) {
+ while (moveSelect >= this._rows.length) {
+ moveSelect--;
+ }
+ this.selection.select(moveSelect);
+ }
+
+ this._rows.splice(index, 1);
+ if (index != 0
+ && this.getLevel(index - 1) < level
+ && (!this._rows[index] || this.getLevel(index) != level)) {
+ this._rows[index - 1].isOpen = false;
+ }
+
+ delete this._rowMap[id];
+ if (!skipMapUpdate) {
+ for (let i in this._rowMap) {
+ if (this._rowMap[i] > index) {
+ this._rowMap[i]--;
+ }
+ }
+ }
+ }
+
+ _refreshRowMap() {
+ var rowMap = {};
+ for (var i = 0; i < this.rowCount; i++) {
+ let row = this.getRow(i);
+ let id = row.id;
+ if (rowMap[id] !== undefined) {
+ Zotero.debug(`WARNING: _refreshRowMap(): ${this.type} row ${rowMap[id]} already found for item ${id} at ${i}`, 2);
+ Zotero.debug(new Error().stack, 2);
+ }
+ rowMap[id] = i;
+ }
+ this._rowMap = rowMap;
+ }
+
+ _onSelectionChange = () => {
+ if (!this._uninitialized) {
+ this.props.onSelectionChange && this.props.onSelectionChange(this.selection);
+ }
+ }
+
+ _onSelectionChangeDebounced = Zotero.Utilities.debounce(this._onSelectionChange, 100)
+
+ handleTwistyMouseUp = (event, index) => {
+ this.toggleOpenState(index);
+ event.stopPropagation();
+ this.tree.focus();
+ }
+
+ // The caller has to ensure the tree is redrawn
+ ensureRowIsVisible(index) {
+ this.tree && this.tree.scrollToRow(index);
+ }
+
+ _updateHeight = () => {
+ this.forceUpdate(() => {
+ if (this.tree) {
+ this.tree.rerender();
+ }
+ });
+ }
+
+ updateHeight = Zotero.Utilities.debounce(this._updateHeight, 200);
+
+ updateFontSize() {
+ this.tree.updateFontSize();
+ }
+
+ setDropEffect(event, effect) {
+ // On Windows (in Fx26), Firefox uses 'move' for unmodified drags
+ // and 'copy'/'link' for drags with system-default modifier keys
+ // as long as the actions are allowed by the initial effectAllowed set
+ // in onDragStart, regardless of the effectAllowed or dropEffect set
+ // in onDragOver. It doesn't seem to be possible to use 'copy' for
+ // the default and 'move' for modified, as we need to in the collections
+ // tree. To prevent inaccurate cursor feedback, we set effectAllowed to
+ // 'copy' in onDragStart, which locks the cursor at 'copy'. ('none' still
+ // changes the cursor, but 'move'/'link' do not.) It'd be better to use
+ // the unadorned 'move', but we use 'copy' instead because with 'move' text
+ // can't be dragged to some external programs (e.g., Chrome, Notepad++),
+ // which seems worse than always showing 'copy' feedback.
+ //
+ // However, since effectAllowed is enforced, leaving it at 'copy'
+ // would prevent our modified 'move' in the collections tree from working,
+ // so we also have to set effectAllowed here (called from onDragOver) to
+ // the same action as the dropEffect. This allows the dropEffect setting
+ // (which we use in the tree's canDrop() and drop() to determine the desired
+ // action) to be changed, even if the cursor doesn't reflect the new setting.
+ if (Zotero.isWin || Zotero.isLinux) {
+ event.dataTransfer.effectAllowed = effect;
+ }
+ event.dataTransfer.dropEffect = effect;
+ }
+};
+
+Zotero.Utilities.Internal.makeClassEventDispatcher(LibraryTree);
+
+module.exports = LibraryTree;
+
diff --git a/chrome/content/zotero/containers/tagSelectorContainer.jsx b/chrome/content/zotero/containers/tagSelectorContainer.jsx
index ce0f6b9d05..8a8a4612ea 100644
--- a/chrome/content/zotero/containers/tagSelectorContainer.jsx
+++ b/chrome/content/zotero/containers/tagSelectorContainer.jsx
@@ -29,7 +29,7 @@
(function () {
const React = require('react');
-const ReactDOM = require('react-dom');
+const ReactDom = require('react-dom');
const PropTypes = require('prop-types');
const { IntlProvider } = require('react-intl');
const TagSelector = require('components/tagSelector.js');
@@ -74,6 +74,16 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
focusTextbox() {
this.searchBoxRef.current.focus();
}
+
+ componentDidCatch(error, info) {
+ // Async operations might attempt to update the react components
+ // after window close in tests, which will cause unnecessary crashing.
+ if (this._uninitialized) return;
+ Zotero.debug("TagSelectorContainer: React threw an error");
+ Zotero.logError(error);
+ Zotero.debug(info);
+ Zotero.crash();
+ }
componentDidUpdate(_prevProps, _prevState) {
Zotero.debug("Tag selector updated");
@@ -756,13 +766,14 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
ref = c } {...opts} />
);
- ReactDOM.render(elem, domEl);
+ ReactDom.render(elem, domEl);
ref.domEl = domEl;
return ref;
}
uninit() {
- ReactDOM.unmountComponentAtNode(this.domEl);
+ this._uninitialized = true;
+ ReactDom.unmountComponentAtNode(this.domEl);
Zotero.Notifier.unregisterObserver(this._notifierID);
Zotero.Prefs.unregisterObserver(this._prefObserverID);
}
diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js
index e526a7497d..534b6527e0 100644
--- a/chrome/content/zotero/fileInterface.js
+++ b/chrome/content/zotero/fileInterface.js
@@ -135,7 +135,7 @@ var Zotero_File_Interface = new function() {
*
* @return {Promise}
*/
- this.exportFile = Zotero.Promise.method(function () {
+ this.exportFile = async function () {
var exporter = new Zotero_File_Exporter();
exporter.libraryID = ZoteroPane_Local.getSelectedLibraryID();
if (exporter.libraryID === false) {
@@ -143,7 +143,7 @@ var Zotero_File_Interface = new function() {
}
exporter.name = Zotero.Libraries.getName(exporter.libraryID);
return exporter.save();
- });
+ };
/*
* exports a collection or saved search
diff --git a/chrome/content/zotero/include.js b/chrome/content/zotero/include.js
index 17f1fe724e..b2f11a03a6 100644
--- a/chrome/content/zotero/include.js
+++ b/chrome/content/zotero/include.js
@@ -2,10 +2,10 @@
/* eslint-disable no-unused-vars */
var Zotero = Components.classes['@zotero.org/Zotero;1']
- // Currently uses only nsISupports
- //.getService(Components.interfaces.chnmIZoteroService).
- .getService(Components.interfaces.nsISupports)
- .wrappedJSObject;
+ // Currently uses only nsISupports
+ //.getService(Components.interfaces.chnmIZoteroService).
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
// Components.utils.import('resource://zotero/require.js');
// Not using Cu.import here since we don't want the require module to be cached
diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js
index b5221dbcab..4503e1ba5f 100644
--- a/chrome/content/zotero/integration/addCitationDialog.js
+++ b/chrome/content/zotero/integration/addCitationDialog.js
@@ -319,7 +319,7 @@ var Zotero_Citation_Dialog = new function () {
_itemSelected(itemDataID);
// turn off highlight in item tree
_suppressNextTreeSelect = true;
- document.getElementById("zotero-items-tree").view.selection.clearSelection();
+ itemsView.selection.clearSelection();
document.getElementById("remove").disabled = !itemDataID;
document.getElementById("add").disabled = true;
_configListPosition(!itemDataID, selectedListIndex);
diff --git a/chrome/content/zotero/integration/addCitationDialog.xul b/chrome/content/zotero/integration/addCitationDialog.xul
index fbe1db4673..a74b9e9cba 100644
--- a/chrome/content/zotero/integration/addCitationDialog.xul
+++ b/chrome/content/zotero/integration/addCitationDialog.xul
@@ -28,6 +28,7 @@
+
@@ -43,6 +44,7 @@
ondialogcancel="Zotero_Citation_Dialog.cancel();"
onclose="Zotero_Citation_Dialog.cancel();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
persist="screenX screenY width height"
resizable="true"
buttons="extra1,extra2,accept,cancel"
@@ -62,191 +64,13 @@
onkeypress="if(event.keyCode == event.DOM_VK_ESCAPE) { if (this.value == '') { cancelDialog(); return false; } this.value = ''; onSearch(); return false; } return true;"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/integration/editBibliographyDialog.js b/chrome/content/zotero/integration/editBibliographyDialog.js
index 816459f363..93d16b250e 100644
--- a/chrome/content/zotero/integration/editBibliographyDialog.js
+++ b/chrome/content/zotero/integration/editBibliographyDialog.js
@@ -36,7 +36,7 @@ var Zotero_Bibliography_Dialog = new function () {
/**
* Initializes add citation dialog
*/
- this.load = function() {
+ this.load = async function() {
bibEditInterface = window.arguments[0].wrappedJSObject;
_revertAllButton = document.documentElement.getButton("extra2");
@@ -44,7 +44,6 @@ var Zotero_Bibliography_Dialog = new function () {
_addButton = document.getElementById("add");
_removeButton = document.getElementById("remove");
_itemList = document.getElementById("item-list");
- _itemTree = document.getElementById("zotero-items-tree");
_revertAllButton.label = Zotero.getString("integration.revertAll.button");
_revertAllButton.disabled = bibEditInterface.isAnyEdited();
@@ -54,7 +53,7 @@ var Zotero_Bibliography_Dialog = new function () {
document.getElementById('editor').format = "RTF";
// load (from selectItemsDialog.js)
- doLoad();
+ await doLoad();
// load bibliography entries
_loadItems();
@@ -119,7 +118,7 @@ var Zotero_Bibliography_Dialog = new function () {
if(_itemList.selectedItems.length) {
_suppressAllSelectEvents = true;
- _itemTree.view.selection.clearSelection();
+ itemsView.selection.clearSelection();
_suppressAllSelectEvents = false;
// only show revert button if at least one selected item has been edited
diff --git a/chrome/content/zotero/integration/editBibliographyDialog.xul b/chrome/content/zotero/integration/editBibliographyDialog.xul
index 836ababb96..ff6bcbc63e 100644
--- a/chrome/content/zotero/integration/editBibliographyDialog.xul
+++ b/chrome/content/zotero/integration/editBibliographyDialog.xul
@@ -27,6 +27,7 @@
+
@@ -60,51 +61,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js
index 97faa2b4fb..7cd9ca61a5 100644
--- a/chrome/content/zotero/itemPane.js
+++ b/chrome/content/zotero/itemPane.js
@@ -24,7 +24,7 @@
*/
import React from 'react';
-import ReactDOM from 'react-dom';
+import ReactDom from 'react-dom';
import TagsBoxContainer from 'containers/tagsBoxContainer';
var ZoteroItemPane = new function() {
@@ -182,7 +182,7 @@ var ZoteroItemPane = new function() {
return;
}
else if (index == 2) {
- ReactDOM.render(
+ ReactDom.render(
-
+
diff --git a/chrome/content/zotero/locateManager.jsx b/chrome/content/zotero/locateManager.jsx
new file mode 100644
index 0000000000..efcb70c574
--- /dev/null
+++ b/chrome/content/zotero/locateManager.jsx
@@ -0,0 +1,141 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+import VirtualizedTable from 'components/virtualized-table';
+const { IntlProvider } = require('react-intl');
+import React from 'react';
+import ReactDom from 'react-dom';
+
+
+var tree;
+var engines;
+const columns = [
+ { dataKey: 'visible', type: 'checkbox', fixedWidth: true, width: 28 },
+ { dataKey: 'name', label: "zotero.preferences.locate.name" },
+ { dataKey: 'description', label: "zotero.preferences.locate.description" },
+];
+
+function init() {
+ engines = Zotero.LocateManager.getEngines();
+ const domEl = document.querySelector('#locateManager-tree');
+ let elem = (
+
+ engines.length}
+ id="locateManager-table"
+ ref={ref => tree = ref}
+ renderItem={VirtualizedTable.makeRowRenderer(getRowData)}
+ showHeader={true}
+ multiSelect={true}
+ columns={columns}
+ onActivate={handleActivate}
+ />
+
+ );
+ return new Promise(resolve => ReactDom.render(elem, domEl, resolve));
+}
+
+function getRowData(index) {
+ var data = {};
+ columns.forEach((column) => {
+ if (column.dataKey == 'visible') {
+ var value = !engines[index].hidden;
+ }
+ else {
+ value = engines[index][column.dataKey];
+ }
+ data[column.dataKey] = value;
+ });
+ return data;
+}
+
+/**
+ * Refreshes the list of locate engines in the locate pane
+ * @param {String} name of locate engine to select
+ */
+function updateTree() {
+ if (!tree) return;
+ tree.forceUpdate(tree.invalidate);
+}
+
+function handleActivate(event, indices) {
+ // Ignore Enter, only run on dblclick
+ if (event.key) return;
+ indices.forEach(index => engines[index].hidden = !engines[index].hidden)
+ updateTree();
+}
+
+/**
+ * Adds a new Locate Engine to the locate pane
+ **/
+/*
+function addLocateEngine() {
+ // alert(Zotero.LocateManager.activeLocateEngines.join(" || "));
+ var textbox = document.getElementById('locate-add-textbox');
+ Zotero.LocateManager.addLocateEngine(textbox.value);
+
+ refreshLocateEnginesList();
+}
+*/
+
+function toggleLocateEngines() {
+ if (!tree) return;
+ const numSelected = tree.selection.count;
+ const numVisible = engines.filter((_, index) => tree.selection.isSelected(index))
+ .reduce((acc, engine) => acc + (engine.hidden ? 0 : 1), 0);
+
+ // Make all visible, unless all selected are already visible
+ var hideAll = numVisible == numSelected;
+
+ engines.forEach((engine, index) => {
+ if (tree.selection.isSelected(index)) {
+ engine.hidden = hideAll;
+ }
+ });
+ updateTree();
+}
+
+/**
+ * Deletes selected Locate Engines from the locate pane
+ **/
+function deleteLocateEngine() {
+ engines.forEach((engine, index) => {
+ if (tree.selection.isSelected(index)) {
+ Zotero.LocateManager.removeLocateEngine(engine);
+ }
+ });
+
+ tree.selection.clearSelection();
+ updateTree();
+}
+
+/**
+ * Restores Default Locate Engines
+ **/
+function restoreDefaultLocateEngines() {
+ Zotero.LocateManager.restoreDefaultEngines();
+ engines = Zotero.LocateManager.getEngines();
+ updateTree();
+}
diff --git a/chrome/content/zotero/locateManager.xul b/chrome/content/zotero/locateManager.xul
index 8e41e92cb3..3865b6bab2 100644
--- a/chrome/content/zotero/locateManager.xul
+++ b/chrome/content/zotero/locateManager.xul
@@ -29,6 +29,7 @@
+
-
+
+ image="chrome://zotero/skin/prefs-styles.png"
+ flex="1">
@@ -63,17 +69,8 @@ To add a new preference:
-
-
-
-
-
-
-
-
-
+
+
@@ -87,11 +84,10 @@ To add a new preference:
-
-
-
+
- %preferencesDTD;
- %zoteroDTD;
-]>
-
-
-
-
-
-
-
-
-
- &zotero.preferences.proxies.desc_before_link;
-
- &zotero.preferences.proxies.desc_after_link;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/chrome/content/zotero/preferences/preferences_proxies.js b/chrome/content/zotero/preferences/preferences_proxies.js
deleted file mode 100644
index 0e0e408dc6..0000000000
--- a/chrome/content/zotero/preferences/preferences_proxies.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2006–2013 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 *****
-*/
-
-"use strict";
-
-Zotero_Preferences.Proxies = {
- _proxies: null,
-
-
- init: function () {
- this.refreshProxyList();
- this.updateCheckboxState();
- },
-
- /**
- * Updates proxy autoRecognize and transparent settings based on checkboxes
- */
- updateProxyPrefs: function () {
- var transparent = document.getElementById('zotero-proxies-transparent').checked;
- Zotero.Prefs.set("proxies.transparent", transparent);
- Zotero.Prefs.set("proxies.autoRecognize", document.getElementById('zotero-proxies-autoRecognize').checked);
- Zotero.Prefs.set("proxies.showRedirectNotification", document.getElementById('zotero-proxies-showRedirectNotification').checked);
- Zotero.Prefs.set("proxies.disableByDomainString", document.getElementById('zotero-proxies-disableByDomain-textbox').value);
- Zotero.Prefs.set("proxies.disableByDomain", document.getElementById('zotero-proxies-disableByDomain-checkbox').checked &&
- document.getElementById('zotero-proxies-disableByDomain-textbox').value != "");
-
- Zotero.Proxies.init();
-
- this.updateCheckboxState();
- },
-
-
- updateCheckboxState: function() {
- var transparent = document.getElementById('zotero-proxies-transparent').checked;
-
- document.getElementById('proxyTree-add').disabled =
- document.getElementById('proxyTree-delete').disabled =
- document.getElementById('proxyTree').disabled =
- document.getElementById('zotero-proxies-autoRecognize').disabled =
- document.getElementById('zotero-proxies-showRedirectNotification').disabled =
- document.getElementById('zotero-proxies-disableByDomain-checkbox').disabled =
- document.getElementById('zotero-proxies-disableByDomain-textbox').disabled =
- !transparent;
- },
-
-
- /**
- * Enables UI buttons when proxy is selected
- */
- enableProxyButtons: function () {
- document.getElementById('proxyTree-edit').disabled = false;
- document.getElementById('proxyTree-delete').disabled = false;
- },
-
- /**
- * Adds a proxy to the proxy pane
- */
- showProxyEditor: function (index) {
- if(index == -1) return;
- window.openDialog('chrome://zotero/content/preferences/proxyEditor.xul',
- "zotero-preferences-proxyEditor", "chrome,modal,centerscreen",
- index !== undefined ? this._proxies[index] : null);
- this.refreshProxyList();
- },
-
-
- /**
- * Deletes the currently selected proxy
- */
- deleteProxy: function () {
- if(document.getElementById('proxyTree').currentIndex == -1) return;
- this._proxies[document.getElementById('proxyTree').currentIndex].erase();
- this.refreshProxyList();
- document.getElementById('proxyTree-delete').disabled = true;
- },
-
-
- /**
- * Refreshes the proxy pane
- */
- refreshProxyList: function () {
- if(!document.getElementById("zotero-prefpane-proxies")) return;
-
- // get and sort proxies
- this._proxies = Zotero.Proxies.proxies.slice();
- for(var i=0; i a.scheme) {
- return 1;
- }
-
- return 0;
- });
-
- // erase old children
- var treechildren = document.getElementById('proxyTree-rows');
- while (treechildren.hasChildNodes()) {
- treechildren.removeChild(treechildren.firstChild);
- }
-
- // add proxies to list
- for (var i=0; i= treechildren.childNodes.length) {
- return;
- }
- var row = treechildren.childNodes[index];
- var val = row.firstChild.childNodes[1].getAttribute('value');
- if (!val) {
- return
- }
+ toggleLibraryToSync: function () {
+ const index = this._tree.selection.focused;
+ if (index == -1 || !this._rows[index].editable) return;
+ const row = this._rows[index];
+ this._rows[index].checked = !this._rows[index].checked;
+ this._tree.invalidateRow(index);
var librariesToSkip = JSON.parse(Zotero.Prefs.get('sync.librariesToSkip') || '[]');
- var indexOfId = librariesToSkip.indexOf(val);
+ var indexOfId = librariesToSkip.indexOf(row.id);
if (indexOfId == -1) {
- librariesToSkip.push(val);
- } else {
+ librariesToSkip.push(row.id);
+ }
+ else {
librariesToSkip.splice(indexOfId, 1);
}
Zotero.Prefs.set('sync.librariesToSkip', JSON.stringify(librariesToSkip));
-
- var cell = row.firstChild.firstChild;
- var spacing = Zotero.isWin ? ' ' : ' ';
- cell.setAttribute('label', spacing + (indexOfId != -1 ? this.checkmarkChar : this.noChar));
- cell.setAttribute('value', indexOfId != -1);
},
- initLibrariesToSync: Zotero.Promise.coroutine(function* () {
- var tree = document.getElementById("libraries-to-sync-tree");
- var treechildren = document.getElementById('libraries-to-sync-rows');
- while (treechildren.hasChildNodes()) {
- treechildren.removeChild(treechildren.firstChild);
+ initLibrariesToSync: async function () {
+ const columns = [
+ {
+ dataKey: "checked",
+ label: "zotero.preferences.sync.librariesToSync.sync",
+ fixedWidth: true,
+ width: '60'
+ },
+ {
+ dataKey: "name",
+ label: "zotero.preferences.sync.librariesToSync.library"
+ }
+ ];
+ this._rows = [];
+ function renderItem(index, selection, oldDiv=null, columns) {
+ const row = this._rows[index];
+ let div;
+ if (oldDiv) {
+ div = oldDiv;
+ div.innerHTML = "";
+ }
+ else {
+ div = document.createElementNS("http://www.w3.org/1999/xhtml", 'div');
+ div.className = "row";
+ }
+ div.classList.toggle('selected', selection.isSelected(index));
+
+ for (let column of columns) {
+ if (column.dataKey === 'checked') {
+ let span = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ span.className = `cell ${column.className}`;
+ if (row.id != 'loading') {
+ span.innerText = row.checked ? this.checkmarkChar : this.noChar;
+ span.style.textAlign = 'center';
+ }
+ div.appendChild(span);
+ }
+ else {
+ div.appendChild(renderCell(index, row[column.dataKey], column));
+ }
+ }
+ return div;
}
+ let elem = (
+
+ this._rows.length}
+ id="librariesToSync-table"
+ ref={ref => this._tree = ref}
+ renderItem={renderItem.bind(this)}
+ showHeader={true}
+ columns={columns}
+ staticColumns={true}
+ onActivate={Zotero_Preferences.Sync.toggleLibraryToSync.bind(this)}
+ />
+
+ );
+
+ ReactDom.render(elem, document.getElementById("libraries-to-sync-tree"));
var addRow = function (libraryName, id, checked=false, editable=true) {
- var treeitem = document.createElement('treeitem');
- var treerow = document.createElement('treerow');
- var checkboxCell = document.createElement('treecell');
- var nameCell = document.createElement('treecell');
-
- nameCell.setAttribute('label', libraryName);
- nameCell.setAttribute('value', id);
- nameCell.setAttribute('editable', false);
- var spacing = Zotero.isWin ? ' ' : ' ';
- checkboxCell.setAttribute(
- 'label',
- id == 'loading' ? '' : (spacing + (checked ? this.checkmarkChar : this.noChar))
- );
- checkboxCell.setAttribute('value', checked);
- checkboxCell.setAttribute('editable', false);
-
- treerow.appendChild(checkboxCell);
- treerow.appendChild(nameCell);
- treeitem.appendChild(treerow);
- treechildren.appendChild(treeitem);
+ this._rows.push({
+ name: libraryName,
+ id,
+ checked,
+ editable
+ });
+ this._tree.invalidate();
}.bind(this);
// Add loading row while we're loading a group list
var loadingLabel = Zotero.getString("zotero.preferences.sync.librariesToSync.loadingLibraries");
addRow(loadingLabel, "loading", false, false);
- var apiKey = yield Zotero.Sync.Data.Local.getAPIKey();
- var client = Zotero.Sync.Runner.getAPIClient({apiKey});
+ var apiKey = await Zotero.Sync.Data.Local.getAPIKey();
+ var client = Zotero.Sync.Runner.getAPIClient({ apiKey });
var groups = [];
try {
// Load up remote groups
- var keyInfo = yield Zotero.Sync.Runner.checkAccess(client, {timeout: 5000});
- groups = yield client.getGroups(keyInfo.userID);
+ var keyInfo = await Zotero.Sync.Runner.checkAccess(client, {timeout: 5000});
+ groups = await client.getGroups(keyInfo.userID);
}
catch (e) {
// Connection problems
@@ -342,11 +353,12 @@ Zotero_Preferences.Sync = {
}
// Remove the loading row
- treechildren.removeChild(treechildren.firstChild);
+ this._rows = [];
+ this._tree.invalidate();
var librariesToSkip = JSON.parse(Zotero.Prefs.get('sync.librariesToSkip') || '[]');
// Add default rows
- addRow(Zotero.getString("pane.collections.libraryAndFeeds"), "L" + Zotero.Libraries.userLibraryID,
+ addRow(Zotero.getString("pane.collections.libraryAndFeeds"), "L" + Zotero.Libraries.userLibraryID,
librariesToSkip.indexOf("L" + Zotero.Libraries.userLibraryID) == -1);
// Sort groups
@@ -356,8 +368,7 @@ Zotero_Preferences.Sync = {
for (let group of groups) {
addRow(group.data.name, "G" + group.id, librariesToSkip.indexOf("G" + group.id) == -1);
}
- }),
-
+ },
updateStorageSettingsUI: Zotero.Promise.coroutine(function* () {
this.unverifyStorageServer();
diff --git a/chrome/content/zotero/preferences/proxyEditor.js b/chrome/content/zotero/preferences/proxyEditor.js
deleted file mode 100644
index 98347728a6..0000000000
--- a/chrome/content/zotero/preferences/proxyEditor.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2009 Center for History and New Media
- George Mason University, Fairfax, Virginia, USA
- http://zotero.org
-
- This file is part of Zotero.
-
- Zotero is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Zotero is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Zotero. If not, see .
-
- ***** END LICENSE BLOCK *****
-*/
-
-var Zotero_ProxyEditor = new function() {
- var treechildren;
- var tree;
- var treecol;
- var multiSite;
-
- /**
- * Called when this window is first opened. Sets values if necessary
- */
- this.load = function() {
- treechildren = document.getElementById("zotero-proxies-hostname-multiSite-tree-children");
- tree = document.getElementById("zotero-proxies-hostname-multiSite-tree");
- multiSite = document.getElementById("zotero-proxies-multiSite");
-
- if(window.arguments && window.arguments[0]) {
- var proxy = window.arguments[0];
- document.getElementById("zotero-proxies-scheme").value = proxy.scheme;
- document.getElementById("zotero-proxies-multiSite").checked = !!proxy.multiHost;
- if(proxy.hosts) {
- if(proxy.multiHost) {
- this.multiSiteChanged();
- for (var i=0; i
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/chrome/content/zotero/progressQueueDialog.jsx b/chrome/content/zotero/progressQueueDialog.jsx
new file mode 100644
index 0000000000..c96d7e0d9d
--- /dev/null
+++ b/chrome/content/zotero/progressQueueDialog.jsx
@@ -0,0 +1,134 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2018 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 *****
+*/
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+import React from 'react';
+import ReactDom from 'react-dom';
+import VirtualizedTable from 'components/virtualized-table';
+const { IntlProvider } = require('react-intl');
+const { getDomElement } = require('components/icons');
+const { renderCell } = VirtualizedTable;
+
+let _progressIndicator = null;
+let _progressQueue;
+let _tree;
+
+function _getImageByStatus(status) {
+ if (status === Zotero.ProgressQueue.ROW_PROCESSING) {
+ return getDomElement('IconArrowRefresh');
+ }
+ else if (status === Zotero.ProgressQueue.ROW_FAILED) {
+ return getDomElement('IconCross');
+ }
+ else if (status === Zotero.ProgressQueue.ROW_SUCCEEDED) {
+ return getDomElement('IconTick');
+ }
+ return document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+}
+
+function _rowToTreeItem(index, selection, oldDiv=null, columns) {
+ let rows = _progressQueue.getRows();
+ let row = rows[index];
+
+ let div;
+ if (oldDiv) {
+ div = oldDiv;
+ div.innerHTML = "";
+ }
+ else {
+ div = document.createElementNS("http://www.w3.org/1999/xhtml", 'div');
+ div.className = "row";
+ }
+
+ div.classList.toggle('selected', selection.isSelected(index));
+
+ for (let column of columns) {
+ if (column.dataKey === 'success') {
+ let span = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ span.className = `cell icon ${column.className}`;
+ span.appendChild(_getImageByStatus(row.status));
+ div.appendChild(span);
+ }
+ else {
+ div.appendChild(renderCell(index, row[column.dataKey], column));
+ }
+ }
+ return div;
+}
+
+function _init() {
+ var io = window.arguments[0];
+ _progressQueue = io.progressQueue;
+ document.title = Zotero.getString(_progressQueue.getTitle());
+
+ let columns = _progressQueue.getColumns();
+
+ const tableColumns = [
+ { dataKey: 'success', fixedWidth: true, width: "26" },
+ { dataKey: 'fileName', label: Zotero.getString(columns[0]) },
+ { dataKey: 'message', label: Zotero.getString(columns[1]) },
+ ];
+
+ const domEl = document.querySelector('#tree');
+ let elem = (
+
+ _progressQueue.getRows().length}
+ id="locateManager-table"
+ ref={ref => io.tree = _tree = ref}
+ renderItem={_rowToTreeItem}
+ showHeader={true}
+ columns={tableColumns}
+ onActivate={_handleActivate}
+ />
+
+ );
+ ReactDom.render(elem, domEl);
+}
+
+/**
+ * Focus items in Zotero library when double-clicking them in the Retrieve
+ * metadata window.
+ * @param {Event} event
+ * @param {Number[]} indices to activate
+ * @private
+ */
+async function _handleActivate(event, indices) {
+ if (event && event.type === 'dblclick') {
+ let itemID = _progressQueue.getRows()[indices[0]].id;
+ if (!itemID) return;
+
+ let item = await Zotero.Items.getAsync(itemID);
+ if (!item) return;
+
+ if (item.parentItemID) itemID = item.parentItemID;
+
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (win) {
+ win.ZoteroPane.selectItem(itemID, false, true);
+ win.focus();
+ }
+ }
+}
diff --git a/chrome/content/zotero/progressQueueDialog.xul b/chrome/content/zotero/progressQueueDialog.xul
index 2db7ae5d9f..dd9a7d1c43 100644
--- a/chrome/content/zotero/progressQueueDialog.xul
+++ b/chrome/content/zotero/progressQueueDialog.xul
@@ -1,10 +1,15 @@
+
+
+
@@ -13,15 +18,8 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/chrome/content/zotero/rtfScan.js b/chrome/content/zotero/rtfScan.jsx
similarity index 58%
rename from chrome/content/zotero/rtfScan.js
rename to chrome/content/zotero/rtfScan.jsx
index 18fc2c1856..4eca4b4acf 100644
--- a/chrome/content/zotero/rtfScan.js
+++ b/chrome/content/zotero/rtfScan.jsx
@@ -28,6 +28,11 @@
*/
import FilePicker from 'zotero/filePicker';
+import React from 'react';
+import ReactDom from 'react-dom';
+import VirtualizedTable from 'components/virtualized-table';
+import { IntlProvider } from 'react-intl';
+import { getDomElement } from 'components/icons';
/**
* Front end for recognizing PDFs
@@ -37,11 +42,24 @@ var Zotero_RTFScan = new function() {
const ACCEPT_ICON = "chrome://zotero/skin/rtfscan-accept.png";
const LINK_ICON = "chrome://zotero/skin/rtfscan-link.png";
const BIBLIOGRAPHY_PLACEHOLDER = "\\{Bibliography\\}";
-
+
+ const columns = [
+ { dataKey: 'rtf', label: "zotero.rtfScan.citation.label", primary: true, flex: 4 },
+ { dataKey: 'item', label: "zotero.rtfScan.itemName.label", flex: 5 },
+ { dataKey: 'action', label: "", fixedWidth: true, width: "26px" },
+ ];
+ var ids = 0;
+ var tree;
+ this._rows = [
+ { id: 'unmapped', rtf: Zotero.Intl.strings['zotero.rtfScan.unmappedCitations.label'], collapsed: false },
+ { id: 'ambiguous', rtf: Zotero.Intl.strings['zotero.rtfScan.ambiguousCitations.label'], collapsed: false },
+ { id: 'mapped', rtf: Zotero.Intl.strings['zotero.rtfScan.mappedCitations.label'], collapsed: false },
+ ];
+ this._rowMap = {};
+ this._rows.forEach((row, index) => this._rowMap[row.id] = index);
+
var inputFile = null, outputFile = null;
- var unmappedCitationsItem, ambiguousCitationsItem, mappedCitationsItem;
- var unmappedCitationsChildren, ambiguousCitationsChildren, mappedCitationsChildren;
- var citations, citationItemIDs, allCitedItemIDs, contents;
+ var citations, citationItemIDs, contents;
/** INTRO PAGE UI **/
@@ -127,28 +145,31 @@ var Zotero_RTFScan = new function() {
/**
* Called when second page is shown.
*/
- this.scanPageShowing = function() {
+ this.scanPageShowing = async function () {
// can't advance
document.documentElement.canAdvance = false;
// wait a ms so that UI thread gets updated
- window.setTimeout(function() { _scanRTF() }, 1);
- }
+ try {
+ await this._scanRTF();
+ }
+ catch (e) {
+ Zotero.logError(e);
+ Zotero.debug(e);
+ }
+ };
/**
* Scans file for citations, then proceeds to next wizard page.
*/
- var _scanRTF = Zotero.Promise.coroutine(function* () {
+ this._scanRTF = async () => {
// set up globals
citations = [];
citationItemIDs = {};
- unmappedCitationsItem = document.getElementById("unmapped-citations-item");
- ambiguousCitationsItem = document.getElementById("ambiguous-citations-item");
- mappedCitationsItem = document.getElementById("mapped-citations-item");
- unmappedCitationsChildren = document.getElementById("unmapped-citations-children");
- ambiguousCitationsChildren = document.getElementById("ambiguous-citations-children");
- mappedCitationsChildren = document.getElementById("mapped-citations-children");
+ let unmappedRow = this._rows[this._rowMap['unmapped']];
+ let ambiguousRow = this._rows[this._rowMap['ambiguous']];
+ let mappedRow = this._rows[this._rowMap['mapped']];
// set up regular expressions
// this assumes that names are >=2 chars or only capital initials and that there are no
@@ -165,57 +186,60 @@ var Zotero_RTFScan = new function() {
contents = Zotero.File.getContents(inputFile).replace(/([^\\\r])\r?\n/, "$1 ").replace("\\'92", "'", "g").replace("\\rquote ", "’");
var m;
var lastCitation = false;
- while((m = citationRe.exec(contents))) {
+ while ((m = citationRe.exec(contents))) {
// determine whether suppressed or standard regular expression was used
- if(m[2]) { // standard parenthetical
+ if (m[2]) { // standard parenthetical
var citationString = m[2];
var creators = m[3];
var etAl = !!m[4];
var title = m[5];
var date = m[6];
var pages = m[7];
- var start = citationRe.lastIndex-m[0].length;
- var end = citationRe.lastIndex+2;
- } else { // suppressed
- var citationString = m[8];
- var creators = m[9];
- var etAl = !!m[10];
- var title = false;
- var date = m[12];
- var pages = false;
- var start = citationRe.lastIndex-m[11].length;
- var end = citationRe.lastIndex;
+ var start = citationRe.lastIndex - m[0].length;
+ var end = citationRe.lastIndex + 2;
+ }
+ else { // suppressed
+ citationString = m[8];
+ creators = m[9];
+ etAl = !!m[10];
+ title = false;
+ date = m[12];
+ pages = false;
+ start = citationRe.lastIndex - m[11].length;
+ end = citationRe.lastIndex;
}
citationString = citationString.replace("\\{", "{", "g").replace("\\}", "}", "g");
var suppressAuthor = !m[2];
- if(lastCitation && lastCitation.end >= start) {
+ if (lastCitation && lastCitation.end >= start) {
// if this citation is just an extension of the last, add items to it
lastCitation.citationStrings.push(citationString);
lastCitation.pages.push(pages);
lastCitation.end = end;
- } else {
+ }
+ else {
// otherwise, add another citation
- var lastCitation = {"citationStrings":[citationString], "pages":[pages], "start":start,
- "end":end, "suppressAuthor":suppressAuthor};
+ lastCitation = { citationStrings: [citationString], pages: [pages],
+ start, end, suppressAuthor };
citations.push(lastCitation);
}
// only add each citation once
- if(citationItemIDs[citationString]) continue;
- Zotero.debug("Found citation "+citationString);
+ if (citationItemIDs[citationString]) continue;
+ Zotero.debug("Found citation " + citationString);
// for each individual match, look for an item in the database
var s = new Zotero.Search;
- creators = creators.replace(".", "");
+ creators = creators.replace(".", "");
// TODO: localize "et al." term
creators = creators.split(creatorSplitRe);
- for(var i=0; i 1) {
+ if (!ids) { // no mapping found
+ let row = _generateItem(citationString, "");
+ row.parent = unmappedRow;
+ this._insertRows(row, this._rowMap.ambiguous);
+ }
+ else { // some mapping found
+ var items = await Zotero.Items.getAsync(ids);
+ if (items.length > 1) {
// check to see how well the author list matches the citation
var matchedItems = [];
- for(var i=0; i this._rowMap[row.id] = index);
return false;
}
/**
- * Called when a tree item is clicked to remap a citation, or accept a suggestion for an
+ * Called when a tree item is clicked to remap a citation, or accept a suggestion for an
* ambiguous citation
*/
this.treeClick = function(event) {
@@ -400,53 +412,7 @@ var Zotero_RTFScan = new function() {
// figure out which item this corresponds to
row = row.value;
var level = tree.view.getLevel(row);
- if(col.value.index == 2 && level > 0) {
- var iconColumn = col.value;
- var itemNameColumn = iconColumn.getPrevious();
- var citationColumn = itemNameColumn.getPrevious();
-
- if(level == 2) { // ambiguous citation item
- // get relevant information
- var parentIndex = tree.view.getParentIndex(row);
- var citation = tree.view.getCellText(parentIndex, citationColumn);
- var itemName = tree.view.getCellText(row, itemNameColumn);
-
- // update item name on parent and delete children
- tree.view.setCellText(parentIndex, itemNameColumn, itemName);
- var treeitem = tree.view.getItemAtIndex(row);
- treeitem.parentNode.parentNode.removeChild(treeitem.parentNode);
-
- // update array
- citationItemIDs[citation] = [citationItemIDs[citation][row-parentIndex-1]];
- } else { // mapped or unmapped citation, or ambiguous citation parent
- var citation = tree.view.getCellText(row, citationColumn);
- var io = {singleSelection:true};
- if(citationItemIDs[citation] && citationItemIDs[citation].length == 1) { // mapped citation
- // specify that item should be selected in window
- io.select = citationItemIDs[citation][0];
- }
-
- window.openDialog('chrome://zotero/content/selectItemsDialog.xul', '', 'chrome,modal', io);
-
- if(io.dataOut && io.dataOut.length) {
- var selectedItemID = io.dataOut[0];
- var selectedItem = Zotero.Items.get(selectedItemID);
-
- var treeitem = tree.view.getItemAtIndex(row);
-
- // remove any children (if ambiguous)
- var children = treeitem.getElementsByTagName("treechildren");
- if(children.length) treeitem.removeChild(children[0]);
-
- // update item name
- tree.view.setCellText(row, itemNameColumn, selectedItem.getField("title"));
-
- // update array
- citationItemIDs[citation] = [selectedItemID];
- }
- }
- }
- _refreshCanAdvance();
+
}
/**
@@ -471,7 +437,8 @@ var Zotero_RTFScan = new function() {
/**
* Called when style page is shown to add styles to listbox.
*/
- this.stylePageShowing = function() {
+ this.stylePageShowing = async function() {
+ await Zotero.Styles.init();
Zotero_File_Interface_Bibliography.init({
supportedNotes: ['footnotes', 'endnotes']
});
@@ -607,4 +574,210 @@ var Zotero_RTFScan = new function() {
document.documentElement.canAdvance = true;
document.documentElement.advance();
}
+
+ this._onTwistyMouseUp = (event, index) => {
+ const row = this._rows[index];
+ if (!row.collapsed) {
+ // Store children rows on the parent when collapsing
+ row.children = [];
+ const depth = this._getRowLevel(index);
+ for (let childIndex = index + 1; childIndex < this._rows.length && this._getRowLevel(this._rows[childIndex]) > depth; childIndex++) {
+ row.children.push(this._rows[childIndex]);
+ }
+ // And then remove them
+ this._removeRows(row.children.map((_, childIndex) => index + 1 + childIndex));
+ }
+ else {
+ // Insert children rows from the ones stored on the parent
+ this._insertRows(row.children, index + 1);
+ delete row.children;
+ }
+ row.collapsed = !row.collapsed;
+ tree.invalidate();
+ };
+
+ this._onActionMouseUp = (event, index) => {
+ let row = this._rows[index];
+ if (!row.parent) return;
+ let level = this._getRowLevel(row);
+ if (level == 2) { // ambiguous citation item
+ let parentIndex = this._rowMap[row.parent.id];
+ // Update parent item
+ row.parent.item = row.item;
+
+ // Remove children
+ let children = [];
+ for (let childIndex = parentIndex + 1; childIndex < this._rows.length && this._getRowLevel(this._rows[childIndex]) >= level; childIndex++) {
+ children.push(this._rows[childIndex]);
+ }
+ this._removeRows(children.map((_, childIndex) => parentIndex + 1 + childIndex));
+
+ // Move citation to mapped rows
+ row.parent.parent = this._rows[this._rowMap.mapped];
+ this._removeRows(parentIndex);
+ this._insertRows(row.parent, this._rows.length);
+
+ // update array
+ citationItemIDs[row.parent.rtf] = [citationItemIDs[row.parent.rtf][index-parentIndex-1]];
+ }
+ else { // mapped or unmapped citation, or ambiguous citation parent
+ var citation = row.rtf;
+ var io = { singleSelection: true };
+ if (citationItemIDs[citation] && citationItemIDs[citation].length == 1) { // mapped citation
+ // specify that item should be selected in window
+ io.select = citationItemIDs[citation][0];
+ }
+
+ window.openDialog('chrome://zotero/content/selectItemsDialog.xul', '', 'chrome,modal', io);
+
+ if (io.dataOut && io.dataOut.length) {
+ var selectedItemID = io.dataOut[0];
+ var selectedItem = Zotero.Items.get(selectedItemID);
+ // update item name
+ row.item = selectedItem.getField("title");
+
+ // Remove children
+ let children = [];
+ for (let childIndex = index + 1; childIndex < this._rows.length && this._getRowLevel(this._rows[childIndex]) > level; childIndex++) {
+ children.push(this._rows[childIndex]);
+ }
+ this._removeRows(children.map((_, childIndex) => index + 1 + childIndex));
+
+ if (row.parent.id != 'mapped') {
+ // Move citation to mapped rows
+ row.parent = this._rows[this._rowMap.mapped];
+ this._removeRows(index);
+ this._insertRows(row, this._rows.length);
+ }
+
+ // update array
+ citationItemIDs[citation] = [selectedItemID];
+ }
+ }
+ tree.invalidate();
+ _refreshCanAdvance();
+ };
+
+ this._insertRows = (rows, beforeRow) => {
+ if (!Array.isArray(rows)) {
+ rows = [rows];
+ }
+ this._rows.splice(beforeRow, 0, ...rows);
+ rows.forEach(row => row.id = ids++);
+ for (let row of rows) {
+ row.id = ids++;
+ }
+ // Refresh the row map
+ this._rowMap = {};
+ this._rows.forEach((row, index) => this._rowMap[row.id] = index);
+ };
+
+ this._removeRows = (indices) => {
+ if (!Array.isArray(indices)) {
+ indices = [indices];
+ }
+ // Reverse sort so we can safely splice out the entries from the rows array
+ indices.sort((a, b) => b - a);
+ for (const index of indices) {
+ this._rows.splice(index, 1);
+ }
+ // Refresh the row map
+ this._rowMap = {};
+ this._rows.forEach((row, index) => this._rowMap[row.id] = index);
+ };
+
+ this._getRowLevel = (row, depth=0) => {
+ if (typeof row == 'number') {
+ row = this._rows[row];
+ }
+ if (!row.parent) {
+ return depth;
+ }
+ return this._getRowLevel(row.parent, depth+1);
+ }
+
+ this._renderItem = (index, selection, oldDiv=null, columns) => {
+ const row = this._rows[index];
+ let div;
+ if (oldDiv) {
+ div = oldDiv;
+ div.innerHTML = "";
+ }
+ else {
+ div = document.createElementNS("http://www.w3.org/1999/xhtml", 'div');
+ div.className = "row";
+ }
+
+ for (const column of columns) {
+ if (column.primary) {
+ let twisty;
+ if (row.children || (this._rows[index + 1] && this._rows[index + 1].parent == row)) {
+ twisty = getDomElement("IconTwisty");
+ twisty.classList.add('twisty');
+ if (!row.collapsed) {
+ twisty.classList.add('open');
+ }
+ twisty.style.pointerEvents = 'auto';
+ twisty.addEventListener('mousedown', event => event.stopPropagation());
+ twisty.addEventListener('mouseup', event => this._onTwistyMouseUp(event, index),
+ { passive: true });
+ }
+ else {
+ twisty = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ twisty.classList.add("spacer-twisty");
+ }
+
+ let textSpan = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ textSpan.className = "cell-text";
+ textSpan.innerText = row[column.dataKey] || "";
+
+ let span = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ span.className = `cell primary ${column.className}`;
+ span.appendChild(twisty);
+ span.appendChild(textSpan);
+ span.style.paddingLeft = (5 + 20 * this._getRowLevel(row)) + 'px';
+ div.appendChild(span);
+ }
+ else if (column.dataKey == 'action') {
+ let span = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ span.className = `cell action ${column.className}`;
+ if (row.parent) {
+ if (row.action) {
+ span.appendChild(getDomElement('IconRTFScanAccept'));
+ }
+ else {
+ span.appendChild(getDomElement('IconRTFScanLink'));
+ }
+ span.addEventListener('mouseup', e => this._onActionMouseUp(e, index), { passive: true });
+ span.style.pointerEvents = 'auto';
+ }
+
+ div.appendChild(span);
+ }
+ else {
+ let span = document.createElementNS("http://www.w3.org/1999/xhtml", 'span');
+ span.className = `cell ${column.className}`;
+ span.innerText = row[column.dataKey] || "";
+ div.appendChild(span);
+ }
+ }
+ return div;
+ };
+
+ this._initCitationTree = function () {
+ const domEl = document.querySelector('#tree');
+ const elem = (
+
+ this._rows.length}
+ id="rtfScan-table"
+ ref={ref => tree = ref}
+ renderItem={this._renderItem}
+ showHeader={true}
+ columns={columns}
+ />
+
+ );
+ return new Promise(resolve => ReactDom.render(elem, domEl, resolve));
+ };
}
diff --git a/chrome/content/zotero/rtfScan.xul b/chrome/content/zotero/rtfScan.xul
index f0fd19d652..311530d69f 100644
--- a/chrome/content/zotero/rtfScan.xul
+++ b/chrome/content/zotero/rtfScan.xul
@@ -2,10 +2,13 @@
+
@@ -52,34 +55,10 @@
onpageshow="Zotero_RTFScan.citationsPageShowing()"
onpagerewound="return Zotero_RTFScan.citationsPageRewound();">
&zotero.rtfScan.citationsPage.description;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
{
+ if (isEditBibliographyDialog) {
+ Zotero_Bibliography_Dialog.treeItemSelected();
+ }
+ else if (isAddEditItemsDialog) {
+ onItemSelected();
+ Zotero_Citation_Dialog.treeItemSelected();
+ }
+ else {
+ onItemSelected();
+ }
+ },
+ id: "select-items-dialog",
+ dragAndDrop: false,
+ persistColumns: false,
+ columnPicker: true,
+ emptyMessage: Zotero.getString('pane.items.loading')
+ });
+ itemsView.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
+
+ collectionsView = await CollectionTree.init(document.getElementById('zotero-collections-tree'), {
+ onSelectionChange: Zotero.Utilities.debounce(() => onCollectionSelected(), 100),
+ });
collectionsView.hideSources = ['duplicates', 'trash', 'feeds'];
- document.getElementById('zotero-collections-tree').view = collectionsView;
-
- yield collectionsView.waitForLoad();
-
- connectionSelectedDeferred = Zotero.Promise.defer();
- yield connectionSelectedDeferred.promise;
-
+
+ await collectionsView.makeVisible();
+
if (io.select) {
- yield collectionsView.selectItem(io.select);
+ await collectionsView.selectItem(io.select);
}
Zotero.updateQuickSearchBox(document);
-});
+};
function doUnload()
{
collectionsView.unregister();
if(itemsView)
itemsView.unregister();
+
+ io.deferred && io.deferred.resolve();
}
-var onCollectionSelected = Zotero.Promise.coroutine(function* ()
-{
- if(itemsView)
- itemsView.unregister();
-
- if(collectionsView.selection.count == 1 && collectionsView.selection.currentIndex != -1)
- {
- var collectionTreeRow = collectionsView.getRow(collectionsView.selection.currentIndex);
- collectionTreeRow.setSearch('');
- Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
-
- setItemsPaneMessage(Zotero.getString('pane.items.loading'));
-
- // Load library data if necessary
- var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
- if (!library.getDataLoaded('item')) {
- Zotero.debug("Waiting for items to load for library " + library.libraryID);
- yield library.waitForDataLoad('item');
- }
-
- // Create items list and wait for it to load
- itemsView = new Zotero.ItemTreeView(collectionTreeRow);
- itemsView.sourcesOnly = !!window.arguments[1];
- document.getElementById('zotero-items-tree').view = itemsView;
- yield itemsView.waitForLoad();
-
- clearItemsPaneMessage();
-
- connectionSelectedDeferred.resolve();
- collectionsView.runListeners('select');
+var onCollectionSelected = async function () {
+ if (!collectionsView.selection.count) return;
+ var collectionTreeRow = collectionsView.getRow(collectionsView.selection.focused);
+ collectionTreeRow.setSearch('');
+ Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
+
+ itemsView.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
+
+ // Load library data if necessary
+ var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
+ if (!library.getDataLoaded('item')) {
+ Zotero.debug("Waiting for items to load for library " + library.libraryID);
+ await library.waitForDataLoad('item');
}
-});
+
+ await itemsView.changeCollectionTreeRow(collectionTreeRow);
+
+ itemsView.clearItemsPaneMessage();
+
+ collectionsView.runListeners('select');
+};
function onSearch()
{
- if(itemsView)
+ if (itemsView)
{
var searchVal = document.getElementById('zotero-tb-search').value;
itemsView.setFilter('search', searchVal);
@@ -115,30 +126,6 @@ function onItemSelected()
itemsView.runListeners('select');
}
-function setItemsPaneMessage(content) {
- var elem = document.getElementById('zotero-items-pane-message-box');
- elem.textContent = '';
- if (typeof content == 'string') {
- let contentParts = content.split("\n\n");
- for (let part of contentParts) {
- var desc = document.createElement('description');
- desc.appendChild(document.createTextNode(part));
- elem.appendChild(desc);
- }
- }
- else {
- elem.appendChild(content);
- }
- document.getElementById('zotero-items-pane-content').selectedIndex = 1;
-}
-
-
-function clearItemsPaneMessage() {
- var box = document.getElementById('zotero-items-pane-message-box');
- document.getElementById('zotero-items-pane-content').selectedIndex = 0;
-}
-
-function doAccept()
-{
+function doAccept() {
io.dataOut = itemsView.getSelectedItems(true);
}
\ No newline at end of file
diff --git a/chrome/content/zotero/selectItemsDialog.xul b/chrome/content/zotero/selectItemsDialog.xul
index a9b209dfdc..a93c7cef8d 100644
--- a/chrome/content/zotero/selectItemsDialog.xul
+++ b/chrome/content/zotero/selectItemsDialog.xul
@@ -27,6 +27,7 @@
+
@@ -54,203 +56,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/tools/data_generator.jsx b/chrome/content/zotero/tools/data_generator.jsx
index b5b8a286ed..e295a2ae1b 100644
--- a/chrome/content/zotero/tools/data_generator.jsx
+++ b/chrome/content/zotero/tools/data_generator.jsx
@@ -24,11 +24,11 @@
*/
const React = require('react');
-const ReactDOM = require('react-dom');
+const ReactDom = require('react-dom');
function init() {
let div = document.querySelector('div');
- ReactDOM.render( , div);
+ ReactDom.render( , div);
}
class DataGeneratorForm extends React.Component {
diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js
index 9b5bf2c30f..e3bf0cdb95 100644
--- a/chrome/content/zotero/xpcom/collectionTreeRow.js
+++ b/chrome/content/zotero/xpcom/collectionTreeRow.js
@@ -29,11 +29,13 @@ Zotero.CollectionTreeRow = function (collectionTreeView, type, ref, level, isOpe
this.view = collectionTreeView;
this.type = type;
this.ref = ref;
- this.level = level || 0
+ this.level = level || 0;
this.isOpen = isOpen || false;
this.onUnload = null;
}
+Zotero.CollectionTreeRow.IDCounter = 0;
+
Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
switch (this.type) {
@@ -73,7 +75,10 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
break;
}
- return '';
+ if (!this._id) {
+ this._id = 'I' + Zotero.CollectionTreeRow.IDCounter++;
+ }
+ return this._id;
});
Zotero.CollectionTreeRow.prototype.isLibrary = function (includeGlobal)
@@ -142,6 +147,10 @@ Zotero.CollectionTreeRow.prototype.isShare = function()
return this.type == 'share';
}
+Zotero.CollectionTreeRow.prototype.isContainer = function() {
+ return this.isLibrary(true) || this.isCollection() || this.isPublications() || this.isBucket();
+}
+
// Special
@@ -439,3 +448,26 @@ Zotero.CollectionTreeRow.prototype.isSearchMode = function() {
return true;
}
}
+
+Zotero.CollectionTreeCache = {
+ "lastTreeRow":null,
+ "lastTempTable":null,
+ "lastSearch":null,
+ "lastResults":null,
+
+ "clear": function () {
+ this.lastTreeRow = null;
+ this.lastSearch = null;
+ if (this.lastTempTable) {
+ let tableName = this.lastTempTable;
+ let id = Zotero.DB.addCallback('commit', async function () {
+ await Zotero.DB.queryAsync(
+ "DROP TABLE IF EXISTS " + tableName, false, { noCache: true }
+ );
+ Zotero.DB.removeCallback('commit', id);
+ });
+ }
+ this.lastTempTable = null;
+ this.lastResults = null;
+ }
+}
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
deleted file mode 100644
index b1956dafb9..0000000000
--- a/chrome/content/zotero/xpcom/collectionTreeView.js
+++ /dev/null
@@ -1,2478 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2009 Center for History and New Media
- George Mason University, Fairfax, Virginia, USA
- http://zotero.org
-
- This file is part of Zotero.
-
- Zotero is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Zotero is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Zotero. If not, see .
-
- ***** END LICENSE BLOCK *****
-*/
-
-////////////////////////////////////////////////////////////////////////////////
-///
-/// CollectionTreeView
-/// -- handles the link between an individual tree and the data layer
-/// -- displays only collections, in a hierarchy (no items)
-///
-////////////////////////////////////////////////////////////////////////////////
-
-/*
- * Constructor for the CollectionTreeView object
- */
-Zotero.CollectionTreeView = function()
-{
- Zotero.LibraryTreeView.apply(this);
-
- this.itemTreeView = null;
- this.itemToSelect = null;
- this.hideSources = [];
-
- this._highlightedRows = {};
- this._unregisterID = Zotero.Notifier.registerObserver(
- this,
- [
- 'collection',
- 'search',
- 'feed',
- 'share',
- 'group',
- 'feedItem',
- 'trash',
- 'bucket'
- ],
- 'collectionTreeView',
- 25
- );
- this._containerState = {};
- this._virtualCollectionLibraries = {};
- this._trashNotEmpty = {};
-}
-
-Zotero.CollectionTreeView.prototype = Object.create(Zotero.LibraryTreeView.prototype);
-Zotero.CollectionTreeView.prototype.type = 'collection';
-
-Object.defineProperty(Zotero.CollectionTreeView.prototype, "selectedTreeRow", {
- get: function () {
- if (!this.selection || !this.selection.count) {
- return false;
- }
- return this.getRow(this.selection.currentIndex);
- }
-});
-
-
-Object.defineProperty(Zotero.CollectionTreeView.prototype, 'window', {
- get: function () {
- return this._ownerDocument.defaultView;
- },
- enumerable: true
-});
-
-
-/*
- * Called by the tree itself
- */
-Zotero.CollectionTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (treebox)
-{
- try {
- if (this._treebox || !treebox) {
- return;
- }
- this._treebox = treebox;
-
- if (!this._ownerDocument) {
- try {
- this._ownerDocument = treebox.treeBody.ownerDocument;
- }
- catch (e) {}
- }
-
- // Add a keypress listener for expand/collapse
- var tree = this._treebox.treeBody.parentNode;
- tree.addEventListener('keypress', function(event) {
- if (tree.editingRow != -1) return; // In-line editing active
-
- var libraryID = this.getSelectedLibraryID();
- if (!libraryID) return;
-
- var key = String.fromCharCode(event.which);
- if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
- this.expandLibrary(libraryID, true);
- }
- else if (key == '-' && !(event.shiftKey || event.ctrlKey ||
- event.altKey || event.metaKey)) {
- this.collapseLibrary(libraryID);
- }
- }.bind(this), false);
-
- yield this.refresh();
- if (!this._treebox.columns) {
- return;
- }
- this.selection.currentColumn = this._treebox.columns.getFirstColumn();
-
- var lastViewedID = Zotero.Prefs.get('lastViewedFolder');
- if (lastViewedID) {
- var selected = yield this.selectByID(lastViewedID);
- }
- if (!selected) {
- this.selection.select(0);
- }
- this.selection.selectEventsSuppressed = false;
-
- yield this.runListeners('load');
- this._initialized = true;
- }
- catch (e) {
- Zotero.debug(e, 1);
- Components.utils.reportError(e);
- if (this.onError) {
- this.onError(e);
- }
- throw e;
- }
-});
-
-
-/**
- * Rebuild the tree from the data access methods and clear the selection
- *
- * Calling code must invalidate the tree, restore the selection, and unsuppress selection events
- */
-Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function* ()
-{
- Zotero.debug("Refreshing collections pane");
-
- // Record open states before refreshing
- if (this._rows) {
- for (var i=0, len=this._rows.length; i extraData[id] && extraData[id].skipSelect)) {
- if (action == 'delete' && (type == 'group' || type == 'feed')) {
- // Don't try to access deleted library
- }
- else {
- scrollPosition = this._saveScrollPosition();
- }
- }
-
- if (action == 'delete') {
- let selectedIndex = this.selection.count ? this.selection.currentIndex : 0;
- let refreshFeeds = false;
-
- // Since a delete involves shifting of rows, we have to do it in reverse order
- let rows = [];
- for (let i = 0; i < ids.length; i++) {
- let id = ids[i];
- switch (type) {
- case 'collection':
- if (this._rowMap['C' + id] !== undefined) {
- rows.push(this._rowMap['C' + id]);
- }
- break;
-
- case 'search':
- if (this._rowMap['S' + id] !== undefined) {
- rows.push(this._rowMap['S' + id]);
- }
- break;
-
- case 'feed':
- case 'group':
- let row = this.getRowIndexByID("L" + extraData[id].libraryID);
- let level = this.getLevel(row);
- do {
- rows.push(row);
- row++;
- }
- while (row < this.rowCount && this.getLevel(row) > level);
-
- if (type == 'feed') {
- refreshFeeds = true;
- }
- break;
- }
- }
-
- if (rows.length > 0) {
- rows.sort(function(a,b) { return a-b });
-
- for (let i = rows.length - 1; i >= 0; i--) {
- let row = rows[i];
- this._removeRow(row);
- }
-
- // If a feed was removed and there are no more, remove Feeds header
- if (refreshFeeds && !Zotero.Feeds.haveFeeds()) {
- for (let i = 0; i < this._rows.length; i++) {
- let row = this._rows[i];
- if (row.ref.id == 'feed-libraries-header') {
- this._removeRow(i);
- this._removeRow(i - 1);
- break;
- }
- }
- }
-
- this._refreshRowMap();
- }
-
- this.selectAfterRowRemoval(selectedIndex);
- }
- else if (action == 'modify') {
- let row;
- let id = ids[0];
- let rowID = "C" + id;
- let selectedIndex = this.selection.count ? this.selection.currentIndex : 0;
-
- switch (type) {
- case 'collection':
- let collection = Zotero.Collections.get(id);
- row = this.getRowIndexByID(rowID);
- // If collection is visible
- if (row !== false) {
- // TODO: Only move if name changed
- let reopen = this.isContainerOpen(row);
- if (reopen) {
- this._closeContainer(row);
- }
- this._removeRow(row);
-
- // Collection was moved to trash, so don't add it back
- if (collection.deleted) {
- this._refreshRowMap();
- this.selectAfterRowRemoval(selectedIndex);
- }
- else {
- yield this._addSortedRow('collection', id);
- yield this.selectByID(currentTreeRow.id);
- if (reopen) {
- let newRow = this.getRowIndexByID(rowID);
- if (!this.isContainerOpen(newRow)) {
- yield this.toggleOpenState(newRow);
- }
- }
- }
- }
- // If collection isn't currently visible and it isn't in the trash (because it was
- // undeleted), add it (if possible without opening any containers)
- else if (!collection.deleted) {
- yield this._addSortedRow('collection', id);
- // Invalidate parent in case it's become non-empty
- let parentRow = this.getRowIndexByID("C" + collection.parentID);
- if (parentRow !== false) {
- this._treebox.invalidateRow(parentRow);
- }
- }
- break;
-
- case 'search':
- let search = Zotero.Searches.get(id);
- row = this.getRowIndexByID("S" + id);
- if (row !== false) {
- // TODO: Only move if name changed
- this._removeRow(row);
-
- // Search moved to trash
- if (search.deleted) {
- this._refreshRowMap();
- this.selectAfterRowRemoval(selectedIndex);
- }
- // If search isn't in trash, add it back
- else {
- yield this._addSortedRow('search', id);
- yield this.selectByID(currentTreeRow.id);
- }
- }
- break;
-
- default:
- yield this.reload();
- yield this.selectByID(currentTreeRow.id);
- break;
- }
- }
- else if(action == 'add')
- {
- // skipSelect isn't necessary if more than one object
- let selectRow = ids.length == 1 && (!extraData[ids[0]] || !extraData[ids[0]].skipSelect);
-
- for (let id of ids) {
- switch (type) {
- case 'collection':
- case 'search':
- yield this._addSortedRow(type, id);
-
- if (selectRow) {
- if (type == 'collection') {
- yield this.selectCollection(id);
- }
- else if (type == 'search') {
- yield this.selectSearch(id);
- }
- }
-
- break;
-
- case 'group':
- case 'feed':
- if (type == 'groups' && ids.length != 1) {
- Zotero.logError("WARNING: Multiple groups shouldn't currently be added "
- + "together in collectionTreeView::notify()")
- }
- yield this.reload();
- yield this.selectByID(
- // Groups only come from sync, so they should never be auto-selected
- (type != 'group' && selectRow)
- ? "L" + id
- : currentTreeRow.id
- );
- break;
- }
- }
- }
-
- this._rememberScrollPosition(scrollPosition);
-
- var promise = this.waitForSelect();
- this.selection.selectEventsSuppressed = false;
- return promise;
-});
-
-/**
- * Add a row in the appropriate place
- *
- * This only adds a row if it would be visible without opening any containers
- *
- * @param {String} objectType
- * @param {Integer} id - collectionID
- * @return {Integer|false} - Index at which the row was added, or false if it wasn't added
- */
-Zotero.CollectionTreeView.prototype._addSortedRow = Zotero.Promise.coroutine(function* (objectType, id) {
- let beforeRow;
- if (objectType == 'collection') {
- let collection = yield Zotero.Collections.getAsync(id);
- let parentID = collection.parentID;
-
- // If parent isn't visible, don't add
- if (parentID && this._rowMap["C" + parentID] === undefined) {
- return false;
- }
-
- let libraryID = collection.libraryID;
- let startRow;
- if (parentID) {
- startRow = this._rowMap["C" + parentID];
- }
- else {
- startRow = this._rowMap['L' + libraryID];
- }
-
- // If container isn't open, don't add
- if (!this.isContainerOpen(startRow)) {
- return false;
- }
-
- let level = this.getLevel(startRow) + 1;
- // If container is empty, just add after
- if (this.isContainerEmpty(startRow)) {
- beforeRow = startRow + 1;
- }
- else {
- // Get all collections at the same level that don't have a different parent
- startRow++;
- loop:
- for (let i = startRow; i < this.rowCount; i++) {
- let treeRow = this.getRow(i);
- beforeRow = i;
-
- // Since collections come first, if we reach something that's not a collection,
- // stop
- if (!treeRow.isCollection()) {
- break;
- }
-
- let rowLevel = this.getLevel(i);
- if (rowLevel < level) {
- break;
- }
- else {
- // Fast forward through subcollections
- while (rowLevel > level) {
- beforeRow = ++i;
- if (i == this.rowCount || !this.getRow(i).isCollection()) {
- break loop;
- }
- treeRow = this.getRow(i);
- rowLevel = this.getLevel(i);
- // If going from lower level to a row higher than the target level, we found
- // our place:
- //
- // - 1
- // - 3
- // - 4
- // - 2 <<<< 5, a sibling of 3, goes above here
- if (rowLevel < level) {
- break loop;
- }
- }
-
- if (Zotero.localeCompare(treeRow.ref.name, collection.name) > 0) {
- break;
- }
- }
- }
- }
- this._addRow(
- new Zotero.CollectionTreeRow(this, 'collection', collection, level),
- beforeRow
- );
- }
- else if (objectType == 'search') {
- let search = Zotero.Searches.get(id);
- let libraryID = search.libraryID;
- let startRow = this._rowMap['L' + libraryID];
-
- // If container isn't open, don't add
- if (!this.isContainerOpen(startRow)) {
- return false;
- }
-
- let level = this.getLevel(startRow) + 1;
- // If container is empty, just add after
- if (this.isContainerEmpty(startRow)) {
- beforeRow = startRow + 1;
- }
- else {
- startRow++;
- var inSearches = false;
- for (let i = startRow; i < this.rowCount; i++) {
- let treeRow = this.getRow(i);
- beforeRow = i;
-
- // If we've reached something other than collections, stop
- if (treeRow.isSearch()) {
- // If current search sorts after, stop
- if (Zotero.localeCompare(treeRow.ref.name, search.name) > 0) {
- break;
- }
- }
- // If it's not a search and it's not a collection, stop
- else if (!treeRow.isCollection()) {
- break;
- }
- }
- }
- this._addRow(
- new Zotero.CollectionTreeRow(this, 'search', search, level),
- beforeRow
- );
- }
- return beforeRow;
-});
-
-
-/*
- * Set the rows that should be highlighted -- actual highlighting is done
- * by getRowProperties based on the array set here
- */
-Zotero.CollectionTreeView.prototype.setHighlightedRows = Zotero.Promise.coroutine(function* (ids) {
- this._highlightedRows = {};
- this._treebox.invalidate();
-
- if (!ids || !ids.length) {
- return;
- }
-
- // Make sure all highlighted collections are shown
- for (let id of ids) {
- if (id[0] == 'C') {
- yield this.expandToCollection(parseInt(id.substr(1)));
- }
- }
-
- // Highlight rows
- var rows = [];
- for (let id of ids) {
- let row = this._rowMap[id];
- this._highlightedRows[row] = true;
- this._treebox.invalidateRow(row);
- rows.push(row);
- }
- rows.sort();
- var firstRow = this._treebox.getFirstVisibleRow();
- var lastRow = this._treebox.getLastVisibleRow();
- var scrolled = false;
- for (let row of rows) {
- // If row is visible, stop
- if (row >= firstRow && row <= lastRow) {
- scrolled = true;
- break;
- }
- }
- // Select first collection
- // TODO: Select closest? Select a few rows above or below?
- if (!scrolled) {
- this._treebox.ensureRowIsVisible(rows[0]);
- }
-});
-
-
-/*
- * Unregisters view from Zotero.Notifier (called on window close)
- */
-Zotero.CollectionTreeView.prototype.unregister = function()
-{
- Zotero.Notifier.unregisterObserver(this._unregisterID);
-}
-
-
-////////////////////////////////////////////////////////////////////////////////
-///
-/// nsITreeView functions
-/// http://www.xulplanet.com/references/xpcomref/ifaces/nsITreeView.html
-///
-////////////////////////////////////////////////////////////////////////////////
-
-Zotero.CollectionTreeView.prototype.getCellText = function(row, column)
-{
- var obj = this.getRow(row);
-
- if (column.id == 'zotero-collections-name-column') {
- return obj.getName();
- }
- else
- return "";
-}
-
-Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
-{
- var suffix = Zotero.hiDPISuffix;
-
- var treeRow = this.getRow(row);
- var collectionType = treeRow.type;
-
- if (collectionType == 'group') {
- collectionType = 'library';
- }
-
- // Show sync icons only in library rows
- if (collectionType != 'library' && col.index != 0) {
- return '';
- }
-
- switch (collectionType) {
- case 'library':
- case 'feed':
- // Better alternative needed: https://github.com/zotero/zotero/pull/902#issuecomment-183185973
- /*
- if (treeRow.ref.updating) {
- collectionType += '-updating';
- } else */if (treeRow.ref.lastCheckError) {
- collectionType += '-error';
- }
- break;
-
- case 'trash':
- if (this._trashNotEmpty[treeRow.ref.libraryID]) {
- collectionType += '-full';
- }
- break;
-
- case 'header':
- if (treeRow.ref.id == 'group-libraries-header') {
- collectionType = 'groups';
- }
- else if (treeRow.ref.id == 'feed-libraries-header') {
- collectionType = 'feedLibrary';
- }
- else if (treeRow.ref.id == 'commons-header') {
- collectionType = 'commons';
- }
- break;
-
-
- collectionType = 'library';
- break;
-
- case 'collection':
- case 'search':
- // Keep in sync with Zotero.(Collection|Search).prototype.treeViewImage
- if (Zotero.isMac) {
- return `chrome://zotero-platform/content/treesource-${collectionType}${Zotero.hiDPISuffix}.png`;
- }
- break;
-
- case 'publications':
- return "chrome://zotero/skin/treeitem-journalArticle" + suffix + ".png";
-
- case 'retracted':
- return "chrome://zotero/skin/cross" + suffix + ".png";
- }
-
- return "chrome://zotero/skin/treesource-" + collectionType + suffix + ".png";
-}
-
-Zotero.CollectionTreeView.prototype.isContainer = function(row)
-{
- var treeRow = this.getRow(row);
- return treeRow.isLibrary(true) || treeRow.isCollection() || treeRow.isPublications() || treeRow.isBucket();
-}
-
-/*
- * Returns true if the collection has no child collections
- */
-Zotero.CollectionTreeView.prototype.isContainerEmpty = function(row)
-{
- var treeRow = this.getRow(row);
- if (treeRow.isLibrary()) {
- return false;
- }
- if (treeRow.isBucket()) {
- return true;
- }
- if (treeRow.isGroup()) {
- var libraryID = treeRow.ref.libraryID;
-
- return !treeRow.ref.hasCollections()
- && !treeRow.ref.hasSearches()
- // Duplicate Items not shown
- && (this.hideSources.indexOf('duplicates') != -1
- || this._virtualCollectionLibraries.duplicates[libraryID] === false)
- // Unfiled Items not shown
- && this._virtualCollectionLibraries.unfiled[libraryID] === false
- // Retracted Items not shown
- && this._virtualCollectionLibraries.retracted[libraryID] === false
- && this.hideSources.indexOf('trash') != -1;
- }
- if (treeRow.isCollection()) {
- return !treeRow.ref.hasChildCollections();
- }
- return true;
-}
-
-Zotero.CollectionTreeView.prototype.getParentIndex = function(row)
-{
- 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.CollectionTreeView.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;
- }
-}
-
-/*
- * Opens/closes the specified row
- */
-Zotero.CollectionTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(function* (row) {
- if (this.isContainerOpen(row)) {
- return this._closeContainer(row);
- }
-
- var count = 0;
-
- var treeRow = this.getRow(row);
- if (treeRow.isLibrary(true) || treeRow.isCollection()) {
- count = yield this._expandRow(this._rows, row, true);
- }
- this.rowCount += count;
- this._treebox.rowCountChanged(row + 1, count);
-
- this._rows[row].isOpen = true;
- this._treebox.invalidateRow(row);
- this._refreshRowMap();
- this._startSaveOpenStatesTimer();
-});
-
-
-Zotero.CollectionTreeView.prototype._closeContainer = function (row) {
- if (!this.isContainerOpen(row)) return;
-
- var count = 0;
- var level = this.getLevel(row);
- var nextRow = row + 1;
-
- // Remove child rows
- while ((nextRow < this._rows.length) && (this.getLevel(nextRow) > level)) {
- this._removeRow(nextRow);
- count--;
- }
-
- this._rows[row].isOpen = false;
- this._treebox.invalidateRow(row);
- this._refreshRowMap();
- this._startSaveOpenStatesTimer();
-}
-
-
-/**
- * After a short delay, persist the open states of the tree, or if already queued, cancel and requeue.
- * This avoids repeated saving while opening or closing multiple rows.
- */
-Zotero.CollectionTreeView.prototype._startSaveOpenStatesTimer = function () {
- if (this._saveOpenStatesTimeoutID) {
- clearTimeout(this._saveOpenStatesTimeoutID);
- }
- this._saveOpenStatesTimeoutID = setTimeout(() => {
- this._saveOpenStates();
- this._saveOpenStatesTimeoutID = null;
- }, 250)
-};
-
-
-Zotero.CollectionTreeView.prototype.isSelectable = function (row, col) {
- var treeRow = this.getRow(row);
- switch (treeRow.type) {
- case 'separator':
- case 'header':
- return false;
- }
- return true;
-}
-
-
-/**
- * Tree method for whether to allow inline editing (not to be confused with this.editable)
- */
-Zotero.CollectionTreeView.prototype.isEditable = function (row, col) {
- return this.selectedTreeRow.isCollection() && this.editable;
-}
-
-
-Zotero.CollectionTreeView.prototype.setCellText = function (row, col, val) {
- val = val.trim();
- if (val === "") {
- return;
- }
- var treeRow = this.getRow(row);
- treeRow.ref.name = val;
- treeRow.ref.saveTx();
-}
-
-
-
-/**
- * Returns TRUE if the underlying view is editable
- */
-Zotero.CollectionTreeView.prototype.__defineGetter__('editable', function () {
- return this.getRow(this.selection.currentIndex).editable;
-});
-
-
-/**
- * @param {Integer} libraryID
- * @param {Boolean} [recursive=false] - Expand all collections and subcollections
- */
-Zotero.CollectionTreeView.prototype.expandLibrary = Zotero.Promise.coroutine(function* (libraryID, recursive) {
- var row = this._rowMap['L' + libraryID]
- if (row === undefined) {
- return false;
- }
- if (!this.isContainerOpen(row)) {
- yield this.toggleOpenState(row);
- }
-
- if (recursive) {
- for (let i = row; i < this.rowCount && this.getRow(i).ref.libraryID == libraryID; i++) {
- if (this.isContainer(i) && !this.isContainerOpen(i)) {
- yield this.toggleOpenState(i);
- }
- }
- }
-
- return true;
-});
-
-
-Zotero.CollectionTreeView.prototype.collapseLibrary = function (libraryID) {
- var row = this._rowMap['L' + libraryID]
- if (row === undefined) {
- return false;
- }
-
- var closed = [];
- var found = false;
- for (let i = this.rowCount - 1; i >= row; i--) {
- let treeRow = this.getRow(i);
- if (treeRow.ref.libraryID !== libraryID) {
- // Once we've moved beyond the original library, stop looking
- if (found) {
- break;
- }
- continue;
- }
- found = true;
-
- if (this.isContainer(i) && this.isContainerOpen(i)) {
- closed.push(treeRow.id);
- this._closeContainer(i);
- }
- }
-
- // Select the collapsed library
- this.selection.select(row);
-
- // We have to manually delete closed rows from the container state object, because otherwise
- // _saveOpenStates() wouldn't see any of the rows under the library (since the library is now
- // collapsed) and they'd remain as open in the persisted object.
- closed.forEach(id => { delete this._containerState[id]; });
- this._saveOpenStates();
-
- return true;
-};
-
-
-Zotero.CollectionTreeView.prototype.expandToCollection = Zotero.Promise.coroutine(function* (collectionID) {
- var col = yield Zotero.Collections.getAsync(collectionID);
- if (!col) {
- Zotero.debug("Cannot expand to nonexistent collection " + collectionID, 2);
- return false;
- }
-
- // Open library if closed
- var libraryRow = this._rowMap['L' + col.libraryID];
- if (!this.isContainerOpen(libraryRow)) {
- yield this.toggleOpenState(libraryRow);
- }
-
- var row = this._rowMap["C" + collectionID];
- if (row !== undefined) {
- return true;
- }
- var path = [];
- var parentID;
- var seen = new Set([col.id])
- while (parentID = col.parentID) {
- // Detect infinite loop due to invalid nesting in DB
- if (seen.has(parentID)) {
- yield Zotero.Schema.setIntegrityCheckRequired(true);
- Zotero.crash();
- return;
- }
- seen.add(parentID);
- path.unshift(parentID);
- col = yield Zotero.Collections.getAsync(parentID);
- }
- for (let id of path) {
- row = this._rowMap["C" + id];
- if (!this.isContainerOpen(row)) {
- yield this.toggleOpenState(row);
- }
- }
- return true;
-});
-
-
-
-////////////////////////////////////////////////////////////////////////////////
-///
-/// Additional functions for managing data in the tree
-///
-////////////////////////////////////////////////////////////////////////////////
-Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(function* (id) {
- var type = id[0];
- id = parseInt(('' + id).substr(1));
-
- switch (type) {
- case 'L':
- return yield this.selectLibrary(id);
-
- case 'C':
- yield this.expandToCollection(id);
- break;
-
- case 'S':
- var search = yield Zotero.Searches.getAsync(id);
- yield this.expandLibrary(search.libraryID);
- break;
-
- case 'D':
- case 'U':
- case 'R':
- yield this.expandLibrary(id);
- break;
-
- case 'T':
- return yield this.selectTrash(id);
- }
-
- var row = this._rowMap[type + id];
- if (!row) {
- return false;
- }
- this._treebox.ensureRowIsVisible(row);
- yield this.selectWait(row);
-
- return true;
-});
-
-
-/**
- * @param {Integer} libraryID Library to select
- */
-Zotero.CollectionTreeView.prototype.selectLibrary = Zotero.Promise.coroutine(function* (libraryID) {
- // Select local library
- if (!libraryID) {
- this._treebox.ensureRowIsVisible(0);
- yield this.selectWait(0);
- return true;
- }
-
- // Check if library is already selected
- if (this.selection && this.selection.count && this.selection.currentIndex != -1) {
- var treeRow = this.getRow(this.selection.currentIndex);
- if (treeRow.isLibrary(true) && treeRow.ref.libraryID == libraryID) {
- this._treebox.ensureRowIsVisible(this.selection.currentIndex);
- return true;
- }
- }
-
- // Find library
- var row = this._rowMap['L' + libraryID];
- if (row !== undefined) {
- this._treebox.ensureRowIsVisible(row);
- yield this.selectWait(row);
- return true;
- }
-
- return false;
-});
-
-
-Zotero.CollectionTreeView.prototype.selectCollection = function (id) {
- return this.selectByID('C' + id);
-}
-
-
-Zotero.CollectionTreeView.prototype.selectSearch = function (id) {
- return this.selectByID('S' + id);
-}
-
-
-Zotero.CollectionTreeView.prototype.selectTrash = Zotero.Promise.coroutine(function* (libraryID) {
- // Check if trash is already selected
- if (this.selection && this.selection.count && this.selection.currentIndex != -1) {
- let itemGroup = this.getRow(this.selection.currentIndex);
- if (itemGroup.isTrash() && itemGroup.ref.libraryID == libraryID) {
- this._treebox.ensureRowIsVisible(this.selection.currentIndex);
- return true;
- }
- }
-
- // Find library trash
- for (let i = 0; i < this.rowCount; i++) {
- let itemGroup = this.getRow(i);
-
- // If library is closed, open it
- if (itemGroup.isLibrary(true) && itemGroup.ref.libraryID == libraryID
- && !this.isContainerOpen(i)) {
- yield this.toggleOpenState(i);
- continue;
- }
-
- if (itemGroup.isTrash() && itemGroup.ref.libraryID == libraryID) {
- this._treebox.ensureRowIsVisible(i);
- this.selection.select(i);
- return true;
- }
- }
-
- return false;
-});
-
-
-/**
- * Find an item in the current collection, or, if not there, in a library root, and select it
- *
- * @param {Integer} - itemID
- * @param {Boolean} [inLibraryRoot=false] - Always show in library root
- * @return {Boolean} - TRUE if the item was selected, FALSE if not
- */
-Zotero.CollectionTreeView.prototype.selectItem = async function (itemID, inLibraryRoot) {
- return !!(await this.selectItems([itemID], inLibraryRoot));
-};
-
-
-/**
- * Find items in current collection, or, if not there, in a library root, and select them
- *
- * @param {Integer[]} itemIDs
- * @param {Boolean} [inLibraryRoot=false] - Always show in library root
- * @return {Integer} - The number of items selected
- */
-Zotero.CollectionTreeView.prototype.selectItems = async function (itemIDs, inLibraryRoot) {
- if (!itemIDs.length) {
- return 0;
- }
-
- var items = await Zotero.Items.getAsync(itemIDs);
- if (!items.length) {
- return 0;
- }
-
- await this.waitForLoad();
-
- // Check if items from multiple libraries were specified
- if (items.length > 1 && new Set(items.map(item => item.libraryID)).size > 1) {
- Zotero.debug("Can't select items in multiple libraries", 2);
- return 0;
- }
-
- var currentLibraryID = this.getSelectedLibraryID();
- var libraryID = items[0].libraryID;
- // If in a different library
- if (libraryID != currentLibraryID) {
- Zotero.debug("Library ID differs; switching library");
- await this.selectLibrary(libraryID);
- }
- // Force switch to library view
- else if (!this.selectedTreeRow.isLibrary() && inLibraryRoot) {
- Zotero.debug("Told to select in library; switching to library");
- await this.selectLibrary(libraryID);
- }
-
- await this.itemTreeView.waitForLoad();
-
- var numSelected = await this.itemTreeView.selectItems(itemIDs);
- if (numSelected == items.length) {
- return numSelected;
- }
-
- // If there's a single item and it's in the trash, switch to that
- if (items.length == 1 && items[0].deleted) {
- Zotero.debug("Item is deleted; switching to trash");
- await this.selectTrash(libraryID);
- }
- else {
- Zotero.debug("Item was not selected; switching to library");
- await this.selectLibrary(libraryID);
- }
-
- await this.itemTreeView.waitForLoad();
- return this.itemTreeView.selectItems(itemIDs);
-};
-
-
-/*
- * Delete the selection
- */
-Zotero.CollectionTreeView.prototype.deleteSelection = Zotero.Promise.coroutine(function* (deleteItems)
-{
- if(this.selection.count == 0)
- return;
-
- //collapse open collections
- for (let i=0; i= this.rowCount) {
- row = this.rowCount - 1;
- };
-
- // Make sure the selection doesn't land on a separator (e.g. deleting last feed)
- while (row >= 0 && !this.isSelectable(row)) {
- // move up, since we got shifted down
- row--;
- }
-
- this.selection.select(row);
-};
-
-
-/**
- * Expand row based on last state
- */
-Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(function* (rows, row, forceOpen) {
- var treeRow = rows[row];
- var level = rows[row].level;
- var isLibrary = treeRow.isLibrary(true);
- var isCollection = treeRow.isCollection();
- var libraryID = treeRow.ref.libraryID;
-
- if (treeRow.isPublications() || treeRow.isFeed()) {
- return false;
- }
-
- if (isLibrary) {
- var collections = Zotero.Collections.getByLibrary(libraryID);
- }
- else if (isCollection) {
- var collections = Zotero.Collections.getByParent(treeRow.ref.id);
- }
-
- if (isLibrary) {
- var savedSearches = yield Zotero.Searches.getAll(libraryID);
- // Virtual collections default to showing if not explicitly hidden
- var showDuplicates = this.hideSources.indexOf('duplicates') == -1
- && this._virtualCollectionLibraries.duplicates[libraryID] !== false;
- var showUnfiled = this._virtualCollectionLibraries.unfiled[libraryID] !== false;
- var showRetracted = this._virtualCollectionLibraries.retracted[libraryID] !== false
- && Zotero.Retractions.libraryHasRetractedItems(libraryID);
- var showPublications = libraryID == Zotero.Libraries.userLibraryID;
- var showTrash = this.hideSources.indexOf('trash') == -1;
- }
- else {
- var savedSearches = [];
- var showDuplicates = false;
- var showUnfiled = false;
- var showRetracted = false;
- var showPublications = false;
- var showTrash = false;
- }
-
- // If not a manual open and either the library is set to be collapsed or this is a collection that isn't explicitly opened,
- // set the initial state to closed
- if (!forceOpen &&
- (this._containerState[treeRow.id] === false
- || (isCollection && !this._containerState[treeRow.id]))) {
- rows[row].isOpen = false;
- return 0;
- }
-
- var startOpen = !!(collections.length || savedSearches.length || showDuplicates || showUnfiled || showRetracted || showTrash);
-
- // If this isn't a manual open, set the initial state depending on whether
- // there are child nodes
- if (!forceOpen) {
- rows[row].isOpen = startOpen;
- }
-
- if (!startOpen) {
- return 0;
- }
-
- var newRows = 0;
-
- // Add collections
- for (var i = 0, len = collections.length; i < len; i++) {
- // Skip collections in trash
- if (collections[i].deleted) continue;
-
- let beforeRow = row + 1 + newRows;
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this, 'collection', collections[i], level + 1),
- beforeRow
- );
- newRows++;
- // Recursively expand child collections that should be open
- newRows += yield this._expandRow(rows, beforeRow);
- }
-
- if (isCollection) {
- return newRows;
- }
-
- // Add searches
- for (var i = 0, len = savedSearches.length; i < len; i++) {
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this, 'search', savedSearches[i], level + 1),
- row + 1 + newRows
- );
- newRows++;
- }
-
- if (showPublications) {
- // Add "My Publications"
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this,
- 'publications',
- {
- libraryID,
- treeViewID: "P" + libraryID
- },
- level + 1
- ),
- row + 1 + newRows
- );
- newRows++
- }
-
- // Duplicate items
- if (showDuplicates) {
- let d = new Zotero.Duplicates(libraryID);
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this, 'duplicates', d, level + 1),
- row + 1 + newRows
- );
- newRows++;
- }
-
- // Unfiled items
- if (showUnfiled) {
- let s = new Zotero.Search;
- s.libraryID = libraryID;
- s.name = Zotero.getString('pane.collections.unfiled');
- s.addCondition('libraryID', 'is', libraryID);
- s.addCondition('unfiled', 'true');
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this, 'unfiled', s, level + 1),
- row + 1 + newRows
- );
- newRows++;
- }
-
- // Retracted items
- if (showRetracted) {
- let s = new Zotero.Search;
- s.libraryID = libraryID;
- s.name = Zotero.getString('pane.collections.retracted');
- s.addCondition('libraryID', 'is', libraryID);
- s.addCondition('retracted', 'true');
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this, 'retracted', s, level + 1),
- row + 1 + newRows
- );
- newRows++;
- }
-
- if (showTrash) {
- let deletedItems = yield Zotero.Items.getDeleted(libraryID);
- if (deletedItems.length || Zotero.Prefs.get("showTrashWhenEmpty")) {
- var ref = {
- libraryID: libraryID
- };
- this._addRowToArray(
- rows,
- new Zotero.CollectionTreeRow(this, 'trash', ref, level + 1),
- row + 1 + newRows
- );
- newRows++;
- }
- this._trashNotEmpty[libraryID] = !!deletedItems.length;
- }
-
- return newRows;
-});
-
-
-/**
- * Return libraryID of selected row (which could be a collection, etc.)
- */
-Zotero.CollectionTreeView.prototype.getSelectedLibraryID = function() {
- if (!this.selection || !this.selection.count || this.selection.currentIndex == -1) return false;
-
- var treeRow = this.getRow(this.selection.currentIndex);
- return treeRow && treeRow.ref && treeRow.ref.libraryID !== undefined
- && treeRow.ref.libraryID;
-}
-
-
-Zotero.CollectionTreeView.prototype.getSelectedCollection = function(asID) {
- if (this.selection
- && this.selection.count > 0
- && this.selection.currentIndex != -1) {
- var collection = this.getRow(this.selection.currentIndex);
- if (collection && collection.isCollection()) {
- return asID ? collection.ref.id : collection.ref;
- }
- }
- return false;
-}
-
-
-/**
- * Creates mapping of item group ids to tree rows
- */
-Zotero.CollectionTreeView.prototype._refreshRowMap = function() {
- this._rowMap = {};
- for (let i = 0, len = this.rowCount; i < len; i++) {
- this._rowMap[this.getRow(i).id] = i;
- }
-}
-
-
-/**
- * Persist the current open/closed state of rows to a pref
- */
-Zotero.CollectionTreeView.prototype._saveOpenStates = Zotero.Promise.coroutine(function* () {
- var state = this._containerState;
-
- // Every so often, remove obsolete rows
- if (Math.random() < 1/20) {
- Zotero.debug("Purging sourceList.persist");
- for (var id in state) {
- var m = id.match(/^C([0-9]+)$/);
- if (m) {
- if (!(yield Zotero.Collections.getAsync(parseInt(m[1])))) {
- delete state[id];
- }
- continue;
- }
-
- var m = id.match(/^G([0-9]+)$/);
- if (m) {
- if (!Zotero.Groups.get(parseInt(m[1]))) {
- delete state[id];
- }
- continue;
- }
- }
- }
-
- for (var i = 0, len = this.rowCount; i < len; i++) {
- if (!this.isContainer(i)) {
- continue;
- }
-
- var treeRow = this.getRow(i);
- if (!treeRow.id) {
- continue;
- }
-
- var open = this.isContainerOpen(i);
-
- // Collections and feeds default to closed
- if ((!open && treeRow.isCollection()) || treeRow.isFeed()) {
- delete state[treeRow.id];
- continue;
- }
-
- state[treeRow.id] = open;
- }
-
- this._containerState = state;
- Zotero.Prefs.set("sourceList.persist", JSON.stringify(state));
-});
-
-
-////////////////////////////////////////////////////////////////////////////////
-///
-/// Command Controller:
-/// for Select All, etc.
-///
-////////////////////////////////////////////////////////////////////////////////
-
-Zotero.CollectionTreeCommandController = function(tree)
-{
- this.tree = tree;
-}
-
-Zotero.CollectionTreeCommandController.prototype.supportsCommand = function(cmd)
-{
-}
-
-Zotero.CollectionTreeCommandController.prototype.isCommandEnabled = function(cmd)
-{
-}
-
-Zotero.CollectionTreeCommandController.prototype.doCommand = function(cmd)
-{
-}
-
-Zotero.CollectionTreeCommandController.prototype.onEvent = function(evt)
-{
-}
-
-////////////////////////////////////////////////////////////////////////////////
-///
-/// Drag-and-drop functions:
-/// canDrop() and drop() are for nsITreeView
-/// onDragStart() and onDrop() are for HTML 5 Drag and Drop
-///
-////////////////////////////////////////////////////////////////////////////////
-
-
-/*
- * Start a drag using HTML 5 Drag and Drop
- */
-Zotero.CollectionTreeView.prototype.onDragStart = function(event) {
- // See note in LibraryTreeView::_setDropEffect()
- if (Zotero.isWin || Zotero.isLinux) {
- event.dataTransfer.effectAllowed = 'copyMove';
- }
-
- var treeRow = this.selectedTreeRow;
- if (!treeRow.isCollection()) {
- return;
- }
- event.dataTransfer.setData("zotero/collection", treeRow.ref.id);
- Zotero.debug("Dragging collection " + treeRow.id);
-}
-
-
-/**
- * Called by treechildren.onDragOver() before setting the dropEffect,
- * which is checked in libraryTreeView.canDrop()
- */
-Zotero.CollectionTreeView.prototype.canDropCheck = function (row, orient, dataTransfer) {
- // TEMP
- 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;
-
- // Empty space below rows
- if (row == -1) {
- return false;
- }
-
- // For dropping collections onto root level
- if (orient == 1 && row == 0 && dataType == 'zotero/collection') {
- return true;
- }
- // Directly on a row
- else if (orient == 0) {
- var treeRow = this.getRow(row); //the collection we are dragging over
-
- if (dataType == 'zotero/item' && treeRow.isBucket()) {
- return true;
- }
-
- if (!treeRow.editable) {
- Zotero.debug("Drop target not editable");
- return false;
- }
-
- if (treeRow.isFeed()) {
- Zotero.debug("Cannot drop into feeds");
- return false;
- }
-
- if (dataType == 'zotero/item') {
- var ids = data;
- var items = Zotero.Items.get(ids);
- items = Zotero.Items.keepParents(items);
- var skip = true;
- for (let item of items) {
- // Can only drag top-level items
- if (!item.isTopLevelItem()) {
- Zotero.debug("Can't drag child item");
- return false;
- }
-
- if (treeRow.isWithinGroup() && item.isAttachment()) {
- // Linked files can't be added to groups
- if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
- Zotero.debug("Linked files cannot be added to groups");
- return false;
- }
- if (!treeRow.filesEditable) {
- Zotero.debug("Drop target does not allow files to be edited");
- return false;
- }
- skip = false;
- continue;
- }
-
- if (treeRow.isPublications()) {
- if (item.isAttachment() || item.isNote()) {
- Zotero.debug("Top-level attachments and notes cannot be added to My Publications");
- return false;
- }
- if(item instanceof Zotero.FeedItem) {
- Zotero.debug("FeedItems cannot be added to My Publications");
- return false;
- }
- if (item.inPublications) {
- Zotero.debug("Item " + item.id + " already exists in My Publications");
- continue;
- }
- if (treeRow.ref.libraryID != item.libraryID) {
- Zotero.debug("Cross-library drag to My Publications not allowed");
- continue;
- }
- skip = false;
- continue;
- }
-
- // Cross-library drag
- if (treeRow.ref.libraryID != item.libraryID) {
- // Only allow cross-library drag to root library and collections
- if (!(treeRow.isLibrary(true) || treeRow.isCollection())) {
- Zotero.debug("Cross-library drag to non-collection not allowed");
- return false;
- }
- skip = false;
- continue;
- }
-
- // Intra-library drag
-
- // Don't allow drag onto root of same library
- if (treeRow.isLibrary(true)) {
- Zotero.debug("Can't drag into same library root");
- return false;
- }
-
- // Make sure there's at least one item that's not already in this destination
- if (treeRow.isCollection()) {
- if (treeRow.ref.hasItem(item.id)) {
- Zotero.debug("Item " + item.id + " already exists in collection");
- continue;
- }
- skip = false;
- continue;
- }
- }
- if (skip) {
- Zotero.debug("Drag skipped");
- return false;
- }
- return true;
- }
- else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
- if (treeRow.isSearch() || treeRow.isPublications()) {
- return false;
- }
- if (dataType == 'application/x-moz-file') {
- // Don't allow folder drag
- if (data[0].isDirectory()) {
- return false;
- }
- // Don't allow drop if no permissions
- if (!treeRow.filesEditable) {
- return false;
- }
- }
-
- return true;
- }
- else if (dataType == 'zotero/collection') {
- if (treeRow.isPublications()) {
- return false;
- }
-
- let draggedCollectionID = data[0];
- let draggedCollection = Zotero.Collections.get(draggedCollectionID);
-
- if (treeRow.ref.libraryID == draggedCollection.libraryID) {
- // Collections cannot be dropped on themselves
- if (draggedCollectionID == treeRow.ref.id) {
- return false;
- }
-
- // Nor in their children
- if (draggedCollection.hasDescendent('collection', treeRow.ref.id)) {
- return false;
- }
- }
- // Dragging a collection to a different library
- else {
- // Allow cross-library drag only to root library and collections
- if (!treeRow.isLibrary(true) && !treeRow.isCollection()) {
- return false;
- }
- }
-
- return true;
- }
- }
- return false;
-};
-
-
-/**
- * Perform additional asynchronous drop checks
- *
- * Called by treechildren.drop()
- */
-Zotero.CollectionTreeView.prototype.canDropCheckAsync = Zotero.Promise.coroutine(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;
-
- if (orient == 0) {
- var treeRow = this.getRow(row); //the collection we are dragging over
-
- if (dataType == 'zotero/item' && treeRow.isBucket()) {
- return true;
- }
-
- if (dataType == 'zotero/item') {
- var ids = data;
- var items = Zotero.Items.get(ids);
- var skip = true;
- for (let i=0; i Zotero.Items.get(itemID).isTopLevelItem());
- yield Zotero.DB.executeTransaction(function* () {
- let collection = yield Zotero.Collections.getAsync(targetCollectionID);
- yield collection.addItems(ids);
- }.bind(this));
- }
- else if (targetTreeRow.isPublications()) {
- yield Zotero.Items.addToPublications(newItems, copyOptions);
- }
-
- // If moving, remove items from source collection
- if (dropEffect == 'move' && toMove.length) {
- if (!sameLibrary) {
- throw new Error("Cannot move items between libraries");
- }
- if (!sourceTreeRow || !sourceTreeRow.isCollection()) {
- throw new Error("Drag source must be a collection for move action");
- }
- yield Zotero.DB.executeTransaction(function* () {
- yield sourceTreeRow.ref.removeItems(toMove);
- }.bind(this));
- }
- }
- else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
- var targetLibraryID = targetTreeRow.ref.libraryID;
- if (targetTreeRow.isCollection()) {
- var parentCollectionID = targetTreeRow.ref.id;
- }
- else {
- var parentCollectionID = false;
- }
- var addedItems = [];
-
- for (var i=0; i val.color);
-
- return Zotero.Tags.generateItemsListImage(colors, uri, retracted);
-});
+ return colorData.sort((a, b) => a.position - b.position).map(val => val.color);
+};
diff --git a/chrome/content/zotero/xpcom/data/library.js b/chrome/content/zotero/xpcom/data/library.js
index 8d210b4055..7a8ffc1597 100644
--- a/chrome/content/zotero/xpcom/data/library.js
+++ b/chrome/content/zotero/xpcom/data/library.js
@@ -55,7 +55,7 @@ Zotero.Library = function(params = {}) {
// Return a proxy so that we can disable the object once it's deleted
return new Proxy(this, {
get: function(obj, prop) {
- if (obj._disabled && !(prop == 'libraryID' || prop == 'id')) {
+ if (obj._disabled && !(prop == 'libraryID' || prop == 'id' || prop == 'name')) {
throw new Error("Library (" + obj.libraryID + ") has been disabled");
}
return obj[prop];
diff --git a/chrome/content/zotero/xpcom/debug.js b/chrome/content/zotero/xpcom/debug.js
index b716b94c08..43c270d4dd 100644
--- a/chrome/content/zotero/xpcom/debug.js
+++ b/chrome/content/zotero/xpcom/debug.js
@@ -238,6 +238,9 @@ Zotero.Debug = new function () {
});
});
+ this._setLevel = function(level) {
+ _level = level;
+ }
this.addListener = function (listener) {
this.enabled = true;
diff --git a/chrome/content/zotero/xpcom/fileDragDataProvider.js b/chrome/content/zotero/xpcom/fileDragDataProvider.js
new file mode 100644
index 0000000000..3c8e9940c4
--- /dev/null
+++ b/chrome/content/zotero/xpcom/fileDragDataProvider.js
@@ -0,0 +1,245 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 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 *****
+*/
+
+
+// Implements nsIFlavorDataProvider for dragging attachment files to OS
+//
+// Not used on Windows in Firefox 3 or higher
+Zotero.FileDragDataProvider = function (itemIDs) {
+ this._itemIDs = itemIDs;
+};
+
+Zotero.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.nsIFile);
+
+ var draggedItems = Zotero.Items.get(this._itemIDs);
+ var items = [];
+
+ // Make sure files exist
+ var notFoundNames = [];
+ for (var 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, 0o755);
+ }
+
+ 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, 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"));
+ }
+ }
+ }
+}
+
diff --git a/chrome/content/zotero/xpcom/intl.js b/chrome/content/zotero/xpcom/intl.js
index 0220559e3e..1f0cb03584 100644
--- a/chrome/content/zotero/xpcom/intl.js
+++ b/chrome/content/zotero/xpcom/intl.js
@@ -51,7 +51,7 @@ Zotero.Intl = new function () {
Zotero.Utilities.Internal.quitZotero(true);
return;
}
- }
+ }
Components.utils.import("resource://gre/modules/PluralForm.jsm");
@@ -76,7 +76,7 @@ Zotero.Intl = new function () {
Zotero.rtl = (Zotero.dir === 'rtl');
this.strings = {};
- const intlFiles = ['zotero.dtd', 'mozilla/editMenuOverlay.dtd'];
+ const intlFiles = ['zotero.dtd', 'preferences.dtd', 'mozilla/editMenuOverlay.dtd'];
for (let intlFile of intlFiles) {
let localeXML = Zotero.File.getContentsFromURL(`chrome://zotero/locale/${intlFile}`);
let regexp = /.
-
- ***** 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) {
- Zotero.LibraryTreeView.apply(this);
-
- this.wrappedJSObject = this;
- this.rowCount = 0;
- this.collectionTreeRow = collectionTreeRow;
- collectionTreeRow.view.itemTreeView = this;
-
- this._skipKeypress = false;
-
- this._ownerDocument = null;
- this._needsSort = false;
- this._introText = null;
-
- this._cellTextCache = {};
- this._itemImages = {};
-
- this._refreshPromise = Zotero.Promise.resolve();
-
- this._notifierObserverID = Zotero.Notifier.registerObserver(
- this,
- ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search'],
- 'itemTreeView',
- 50
- );
- this._prefObserverID = Zotero.Prefs.registerObserver('recursiveCollections', this.refresh.bind(this));
-}
-
-Zotero.ItemTreeView.prototype = Object.create(Zotero.LibraryTreeView.prototype);
-Zotero.ItemTreeView.prototype.type = 'item';
-Zotero.ItemTreeView.prototype.regularOnly = false;
-Zotero.ItemTreeView.prototype.expandAll = false;
-Zotero.ItemTreeView.prototype.collapseAll = false;
-
-Object.defineProperty(Zotero.ItemTreeView.prototype, 'window', {
- get: function () {
- return this._ownerDocument.defaultView;
- },
- enumerable: true
-});
-
-/**
- * Called by the tree itself
- */
-Zotero.ItemTreeView.prototype.setTree = async function (treebox) {
- try {
- if (this._treebox) {
- if (this._needsSort) {
- this.sort();
- }
- return;
- }
-
- var start = Date.now();
-
- Zotero.debug("Setting tree for " + this.collectionTreeRow.id + " items view " + this.id);
-
- if (!treebox) {
- Zotero.debug("Treebox not passed in setTree()", 2);
- return;
- }
- this._treebox = treebox;
-
- if (!this._ownerDocument) {
- try {
- this._ownerDocument = treebox.treeBody.ownerDocument;
- }
- catch (e) {}
-
- if (!this._ownerDocument) {
- Zotero.debug("No owner document in setTree()", 2);
- return;
- }
- }
-
- this.setSortColumn();
-
- if (this.window.ZoteroPane) {
- this.window.ZoteroPane.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
- }
-
- if (Zotero.locked) {
- Zotero.debug("Zotero is locked -- not loading items tree", 2);
-
- if (this.window.ZoteroPane) {
- this.window.ZoteroPane.clearItemsPaneMessage();
- }
- return;
- }
-
- // Don't expand to show search matches in My Publications
- var skipExpandMatchParents = this.collectionTreeRow.isPublications();
-
- await this.refresh(skipExpandMatchParents);
- if (!this._treebox.treeBody) {
- return;
- }
-
- // Expand all parent items in the view, regardless of search matches. We do this here instead
- // of refresh so that it doesn't get reverted after item changes.
- if (this.expandAll) {
- var t = new Date();
- for (let i = 0; i < this._rows.length; i++) {
- if (this.isContainer(i) && !this.isContainerOpen(i)) {
- this.toggleOpenState(i, true);
- }
- }
- Zotero.debug(`Opened all parent items in ${new Date() - t} ms`);
- }
- this._refreshItemRowMap();
-
- // Add a keypress listener for expand/collapse
- var tree = this._getTreeElement();
- var self = this;
- var coloredTagsRE = new RegExp("^[0-" + 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 == event.DOM_VK_RIGHT || event.keyCode == event.DOM_VK_LEFT) {
- if (self._treebox.view.selection.count > 1) {
- switch (event.keyCode) {
- case event.DOM_VK_RIGHT:
- self.expandSelectedRows();
- break;
-
- case event.DOM_VK_LEFT:
- 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();
- event.stopPropagation();
-
- Zotero.spawn(function* () {
- if (coloredTagsRE.test(key)) {
- let libraryID = self.collectionTreeRow.ref.libraryID;
- let position = parseInt(key) - 1;
- // When 0 is pressed, remove all colored tags
- if (position == -1) {
- let items = self.getSelectedItems();
- return Zotero.Tags.removeColoredTagsFromItems(items);
- }
- 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(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 clonedEvent = new this.window.KeyboardEvent("keypress", event);
- event.explicitOriginalTarget.dispatchEvent(clonedEvent);
- tree.disableKeyNavigation = true;
- }.bind(this))
- .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._updateIntroText();
-
- if (this.collectionTreeRow && this.collectionTreeRow.itemsToSelect) {
- await this.selectItems(this.collectionTreeRow.itemsToSelect);
- this.collectionTreeRow.itemsToSelect = null;
- }
-
- Zotero.debug("Set tree for items view " + this.id + " in " + (Date.now() - start) + " ms");
-
- this._initialized = true;
- await 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 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* (skipExpandMatchParents) {
- 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();
- // Get the full set of items we want to show
- let newSearchItems = yield this.collectionTreeRow.getItems();
- // TEMP: Hide annotations
- newSearchItems = newSearchItems.filter(item => !item.isAnnotation());
- // A temporary workaround to make item tree crash less often
- newSearchItems = newSearchItems.filter(item => !(item.isAttachment() && item.attachmentLinkMode === Zotero.Attachments.LINK_MODE_EMBEDDED_IMAGE));
- // Remove notes and attachments if necessary
- if (this.regularOnly) {
- newSearchItems = newSearchItems.filter(item => item.isRegularItem());
- }
- let newSearchItemIDs = new Set(newSearchItems.map(item => item.id));
- // Find the items that aren't yet in the tree
- let itemsToAdd = newSearchItems.filter(item => this._rowMap[item.id] === undefined);
- // Find the parents of search matches
- let newSearchParentIDs = new Set(
- this.regularOnly
- ? []
- : newSearchItems.filter(item => !!item.parentItemID).map(item => item.parentItemID)
- );
- newSearchItems = new Set(newSearchItems);
-
- if (!this.selection.selectEventsSuppressed) {
- var unsuppress = this.selection.selectEventsSuppressed = true;
- this._treebox.beginUpdateBatch();
- }
- var savedSelection = this.getSelectedItems(true);
-
- var oldCount = this.rowCount;
- var newCellTextCache = {};
- var newSearchMode = this.collectionTreeRow.isSearchMode();
- var newRows = [];
- var allItemIDs = new Set();
- var addedItemIDs = new Set();
-
- // Copy old rows to new array, omitting top-level items not in the new set and their children
- //
- // This doesn't add new child items to open parents or remove child items that no longer exist,
- // which is done by toggling all open containers below.
- var skipChildren;
- for (let i = 0; i < this._rows.length; i++) {
- let row = this._rows[i];
- // Top-level items
- if (row.level == 0) {
- let isSearchParent = newSearchParentIDs.has(row.ref.id);
- // If not showing children or no children match the search, close
- if (this.regularOnly || !isSearchParent) {
- row.isOpen = false;
- skipChildren = true;
- }
- else {
- skipChildren = false;
- }
- // Skip items that don't match the search and don't have children that do
- if (!newSearchItems.has(row.ref) && !isSearchParent) {
- continue;
- }
- }
- // Child items
- else if (skipChildren) {
- continue;
- }
- newRows.push(row);
- allItemIDs.add(row.ref.id);
- }
-
- // Add new items
- for (let i = 0; i < itemsToAdd.length; i++) {
- let item = itemsToAdd[i];
-
- // If child item matches search and parent hasn't yet been added, add parent
- let parentItemID = item.parentItemID;
- if (parentItemID) {
- if (allItemIDs.has(parentItemID)) {
- continue;
- }
- item = Zotero.Items.get(parentItemID);
- }
- // Parent item may have already been added from child
- else if (allItemIDs.has(item.id)) {
- continue;
- }
-
- // Add new top-level items
- let row = new Zotero.ItemTreeRow(item, 0, false);
- newRows.push(row);
- allItemIDs.add(item.id);
- addedItemIDs.add(item.id);
- }
-
- this._rows = newRows;
- this.rowCount = this._rows.length;
- this._refreshItemRowMap();
- // Sort only the new items
- //
- // This still results in a lot of extra work (e.g., when clearing a quick search, we have to
- // re-sort all items that didn't match the search), so as a further optimization we could keep
- // a sorted list of items for a given column configuration and restore items from that.
- this.sort([...addedItemIDs]);
-
- var diff = this.rowCount - oldCount;
- if (diff != 0) {
- this._treebox.rowCountChanged(0, diff);
- }
-
- // Toggle all open containers closed and open to refresh child items
- //
- // This could be avoided by making sure that items in notify() that aren't present are always
- // added.
- var t = new Date();
- for (let i = 0; i < this._rows.length; i++) {
- if (this.isContainer(i) && this.isContainerOpen(i)) {
- this.toggleOpenState(i, true);
- this.toggleOpenState(i, true);
- }
- }
- Zotero.debug(`Refreshed open parents in ${new Date() - t} ms`);
-
- this._refreshItemRowMap();
-
- this._searchMode = newSearchMode;
- this._searchItemIDs = newSearchItemIDs; // items matching the search
- this._cellTextCache = {};
-
- this.rememberSelection(savedSelection);
- if (!skipExpandMatchParents) {
- this.expandMatchParents(newSearchParentIDs);
- }
- if (unsuppress) {
- this._treebox.endUpdateBatch();
- this.selection.selectEventsSuppressed = false;
- }
-
- // Clear My Publications intro text on a refresh with items
- if (this.collectionTreeRow.isPublications() && this.rowCount) {
- this.window.ZoteroPane.clearItemsPaneMessage();
- }
-
- yield this.runListeners('refresh');
-
- 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();
- 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-
- // '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;
- }
- }
- // If refreshing a single item, clear caches and then deselect and reselect row
- else if (savedSelection.length == 1 && savedSelection[0] == ids[0]) {
- let id = ids[0];
- let row = this._rowMap[id];
- delete this._cellTextCache[row];
- delete this._itemImages[id];
- this._treebox.invalidateRow(row);
-
- this.selection.clearSelection();
- this.rememberSelection(savedSelection);
- }
- else {
- for (let id of ids) {
- let row = this._rowMap[id];
- if (row === undefined) continue;
- delete this._cellTextCache[row];
- delete this._itemImages[id];
- this._treebox.invalidateRow(row);
- }
- }
-
- // For a refresh on an item in the trash, check if the item still belongs
- if (type == 'item' && collectionTreeRow.isTrash()) {
- let rows = [];
- for (let id of ids) {
- let row = this.getRowIndexByID(id);
- if (row === false) continue;
- let item = Zotero.Items.get(id);
- if (!item.deleted && !item.numChildren()) {
- rows.push(row);
- }
- }
- this._removeRows(rows);
- }
-
- 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');
- var hasQuickSearch = quickSearch && quickSearch.value != '';
-
- // '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 0) {
- rows.push(row);
- }
- }
- }
- }
-
- if (rows.length > 0) {
- this._removeRows(rows);
- madeChanges = true;
- }
- }
- else if (type == 'item' && action == 'modify')
- {
- // Clear row caches
- var items = Zotero.Items.get(ids);
- for (let i=0; i this.getRowIndexByID(id) === false)) {
- // 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);
- reselect = true;
- }
- }
- 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);
- reselect = true;
- }
- // 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);
- reselect = true;
- }
- }
- }
- else {
- this.rememberSelection(savedSelection);
- reselect = true;
- }
- }
-
- this._rememberScrollPosition(scrollPosition);
- }
- // 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._updateIntroText();
-
- this._treebox.endUpdateBatch();
-
- // If we made changes to the selection (including reselecting the same item, which will register as
- // a selection when selectEventsSuppressed is set to false), wait for a select event on the tree
- // view (e.g., as triggered by itemsView.runListeners('select') in ZoteroPane::itemSelected())
- // before returning. This guarantees that changes are reflected in the middle and right-hand panes
- // before returning from the save transaction.
- //
- // If no onselect handler is set on the tree element, as is the case in the Advanced Search window,
- // the select listeners never get called, so don't wait.
- let selectPromise;
- var tree = this._getTreeElement();
- var hasOnSelectHandler = tree.getAttribute('onselect') != '';
- if (reselect && hasOnSelectHandler) {
- selectPromise = this.waitForSelect();
- this.selection.selectEventsSuppressed = false;
- Zotero.debug("Yielding for select promise"); // TEMP
- return selectPromise;
- }
- else {
- this.selection.selectEventsSuppressed = false;
- }
-});
-
-
-Zotero.ItemTreeView.prototype.unregister = async function() {
- Zotero.Notifier.unregisterObserver(this._notifierObserverID);
- Zotero.Prefs.unregisterObserver(this._prefObserverID);
-
- if (this.collectionTreeRow.onUnload) {
- await this.collectionTreeRow.onUnload();
- }
-
- if (this.listener) {
- if (!this._treebox.treeBody) {
- Zotero.debug("No more tree body in Zotero.ItemTreeView::unregister()");
- this.listener = null;
- return;
- }
- let tree = this._getTreeElement();
- 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);
- if (val) {
- // Don't show anything for unparsed year
- if (val === "0000") {
- val = "";
- }
- // Show pre-1000 year without leading zeros
- else if (val < 1000) {
- val = parseInt(val);
- }
- }
- }
- else if (column.id === "zotero-items-column-numNotes") {
- val = obj.numNotes();
- if (!val) {
- val = '';
- }
- }
- 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) {
- let date = Zotero.Date.sqlToDate(val, true);
- if (date) {
- // If no time, interpret as local, not UTC
- if (Zotero.Date.isSQLDate(val)) {
- date = Zotero.Date.sqlToDate(val);
- val = date.toLocaleDateString();
- }
- else {
- val = date.toLocaleString();
- }
- }
- else {
- val = '';
- }
- }
- }
-
- 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.regularOnly) {
- 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 item row 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 item row 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 this._rowMap[itemID]);
- parentRows.sort();
-
- for (let i = parentRows.length - 1; i >= 0; i--) {
- let row = parentRows[i];
- this._closeContainer(row, true);
- this.toggleOpenState(row, true);
- }
- this._refreshItemRowMap();
-
- let numSorted = itemIDs.length - skipped.length;
- if (numSorted) {
- Zotero.debug(`Sorted ${numSorted} child items by parent toggle`);
- }
- if (!skipped.length) {
- return;
- }
- itemIDs = skipped;
- if (numSorted) {
- Zotero.debug(`${itemIDs.length} items left to sort`);
- }
- }
-
- 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} `
- + (itemIDs && itemIDs.length
- ? `for ${itemIDs.length} ` + Zotero.Utilities.pluralize(itemIDs.length, ['item', 'items'])
- : ""));
-
- // 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) {
- for (let i = 0; i < sortFields.length; i++) {
- let cmp = fieldCompare(a, b, sortFields[i]);
- 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);
-
- // Sort specific items
- if (itemIDs) {
- let idsToSort = new Set(itemIDs);
- this._rows.sort((a, b) => {
- // Don't re-sort existing items. This assumes a stable sort(), which is the case in Firefox
- // but not Chrome/v8.
- if (!idsToSort.has(a.ref.id) && !idsToSort.has(b.ref.id)) return 0;
- return rowSort(a, b) * order;
- });
- }
- // Full sort
- else {
- this._rows.sort((a, b) => rowSort(a, b) * order);
- }
-
- this._refreshItemRowMap();
-
- this.rememberOpenState(openItemIDs);
- this.rememberSelection(savedSelection);
-
- if (unsuppress) {
- this._treebox.endUpdateBatch();
- this.selection.selectEventsSuppressed = false;
- }
-
- this._treebox.invalidate();
-
- var numSorted = itemIDs ? itemIDs.length : this._rows.length;
- Zotero.debug(`Sorted ${numSorted} ${Zotero.Utilities.pluralize(numSorted, ['item', 'items'])} `
- + `in ${new Date - t} ms`);
-};
-
-
-/**
- * Show intro text in middle pane for some views when no items
- */
-Zotero.ItemTreeView.prototype._updateIntroText = function() {
- if (!this.window.ZoteroPane) {
- return;
- }
-
- if (this.collectionTreeRow && !this.rowCount) {
- let doc = this._ownerDocument;
- let ns = 'http://www.w3.org/1999/xhtml'
- let div;
-
- // My Library and no groups
- if (this.collectionTreeRow.isLibrary() && !Zotero.Groups.getAll().length) {
- div = doc.createElementNS(ns, 'div');
- let p = doc.createElementNS(ns, 'p');
- let html = Zotero.getString(
- 'pane.items.intro.text1',
- [
- Zotero.clientName
- ]
- );
- // Encode special chars, which shouldn't exist
- html = Zotero.Utilities.htmlSpecialChars(html);
- html = `${html} `;
- p.innerHTML = html;
- div.appendChild(p);
-
- p = doc.createElementNS(ns, 'p');
- html = Zotero.getString(
- 'pane.items.intro.text2',
- [
- Zotero.getString('connector.name', Zotero.clientName),
- Zotero.clientName
- ]
- );
- // Encode special chars, which shouldn't exist
- html = Zotero.Utilities.htmlSpecialChars(html);
- html = html.replace(
- /\[([^\]]+)](.+)\[([^\]]+)]/,
- `$1 `
- + '$2'
- + `$3 `
- );
- p.innerHTML = html;
- div.appendChild(p);
-
- p = doc.createElementNS(ns, 'p');
- html = Zotero.getString('pane.items.intro.text3', [Zotero.clientName]);
- // Encode special chars, which shouldn't exist
- html = Zotero.Utilities.htmlSpecialChars(html);
- html = html.replace(
- /\[([^\]]+)]/,
- '$1 '
- );
- p.innerHTML = html;
- div.appendChild(p);
-
- // Activate text links
- for (let span of div.getElementsByTagName('span')) {
- if (span.classList.contains('text-link')) {
- if (span.hasAttribute('data-href')) {
- span.onclick = function () {
- doc.defaultView.ZoteroPane.loadURI(this.getAttribute('data-href'));
- };
- }
- else if (span.hasAttribute('data-action')) {
- if (span.getAttribute('data-action') == 'open-sync-prefs') {
- span.onclick = () => {
- Zotero.Utilities.Internal.openPreferences('zotero-prefpane-sync');
- };
- }
- }
- }
- }
-
- div.setAttribute('allowdrop', true);
- }
- // My Publications
- else if (this.collectionTreeRow.isPublications()) {
- div = doc.createElementNS(ns, 'div');
- div.className = 'publications';
- 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 tags to placeholders
- html = html.replace('', ':b:').replace(' ', ':/b:');
- // Encode any other special chars, which shouldn't exist
- html = Zotero.Utilities.htmlSpecialChars(html);
- // Restore bold text
- html = html.replace(':b:', '').replace(':/b:', ' ');
- p.innerHTML = html; // AMO note: markup from hard-coded strings and filtered above
- div.appendChild(p);
- }
- if (div) {
- this._introText = true;
- doc.defaultView.ZoteroPane_Local.setItemsPaneMessage(div);
- return;
- }
- this._introText = null;
- }
-
- if (this._introText || this._introText === null) {
- this.window.ZoteroPane.clearItemsPaneMessage();
- this._introText = false;
- }
-};
-
-
-////////////////////////////////////////////////////////////////////////////////
-///
-/// Additional functions for managing data in the tree
-///
-////////////////////////////////////////////////////////////////////////////////
-
-
-/*
- * Select an item
- */
-Zotero.ItemTreeView.prototype.selectItem = async function (id) {
- return this.selectItems([id]);
-};
-
-Zotero.ItemTreeView.prototype.selectItems = async function (ids, noRecurse) {
- if (!ids.length) return 0;
-
- // If no row map, we're probably in the process of switching collections,
- // so store the items to select on the item group for later
- if (!this._rowMap) {
- if (this.collectionTreeRow) {
- this.collectionTreeRow.itemsToSelect = ids;
- Zotero.debug("_rowMap not yet set; not selecting items");
- return 0;
- }
-
- Zotero.debug('Item group not found and no row map in ItemTreeView.selectItem() -- discarding select', 2);
- return 0;
- }
-
- var idsToSelect = [];
- for (let id of ids) {
- let row = this._rowMap[id];
- let item = Zotero.Items.get(id);
-
- // Can't select a deleted item if we're not in the trash
- if (item.deleted && !this.collectionTreeRow.isTrash()) {
- continue;
- }
-
- // Get the row of the parent, if there is one
- let parent = item.parentItemID;
- let parentRow = parent && this._rowMap[parent];
-
- // If row with id isn't visible, check to see if it's hidden under a parent
- if (row == undefined) {
- if (!parent || parentRow === undefined) {
- // No parent -- it's not here
-
- // Clear the quick search and tag selection and try again (once)
- if (!noRecurse && this.window.ZoteroPane) {
- let cleared1 = await this.window.ZoteroPane.clearQuicksearch();
- let cleared2 = this.window.ZoteroPane.tagSelector
- && this.window.ZoteroPane.tagSelector.clearTagSelection();
- if (cleared1 || cleared2) {
- return this.selectItems(ids, true);
- }
- }
-
- Zotero.debug(`Couldn't find row for item ${id} -- not selecting`);
- continue;
- }
-
- // 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);
- }
-
- // Since we're opening containers, we still need to reference by id
- idsToSelect.push(id);
- }
-
- // Now that all items have been expanded, get associated rows
- var rowsToSelect = [];
- for (let id of idsToSelect) {
- let row = this._rowMap[id];
- if (row === undefined) {
- Zotero.debug(`Item ${id} not in row map -- skipping`);
- continue;
- }
- rowsToSelect.push(row);
- }
-
- if (!rowsToSelect.length) {
- return 0;
- }
-
- // If items are already selected, just scroll to the top-most one
- var selectedRows = new Set(this.getSelectedRowIndexes());
- if (rowsToSelect.length == selectedRows.size && rowsToSelect.every(row => selectedRows.has(row))) {
- this.ensureRowsAreVisible(rowsToSelect);
- return rowsToSelect.length;
- }
-
- // Single item
- if (rowsToSelect.length == 1) {
- // 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.
- let promise;
- try {
- if (!this.selection.selectEventsSuppressed) {
- promise = this.waitForSelect();
- }
- this.selection.select(rowsToSelect[0]);
- }
- // Ignore NS_ERROR_UNEXPECTED from nsITreeSelection::select(), apparently when the tree
- // disappears before it's called (though I can't reproduce it):
- //
- // https://forums.zotero.org/discussion/comment/297039/#Comment_297039
- catch (e) {
- Zotero.logError(e);
- }
-
- if (promise) {
- await promise;
- }
- }
- // Multiple items
- else {
- this.selection.clearSelection();
- this.selection.selectEventsSuppressed = true;
-
- var lastStart = 0;
- for (let i = 0, len = rowsToSelect.length; i < len; i++) {
- if (i == len - 1 || rowsToSelect[i + 1] != rowsToSelect[i] + 1) {
- this.selection.rangedSelect(rowsToSelect[lastStart], rowsToSelect[i], true);
- lastStart = i + 1;
- }
- }
-
- this.selection.selectEventsSuppressed = false;
- }
-
- this.ensureRowsAreVisible(rowsToSelect);
-
- return rowsToSelect.length;
-};
-
-
-Zotero.ItemTreeView.prototype.ensureRowsAreVisible = function (rows) {
- const firstVisibleRow = this._treebox.getFirstVisibleRow();
- const pageLength = this._treebox.getPageLength();
- const lastVisibleRow = firstVisibleRow + pageLength;
- const maxBuffer = 5;
-
- var isRowVisible = function (row) {
- return row >= firstVisibleRow && row <= lastVisibleRow;
- };
-
- rows = rows.concat();
- rows.sort((a, b) => a - b);
-
- var rowsWithParents = [];
- for (let row of rows) {
- let parent = this.getParentIndex(row);
- rowsWithParents.push(parent != -1 ? parent : row);
- }
-
- // If all rows are visible, don't change anything
- if (rows.every(row => isRowVisible(row))) {
- //Zotero.debug("All rows are visible");
- return;
- }
-
- // If we can fit all parent rows in view, do that
- for (let buffer = maxBuffer; buffer >= 0; buffer--) {
- if (rowsWithParents[rowsWithParents.length - 1] - rowsWithParents[0] - buffer < pageLength) {
- //Zotero.debug(`We can fit all parent rows with buffer ${buffer}`);
- this._treebox.scrollToRow(rowsWithParents[0] - buffer);
- return;
- }
- }
-
- // If we can fit all rows in view, do that
- for (let buffer = maxBuffer; buffer >= 0; buffer--) {
- if (rows[rows.length - 1] - rows[0] - buffer < pageLength) {
- //Zotero.debug(`We can fit all rows with buffer ${buffer}`);
- this._treebox.scrollToRow(rows[0] - buffer);
- return;
- }
- }
-
- // If more than half of the rows are visible, don't change anything
- var visible = 0;
- for (let row of rows) {
- if (isRowVisible(row)) {
- visible++;
- }
- }
- if (visible > rows / 2) {
- //Zotero.debug("More than half of rows are visible");
- return;
- }
-
- // If the first parent row isn't in view and we have enough room, make it visible, trying to
- // put it five rows from the top
- if (rows[0] != rowsWithParents[0]) {
- for (let buffer = maxBuffer; buffer >= 0; buffer--) {
- if (rows[0] - rowsWithParents[0] - buffer <= pageLength) {
- //Zotero.debug(`Scrolling to first parent minus ${buffer}`);
- this._treebox.scrollToRow(rowsWithParents[0] - buffer);
- return;
- }
- }
- }
-
- // Otherwise just put the first row at the top
- //Zotero.debug("Scrolling to first row " + Math.max(rows[0] - maxBuffer, 0));
- this._treebox.scrollToRow(Math.max(rows[0] - maxBuffer, 0));
-};
-
-
-/*
- * 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 Zotero.Items.get(id)));
- }
-
- //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._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=0; i--) {
- this.toggleOpenState(rowsToOpen[i], true);
- }
- this._refreshItemRowMap();
- if (unsuppress) {
- this._treebox.endUpdateBatch();
- this.selection.selectEventsSuppressed = false;
- }
-}
-
-
-Zotero.ItemTreeView.prototype.expandMatchParents = function (searchParentIDs) {
- var t = new Date();
- var time = 0;
- // Expand parents of child matches
- if (!this._searchMode) {
- return;
- }
-
- if (!this.selection.selectEventsSuppressed) {
- var unsuppress = this.selection.selectEventsSuppressed = true;
- this._treebox.beginUpdateBatch();
- }
- for (var i=0; i= start.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 asIDs ? row.ref.id : row.ref);
-}
-
-
-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 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 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 (set via /ping from connector)
- var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
-
- 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.nsIFile);
-
- var draggedItems = Zotero.Items.get(this._itemIDs);
- var items = [];
-
- // Make sure files exist
- var notFoundNames = [];
- for (var 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, 0o755);
- }
-
- 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, 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 or publications
- else if (collectionTreeRow.isSearch() || collectionTreeRow.isPublications()) {
- 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 .
-
- ***** END LICENSE BLOCK *****
-*/
-
-Zotero.LibraryTreeView = function () {
- this._initialized = false;
- this._listeners = {};
- this._rows = [];
- this._rowMap = {};
-
- this.id = Zotero.Utilities.randomString();
- Zotero.debug("Creating " + this.type + "s view with id " + this.id);
-
- //
- // Create .on(Load|Select|Refresh).addListener() methods
- //
- var _createEventBinding = function (event, alwaysOnce) {
- return alwaysOnce
- ? {
- addListener: listener => this._addListener(event, listener, true)
- }
- : {
- addListener: (listener, once) => this._addListener(event, listener, once)
- };
- }.bind(this);
-
- this.onLoad = _createEventBinding('load', true);
- this.onSelect = _createEventBinding('select');
- this.onRefresh = _createEventBinding('refresh');
-};
-
-Zotero.LibraryTreeView.prototype = {
- get initialized() {
- return this._initialized;
- },
-
-
- addEventListener: function (event, listener) {
- Zotero.logError("Zotero.LibraryTreeView::addEventListener() is deprecated");
- this.addListener(event, listener);
- },
-
-
- waitForLoad: function () {
- return this._waitForEvent('load');
- },
-
-
- waitForSelect: function () {
- return this._waitForEvent('select');
- },
-
-
- runListeners: Zotero.Promise.coroutine(function* (event) {
- //Zotero.debug(`Calling ${event} listeners on ${this.type} tree ${this.id}`);
- if (!this._listeners[event]) return;
- for (let [listener, once] of this._listeners[event].entries()) {
- yield Zotero.Promise.resolve(listener.call(this));
- if (once) {
- this._listeners[event].delete(listener);
- }
- }
- }),
-
-
- _addListener: function(event, listener, once) {
- // If already initialized run now
- if (event == 'load' && this._initialized) {
- listener.call(this);
- }
- else {
- if (!this._listeners[event]) {
- this._listeners[event] = new Map();
- }
- this._listeners[event].set(listener, once);
- }
- },
-
-
- _waitForEvent: Zotero.Promise.coroutine(function* (event) {
- if (event == 'load' && this._initialized) {
- return;
- }
- return new Zotero.Promise((resolve, reject) => {
- this._addListener(event, () => resolve(), true);
- });
- }),
-
-
- /**
- * Return a reference to the tree row at a given row
- *
- * @return {Zotero.CollectionTreeRow|Zotero.ItemTreeRow}
- */
- getRow: function(row) {
- return this._rows[row];
- },
-
-
- /**
- * Return the index of the row with a given ID (e.g., "C123" for collection 123)
- *
- * @param {String} - Row id
- * @return {Integer|false}
- */
- getRowIndexByID: function (id) {
- var type = "";
- if (this.type != 'item') {
- var type = id[0];
- id = ('' + id).substr(1);
- }
- return this._rowMap[type + id] !== undefined ? this._rowMap[type + id] : false;
- },
-
-
- getSelectedRowIndexes: function () {
- var rows = [];
- var start = {};
- var end = {};
- for (let i = 0, len = this.selection.getRangeCount(); i < len; i++) {
- this.selection.getRangeAt(i, start, end);
- for (let j = start.value; j <= end.value; j++) {
- rows.push(j);
- }
- }
- return rows;
- },
-
-
- /**
- * Return an object describing the current scroll position to restore after changes
- *
- * @return {Object|Boolean} - Object with .id (a treeViewID) and .offset, or false if no rows
- */
- _saveScrollPosition: function() {
- var treebox = this._treebox;
- var first = treebox.getFirstVisibleRow();
- if (!first) {
- return false;
- }
- var last = treebox.getLastVisibleRow();
- var firstSelected = null;
- for (let i = first; i <= last; i++) {
- // If an object is selected, keep the first selected one in position
- if (this.selection.isSelected(i)) {
- return {
- id: this.getRow(i).ref.treeViewID,
- offset: i - first
- };
- }
- }
-
- // Otherwise keep the first visible row in position
- return {
- id: this.getRow(first).ref.treeViewID,
- offset: 0
- };
- },
-
-
- /**
- * Restore a scroll position returned from _saveScrollPosition()
- */
- _rememberScrollPosition: function (scrollPosition) {
- if (!scrollPosition || !scrollPosition.id) {
- return;
- }
- var row = this.getRowIndexByID(scrollPosition.id);
- if (row === false) {
- return;
- }
- this._treebox.scrollToRow(Math.max(row - scrollPosition.offset, 0));
- },
-
-
- runSelectListeners: function () {
- return this._runListeners('select');
- },
-
-
- /**
- * Add a tree row to the main array, update the row count, tell the treebox that the row
- * count changed, and update the row map
- *
- * @param {Array} newRows - Array to operate on
- * @param {Zotero.ItemTreeRow} itemTreeRow
- * @param {Number} [beforeRow] - Row index to insert new row before
- */
- _addRow: function (treeRow, beforeRow, skipRowMapRefresh) {
- this._addRowToArray(this._rows, treeRow, beforeRow);
- this.rowCount++;
- this._treebox.rowCountChanged(beforeRow, 1);
- if (!skipRowMapRefresh) {
- // Increment all rows in map at or above insertion point
- for (let i in this._rowMap) {
- if (this._rowMap[i] >= beforeRow) {
- this._rowMap[i]++
- }
- }
- // Add new row to map
- this._rowMap[treeRow.id] = beforeRow;
- }
- },
-
-
- /**
- * Add a tree row into a given array
- *
- * @param {Array} array - Array to operate on
- * @param {Zotero.CollectionTreeRow|ItemTreeRow} treeRow
- * @param {Number} beforeRow - Row index to insert new row before
- */
- _addRowToArray: function (array, treeRow, beforeRow) {
- array.splice(beforeRow, 0, treeRow);
- },
-
-
- /**
- * Remove a row from the main array, decrement the row count, tell the treebox that the row
- * count changed, update the parent isOpen if necessary, delete the row from the map, and
- * optionally update all rows above it in the map
- */
- _removeRow: function (row, skipMapUpdate) {
- var id = this._rows[row].id;
- var level = this.getLevel(row);
-
- var lastRow = row == this.rowCount - 1;
- if (lastRow && this.selection.isSelected(row)) {
- // Deselect removed row
- this.selection.toggleSelect(row);
- // If no other rows selected, select first selectable row before
- if (this.selection.count == 0 && row !== 0) {
- let previous = row;
- while (true) {
- previous--;
- // Should ever happen
- if (previous < 0) {
- break;
- }
- if (!this.isSelectable(previous)) {
- continue;
- }
-
- this.selection.toggleSelect(previous);
- break;
- }
- }
- }
-
- this._rows.splice(row, 1);
- this.rowCount--;
- // According to the example on https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsITreeBoxObject#rowCountChanged
- // this should start at row + 1 ("rowCountChanged(rowIndex+1, -1);"), but that appears to
- // just be wrong. A negative count indicates removed rows, but the index should still
- // start at the place where the removals begin, not after it going backward.
- this._treebox.rowCountChanged(row, -1);
- // Update isOpen if parent and no siblings
- if (row != 0
- && this.getLevel(row - 1) < level
- && (!this._rows[row] || this.getLevel(row) != level)) {
- this._rows[row - 1].isOpen = false;
- this._treebox.invalidateRow(row - 1);
- }
- delete this._rowMap[id];
- if (!skipMapUpdate) {
- for (let i in this._rowMap) {
- if (this._rowMap[i] > row) {
- this._rowMap[i]--;
- }
- }
- }
- },
-
-
- _removeRows: function (rows) {
- rows = Zotero.Utilities.arrayUnique(rows);
- rows.sort((a, b) => a - b);
- for (let i = rows.length - 1; i >= 0; i--) {
- this._removeRow(rows[i]);
- }
- },
-
-
- getLevel: function (row) {
- return this._rows[row].level;
- },
-
-
- isContainerOpen: function(row) {
- return this._rows[row].isOpen;
- },
-
-
- /**
- * Called while a drag is over the tree
- */
- canDrop: function(row, orient, dataTransfer) {
- // onDragOver() calls the view's canDropCheck() and sets the
- // dropEffect, which we check here. Setting the dropEffect on the
- // dataTransfer here seems to have no effect.
-
- // ondragover doesn't have access to the orientation on its own,
- // so we stuff it in Zotero.DragDrop
- Zotero.DragDrop.currentOrientation = orient;
-
- return dataTransfer.dropEffect && dataTransfer.dropEffect != "none";
- },
-
-
- /*
- * Called by HTML 5 Drag and Drop when dragging over the tree
- */
- onDragEnter: function (event) {
- Zotero.DragDrop.currentEvent = event;
- return false;
- },
-
-
- /**
- * Called by HTML 5 Drag and Drop when dragging over the tree
- *
- * We use this to set the drag action, which is used by view.canDrop(),
- * based on the view's canDropCheck() and modifier keys.
- */
- onDragOver: function (event) {
- // Prevent modifier keys from doing their normal things
- event.preventDefault();
-
- Zotero.DragDrop.currentEvent = event;
-
- var target = event.target;
- if (target.tagName != 'treechildren') {
- let doc = target.ownerDocument;
- // Consider a drop on the items pane message box (e.g., when showing the welcome text)
- // a drop on the items tree
- let msgBox = doc.getElementById('zotero-items-pane-message-box');
- if (msgBox.contains(target) && msgBox.firstChild.hasAttribute('allowdrop')) {
- target = doc.querySelector('#zotero-items-tree treechildren');
- }
- else {
- this._setDropEffect(event, "none");
- return false;
- }
- }
- var tree = target.parentNode;
- let row = {}, col = {}, obj = {};
- tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
- if (tree.id == 'zotero-collections-tree') {
- var view = tree.ownerDocument.defaultView.ZoteroPane.collectionsView;
- }
- else if (tree.id == 'zotero-items-tree') {
- var view = tree.ownerDocument.defaultView.ZoteroPane.itemsView;
- }
- else {
- throw new Error("Invalid tree id '" + tree.id + "'");
- }
-
- if (!view.canDropCheck(row.value, Zotero.DragDrop.currentOrientation, event.dataTransfer)) {
- this._setDropEffect(event, "none");
- return;
- }
-
- if (event.dataTransfer.getData("zotero/item")) {
- var sourceCollectionTreeRow = Zotero.DragDrop.getDragSource(event.dataTransfer);
- if (sourceCollectionTreeRow) {
- if (this.type == 'collection') {
- var targetCollectionTreeRow = Zotero.DragDrop.getDragTarget(event);
- }
- else if (this.type == 'item') {
- var targetCollectionTreeRow = this.collectionTreeRow;
- }
- else {
- throw new Error("Invalid type '" + this.type + "'");
- }
-
- if (!targetCollectionTreeRow) {
- this._setDropEffect(event, "none");
- return false;
- }
-
- if (sourceCollectionTreeRow.id == targetCollectionTreeRow.id) {
- // Ignore drag into the same collection
- if (this.type == 'collection') {
- this._setDropEffect(event, "none");
- }
- // If dragging from the same source, do a move
- else {
- this._setDropEffect(event, "move");
- }
- return false;
- }
- // If the source isn't a collection, the action has to be a copy
- if (!sourceCollectionTreeRow.isCollection()) {
- this._setDropEffect(event, "copy");
- return false;
- }
- // For now, all cross-library drags are copies
- if (sourceCollectionTreeRow.ref.libraryID != targetCollectionTreeRow.ref.libraryID) {
- this._setDropEffect(event, "copy");
- return false;
- }
- }
-
- if ((Zotero.isMac && event.metaKey) || (!Zotero.isMac && event.shiftKey)) {
- this._setDropEffect(event, "move");
- }
- else {
- this._setDropEffect(event, "copy");
- }
- }
- else if (event.dataTransfer.getData("zotero/collection")) {
- let collectionID = Zotero.DragDrop.getDataFromDataTransfer(event.dataTransfer).data[0];
- let { libraryID: sourceLibraryID } = Zotero.Collections.getLibraryAndKeyFromID(collectionID);
-
- if (this.type == 'collection') {
- var targetCollectionTreeRow = Zotero.DragDrop.getDragTarget(event);
- }
- else {
- throw new Error("Invalid type '" + this.type + "'");
- }
-
- // For now, all cross-library drags are copies
- if (sourceLibraryID != targetCollectionTreeRow.ref.libraryID) {
- /*if ((Zotero.isMac && event.metaKey) || (!Zotero.isMac && event.shiftKey)) {
- this._setDropEffect(event, "move");
- }
- else {
- this._setDropEffect(event, "copy");
- }*/
- this._setDropEffect(event, "copy");
- return false;
- }
-
- // And everything else is a move
- this._setDropEffect(event, "move");
- }
- else if (event.dataTransfer.types.contains("application/x-moz-file")) {
- // As of Aug. 2013 nightlies:
- //
- // - Setting the dropEffect only works on Linux and OS X.
- //
- // - Modifier keys don't show up in the drag event on OS X until the
- // drop (https://bugzilla.mozilla.org/show_bug.cgi?id=911918),
- // so since we can't show a correct effect, we leave it at
- // the default 'move', the least misleading option, and set it
- // below in onDrop().
- //
- // - The cursor effect gets set by the system on Windows 7 and can't
- // be overridden.
- if (!Zotero.isMac) {
- if (event.shiftKey) {
- if (event.ctrlKey) {
- event.dataTransfer.dropEffect = "link";
- }
- else {
- event.dataTransfer.dropEffect = "move";
- }
- }
- else {
- event.dataTransfer.dropEffect = "copy";
- }
- }
- }
- return false;
- },
-
-
- /*
- * Called by HTML 5 Drag and Drop when dropping onto the tree
- */
- onDrop: function (event) {
- // See note above
- if (event.dataTransfer.types.contains("application/x-moz-file")) {
- if (Zotero.isMac) {
- Zotero.DragDrop.currentEvent = event;
- if (event.metaKey) {
- if (event.altKey) {
- event.dataTransfer.dropEffect = 'link';
- }
- else {
- event.dataTransfer.dropEffect = 'move';
- }
- }
- else {
- event.dataTransfer.dropEffect = 'copy';
- }
- }
- }
- return false;
- },
-
-
- onDragExit: function (event) {
- //Zotero.debug("Clearing drag data");
- Zotero.DragDrop.currentEvent = null;
- },
-
-
- _setDropEffect: function (event, effect) {
- // On Windows (in Fx26), Firefox uses 'move' for unmodified drags
- // and 'copy'/'link' for drags with system-default modifier keys
- // as long as the actions are allowed by the initial effectAllowed set
- // in onDragStart, regardless of the effectAllowed or dropEffect set
- // in onDragOver. It doesn't seem to be possible to use 'copy' for
- // the default and 'move' for modified, as we need to in the collections
- // tree. To prevent inaccurate cursor feedback, we set effectAllowed to
- // 'copy' in onDragStart, which locks the cursor at 'copy'. ('none' still
- // changes the cursor, but 'move'/'link' do not.) It'd be better to use
- // the unadorned 'move', but we use 'copy' instead because with 'move' text
- // can't be dragged to some external programs (e.g., Chrome, Notepad++),
- // which seems worse than always showing 'copy' feedback.
- //
- // However, since effectAllowed is enforced, leaving it at 'copy'
- // would prevent our modified 'move' in the collections tree from working,
- // so we also have to set effectAllowed here (called from onDragOver) to
- // the same action as the dropEffect. This allows the dropEffect setting
- // (which we use in the tree's canDrop() and drop() to determine the desired
- // action) to be changed, even if the cursor doesn't reflect the new setting.
- if (Zotero.isWin || Zotero.isLinux) {
- event.dataTransfer.effectAllowed = effect;
- }
- event.dataTransfer.dropEffect = effect;
- }
-};
diff --git a/chrome/content/zotero/xpcom/prefs.js b/chrome/content/zotero/xpcom/prefs.js
index 2e38435556..1453b0e936 100644
--- a/chrome/content/zotero/xpcom/prefs.js
+++ b/chrome/content/zotero/xpcom/prefs.js
@@ -227,6 +227,8 @@ Zotero.Prefs = new function(){
Zotero.setFontSize(
Zotero.getActiveZoteroPane().document.getElementById('zotero-pane')
);
+ Zotero.getActiveZoteroPane().collectionsView && Zotero.getActiveZoteroPane().collectionsView.updateFontSize();
+ Zotero.getActiveZoteroPane().itemsView && Zotero.getActiveZoteroPane().itemsView.updateFontSize();
}],
[ "layout", function(val) {
Zotero.getActiveZoteroPane().updateLayout();
@@ -466,4 +468,59 @@ Zotero.Prefs = new function(){
});
});
}
+
+ this.getVirtualCollectionState = function (type) {
+ const prefKeys = {
+ duplicates: 'duplicateLibraries',
+ unfiled: 'unfiledLibraries',
+ retracted: 'retractedLibraries'
+ };
+ let prefKey = prefKeys[type];
+ if (!prefKey) {
+ throw new Error("Invalid virtual collection type '" + type + "'");
+ }
+
+ var libraries;
+ try {
+ libraries = JSON.parse(Zotero.Prefs.get(prefKey) || '{}');
+ if (typeof libraries != 'object') {
+ throw true;
+ }
+ }
+ // Ignore old/incorrect formats
+ catch (e) {
+ Zotero.Prefs.clear(prefKey);
+ libraries = {};
+ }
+
+ return libraries;
+ };
+
+
+ this.getVirtualCollectionStateForLibrary = function (libraryID, type) {
+ return this.getVirtualCollectionState(type)[libraryID] !== false;
+ };
+
+
+ this.setVirtualCollectionStateForLibrary = function (libraryID, type, show) {
+ const prefKeys = {
+ duplicates: 'duplicateLibraries',
+ unfiled: 'unfiledLibraries',
+ retracted: 'retractedLibraries'
+ };
+ let prefKey = prefKeys[type];
+ if (!prefKey) {
+ throw new Error("Invalid virtual collection type '" + type + "'");
+ }
+
+ var libraries = this.getVirtualCollectionState(type);
+
+ // Update current library
+ libraries[libraryID] = !!show;
+ // Remove libraries that don't exist or that are set to true
+ for (let id of Object.keys(libraries).filter(id => libraries[id] || !Zotero.Libraries.exists(id))) {
+ delete libraries[id];
+ }
+ Zotero.Prefs.set(prefKey, JSON.stringify(libraries));
+ };
}
diff --git a/chrome/content/zotero/xpcom/progressQueue.js b/chrome/content/zotero/xpcom/progressQueue.js
index 4a5fa09ef1..91461044d0 100644
--- a/chrome/content/zotero/xpcom/progressQueue.js
+++ b/chrome/content/zotero/xpcom/progressQueue.js
@@ -163,6 +163,7 @@ Zotero.ProgressQueue = function (options) {
* @param {String} message
*/
this.updateRow = function(itemID, status, message) {
+ Zotero.debug(`ProgressQueue: updating row ${itemID}, ${status}, ${message}`);
for (let row of _rows) {
if (row.id === itemID) {
row.status = status;
diff --git a/chrome/content/zotero/xpcom/progressQueueDialog.js b/chrome/content/zotero/xpcom/progressQueueDialog.js
index 6f64da744e..6d6383c3b7 100644
--- a/chrome/content/zotero/xpcom/progressQueueDialog.js
+++ b/chrome/content/zotero/xpcom/progressQueueDialog.js
@@ -32,7 +32,7 @@ Zotero.ProgressQueueDialog = function (progressQueue) {
let _progressWindow = null;
let _progressIndicator = null;
- let _rowIDs = [];
+ let _io = { progressQueue: _progressQueue };
let _status = null;
let _showMinimize = true;
@@ -45,11 +45,11 @@ Zotero.ProgressQueueDialog = function (progressQueue) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (win) {
_progressWindow = win.openDialog("chrome://zotero/content/progressQueueDialog.xul",
- "", "chrome,close=yes,resizable=yes,dependent,dialog,centerscreen");
+ "", "chrome,close=yes,resizable=yes,dependent,dialog,centerscreen", _io);
}
else {
_progressWindow = Services.ww.openWindow(null, "chrome://zotero/content/progressQueueDialog.xul",
- "", "chrome,close=yes,resizable=yes,dependent,dialog,centerscreen", null);
+ "", "chrome,close=yes,resizable=yes,dependent,dialog,centerscreen", _io);
}
_progressWindow.addEventListener('pageshow', _onWindowLoaded.bind(this), false);
@@ -82,70 +82,9 @@ Zotero.ProgressQueueDialog = function (progressQueue) {
_progressQueue.cancel();
};
- function _getImageByStatus(status) {
- if (status === Zotero.ProgressQueue.ROW_PROCESSING) {
- return LOADING_IMAGE;
- }
- else if (status === Zotero.ProgressQueue.ROW_FAILED) {
- return FAILURE_IMAGE;
- }
- else if (status === Zotero.ProgressQueue.ROW_SUCCEEDED) {
- return SUCCESS_IMAGE;
- }
- return '';
- }
-
- function _rowToTreeItem(row) {
- let treeitem = _progressWindow.document.createElement('treeitem');
- treeitem.setAttribute('id', 'item-' + row.id);
-
- let treerow = _progressWindow.document.createElement('treerow');
-
- let treecell = _progressWindow.document.createElement('treecell');
- treecell.setAttribute('id', 'item-' + row.id + '-icon');
- treecell.setAttribute('src', _getImageByStatus(row.status));
-
- treerow.appendChild(treecell);
-
- treecell = _progressWindow.document.createElement('treecell');
- treecell.setAttribute('label', row.fileName);
- treerow.appendChild(treecell);
-
- treecell = _progressWindow.document.createElement('treecell');
- treecell.setAttribute('id', 'item-' + row.id + '-title');
- treecell.setAttribute('label', row.message);
- treerow.appendChild(treecell);
-
- treeitem.appendChild(treerow);
- return treeitem;
- }
-
function _onWindowLoaded() {
- let rows = _progressQueue.getRows();
- _rowIDs = [];
-
- _progressWindow.document.title = Zotero.getString(_progressQueue.getTitle());
-
- let col1 = _progressWindow.document.getElementById('col1');
- let col2 = _progressWindow.document.getElementById('col2');
-
- let columns = _progressQueue.getColumns();
- col1.setAttribute('label', Zotero.getString(columns[0]));
- col2.setAttribute('label', Zotero.getString(columns[1]));
-
- let treechildren = _progressWindow.document.getElementById('treechildren');
-
- for (let row of rows) {
- _rowIDs.push(row.id);
- let treeitem = _rowToTreeItem(row);
- treechildren.appendChild(treeitem);
- }
-
- _progressWindow.document.getElementById('tree').addEventListener('dblclick',
- function (event) {
- _onDblClick(event, this);
- }
- );
+ var rootElement = document.getElementById('zotero-progress');
+ Zotero.setFontSize(rootElement);
_progressIndicator = _progressWindow.document.getElementById('progress-indicator');
_progressWindow.document.getElementById('cancel-button')
@@ -181,33 +120,24 @@ Zotero.ProgressQueueDialog = function (progressQueue) {
_progressIndicator = null;
_status = null;
_showMinimize = true;
- _rowIDs = [];
});
- _updateProgress();
-
_progressQueue.addListener('rowadded', function (row) {
- _rowIDs.push(row.id);
- let treeitem = _rowToTreeItem(row);
- treechildren.appendChild(treeitem);
+ _io.tree.invalidate();
_updateProgress();
});
_progressQueue.addListener('rowupdated', function (row) {
- let itemIcon = _progressWindow.document.getElementById('item-' + row.id + '-icon');
- let itemTitle = _progressWindow.document.getElementById('item-' + row.id + '-title');
-
- itemIcon.setAttribute('src', _getImageByStatus(row.status));
- itemTitle.setAttribute('label', row.message);
+ _io.tree.invalidate();
_updateProgress();
});
_progressQueue.addListener('rowdeleted', function (row) {
- _rowIDs.splice(_rowIDs.indexOf(row.id), 1);
- let treeitem = _progressWindow.document.getElementById('item-' + row.id);
- treeitem.parentNode.removeChild(treeitem);
+ _io.tree.invalidate();
_updateProgress();
});
+
+ _updateProgress();
}
function _updateProgress() {
@@ -228,29 +158,4 @@ Zotero.ProgressQueueDialog = function (progressQueue) {
_progressWindow.document.getElementById("label").value = _status || Zotero.getString('general.processing');
}
}
-
- /**
- * Focus items in Zotero library when double-clicking them in the Retrieve
- * metadata window.
- * @param {Event} event
- * @param {tree} tree XUL tree object
- * @private
- */
- async function _onDblClick(event, tree) {
- if (event && tree && event.type === 'dblclick') {
- let itemID = _rowIDs[tree.treeBoxObject.getRowAt(event.clientX, event.clientY)];
- if (!itemID) return;
-
- let item = await Zotero.Items.getAsync(itemID);
- if (!item) return;
-
- if (item.parentItemID) itemID = item.parentItemID;
-
- let win = Services.wm.getMostRecentWindow("navigator:browser");
- if (win) {
- win.ZoteroPane.selectItem(itemID, false, true);
- win.focus();
- }
- }
- }
};
diff --git a/chrome/content/zotero/xpcom/retractions.js b/chrome/content/zotero/xpcom/retractions.js
index 707ce7a2d1..0f714c5219 100644
--- a/chrome/content/zotero/xpcom/retractions.js
+++ b/chrome/content/zotero/xpcom/retractions.js
@@ -303,7 +303,7 @@ Zotero.Retractions = {
// Changed
&& (previous != current
// Explicitly hidden
- || (current && !Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(libraryID, 'retracted')))) {
+ || (current && !Zotero.Prefs.getVirtualCollectionStateForLibrary(libraryID, 'retracted')))) {
let promises = [];
for (let zp of Zotero.getZoteroPanes()) {
promises.push(zp.setVirtual(libraryID, 'retracted', current));
diff --git a/chrome/content/zotero/xpcom/utilities b/chrome/content/zotero/xpcom/utilities
index db52081e6e..10ffb4a766 160000
--- a/chrome/content/zotero/xpcom/utilities
+++ b/chrome/content/zotero/xpcom/utilities
@@ -1 +1 @@
-Subproject commit db52081e6eedfb0aa250dfc7f93ab2cf7ed6e468
+Subproject commit 10ffb4a766ce7d43aac3b321863a4e585669be02
diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js
index 714268fbd4..ba2880fdf7 100644
--- a/chrome/content/zotero/xpcom/utilities_internal.js
+++ b/chrome/content/zotero/xpcom/utilities_internal.js
@@ -32,6 +32,71 @@
Zotero.Utilities.Internal = {
SNAPSHOT_SAVE_TIMEOUT: 30000,
+ makeClassEventDispatcher: function (cls) {
+ cls.prototype._events = null;
+ cls.prototype.runListeners = async function (event) {
+ // Zotero.debug(`Running ${event} listeners on ${cls.toString()}`);
+ if (!this._events) this._events = {};
+ if (!this._events[event]) {
+ this._events[event] = {
+ listeners: new Map(),
+ };
+ }
+ this._events[event].triggered = true;
+ // Array.from(entries) since entries() returns an iterator and we want a snapshot of the entries
+ // at the time of runListeners() call to prevent triggering listeners that are added right
+ // runListeners() invocation
+ for (let [listener, once] of Array.from(this._events[event].listeners.entries())) {
+ await Zotero.Promise.resolve(listener.call(this));
+ if (once) {
+ this._events[event].listeners.delete(listener);
+ }
+ }
+ };
+
+ /**
+ * @param event {String} name of the event
+ * @param alwaysOnce {Boolean} whether all event listeners on this event will only be triggered once
+ * @param immediateAfterTrigger {Boolean} whether the event listeners should be triggered immediately
+ * upon being added if the event had been triggered at least once
+ * @returns {Object} A listener object with an addListener(listener, once) method
+ * @private
+ */
+ cls.prototype._createEventBinding = function (event, alwaysOnce, immediateAfterTrigger) {
+ if (!this._events) this._events = {};
+ this._events[event] = {
+ listeners: new Map(),
+ immediateAfterTrigger
+ };
+ return {
+ addListener: (listener, once) => {
+ this._addListener(event, listener, alwaysOnce || once, immediateAfterTrigger)
+ }
+ }
+ };
+
+ cls.prototype._addListener = function (event, listener, once, immediateAfterTrigger) {
+ if (!this._events) this._events = {};
+ let ev = this._events[event];
+ if (!ev) {
+ this._events[event] = {
+ listeners: new Map(),
+ immediateAfterTrigger
+ };
+ }
+ if ((immediateAfterTrigger || ev.immediateAfterTrigger) && ev.triggered) {
+ return listener.call(this);
+ }
+ this._events[event].listeners.set(listener, once);
+ };
+
+ cls.prototype._waitForEvent = async function (event) {
+ return new Zotero.Promise((resolve, reject) => {
+ this._addListener(event, () => resolve(), true);
+ });
+ };
+ },
+
/**
* Run a function on chunks of a given size of an array's elements.
*
@@ -1797,77 +1862,6 @@ Zotero.Utilities.Internal = {
return menu;
},
-
- // TODO: Move somewhere better
- getVirtualCollectionState: function (type) {
- switch (type) {
- case 'duplicates':
- var prefKey = 'duplicateLibraries';
- break;
-
- case 'unfiled':
- var prefKey = 'unfiledLibraries';
- break;
-
- case 'retracted':
- var prefKey = 'retractedLibraries';
- break;
-
- default:
- throw new Error("Invalid virtual collection type '" + type + "'");
- }
- var libraries;
- try {
- libraries = JSON.parse(Zotero.Prefs.get(prefKey) || '{}');
- if (typeof libraries != 'object') {
- throw true;
- }
- }
- // Ignore old/incorrect formats
- catch (e) {
- Zotero.Prefs.clear(prefKey);
- libraries = {};
- }
-
- return libraries;
- },
-
-
- getVirtualCollectionStateForLibrary: function (libraryID, type) {
- return this.getVirtualCollectionState(type)[libraryID] !== false;
- },
-
-
- setVirtualCollectionStateForLibrary: function (libraryID, type, show) {
- switch (type) {
- case 'duplicates':
- var prefKey = 'duplicateLibraries';
- break;
-
- case 'unfiled':
- var prefKey = 'unfiledLibraries';
- break;
-
- case 'retracted':
- var prefKey = 'retractedLibraries';
- break;
-
- default:
- throw new Error("Invalid virtual collection type '" + type + "'");
- }
-
- var libraries = this.getVirtualCollectionState(type);
-
- // Update current library
- libraries[libraryID] = !!show;
- // Remove libraries that don't exist or that are set to true
- for (let id of Object.keys(libraries).filter(id => libraries[id] || !Zotero.Libraries.exists(id))) {
- delete libraries[id];
- }
- Zotero.Prefs.set(prefKey, JSON.stringify(libraries));
- },
-
-
openPreferences: function (paneID, options = {}) {
if (typeof options == 'string') {
Zotero.debug("ZoteroPane.openPreferences() now takes an 'options' object -- update your code", 2);
@@ -2055,14 +2049,20 @@ Zotero.Utilities.Internal = {
if (size <= 1) {
size = 'small';
}
- else if (size <= 1.25) {
+ else if (size <= 1.15) {
size = 'medium';
}
- else {
+ else if (size <= 1.3) {
size = 'large';
}
+ else {
+ size = 'x-large';
+ }
// Custom attribute -- allows for additional customizations in zotero.css
rootElement.setAttribute('zoteroFontSize', size);
+ if (Zotero.rtl) {
+ rootElement.setAttribute('dir', 'rtl');
+ }
},
getAncestorByTagName: function (elem, tagName){
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
index 735f84a10c..d0c08090e7 100644
--- a/chrome/content/zotero/xpcom/zotero.js
+++ b/chrome/content/zotero/xpcom/zotero.js
@@ -1991,7 +1991,6 @@ Zotero.VersionHeader = {
Zotero.DragDrop = {
currentEvent: null,
currentOrientation: 0,
- currentSourceNode: null,
getDataFromDataTransfer: function (dataTransfer, firstOnly) {
var dt = dataTransfer;
@@ -2050,28 +2049,8 @@ Zotero.DragDrop = {
},
- getDragSource: function (dataTransfer) {
- if (!dataTransfer) {
- //Zotero.debug("Drag data not available", 2);
- return false;
- }
-
- // For items, the drag source is the CollectionTreeRow of the parent window
- // of the source tree
- if (dataTransfer.types.contains("zotero/item")) {
- let sourceNode = dataTransfer.mozSourceNode || this.currentSourceNode;
- if (!sourceNode || sourceNode.tagName != 'treechildren'
- || sourceNode.parentElement.id != 'zotero-items-tree') {
- return false;
- }
- var win = sourceNode.ownerDocument.defaultView;
- if (win.document.documentElement.getAttribute('windowtype') == 'zotero:search') {
- return win.ZoteroAdvancedSearch.itemsView.collectionTreeRow;
- }
- return win.ZoteroPane.collectionsView.selectedTreeRow;
- }
-
- return false;
+ getDragSource: function () {
+ return this.currentDragSource;
},
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index bf9877d34b..90b819467d 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -144,7 +144,7 @@ var ZoteroPane = new function()
* Called on window load or when pane has been reloaded after switching into or out of connector
* mode
*/
- function _loadPane() {
+ async function _loadPane() {
if (!Zotero || !Zotero.initialized) return;
// Set flags for hi-res displays
@@ -152,31 +152,13 @@ var ZoteroPane = new function()
Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
Zotero_Tabs.init();
- ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
+ await ZoteroPane.initCollectionsTree();
+ await ZoteroPane.initItemsTree();
// Add a default progress window
- ZoteroPane_Local.progressWindow = new Zotero.ProgressWindow({ window });
+ ZoteroPane.progressWindow = new Zotero.ProgressWindow({ window });
- //Initialize collections view
- ZoteroPane_Local.collectionsView = new Zotero.CollectionTreeView();
- // Handle an error in setTree()/refresh()
- ZoteroPane_Local.collectionsView.onError = function (e) {
- Zotero.crash();
- };
- var collectionsTree = document.getElementById('zotero-collections-tree');
- collectionsTree.view = ZoteroPane_Local.collectionsView;
- collectionsTree.controllers.appendController(new Zotero.CollectionTreeCommandController(collectionsTree));
- collectionsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
- collectionsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
-
- // Clear items view, so that the load registers as a new selected collection when switching
- // between modes
- ZoteroPane_Local.itemsView = null;
-
- var itemsTree = document.getElementById('zotero-items-tree');
- itemsTree.controllers.appendController(new Zotero.ItemTreeCommandController(itemsTree));
- itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
- itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
+ ZoteroPane.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
Zotero.Keys.windowInit(document);
@@ -274,7 +256,7 @@ var ZoteroPane = new function()
this.uninitContainers = function () {
- this.tagSelector.uninit();
+ if (this.tagSelector) this.tagSelector.uninit();
};
@@ -364,11 +346,12 @@ var ZoteroPane = new function()
}
this.serializePersist();
- this.uninitContainers();
-
+
if(this.collectionsView) this.collectionsView.unregister();
if(this.itemsView) this.itemsView.unregister();
+ this.uninitContainers();
+
observerService.removeObserver(_reloadObserver, "zotero-reloaded");
Zotero_Tabs.closeAll();
@@ -387,7 +370,6 @@ var ZoteroPane = new function()
yield Zotero.unlockPromise;
// The items pane is hidden initially to avoid showing column lines
- document.getElementById('zotero-items-tree').hidden = false;
Zotero.hideZoteroPaneOverlays();
// If pane not loaded, load it or display an error message
@@ -405,30 +387,12 @@ var ZoteroPane = new function()
this.buildItemTypeSubMenu();
}
_madeVisible = true;
-
+
this.unserializePersist();
this.updateLayout();
this.updateToolbarPosition();
this.initContainers();
- // restore saved row selection (for tab switching)
- // TODO: Remove now that no tab mode?
- var containerWindow = window;
- if(containerWindow.zoteroSavedCollectionSelection) {
- this.collectionsView.onLoad.addListener(Zotero.Promise.coroutine(function* () {
- yield this.collectionsView.selectByID(containerWindow.zoteroSavedCollectionSelection);
-
- if (containerWindow.zoteroSavedItemSelection) {
- this.itemsView.onLoad.addListener(function () {
- this.itemsView.rememberSelection(containerWindow.zoteroSavedItemSelection);
- delete containerWindow.zoteroSavedItemSelection;
- }.bind(this));
- }
-
- delete containerWindow.zoteroSavedCollectionSelection;
- }.bind(this)));
- }
-
// Focus the quicksearch on pane open
var searchBar = document.getElementById('zotero-tb-search');
setTimeout(function () {
@@ -717,30 +681,12 @@ var ZoteroPane = new function()
function handleKeyPress(event) {
var from = event.originalTarget.id;
- // Ignore keystrokes if Zotero pane is closed
- var zoteroPane = document.getElementById('zotero-pane-stack');
- if (zoteroPane.getAttribute('hidden') == 'true' ||
- zoteroPane.getAttribute('collapsed') == 'true') {
- return;
- }
-
if (Zotero.locked) {
event.preventDefault();
return;
}
-
- var command = Zotero.Keys.getCommand(event.key);
-
- if (from == 'zotero-collections-tree') {
- if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) ||
- event.keyCode == event.DOM_VK_DELETE) {
- var deleteItems = event.metaKey || (!Zotero.isMac && event.shiftKey);
- ZoteroPane_Local.deleteSelectedCollection(deleteItems);
- event.preventDefault();
- return;
- }
- }
- else if (from == 'zotero-items-tree') {
+
+ if (from == this.itemsView.id) {
// Focus TinyMCE explicitly on tab key, since the normal focusing doesn't work right
if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) {
var deck = document.getElementById('zotero-item-pane-content');
@@ -751,7 +697,7 @@ var ZoteroPane = new function()
}
}
else if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) ||
- event.keyCode == event.DOM_VK_DELETE) {
+ event.keyCode == event.DOM_VK_DELETE) {
// If Cmd/Shift delete, use forced mode, which does different
// things depending on the context
var force = event.metaKey || (!Zotero.isMac && event.shiftKey);
@@ -759,38 +705,15 @@ var ZoteroPane = new function()
event.preventDefault();
return;
}
- else if (event.keyCode == event.DOM_VK_RETURN) {
- var items = this.itemsView.getSelectedItems();
- // Don't do anything if more than 20 items selected
- if (!items.length || items.length > 20) {
- return;
- }
- ZoteroPane_Local.viewItems(items, event);
- // These don't seem to do anything. Instead we override
- // the tree binding's _handleEnter method in itemTreeView.js.
- //event.preventDefault();
- //event.stopPropagation();
- return;
- }
- else if (command == 'toggleRead') {
- // Toggle read/unread
- let row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
- if (!row || !row.isFeed()) return;
- this.toggleSelectedItemsRead();
- if (itemReadPromise) {
- itemReadPromise.cancel();
- itemReadPromise = null;
- }
- return;
- }
}
- // Ignore modifiers other than Ctrl-Shift/Cmd-Shift
- if (!((Zotero.isMac ? event.metaKey : event.ctrlKey) && event.shiftKey)) {
+ var command = Zotero.Keys.getCommand(event.key);
+ if (!command) {
return;
}
-
- if (!command) {
+
+ // Ignore modifiers other than Ctrl-Shift/Cmd-Shift
+ if (!((Zotero.isMac ? event.metaKey : event.ctrlKey) && event.shiftKey)) {
return;
}
@@ -857,7 +780,7 @@ var ZoteroPane = new function()
Zotero.Sync.Runner.sync();
break;
case 'saveToZotero':
- var collectionTreeRow = this.collectionsView.selectedTreeRow;
+ var collectionTreeRow = this.getCollectionTreeRow();
if (collectionTreeRow.isFeed()) {
ZoteroItemPane.translateSelectedItems();
} else {
@@ -865,11 +788,21 @@ var ZoteroPane = new function()
}
break;
case 'toggleAllRead':
- var collectionTreeRow = this.collectionsView.selectedTreeRow;
+ var collectionTreeRow = this.getCollectionTreeRow();
if (collectionTreeRow.isFeed()) {
this.markFeedRead();
}
break;
+ case 'toggleRead':
+ // Toggle read/unread
+ let row = this.getCollectionTreeRow();
+ if (!row || !row.isFeed()) return;
+ this.toggleSelectedItemsRead();
+ if (itemReadPromise) {
+ itemReadPromise.cancel();
+ itemReadPromise = null;
+ }
+ break;
// Handled by s in standalone.js, pointing to s in zoteroPane.xul,
// which are enabled or disabled by this.updateQuickCopyCommands(), called by
@@ -879,7 +812,7 @@ var ZoteroPane = new function()
return;
default:
- throw ('Command "' + command + '" not found in ZoteroPane_Local.handleKeyDown()');
+ throw new Error('Command "' + command + '" not found in ZoteroPane_Local.handleKeyPress()');
}
}
catch (e) {
@@ -898,8 +831,8 @@ var ZoteroPane = new function()
*/
this.newItem = Zotero.Promise.coroutine(function* (typeID, data, row, manual)
{
- if ((row === undefined || row === null) && this.collectionsView.selection) {
- row = this.collectionsView.selection.currentIndex;
+ if ((row === undefined || row === null) && this.getCollectionTreeRow()) {
+ row = this.collectionsView.selection.focused;
// Make sure currently selected view is editable
if (!this.canEdit(row)) {
@@ -1080,52 +1013,9 @@ var ZoteroPane = new function()
return s.id;
});
-
- this.setVirtual = Zotero.Promise.coroutine(function* (libraryID, type, show, select) {
- switch (type) {
- case 'duplicates':
- var treeViewID = 'D' + libraryID;
- break;
-
- case 'unfiled':
- var treeViewID = 'U' + libraryID;
- break;
-
- case 'retracted':
- var treeViewID = 'R' + libraryID;
- break;
-
- default:
- throw new Error("Invalid virtual collection type '" + type + "'");
- }
-
- Zotero.Utilities.Internal.setVirtualCollectionStateForLibrary(libraryID, type, show);
-
- var cv = this.collectionsView;
-
- var promise = cv.waitForSelect();
- var selectedRowID = cv.selectedTreeRow.id;
- var selectedRow = cv.selection.currentIndex;
-
- yield cv.refresh();
-
- // Select new or original row
- if (show) {
- yield this.collectionsView.selectByID(select ? treeViewID : selectedRowID);
- }
- else if (type == 'retracted') {
- yield this.collectionsView.selectByID("L" + libraryID);
- }
- // Select next appropriate row after removal
- else {
- this.collectionsView.selectAfterRowRemoval(selectedRow);
- }
-
- this.collectionsView.selection.selectEventsSuppressed = false;
-
- return promise;
- });
-
+ this.setVirtual = function(libraryID, type, show, select) {
+ return this.collectionsView.toggleVirtualCollection(libraryID, type, show, select);
+ };
this.openAdvancedSearchWindow = function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
@@ -1147,18 +1037,62 @@ var ZoteroPane = new function()
var io = {dataIn: {search: s}, dataOut: null};
window.openDialog('chrome://zotero/content/advancedSearch.xul', '', 'chrome,dialog=no,centerscreen', io);
};
-
-
+
+ this.initItemsTree = async function () {
+ try {
+ const ItemTree = require('containers/itemTree');
+ var itemsTree = document.getElementById('zotero-items-tree');
+ ZoteroPane.itemsView = await ItemTree.init(itemsTree, {
+ id: "main",
+ dragAndDrop: true,
+ persistColumns: true,
+ columnPicker: true,
+ onSelectionChange: selection => ZoteroPane.itemSelected(selection),
+ onContextMenu: event => ZoteroPane.onItemsContextMenuOpen(event),
+ onActivate: (event, items) => ZoteroPane.onItemTreeActivate(event, items),
+ emptyMessage: Zotero.getString('pane.items.loading')
+ });
+ ZoteroPane.itemsView.onRefresh.addListener(() => ZoteroPane.setTagScope());
+ ZoteroPane.itemsView.waitForLoad().then(() => Zotero.uiIsReady());
+ }
+ catch (e) {
+ Zotero.logError(e);
+ Zotero.debug(e, 1);
+ }
+ }
+
+ this.initCollectionsTree = async function () {
+ try {
+ const CollectionTree = require('containers/collectionTree');
+ var collectionsTree = document.getElementById('zotero-collections-tree');
+ ZoteroPane.collectionsView = await CollectionTree.init(collectionsTree, {
+ onSelectionChange: prevSelection => ZoteroPane.onCollectionSelected(prevSelection),
+ onContextMenu: e => ZoteroPane.onCollectionsContextMenuOpen(e),
+ dragAndDrop: true
+ });
+ }
+ catch (e) {
+ Zotero.logError(e);
+ Zotero.debug(e, 1);
+ }
+ };
+
this.initTagSelector = function () {
- var container = document.getElementById('zotero-tag-selector-container');
- if (!container.hasAttribute('collapsed') || container.getAttribute('collapsed') == 'false') {
- this.tagSelector = Zotero.TagSelector.init(
- document.getElementById('zotero-tag-selector'),
- {
- container: 'zotero-tag-selector-container',
- onSelection: this.updateTagFilter.bind(this),
- }
- );
+ try {
+ var container = document.getElementById('zotero-tag-selector-container');
+ if (!container.hasAttribute('collapsed') || container.getAttribute('collapsed') == 'false') {
+ this.tagSelector = Zotero.TagSelector.init(
+ document.getElementById('zotero-tag-selector'),
+ {
+ container: 'zotero-tag-selector-container',
+ onSelection: this.updateTagFilter.bind(this),
+ }
+ );
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+ Zotero.debug(e, 1);
}
};
@@ -1172,6 +1106,9 @@ var ZoteroPane = new function()
}
this.tagSelector.handleResize();
}
+ if (this.collectionsView) {
+ this.collectionsView.updateHeight();
+ }
}, 100);
@@ -1237,39 +1174,22 @@ var ZoteroPane = new function()
};
- this.onCollectionSelected = function () {
- return Zotero.spawn(function* () {
+ this.onCollectionSelected = async function () {
+ try {
var collectionTreeRow = this.getCollectionTreeRow();
if (!collectionTreeRow) {
+ Zotero.debug('ZoteroPane.onCollectionSelected: No selected collection found');
return;
}
- if (this.itemsView && this.itemsView.collectionTreeRow.id == collectionTreeRow.id) {
+ if (this.itemsView && this.itemsView.collectionTreeRow && this.itemsView.collectionTreeRow.id == collectionTreeRow.id) {
Zotero.debug("Collection selection hasn't changed");
-
+
// Update toolbar, in case editability has changed
this._updateToolbarIconsForRow(collectionTreeRow);
return;
}
- if (this.itemsView) {
- // Wait for existing items view to finish loading before unloading it
- //
- // TODO: Cancel loading
- let promise = this.itemsView.waitForLoad();
- if (promise.isPending()) {
- Zotero.debug("Waiting for items view " + this.itemsView.id + " to finish loading");
- yield promise;
- }
-
- this.itemsView.unregister();
- document.getElementById('zotero-items-tree').view = this.itemsView = null;
- }
-
- if (this.collectionsView.selection.count != 1) {
- return;
- }
-
// Rename tab
if (Zotero.isPDFBuild) {
Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName());
@@ -1284,109 +1204,28 @@ var ZoteroPane = new function()
ZoteroPane.tagSelector.clearTagSelection();
}
- // Not necessary with seltype="cell", which calls nsITreeView::isSelectable()
- /*if (collectionTreeRow.isSeparator()) {
- document.getElementById('zotero-items-tree').view = this.itemsView = null;
- return;
- }*/
-
collectionTreeRow.setSearch('');
if (ZoteroPane.tagSelector) {
collectionTreeRow.setTags(ZoteroPane.tagSelector.getTagSelection());
}
this._updateToolbarIconsForRow(collectionTreeRow);
-
- this.itemsView = new Zotero.ItemTreeView(collectionTreeRow);
- if (collectionTreeRow.isPublications()) {
- this.itemsView.collapseAll = true;
- }
- this.itemsView.onError = function () {
- // Don't reload last folder, in case that's the problem
- Zotero.Prefs.clear('lastViewedFolder');
- Zotero.crash();
- };
- this.itemsView.onRefresh.addListener(() => {
- this.setTagScope();
- });
- this.itemsView.onLoad.addListener(() => {
- // Show error if items list couldn't loaded (e.g., bad search), as set in
- // Zotero.CollectionTreeRow::getSearchResults()
- if (Zotero.CollectionTreeCache.error) {
- this.setItemsPaneMessage(Zotero.getString('pane.items.loadError'));
- }
- Zotero.uiIsReady();
- });
-
+
// If item data not yet loaded for library, load it now.
// Other data types are loaded at startup
var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
if (!library.getDataLoaded('item')) {
Zotero.debug("Waiting for items to load for library " + library.libraryID);
ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
- yield library.waitForDataLoad('item');
+ await library.waitForDataLoad('item');
}
- document.getElementById('zotero-items-tree').view = this.itemsView;
-
- try {
- let tree = document.getElementById('zotero-items-tree');
- let treecols = document.getElementById('zotero-items-columns-header');
- let treecolpicker = treecols.boxObject.firstChild.nextSibling;
- let menupopup = treecolpicker.boxObject.firstChild.nextSibling;
- // Add events to treecolpicker to update menu before showing/hiding
- let attr = menupopup.getAttribute('onpopupshowing');
- if (attr.indexOf('Zotero') == -1) {
- menupopup.setAttribute('onpopupshowing', 'ZoteroPane.itemsView.onColumnPickerShowing(event); '
- // Keep whatever else is there
- + attr);
- menupopup.setAttribute('onpopuphidden', 'ZoteroPane.itemsView.onColumnPickerHidden(event); '
- // Keep whatever else is there
- + menupopup.getAttribute('onpopuphidden'));
- }
-
- // Items view column visibility for different groups
- let prevViewGroup = tree.getAttribute('current-view-group');
- let curViewGroup = collectionTreeRow.visibilityGroup;
- tree.setAttribute('current-view-group', curViewGroup);
- if (curViewGroup != prevViewGroup) {
- let cols = Array.from(treecols.getElementsByTagName('treecol'));
- let settings = JSON.parse(Zotero.Prefs.get('itemsView.columnVisibility') || '{}');
- if (prevViewGroup) {
- // Store previous view settings
- let setting = {};
- for (let col of cols) {
- let colType = col.id.substring('zotero-items-column-'.length);
- setting[colType] = col.getAttribute('hidden') == 'true' ? 0 : 1
- }
- settings[prevViewGroup] = setting;
- Zotero.Prefs.set('itemsView.columnVisibility', JSON.stringify(settings));
- }
-
- // Recover current view settings
- if (settings[curViewGroup]) {
- for (let col of cols) {
- let colType = col.id.substring('zotero-items-column-'.length);
- col.setAttribute('hidden', !settings[curViewGroup][colType]);
- }
- } else {
- cols.forEach((col) => {
- col.setAttribute('hidden', !(col.hasAttribute('default-in') &&
- col.getAttribute('default-in').split(' ').indexOf(curViewGroup) != -1)
- )
- })
- }
- }
- }
- catch (e) {
- Zotero.debug(e);
- }
+ this.itemsView.changeCollectionTreeRow(collectionTreeRow);
Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
- }, this)
- .finally(function () {
- return this.collectionsView.runListeners('select');
- }.bind(this));
+ } finally {
+ this.collectionsView.runListeners('select');
+ }
};
@@ -1432,10 +1271,8 @@ var ZoteroPane = new function()
this.getCollectionTreeRow = function () {
- if (!this.collectionsView || !this.collectionsView.selection.count) {
- return false;
- }
- return this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
+ return this.collectionsView && this.collectionsView.selection.count
+ && this.collectionsView.getRow(this.collectionsView.selection.focused);
}
@@ -1444,15 +1281,15 @@ var ZoteroPane = new function()
* or false if not (used for tests, though there could possibly
* be a better test for whether the item pane changed)
*/
- this.itemSelected = function (event) {
+ this.itemSelected = function () {
return Zotero.Promise.coroutine(function* () {
// Don't select item until items list has loaded
//
// This avoids an error if New Item is used while the pane is first loading.
- var promise = this.itemsView.waitForLoad();
- if (promise.isPending()) {
- yield promise;
- }
+ // var promise = this.itemsView.waitForLoad();
+ // if (promise.isPending()) {
+ // yield promise;
+ // }
if (!this.itemsView || !this.itemsView.selection) {
Zotero.debug("Items view not available in itemSelected", 2);
@@ -1705,7 +1542,7 @@ var ZoteroPane = new function()
return;
}
- var collectionTreeRow = this.collectionsView.selectedTreeRow;
+ var collectionTreeRow = this.getCollectionTreeRow();
var canEditFiles = this.canEditFiles();
var prefix = "menuitem-iconic zotero-menuitem-attachments-";
@@ -1795,8 +1632,8 @@ var ZoteroPane = new function()
yield Zotero.DB.executeTransaction(function* () {
newItem = item.clone();
// If in a collection, add new item to it
- if (self.collectionsView.selectedTreeRow.isCollection() && newItem.isTopLevelItem()) {
- newItem.setCollections([self.collectionsView.selectedTreeRow.ref.id]);
+ if (self.getCollectionTreeRow().isCollection() && newItem.isTopLevelItem()) {
+ newItem.setCollections([self.getCollectionTreeRow().ref.id]);
}
yield newItem.save();
if (item.isNote()) {
@@ -1839,7 +1676,7 @@ var ZoteroPane = new function()
if (!this.itemsView || !this.itemsView.selection.count) {
return;
}
- var collectionTreeRow = this.collectionsView.selectedTreeRow;
+ var collectionTreeRow = this.getCollectionTreeRow();
if (!collectionTreeRow.isTrash() && !collectionTreeRow.isBucket() && !this.canEdit()) {
this.displayCannotEditLibraryMessage();
@@ -1898,14 +1735,9 @@ var ZoteroPane = new function()
}
// Do nothing in trash view if any non-deleted items are selected
else if (collectionTreeRow.isTrash()) {
- var start = {};
- var end = {};
- for (var i=0, len=this.itemsView.selection.getRangeCount(); i 0) {
- var row = this.collectionsView.selectedTreeRow;
-
+
+ var row = this.getCollectionTreeRow();
+ if (row) {
if (row.isCollection()) {
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
@@ -2166,18 +1997,20 @@ var ZoteroPane = new function()
});
this.markFeedRead = Zotero.Promise.coroutine(function* () {
- if (!this.collectionsView.selection.count) return;
+ var row = this.getCollectionTreeRow();
+ if (!row) return;
- let feed = this.collectionsView.selectedTreeRow.ref;
+ let feed = row.ref;
let feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID, true, false, true);
yield Zotero.FeedItems.toggleReadByID(feedItemIDs, true);
});
this.editSelectedFeed = Zotero.Promise.coroutine(function* () {
- if (!this.collectionsView.selection.count) return;
+ var row = this.getCollectionTreeRow();
+ if (!row) return;
- let feed = this.collectionsView.selectedTreeRow.ref;
+ let feed = row.ref;
let data = {
url: feed.url,
title: feed.name,
@@ -2198,9 +2031,10 @@ var ZoteroPane = new function()
});
this.refreshFeed = function() {
- if (!this.collectionsView.selection.count) return;
+ var row = this.getCollectionTreeRow();
+ if (!row) return;
- let feed = this.collectionsView.selectedTreeRow.ref;
+ let feed = row.ref;
return feed.updateFeed();
}
@@ -2352,22 +2186,20 @@ var ZoteroPane = new function()
function getSelectedCollection(asID) {
- return this.collectionsView ? this.collectionsView.getSelectedCollection(asID) : false;
+ return this.collectionsView.getSelectedCollection(asID);
}
- function getSelectedSavedSearch(asID)
- {
- if (this.collectionsView.selection.count > 0 && this.collectionsView.selection.currentIndex != -1) {
- var collection = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
- if (collection && collection.isSearch()) {
- return asID ? collection.ref.id : collection.ref;
- }
- }
- return false;
+ function getSelectedSavedSearch(asID) {
+ return this.collectionsView.getSelectedSearch(asID);
}
+ this.getSelectedGroup = function (asID) {
+ return this.collectionsView.getSelectedGroup(asID);
+ }
+
+
/*
* Return an array of Item objects for selected items
*
@@ -2382,21 +2214,7 @@ var ZoteroPane = new function()
return this.itemsView.getSelectedItems(asIDs);
}
-
- this.getSelectedGroup = function (asID) {
- if (this.collectionsView.selection
- && this.collectionsView.selection.count > 0
- && this.collectionsView.selection.currentIndex != -1) {
-
- var collectionTreeRow = this.getCollectionTreeRow();
- if (collectionTreeRow && collectionTreeRow.isGroup()) {
- return asID ? collectionTreeRow.ref.id : collectionTreeRow.ref;
- }
- }
- return false;
- }
-
-
+
/*
* Returns an array of Zotero.Item objects of visible items in current sort order
*
@@ -2435,8 +2253,7 @@ var ZoteroPane = new function()
this.onCollectionsContextMenuOpen = async function (event) {
await ZoteroPane.buildCollectionContextMenu();
document.getElementById('zotero-collectionmenu').openPopup(
- null, null, event.clientX + 1, event.clientY + 1, true, false, event
- );
+ null, null, event.clientX + 1, event.clientY + 1);
};
@@ -2592,7 +2409,7 @@ var ZoteroPane = new function()
var libraryID = this.getSelectedLibraryID();
var options = _collectionContextMenuOptions;
- var collectionTreeRow = this.collectionsView.selectedTreeRow;
+ var collectionTreeRow = this.getCollectionTreeRow();
// This can happen if selection is changing during delayed second call below
if (!collectionTreeRow) {
return;
@@ -2603,11 +2420,7 @@ var ZoteroPane = new function()
// done. This causes some menu items (e.g., export/createBib/loadReport) to appear gray
// in the menu at first and then turn black once there are items
if (!collectionTreeRow.isHeader() && !this.itemsView.initialized) {
- await new Promise((resolve) => {
- this.itemsView.onLoad.addListener(() => {
- resolve();
- });
- });
+ await this.itemsView.waitForLoad();
}
// Set attributes on the menu from the configuration object
@@ -2731,14 +2544,14 @@ var ZoteroPane = new function()
'newSavedSearch'
);
}
- // Only show "Show Duplicates", "Show Unfiled Items", and "Show Retracted" if rows are hidden
- let duplicates = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
+ // Only show "Show Duplicates", "Show Unfiled Items", and "Show Retracted" if rows are hidden
+ let duplicates = Zotero.Prefs.getVirtualCollectionStateForLibrary(
libraryID, 'duplicates'
);
- let unfiled = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
+ let unfiled = Zotero.Prefs.getVirtualCollectionStateForLibrary(
libraryID, 'unfiled'
);
- let retracted = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
+ let retracted = Zotero.Prefs.getVirtualCollectionStateForLibrary(
libraryID, 'retracted'
);
if (!duplicates || !unfiled || !retracted) {
@@ -3177,240 +2990,12 @@ var ZoteroPane = new function()
yield Zotero_LocateMenu.buildContextMenu(menu, true);
});
-
- this.onTreeMouseDown = function (event) {
- var t = event.originalTarget;
- var tree = t.parentNode;
-
- // Ignore click on column headers
- if (!tree.treeBoxObject) {
- return;
+ this.onItemTreeActivate = function(event, items) {
+ var viewOnDoubleClick = Zotero.Prefs.get('viewOnDoubleClick');
+ if (items.length == 1 && (!event.button || viewOnDoubleClick)) {
+ ZoteroPane.viewItems([items[0]], event);
}
-
- var row = {}, col = {}, obj = {};
- tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
- if (row.value == -1) {
- return;
- }
-
- if (tree.id == 'zotero-collections-tree') {
- let collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row.value);
-
- // Prevent the tree's select event from being called for a click
- // on a library sync error icon
- if (collectionTreeRow.isLibrary(true)) {
- if (col.value.id == 'zotero-collections-sync-status-column') {
- var errors = Zotero.Sync.Runner.getErrors(collectionTreeRow.ref.libraryID);
- if (errors) {
- event.stopPropagation();
- return;
- }
- }
- }
- }
- else if (tree.id == 'zotero-items-tree') {
- let collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow();
-
- // Automatically select all equivalent items when clicking on an item
- // in duplicates view
- if (collectionTreeRow.isDuplicates()) {
- // Trigger only on primary-button single clicks without modifiers
- // (so that items can still be selected and deselected manually)
- if (!event || event.detail != 1 || event.button != 0 || event.metaKey
- || event.shiftKey || event.altKey || event.ctrlKey) {
- return;
- }
-
- var t = event.originalTarget;
-
- if (t.localName != 'treechildren') {
- return;
- }
-
- var tree = t.parentNode;
-
- var row = {}, col = {}, obj = {};
- tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
-
- // obj.value == 'cell'/'text'/'image'/'twisty'
- if (!obj.value) {
- return;
- }
-
- // Duplicated in itemTreeView.js::notify()
- var itemID = ZoteroPane_Local.itemsView.getRow(row.value).ref.id;
- var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID);
- ZoteroPane_Local.itemsView.selectItems(setItemIDs);
-
- // Prevent the tree's select event from being called here,
- // since it's triggered by the multi-select
- event.stopPropagation();
- }
- }
- }
-
-
- // Adapted from: http://www.xulplanet.com/references/elemref/ref_tree.html#cmnote-9
- this.onTreeClick = function (event) {
- var t = event.originalTarget;
-
- if (t.localName != 'treechildren') {
- return;
- }
-
- var tree = t.parentNode;
-
- var row = {}, col = {}, obj = {};
- tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
-
- // We care only about primary-button double and triple clicks
- if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) {
- if (row.value == -1) {
- return;
- }
-
- if (tree.id == 'zotero-collections-tree') {
- let collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row.value);
-
- // Show the error panel when clicking a library-specific
- // sync error icon
- if (collectionTreeRow.isLibrary(true)) {
- if (col.value.id == 'zotero-collections-sync-status-column') {
- var errors = Zotero.Sync.Runner.getErrors(collectionTreeRow.ref.libraryID);
- if (!errors) {
- return;
- }
-
- var panel = Zotero.Sync.Runner.updateErrorPanel(window.document, errors);
-
- var anchor = document.getElementById('zotero-collections-tree-shim');
-
- var x = {}, y = {}, width = {}, height = {};
- tree.treeBoxObject.getCoordsForCellItem(row.value, col.value, 'image', x, y, width, height);
-
- x = x.value + Math.round(width.value / 2);
- y = y.value + height.value + 3;
-
- panel.openPopup(anchor, "after_start", x, y, false, false);
- }
- return;
- }
- }
-
- // The Mozilla tree binding fires select() in mousedown(),
- // but if when it gets to click() the selection differs from
- // what it expects (say, because multiple items had been
- // selected during mousedown(), as is the case in duplicates mode),
- // it fires select() again. We prevent that here.
- else if (tree.id == 'zotero-items-tree') {
- let collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow();
- if (collectionTreeRow.isDuplicates()) {
- if (event.button != 0 || event.metaKey || event.shiftKey
- || event.altKey || event.ctrlKey) {
- return;
- }
-
- if (obj.value == 'twisty') {
- return;
- }
-
- event.stopPropagation();
- event.preventDefault();
- }
- }
-
- return;
- }
-
- var collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow();
-
- // Ignore double-clicks in duplicates view on everything except attachments
- if (collectionTreeRow.isDuplicates()) {
- var items = ZoteroPane_Local.getSelectedItems();
- if (items.length != 1 || !items[0].isAttachment()) {
- event.stopPropagation();
- event.preventDefault();
- return;
- }
- }
-
- // obj.value == 'cell'/'text'/'image'
- if (!obj.value) {
- return;
- }
-
- if (tree.id == 'zotero-collections-tree') {
- // Ignore triple clicks for collections
- if (event.detail != 2) {
- return;
- }
-
- if (collectionTreeRow.isLibrary()) {
- var uri = Zotero.URI.getCurrentUserLibraryURI();
- if (uri) {
- ZoteroPane_Local.loadURI(uri);
- event.stopPropagation();
- }
- return;
- }
-
- if (collectionTreeRow.isSearch()) {
- ZoteroPane_Local.editSelectedCollection();
- return;
- }
-
- if (collectionTreeRow.isGroup()) {
- var uri = Zotero.URI.getGroupURI(collectionTreeRow.ref, true);
- ZoteroPane_Local.loadURI(uri);
- event.stopPropagation();
- return;
- }
-
- // Ignore double-clicks on Unfiled/Retracted Items source rows
- if (collectionTreeRow.isUnfiled() || collectionTreeRow.isRetracted()) {
- return;
- }
-
- if (collectionTreeRow.isHeader()) {
- if (collectionTreeRow.ref.id == 'group-libraries-header') {
- var uri = Zotero.URI.getGroupsURL();
- ZoteroPane_Local.loadURI(uri);
- event.stopPropagation();
- }
- return;
- }
-
- if (collectionTreeRow.isBucket()) {
- ZoteroPane_Local.loadURI(collectionTreeRow.ref.uri);
- event.stopPropagation();
- }
- }
- else if (tree.id == 'zotero-items-tree') {
- var viewOnDoubleClick = Zotero.Prefs.get('viewOnDoubleClick');
- if (viewOnDoubleClick) {
- // Expand/collapse on triple-click, though the double-click
- // will still trigger
- if (event.detail == 3) {
- tree.view.toggleOpenState(tree.view.selection.currentIndex);
- return;
- }
-
- // Don't expand/collapse on double-click
- event.stopPropagation();
- }
-
- if (tree.view && tree.view.selection.currentIndex > -1) {
- var item = ZoteroPane_Local.getSelectedItems()[0];
- if (item) {
- if (!viewOnDoubleClick && item.isRegularItem()) {
- return;
- }
- ZoteroPane_Local.viewItems([item], event);
- }
- }
- }
- }
-
+ };
this.openPreferences = function (paneID, action) {
Zotero.warn("ZoteroPane.openPreferences() is deprecated"
@@ -3462,44 +3047,37 @@ var ZoteroPane = new function()
}
}
-
+ // TODO upon electron:
+ // Technically just forwards to the react itemsView
+ // but it is not as robust as XUL. Unfortunately we cannot use the original XUL
+ // version since it causes terrible layout issues when mixing XUL and HTML
+ // Keeping this function here since setting this message is technically
+ // the responsibility of the ZoteroPane and should be independent upon itemsView,
+ // which hopefully we will fix once electronero arrives
function setItemsPaneMessage(content, lock) {
- var elem = document.getElementById('zotero-items-pane-message-box');
-
- if (elem.getAttribute('locked') == 'true') {
+ if (this._itemsPaneMessageLocked) {
return;
}
-
- elem.textContent = '';
- if (typeof content == 'string') {
- let contentParts = content.split("\n\n");
- for (let part of contentParts) {
- var desc = document.createElement('description');
- desc.appendChild(document.createTextNode(part));
- elem.appendChild(desc);
- }
- }
- else {
- elem.appendChild(content);
- }
-
+
// Make message permanent
if (lock) {
- elem.setAttribute('locked', true);
+ this._itemsPaneMessageLocked = true;
+ }
+
+ if (this.itemsView) {
+ this.itemsView.setItemsPaneMessage(content);
}
-
- document.getElementById('zotero-items-pane-content').selectedIndex = 1;
}
-
function clearItemsPaneMessage() {
// If message box is locked, don't clear
- var box = document.getElementById('zotero-items-pane-message-box');
- if (box.getAttribute('locked') == 'true') {
+ if (this._itemsPaneMessageLocked) {
return;
}
- document.getElementById('zotero-items-pane-content').selectedIndex = 0;
+ if (this.itemsView) {
+ this.itemsView.clearItemsPaneMessage();
+ }
}
@@ -3561,8 +3139,8 @@ var ZoteroPane = new function()
if (parentKey) {
item.parentKey = parentKey;
}
- else if (this.collectionsView.selectedTreeRow.isCollection()) {
- item.addToCollection(this.collectionsView.selectedTreeRow.ref.id);
+ else if (this.getCollectionTreeRow().isCollection()) {
+ item.addToCollection(this.getCollectionTreeRow().ref.id);
}
var itemID = yield item.saveTx({
notifierData: {
@@ -3927,8 +3505,8 @@ var ZoteroPane = new function()
}
// Currently selected row
- if (row === undefined && this.collectionsView && this.collectionsView.selection) {
- row = this.collectionsView.selection.currentIndex;
+ if (row === undefined && this.collectionsView && this.getCollectionTreeRow()) {
+ row = this.collectionsView.selection.focused;
}
if (row && !this.canEdit(row)) {
@@ -4048,7 +3626,7 @@ var ZoteroPane = new function()
// Currently selected row
if (row === undefined) {
- row = ZoteroPane_Local.collectionsView.selection.currentIndex;
+ row = ZoteroPane_Local.collectionsView.selection.focused;
}
if (!ZoteroPane_Local.canEdit(row)) {
@@ -4512,7 +4090,7 @@ var ZoteroPane = new function()
this.canEdit = function (row) {
// Currently selected row
if (row === undefined) {
- row = this.collectionsView.selection.currentIndex;
+ row = this.collectionsView.selection.focused;
}
var collectionTreeRow = this.collectionsView.getRow(row);
@@ -4529,7 +4107,7 @@ var ZoteroPane = new function()
this.canEditLibrary = function (row) {
// Currently selected row
if (row === undefined) {
- row = this.collectionsView.selection.currentIndex;
+ row = this.collectionsView.selection.focused;
}
var collectionTreeRow = this.collectionsView.getRow(row);
@@ -4547,7 +4125,7 @@ var ZoteroPane = new function()
this.canEditFiles = function (row) {
// Currently selected row
if (row === undefined) {
- row = this.collectionsView.selection.currentIndex;
+ row = this.collectionsView.selection.focused;
}
var collectionTreeRow = this.collectionsView.getRow(row);
@@ -5099,10 +4677,6 @@ var ZoteroPane = new function()
// Check to make sure we're still on the same item
var items = this.getSelectedItems();
if (items.length != 1 || items[0].id != feedItemID) {
- Zotero.debug(items.length);
- Zotero.debug(items[0].id);
- Zotero.debug(feedItemID);
-
return;
}
var feedItem = items[0];
@@ -5271,6 +4845,11 @@ var ZoteroPane = new function()
this.updateToolbarPosition();
this.updateTagsBoxSize();
+ if (ZoteroPane.itemsView) {
+ // Need to immediately rerender the items here without any debouncing
+ // since tree height will have changed
+ ZoteroPane.itemsView._updateHeight();
+ }
ZoteroContextPane.update();
}
@@ -5284,24 +4863,10 @@ var ZoteroPane = new function()
if(!serializedValues) return;
serializedValues = JSON.parse(serializedValues);
- // Somehow all the columns can end up non-hidden, so fix that if it happens
- var maxColumns = 30; // 31 as of 4/2020
- var numColumns = Object.keys(serializedValues)
- .filter(id => id.startsWith('zotero-items-column-') && serializedValues[id].hidden != "true")
- .length;
- var fixColumns = numColumns > maxColumns;
- if (fixColumns) {
- Zotero.logError("Repairing corrupted pane.persist");
- }
-
- for(var id in serializedValues) {
+ for (var id in serializedValues) {
var el = document.getElementById(id);
- if(!el) return;
-
- // In one case where this happened, all zotero-items-column- elements were present
- // with just "ordinal" (and no "hidden"), and "zotero-items-tree" was set to
- // {"current-view-group":"default"}. Clearing only the columns didn't work for some reason.
- if (fixColumns && (id.startsWith('zotero-items-column-') || id == 'zotero-items-tree')) {
+ if (!el) {
+ Zotero.debug(`Trying to restore persist data for #${id} but elem not found`, 5);
continue;
}
@@ -5319,7 +4884,7 @@ var ZoteroPane = new function()
}
}
- if(this.itemsView) {
+ if (this.itemsView) {
// may not yet be initialized
try {
this.itemsView.sort();
@@ -5331,12 +4896,17 @@ var ZoteroPane = new function()
* Serializes zotero-persist elements to preferences
*/
this.serializePersist = function() {
- if(!_unserialized) return;
- var serializedValues = {};
+ if (!_unserialized) return;
+ try {
+ var serializedValues = JSON.parse(Zotero.Prefs.get('pane.persist'));
+ }
+ catch (e) {
+ serializedValues = {};
+ }
for (let el of document.getElementsByAttribute("zotero-persist", "*")) {
- if(!el.getAttribute) continue;
+ if (!el.getAttribute) continue;
var id = el.getAttribute("id");
- if(!id) continue;
+ if (!id) continue;
var elValues = {};
for (let attr of el.getAttribute("zotero-persist").split(/[\s,]+/)) {
if (el.hasAttribute(attr)) {
@@ -5374,15 +4944,22 @@ var ZoteroPane = new function()
var collectionsPane = document.getElementById("zotero-collections-pane");
var collectionsToolbar = document.getElementById("zotero-collections-toolbar");
+ var collectionsTree = document.querySelector('#zotero-collections-tree .tree');
var itemsPane = document.getElementById("zotero-items-pane");
var itemsToolbar = document.getElementById("zotero-items-toolbar");
var itemPane = document.getElementById("zotero-item-pane");
var itemToolbar = document.getElementById("zotero-item-toolbar");
var tagSelector = document.getElementById("zotero-tag-selector");
- var collectionsPaneWidth = collectionsPane.boxObject.width + 'px';
- collectionsToolbar.style.width = collectionsPaneWidth;
- tagSelector.style.maxWidth = collectionsPaneWidth;
+ collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px';
+ tagSelector.style.maxWidth = collectionsPane.boxObject.width + 'px';
+ if (collectionsTree) {
+ let borderSize = Zotero.isMac ? 0 : 2;
+ collectionsTree.style.maxWidth = (collectionsPane.boxObject.width - borderSize) + 'px';
+ }
+ if (ZoteroPane.itemsView) {
+ ZoteroPane.itemsView.updateHeight();
+ }
if (stackedLayout || itemPane.collapsed) {
// The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing).
@@ -5405,10 +4982,6 @@ var ZoteroPane = new function()
itemToolbar.setAttribute("flex", "1");
}
- // Allow item pane to shrink to available height in stacked mode, but don't expand to be too
- // wide when there's no persisted width in non-stacked mode
- itemPane.setAttribute("flex", stackedLayout ? 1 : 0);
-
this.handleTagSelectorResize();
}
diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul
index f5c7b68340..5ec62ce58b 100644
--- a/chrome/content/zotero/zoteroPane.xul
+++ b/chrome/content/zotero/zoteroPane.xul
@@ -330,32 +330,17 @@
-
-
-
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
diff --git a/chrome/locale/en-US/zotero/preferences.dtd b/chrome/locale/en-US/zotero/preferences.dtd
index f578232d12..c4c5bbefdb 100644
--- a/chrome/locale/en-US/zotero/preferences.dtd
+++ b/chrome/locale/en-US/zotero/preferences.dtd
@@ -83,6 +83,7 @@
+
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
index 9f5f37c616..b3b5a00c89 100644
--- a/chrome/locale/en-US/zotero/zotero.properties
+++ b/chrome/locale/en-US/zotero/zotero.properties
@@ -228,6 +228,7 @@ date.relative.daysAgo.multiple = %S days ago
date.relative.yearsAgo.one = 1 year ago
date.relative.yearsAgo.multiple = %S years ago
+pane.collections.title = Collections
pane.collections.delete.title = Delete Collection
pane.collections.delete = Are you sure you want to delete the selected collection?
pane.collections.delete.keepItems = Items within this collection will not be deleted.
@@ -299,6 +300,7 @@ pane.items.intro.text1 = Welcome to %S!
pane.items.intro.text2 = View the [Quick Start Guide] to learn how to begin building your library, and be sure to [install a %S] so you can add items to %S as you browse the web.
pane.items.intro.text3 = Already using %S on another computer? [Set up syncing] to pick up right where you left off.
+pane.items.title = Items
pane.items.loading = Loading items…
pane.items.loadError = Error loading items list
pane.items.columnChooser.moreColumns = More Columns
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
index 286cdcd153..d40059b8dc 100644
--- a/chrome/skin/default/zotero/overlay.css
+++ b/chrome/skin/default/zotero/overlay.css
@@ -4,83 +4,12 @@
overflow: hidden;
}
-/* Why is this necessary? */
-#zotero-collections-tree treechildren::-moz-tree-image,
-#zotero-items-tree treechildren::-moz-tree-image {
- margin-right: 5px;
-}
-
#zotero-collections-pane
{
min-width: 150px;
width: 250px;
}
-#zotero-collections-tree {
- min-height: 5.2em;
-}
-
-#zotero-collections-tree treechildren::-moz-tree-row {
- height: 1.7em;
-}
-
-/* As of Fx37, the tree doesn't scale HiDPI images properly on Windows and Linux */
-#zotero-collections-tree treechildren::-moz-tree-image,
-#zotero-items-tree treechildren::-moz-tree-image {
- height: 16px;
-}
-
-#zotero-collections-tree treechildren::-moz-tree-image(primary)
-{
- margin-right: 5px;
-}
-
-#zotero-collections-tree treechildren::-moz-tree-separator {
- border: none;
-}
-
-#zotero-collections-tree treechildren::-moz-tree-twisty(notwisty),
-#zotero-collections-tree treechildren::-moz-tree-twisty(header) {
- width: 0;
-}
-
-/* Set by setHighlightedRows() and getRowProperties() in collectionTreeView.js) */
-#zotero-collections-tree treechildren::-moz-tree-row(highlighted)
-{
- background: #FFFF99 !important;
-}
-
-#zotero-items-column-hasAttachment, #zotero-items-column-numNotes {
- min-width: 21px;
-}
-
-#zotero-items-column-hasAttachment {
- list-style-image: url(chrome://zotero/skin/attach-small.png);
-}
-
-#zotero-items-column-hasAttachment .treecol-icon {
- width: 13px;
-}
-
-#zotero-items-column-numNotes {
- list-style-image: url(chrome://zotero/skin/treeitem-note-small.png);
-}
-
-#zotero-items-column-numNotes .treecol-icon {
- width: 12px;
-}
-
-@media (min-resolution: 1.25dppx) {
- .tree-columnpicker-icon {
- list-style-image: url(chrome://zotero/skin/firefox/columnpicker@2x.gif);
- width: 14px;
- }
-}
-
-#zotero-items-column-numNotes {
- text-align: center;
-}
-
#zotero-items-tree treechildren::-moz-tree-image(hasAttachment, pie)
{
margin: 1px 0 0;
@@ -219,75 +148,37 @@
#zotero-items-tree treechildren::-moz-tree-image(selected, hasAttachment, pie63) { -moz-image-region: rect(32px, 2016px, 64px, 1984px); }
#zotero-items-tree treechildren::-moz-tree-image(selected, hasAttachment, pie64) { -moz-image-region: rect(32px, 2048px, 64px, 2016px); }
-/* Style search results, display non-matches in gray */
-#zotero-items-tree treechildren::-moz-tree-cell-text(contextRow) {
- color: gray;
-}
-
-#zotero-items-tree treechildren::-moz-tree-cell-text(contextRow, selected, focus) {
- /* This is the default color, not the (platform-specific) highlight color, but at least it
- helps to differentiate when clicking on a context row. */
- color: inherit;
-}
-
-/* Style unread items/collections in bold */
-#zotero-items-tree treechildren::-moz-tree-cell-text(unread),
-#zotero-collections-tree treechildren::-moz-tree-cell-text(unread) {
- font-weight: bold;
-}
-
-#zotero-items-pane
-{
- min-width: 290px;
- min-height: 150px;
-}
-
-/* Used for intro text */
-#zotero-items-pane-message-box {
- overflow-y: auto;
- cursor: default;
-}
-
-#zotero-items-pane-message-box div {
+.items-tree-message div {
padding: 0 35px;
}
-#zotero-items-pane-message-box p {
+.items-tree-message p {
max-width: 800px;
font-size: 1.45em;
line-height: 1.7em;
text-align: left;
}
-#zotero-items-pane-message-box div.publications p {
+.items-tree-message div.publications p {
font-size: 1.2em;
}
/* Increase size when window is wider */
-#zotero-pane.width-1000 #zotero-items-pane-message-box div.publications p {
+#zotero-pane.width-1000 .items-tree-message div.publications p {
font-size: 1.35em;
}
-#zotero-items-pane-message-box span.text-link {
+.items-tree-message span.text-link {
color: rgb(0, 149, 221);
cursor: pointer;
text-decoration: underline;
}
-#zotero-items-pane-message-box {
- -moz-appearance: listbox;
- text-align: center;
- padding: 20px;
-}
-
-#zotero-items-pane-message-box description:not(:first-child) {
- margin-top: .75em;
-}
-
#zotero-item-pane
{
width: 338px;
min-width: 338px;
+ min-height: 200px;
overflow-y: hidden;
}
diff --git a/chrome/skin/default/zotero/zotero.css b/chrome/skin/default/zotero/zotero.css
index 7ba615b9b6..1924ce4911 100644
--- a/chrome/skin/default/zotero/zotero.css
+++ b/chrome/skin/default/zotero/zotero.css
@@ -9,12 +9,12 @@
height: 1.5em;
}
-*[zoteroFontSize=large] treechildren::-moz-tree-row
+*[zoteroFontSize=large] treechildren::-moz-tree-row, *[zoteroFontSize=x-large] treechildren::-moz-tree-row
{
height: 1.5em;
}
-*[zoteroFontSize=large] .treecol-text
+*[zoteroFontSize=large] .treecol-text, *[zoteroFontSize=x-large] .treecol-text
{
margin:0;
padding:0;
diff --git a/components/zotero-service.js b/components/zotero-service.js
index ccf295e29a..6f3914de6a 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -65,8 +65,6 @@ const xpcomFilesAll = [
/** XPCOM files to be loaded only for local translation and DB access **/
const xpcomFilesLocal = [
- 'libraryTreeView',
- 'collectionTreeView',
'collectionTreeRow',
'annotations',
'api',
@@ -102,10 +100,10 @@ const xpcomFilesLocal = [
'duplicates',
'editorInstance',
'feedReader',
+ 'fileDragDataProvider',
'fulltext',
'id',
'integration',
- 'itemTreeView',
'locale',
'locateManager',
'mime',
diff --git a/resource/react-dom-server.js b/resource/react-dom-server.js
new file mode 120000
index 0000000000..97d12a4d42
--- /dev/null
+++ b/resource/react-dom-server.js
@@ -0,0 +1 @@
+../node_modules/react-dom/umd/react-dom-server.browser.development.js
\ No newline at end of file
diff --git a/resource/require.js b/resource/require.js
index 2485d2e9a0..f58d522f49 100644
--- a/resource/require.js
+++ b/resource/require.js
@@ -61,13 +61,15 @@ var require = (function() {
}
function getZotero() {
+ if (win.Zotero) Zotero = win.Zotero;
+
if (typeof Zotero === 'undefined') {
try {
Zotero = Components.classes["@zotero.org/Zotero;1"]
.getService(Components.interfaces.nsISupports).wrappedJSObject;
} catch (e) {}
}
- return Zotero || {};
+ return Zotero || {};
}
var cons;
@@ -87,6 +89,8 @@ var require = (function() {
navigator: typeof win.navigator !== 'undefined' && win.navigator || {},
setTimeout: win.setTimeout,
clearTimeout: win.clearTimeout,
+ requestAnimationFrame: win.setTimeout,
+ cancelAnimationFrame: win.clearTimeout
};
Object.defineProperty(globals, 'Zotero', { get: getZotero });
var loader = Loader({
@@ -101,5 +105,5 @@ var require = (function() {
globals
});
let require = Require(loader, requirer);
- return require
+ return require;
})();
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss
index 4e08c775b1..6c26b7ed73 100644
--- a/scss/_zotero-react-client.scss
+++ b/scss/_zotero-react-client.scss
@@ -37,3 +37,6 @@
@import "components/tabBar";
@import "components/tagsBox";
@import "components/tagSelector";
+@import "components/collection-tree";
+@import "components/virtualized-table";
+@import "components/item-tree";
diff --git a/scss/components/_button.scss b/scss/components/_button.scss
index a18d4825f7..21efff6e82 100644
--- a/scss/components/_button.scss
+++ b/scss/components/_button.scss
@@ -33,7 +33,6 @@
span.menu-marker {
-moz-appearance: toolbarbutton-dropdown;
display: inline-block;
- vertical-align: middle;
margin-right: -5px;
}
}
diff --git a/scss/components/_collection-tree.scss b/scss/components/_collection-tree.scss
new file mode 100644
index 0000000000..67fb76ba7f
--- /dev/null
+++ b/scss/components/_collection-tree.scss
@@ -0,0 +1,45 @@
+#zotero-collections-tree-container {
+ height: 5.2em;
+}
+
+#zotero-collections-tree {
+ width: 100%;
+
+ .virtualized-table {
+ overflow-y: auto;
+ flex: 1 0;
+ text-overflow: ellipsis;
+ }
+
+ .cell.primary {
+ display: flex;
+ align-items: center;
+
+ :not(.cell-text) {
+ flex-shrink: 0
+ }
+
+ .cell-text {
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin-left: 6px;
+ }
+
+ input.cell-text {
+ border: 1px highlight solid;
+ padding: 1px 2px;
+ margin-right: 5px;
+ width: 100%;
+ font-size: inherit;
+ }
+
+ .cell-icon {
+ min-width: 16px;
+ }
+ }
+
+ .row.editing .cell {
+ pointer-events: auto;
+ }
+}
\ No newline at end of file
diff --git a/scss/components/_icons.scss b/scss/components/_icons.scss
index 355d214d1d..06aed271cf 100644
--- a/scss/components/_icons.scss
+++ b/scss/components/_icons.scss
@@ -2,6 +2,20 @@
width: 16px;
}
-.icon.icon-downchevron > img {
- width: 7px;
+.icon-bg {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ vertical-align: middle;
+}
+
+.icon.icon-downchevron {
+ width: 7px !important;
+}
+
+.icon {
+ -moz-appearance: none !important;
}
\ No newline at end of file
diff --git a/scss/components/_item-tree.scss b/scss/components/_item-tree.scss
new file mode 100644
index 0000000000..87ca3c6b50
--- /dev/null
+++ b/scss/components/_item-tree.scss
@@ -0,0 +1,47 @@
+#zotero-items-pane {
+ min-width: 290px;
+ min-height: 150px;
+ height: 150px;
+ width: 290px;
+
+ .items-tree-message {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+}
+#zotero-items-tree {
+ .virtualized-table-header .icon {
+ width: 13px;
+ height: 13px;
+ }
+
+ .cell.primary {
+ .retracted {
+ width: 12px;
+ margin-inline-start: 3px;
+ }
+
+ .tag-swatch {
+ display: inline-block;
+ min-width: 8px;
+ min-height: 8px;
+ margin-inline-start: 3px;
+ border-radius: 1px;
+ }
+ }
+
+ .cell.hasAttachment {
+ box-sizing: content-box;
+ padding: 0 4px;
+ }
+
+ .cell.numNotes {
+ text-align: center;
+ }
+}
diff --git a/scss/components/_tagSelector.scss b/scss/components/_tagSelector.scss
index 1a7b7dfb40..e0601c6968 100644
--- a/scss/components/_tagSelector.scss
+++ b/scss/components/_tagSelector.scss
@@ -75,6 +75,7 @@
text-overflow: ellipsis;
white-space: pre;
padding: 1px 4px 3px; // See also TagSelectorList.jsx
+ background-color: $tag-selector-bg;
&.colored {
font-weight: bold;
diff --git a/scss/components/_virtualized-table.scss b/scss/components/_virtualized-table.scss
new file mode 100644
index 0000000000..44e6c7ff47
--- /dev/null
+++ b/scss/components/_virtualized-table.scss
@@ -0,0 +1,251 @@
+//
+// Virtualized table
+// --------------------------------------------------
+
+/**
+
+
+
+ */
+.virtualized-table-container {
+ display: flex;
+ height: 0;
+ flex-direction: column;
+ > div {
+ display: flex;
+ flex: 1;
+ background-color: -moz-field;
+ overflow: hidden;
+ position: relative;
+ }
+}
+
+.virtualized-table, .drag-image-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.resizing {
+ cursor: col-resize;
+ .cell {
+ cursor: col-resize;
+ }
+ }
+
+ .cell {
+ min-width: 30px;
+ cursor: default;
+ white-space: nowrap;
+ flex-grow: 1;
+ flex-shrink: 1;
+ box-sizing: border-box;
+
+ &.primary {
+ display: flex;
+ align-items: center;
+
+ :not(.cell-text) {
+ flex-shrink: 0
+ }
+
+ .cell-text {
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin-inline-start: 6px;
+ }
+
+ .twisty + .cell-text, .spacer-twisty + .cell-text {
+ margin-inline-start: 0;
+ }
+ }
+
+ .cell-icon {
+ min-width: 16px;
+ }
+ }
+
+ .row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+
+ &.drop {
+ color: $shade-0 !important;
+ background: $shade-5 !important;
+ * {
+ pointer-events: none !important;
+ }
+ }
+
+ span.drop-before, span.drop-after {
+ position: absolute;
+ width: 20%;
+ height: 1px;
+ background-color: $shade-5;
+ pointer-events: none;
+ }
+ span.drop-before {
+ top: 0;
+ }
+ span.drop-after {
+ bottom: -1px;
+ }
+
+ &.selected:not(.highlighted) {
+ background-color: highlight;
+ color: highlighttext;
+ }
+
+ &.highlighted {
+ background: #FFFF99;
+ }
+
+ &.unread {
+ font-weight: bold;
+ }
+
+ &.context-row {
+ color: gray;
+ }
+ }
+
+ .column-drag-marker {
+ z-index: 99999;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ width: 2px;
+ background-color: #ccc;
+ }
+}
+
+.virtualized-table-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: calc(100% - 11px);
+ border-bottom: 1px solid #ccc;
+ background: #f6f6f6;
+ height: 14px;
+ overflow: hidden;
+ border-inline-end: 1px solid #ddd;
+
+ &.static-columns {
+ pointer-events: none;
+ }
+
+ .column-picker {
+ text-align: center;
+ }
+
+ .cell {
+ display: flex;
+ position: relative;
+ height: 100%;
+ align-items: center;
+
+ &:hover {
+ background: #fff;
+ }
+
+ &.dragging {
+ background: #e9e9e9;
+ }
+
+ .resizer {
+ background: linear-gradient(#ddd, #ddd) no-repeat center/1px 80%;
+ cursor: col-resize;
+ height: 100%;
+ content: "\00a0";
+ display: block;
+ position: absolute;
+ left: -5px;
+ min-width: 10px;
+ }
+
+ .label {
+ margin-inline-start: 10px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ &.cell-icon {
+ > .label {
+ margin-inline-start: 0;
+ }
+ justify-content: center;
+ }
+
+ .sort-indicator {
+ -moz-appearance: toolbarbutton-dropdown;
+ display: block;
+ position: absolute;
+ right: 10px;
+
+ &.ascending {
+ transform: rotate(180deg);
+ }
+ }
+ }
+}
+
+.virtualized-table-body, .drag-image-container{
+ flex: 1 0;
+ max-width: 100%;
+ overflow: auto;
+
+ .row {
+ padding-inline-start: 6px;
+ }
+
+ .cell {
+ padding: 2px 5px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ pointer-events: none;
+ min-height: 90%;
+ }
+}
+.spacer-twisty {
+ display: inline-block;
+ min-width: 8px;
+}
+
+.twisty {
+ margin-inline-end: 0 !important;
+ display: flex;
+ align-items: center;
+
+ svg {
+ fill: #444;
+ transition: transform 0.125s ease;
+ transform: rotate(-90deg);
+ }
+
+ &.open svg {
+ transform: rotate(0deg) !important;
+ }
+}
+
+*[dir=rtl] {
+ .virtualized-table-header {
+ .cell .sort-indicator {
+ left: 10px;
+ right: initial;
+ }
+ .resizer {
+ right: -5px;
+ left: initial;
+ }
+ }
+ .twisty svg {
+ transform: rotate(90deg);
+ }
+}
diff --git a/scss/linux/_collection-tree.scss b/scss/linux/_collection-tree.scss
new file mode 100644
index 0000000000..866e6a89ca
--- /dev/null
+++ b/scss/linux/_collection-tree.scss
@@ -0,0 +1,8 @@
+#zotero-collections-tree-container {
+ margin-bottom: -1px;
+}
+
+#zotero-collections-tree .virtualized-table .row {
+ height: 1.333em;
+}
+
diff --git a/scss/linux/_item-tree.scss b/scss/linux/_item-tree.scss
new file mode 100644
index 0000000000..57f2fea177
--- /dev/null
+++ b/scss/linux/_item-tree.scss
@@ -0,0 +1,5 @@
+#zotero-items-tree {
+ .cell.hasAttachment, .cell.numNotes {
+ padding: 0 8px;
+ }
+}
\ No newline at end of file
diff --git a/scss/linux/_virtualized-table.scss b/scss/linux/_virtualized-table.scss
new file mode 100644
index 0000000000..c40a1d6d1a
--- /dev/null
+++ b/scss/linux/_virtualized-table.scss
@@ -0,0 +1,28 @@
+.virtualized-table {
+ border: 1px solid #ccc;
+}
+.virtualized-table, .drag-image-container {
+ .twisty {
+ padding-inline-end: 3px;
+ svg {
+ width: 5px;
+ }
+ }
+}
+.virtualized-table-header {
+ height: 28px;
+ background-image: linear-gradient(#fff, #fafafa);
+
+ &.dragging {
+ color: #666;
+ background: #f8f8f8;
+ }
+
+ .cell .sort-indicator {
+ transform: scale(1.25);
+
+ &.ascending {
+ transform: scale(1.25) rotate(180deg);
+ }
+ }
+}
diff --git a/scss/mac/_collection-tree.scss b/scss/mac/_collection-tree.scss
new file mode 100644
index 0000000000..b6268bbbf3
--- /dev/null
+++ b/scss/mac/_collection-tree.scss
@@ -0,0 +1,54 @@
+#zotero-collections-tree {
+ .virtualized-table {
+ background-color: #d2d8e2;
+
+ .row {
+ height: 1.818em;
+
+ &.selected:not(.highlighted) {
+ background: -moz-linear-gradient(top, #6494D4, #2559AC) repeat-x;
+ border-top: 1px solid #5382C5;
+ font-weight: bold !important;
+ color: #ffffff !important;
+ height: calc(1.818em - 1px);
+ padding-bottom: 1px;
+ }
+ }
+
+ &:not(:focus) .row.selected:not(.highlighted) {
+ background: -moz-linear-gradient(top, #A0B0CF, #7386AB) repeat-x;
+ border-top: 1px solid #94A1C0;
+ }
+
+ &:-moz-window-inactive {
+ background-color: rgb(232, 232, 232);
+
+ .row.selected:not(.highlighted) {
+ background: -moz-linear-gradient(top, #B4B4B4, #8A8A8A) repeat-x;
+ border-top: 1px solid #979797;
+ }
+ }
+ }
+}
+
+// IDK why, but these heights are all over the place on macOS (not a f(x)=x)
+*[zoteroFontSize=medium] #zotero-collections-tree .virtualized-table .row {
+ height: 1.739em;
+ &.focused:not(.highlighted) {
+ height: calc(1.738em - 1px);
+ }
+}
+
+*[zoteroFontSize=large] #zotero-collections-tree .virtualized-table .row {
+ height: 1.68em;
+ &.focused:not(.highlighted) {
+ height: calc(1.68em - 1px);
+ }
+}
+
+*[zoteroFontSize=x-large] #zotero-collections-tree .virtualized-table .row {
+ height: 1.697em;
+ &.focused:not(.highlighted) {
+ height: calc(1.697em - 1px);
+ }
+}
diff --git a/scss/mac/_item-tree.scss b/scss/mac/_item-tree.scss
new file mode 100644
index 0000000000..07e6024df9
--- /dev/null
+++ b/scss/mac/_item-tree.scss
@@ -0,0 +1,9 @@
+#zotero-items-tree {
+ // Selected rows when the tree is not the focused element
+ .virtualized-table:not(:focus) {
+ .row.selected {
+ background: #dcdcdc;
+ color: initial;
+ }
+ }
+}
\ No newline at end of file
diff --git a/scss/mac/_virtualized-table.scss b/scss/mac/_virtualized-table.scss
new file mode 100644
index 0000000000..2ada791560
--- /dev/null
+++ b/scss/mac/_virtualized-table.scss
@@ -0,0 +1,24 @@
+.virtualized-table, .drag-image-container {
+ .twisty {
+ width: 19px;
+
+ svg {
+ fill: #888;
+ width: 16px;
+ }
+ }
+
+ .focused:not(.highlighted) .twisty svg {
+ fill: #fff;
+ }
+
+ .spacer-twisty {
+ min-width: 19px;
+ }
+}
+
+.virtualized-table-body, .drag-image-container{
+ .cell:not(:first-child) {
+ border-inline-start: 1px solid #ddd;
+ }
+}
diff --git a/scss/win/_item-tree.scss b/scss/win/_item-tree.scss
new file mode 100644
index 0000000000..0b2903f1c2
--- /dev/null
+++ b/scss/win/_item-tree.scss
@@ -0,0 +1,33 @@
+#zotero-items-tree .virtualized-table {
+ .row {
+ padding-inline-end: 1px;
+ &.selected {
+ background-color: #e5f3ff;
+ border: 1px solid #7bc3ff;
+ color: inherit;
+ padding-inline-start: 1px;
+ padding-inline-end: 0;
+ }
+ &:hover {
+ background-color: #e5f3ff;
+ }
+ }
+}
+
+#zotero-items-tree .virtualized-table:not(:focus) {
+ .row {
+ &.selected {
+ color: inherit;
+ background: #f0f0f0;
+ border: none;
+ padding-inline-start: 2px;
+ padding-inline-end: 1px;
+ }
+ &.selected:hover {
+ background-color: #e5f3ff;
+ border: 1px solid #7bc3ff;
+ padding-inline-start: 1px;
+ padding-inline-end: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/scss/win/_virtualized-table.scss b/scss/win/_virtualized-table.scss
new file mode 100644
index 0000000000..ba6f567cdd
--- /dev/null
+++ b/scss/win/_virtualized-table.scss
@@ -0,0 +1,50 @@
+.spacer-twisty {
+ min-width: 16px;
+}
+
+.virtualized-table {
+ &:not(:focus) .row.selected:not(.highlighted) {
+ color: inherit;
+ background: #f0f0f0;
+ }
+
+ .row {
+ padding-inline-start: 2px;
+
+ .twisty svg {
+ fill: #b6b6b6;
+ width: 16px;
+ }
+
+ .twisty.open svg {
+ fill: #636363;
+ }
+
+ &:hover .twisty svg {
+ fill: #4ed0f9;
+ }
+
+ &.drop {
+ background-color: highlight;
+ color: highlighttext;
+ }
+ }
+}
+
+.virtualized-table-header {
+ height: 24px;
+ background: #fff;
+
+ .cell {
+ &:hover {
+ background: #d9ebf9;
+ }
+ &:active {
+ background: #bcdcf4;
+ }
+ &.dragging {
+ background: #d9ebf9 !important;
+ color: #6d6d6d;
+ }
+ }
+}
diff --git a/scss/zotero-react-client-mac.scss b/scss/zotero-react-client-mac.scss
index 29c82fd8eb..5675f075d6 100644
--- a/scss/zotero-react-client-mac.scss
+++ b/scss/zotero-react-client-mac.scss
@@ -11,3 +11,6 @@
@import "mac/search";
@import "mac/tabBar";
@import "mac/tag-selector";
+@import "mac/virtualized-table";
+@import "mac/collection-tree";
+@import "mac/item-tree";
diff --git a/scss/zotero-react-client-unix.scss b/scss/zotero-react-client-unix.scss
index a0929b7a1f..a3ba5a8901 100644
--- a/scss/zotero-react-client-unix.scss
+++ b/scss/zotero-react-client-unix.scss
@@ -10,3 +10,6 @@
@import "linux/search";
@import "linux/tagsBox";
@import "linux/about";
+@import "linux/virtualized-table";
+@import "linux/item-tree";
+@import "linux/collection-tree";
diff --git a/scss/zotero-react-client-win.scss b/scss/zotero-react-client-win.scss
index 4a78be0a85..65080dc9ac 100644
--- a/scss/zotero-react-client-win.scss
+++ b/scss/zotero-react-client-win.scss
@@ -7,3 +7,5 @@
@import "win/createParent";
@import "win/search";
@import "win/tag-selector";
+@import "win/item-tree";
+@import "win/virtualized-table";
diff --git a/test/content/support.js b/test/content/support.js
index 8d311f4e00..d1a7ce83d0 100644
--- a/test/content/support.js
+++ b/test/content/support.js
@@ -257,6 +257,11 @@ var waitForTagSelector = function (win, numUpdates = 1) {
return deferred.promise;
};
+var waitForCollectionTree = function(win) {
+ let cv = win.ZoteroPane.collectionsView;
+ return cv._waitForEvent('refresh');
+}
+
/**
* Waits for a single item event. Returns a promise for the item ID(s).
*/
@@ -324,29 +329,10 @@ function waitForCallback(cb, interval, timeout) {
}
-function clickOnItemsRow(itemsView, row, button = 0) {
- var x = {};
- var y = {};
- var width = {};
- var height = {};
- itemsView._treebox.getCoordsForCellItem(
- row,
- itemsView._treebox.columns.getNamedColumn('zotero-items-column-title'),
- 'text',
- x, y, width, height
- );
-
- // Select row to trigger multi-select
- var tree = itemsView._treebox.treeBody;
- var rect = tree.getBoundingClientRect();
- var x = rect.left + x.value;
- var y = rect.top + y.value;
- tree.dispatchEvent(new MouseEvent("mousedown", {
- clientX: x,
- clientY: y,
- button,
- detail: 1
- }));
+function clickOnItemsRow(win, itemsView, row) {
+ itemsView._treebox.scrollToRow(row);
+ let elem = win.document.querySelector(`#${itemsView.id}-row-${row}`);
+ elem.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }));
}
diff --git a/test/tests/bibliographyTest.js b/test/tests/bibliographyTest.js
index 8e7b805ee3..2f6f643487 100644
--- a/test/tests/bibliographyTest.js
+++ b/test/tests/bibliographyTest.js
@@ -3,8 +3,8 @@
describe("Create Bibliography Dialog", function () {
var win, zp;
- before(function* () {
- win = yield loadZoteroPane();
+ before(async function () {
+ win = await loadZoteroPane();
zp = win.ZoteroPane;
});
@@ -12,19 +12,19 @@ describe("Create Bibliography Dialog", function () {
win.close();
});
- it("should perform a search", function* () {
- yield Zotero.Styles.init();
- var item = yield createDataObject('item');
+ it("should perform a search", async function () {
+ await Zotero.Styles.init();
+ var item = await createDataObject('item');
var deferred = Zotero.Promise.defer();
var called = false;
waitForWindow("chrome://zotero/content/bibliography.xul", function (dialog) {
waitForWindow("chrome://zotero/content/preferences/preferences.xul", function (window) {
// Wait for pane switch
- Zotero.Promise.coroutine(function* () {
+ (async function () {
do {
Zotero.debug("Checking for pane");
- yield Zotero.Promise.delay(5);
+ await Zotero.Promise.delay(5);
}
while (window.document.documentElement.currentPane.id != 'zotero-prefpane-cite');
let pane = window.document.documentElement.currentPane;
@@ -37,8 +37,8 @@ describe("Create Bibliography Dialog", function () {
});
dialog.document.getElementById('manage-styles').click();
});
- win.Zotero_File_Interface.bibliographyFromItems();
- yield deferred.promise;
+ await win.Zotero_File_Interface.bibliographyFromItems();
+ await deferred.promise;
assert.ok(called);
});
diff --git a/test/tests/collectionTreeViewTest.js b/test/tests/collectionTreeTest.js
similarity index 92%
rename from test/tests/collectionTreeViewTest.js
rename to test/tests/collectionTreeTest.js
index ec7afe2d04..717db9ea17 100644
--- a/test/tests/collectionTreeViewTest.js
+++ b/test/tests/collectionTreeTest.js
@@ -1,6 +1,6 @@
"use strict";
-describe("Zotero.CollectionTreeView", function() {
+describe("Zotero.CollectionTree", function() {
var win, zp, cv, userLibraryID;
before(function* () {
@@ -51,8 +51,10 @@ describe("Zotero.CollectionTreeView", function() {
if (cv.isContainerOpen(group2Row)) {
yield cv.toggleOpenState(group2Row);
}
- // Don't wait for delayed save
+
cv._saveOpenStates();
+ // #_saveOpenStates is debounced
+ yield Zotero.Promise.delay(500);
group1Row = cv.getRowIndexByID(group1.treeViewID);
group2Row = cv.getRowIndexByID(group2.treeViewID);
@@ -79,30 +81,22 @@ describe("Zotero.CollectionTreeView", function() {
describe("collapse/expand", function () {
it("should close and open My Library repeatedly", function* () {
yield cv.selectLibrary(userLibraryID);
- var row = cv.selection.currentIndex;
+ var row = cv.selection.focused;
cv.collapseLibrary(userLibraryID);
- var nextRow = cv.getRow(row + 1);
- assert.equal(cv.selection.currentIndex, row);
- assert.ok(nextRow.isSeparator());
+ assert.equal(cv.selection.focused, row);
assert.isFalse(cv.isContainerOpen(row));
yield cv.expandLibrary(userLibraryID);
- nextRow = cv.getRow(row + 1);
- assert.equal(cv.selection.currentIndex, row);
- assert.ok(!nextRow.isSeparator());
+ assert.equal(cv.selection.focused, row);
assert.ok(cv.isContainerOpen(row));
cv.collapseLibrary(userLibraryID);
- nextRow = cv.getRow(row + 1);
- assert.equal(cv.selection.currentIndex, row);
- assert.ok(nextRow.isSeparator());
+ assert.equal(cv.selection.focused, row);
assert.isFalse(cv.isContainerOpen(row));
yield cv.expandLibrary(userLibraryID);
- nextRow = cv.getRow(row + 1);
- assert.equal(cv.selection.currentIndex, row);
- assert.ok(!nextRow.isSeparator());
+ assert.equal(cv.selection.focused, row);
assert.ok(cv.isContainerOpen(row));
})
})
@@ -112,7 +106,7 @@ describe("Zotero.CollectionTreeView", function() {
before(function* () {
yield cv.selectLibrary(userLibraryID);
- libraryRow = cv.selection.currentIndex;
+ libraryRow = cv.selection.focused;
});
beforeEach(function* () {
@@ -128,28 +122,31 @@ describe("Zotero.CollectionTreeView", function() {
it("should open a library and respect stored container state", function* () {
// Collapse B
yield cv.toggleOpenState(cv.getRowIndexByID(col2.treeViewID));
- yield cv._saveOpenStates();
+ cv._saveOpenStates();
+ // #_saveOpenStates is debounced
+ yield Zotero.Promise.delay(500);
// Close and reopen library
yield cv.toggleOpenState(libraryRow);
yield cv.expandLibrary(userLibraryID);
-
- assert.ok(cv.getRowIndexByID(col1.treeViewID))
- assert.ok(cv.getRowIndexByID(col2.treeViewID))
- assert.isFalse(cv.getRowIndexByID(col3.treeViewID))
+
+ assert.isTrue(cv.isContainerOpen(libraryRow));
+ assert.isTrue(cv.isContainerOpen(cv.getRowIndexByID(col1.treeViewID)));
+ assert.isFalse(cv.isContainerOpen(cv.getRowIndexByID(col2.treeViewID)));
});
it("should open a library and all subcollections in recursive mode", function* () {
yield cv.toggleOpenState(cv.getRowIndexByID(col2.treeViewID));
- yield cv._saveOpenStates();
+ cv._saveOpenStates();
+ // #_saveOpenStates is debounced
+ yield Zotero.Promise.delay(500);
// Close and reopen library
yield cv.toggleOpenState(libraryRow);
yield cv.expandLibrary(userLibraryID, true);
-
- assert.ok(cv.getRowIndexByID(col1.treeViewID))
- assert.ok(cv.getRowIndexByID(col2.treeViewID))
- assert.ok(cv.getRowIndexByID(col3.treeViewID))
+
+ assert.isTrue(cv.isContainerOpen(cv.getRowIndexByID(col1.treeViewID)));
+ assert.isTrue(cv.isContainerOpen(cv.getRowIndexByID(col2.treeViewID)));
});
it("should open a group and show top-level collections", function* () {
@@ -162,9 +159,11 @@ describe("Zotero.CollectionTreeView", function() {
var col5 = yield createDataObject('collection', { libraryID, parentID: col4.id });
// Close everything
- [col4, col1, group].forEach(o => cv._closeContainer(cv.getRowIndexByID(o.treeViewID)));
+ yield Zotero.Promise.all([col4, col1, group]
+ .map(o => cv.toggleOpenState(cv.getRowIndexByID(o.treeViewID), false)));
yield cv.expandLibrary(libraryID);
+
assert.isNumber(cv.getRowIndexByID(col1.treeViewID));
assert.isNumber(cv.getRowIndexByID(col2.treeViewID));
assert.isNumber(cv.getRowIndexByID(col3.treeViewID));
@@ -185,6 +184,7 @@ describe("Zotero.CollectionTreeView", function() {
assert.isFalse(cv.isContainerOpen(row));
yield cv.expandToCollection(collection2.id);
+ cv.forceUpdate();
// Make sure parent row position hasn't changed
assert.equal(cv.getRowIndexByID("C" + collection1.id), row);
@@ -196,7 +196,7 @@ describe("Zotero.CollectionTreeView", function() {
describe("#selectByID()", function () {
it("should select the trash", function* () {
yield cv.selectByID("T1");
- var row = cv.selection.currentIndex;
+ var row = cv.selection.focused;
var treeRow = cv.getRow(row);
assert.ok(treeRow.isTrash());
assert.equal(treeRow.ref.libraryID, userLibraryID);
@@ -206,7 +206,7 @@ describe("Zotero.CollectionTreeView", function() {
describe("#selectWait()", function () {
it("shouldn't hang if row is already selected", function* () {
var row = cv.getRowIndexByID("T" + userLibraryID);
- cv.selection.select(row);
+ yield cv.selectWait(row);
yield Zotero.Promise.delay(50);
yield cv.selectWait(row);
})
@@ -288,14 +288,14 @@ describe("Zotero.CollectionTreeView", function() {
yield cv.selectLibrary(group.libraryID);
yield waitForItemsLoad(win);
- assert.isFalse(cv.selectedTreeRow.editable);
+ assert.isFalse(zp.getCollectionTreeRow().editable);
var cmd = win.document.getElementById('cmd_zotero_newStandaloneNote');
assert.isTrue(cmd.getAttribute('disabled') == 'true');
group.editable = true;
yield group.saveTx();
- assert.isTrue(cv.selectedTreeRow.editable);
+ assert.isTrue(zp.getCollectionTreeRow().editable);
assert.isFalse(cmd.getAttribute('disabled') == 'true');
});
@@ -375,7 +375,7 @@ describe("Zotero.CollectionTreeView", function() {
assert.isAbove(aRow, 0);
assert.isAbove(bRow, 0);
// skipSelect is implied for multiple collections, so library should still be selected
- assert.equal(cv.selection.currentIndex, 0);
+ assert.equal(cv.selection.focused, 0);
});
@@ -432,7 +432,7 @@ describe("Zotero.CollectionTreeView", function() {
// since otherwise they'll interfere with the count
yield getGroup();
- var originalRowCount = cv.rowCount;
+ var originalRowCount = cv._rows.length;
var group = yield createGroup();
yield createDataObject('collection', { libraryID: group.libraryID });
@@ -442,7 +442,7 @@ describe("Zotero.CollectionTreeView", function() {
yield createDataObject('collection', { libraryID: group.libraryID });
// Group, collections, Duplicates, Unfiled, and trash
- assert.equal(cv.rowCount, originalRowCount + 9);
+ assert.equal(cv._rows.length, originalRowCount + 9);
// Select group
yield cv.selectLibrary(group.libraryID);
@@ -452,7 +452,7 @@ describe("Zotero.CollectionTreeView", function() {
try {
yield group.eraseTx();
- assert.equal(cv.rowCount, originalRowCount);
+ assert.equal(cv._rows.length, originalRowCount);
// Make sure the tree wasn't refreshed
sinon.assert.notCalled(spy);
}
@@ -482,7 +482,9 @@ describe("Zotero.CollectionTreeView", function() {
yield cv.selectLibrary(feed.libraryID);
waitForDialog();
var id = feed.treeViewID;
+ let promise = waitForCollectionTree(win);
yield win.ZoteroPane.deleteSelectedCollection();
+ yield promise;
assert.isFalse(cv.getRowIndexByID(id))
})
});
@@ -491,10 +493,11 @@ describe("Zotero.CollectionTreeView", function() {
it("should switch to library root if item isn't in collection", async function () {
var item = await createDataObject('item');
var collection = await createDataObject('collection');
+ Zotero.debug(zp.itemsView._rows);
await cv.selectItem(item.id);
await waitForItemsLoad(win);
- assert.equal(cv.selection.currentIndex, 0);
- assert.sameMembers(zp.itemsView.getSelectedItems(true), [item.id]);
+ assert.equal(cv.selection.focused, 0);
+ assert.sameMembers(zp.itemsView.getSelectedItems(), [item]);
});
});
@@ -505,12 +508,12 @@ describe("Zotero.CollectionTreeView", function() {
var item2 = await createDataObject('item');
await cv.selectItems([item1.id, item2.id]);
await waitForItemsLoad(win);
- assert.equal(cv.selection.currentIndex, 0);
+ assert.equal(cv.selection.focused, 0);
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item1.id, item2.id]);
});
});
- describe("#drop()", function () {
+ describe("#onDrop()", function () {
/**
* Simulate a drag and drop
*
@@ -521,7 +524,7 @@ describe("Zotero.CollectionTreeView", function() {
* value returned after the drag. Otherwise, an 'add' event will be waited for, and
* an object with 'ids' and 'extraData' will be returned.
*/
- var drop = Zotero.Promise.coroutine(function* (objectType, targetRow, ids, promise, action = 'copy') {
+ var onDrop = Zotero.Promise.coroutine(function* (objectType, targetRow, ids, promise, action = 'copy') {
if (typeof targetRow == 'string') {
var row = cv.getRowIndexByID(targetRow);
var orient = 0;
@@ -530,30 +533,33 @@ describe("Zotero.CollectionTreeView", function() {
var { row, orient } = targetRow;
}
- var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
- stub.returns(cv.getRow(row));
+ Zotero.DragDrop.currentDragSource = objectType == "item" && zp.itemsView.collectionTreeRow;
+
if (!promise) {
promise = waitForNotifierEvent("add", objectType);
}
- yield cv.drop(row, orient, {
- dropEffect: action,
- effectAllowed: action,
- mozSourceNode: win.document.getElementById(`zotero-${objectType}s-tree`).treeBoxObject.treeBody,
- types: {
- contains: function (type) {
- return type == `zotero/${objectType}`;
- }
- },
- getData: function (type) {
- if (type == `zotero/${objectType}`) {
- return ids.join(",");
+ yield cv.onDrop({
+ persist: () => 0,
+ target: {ownerDocument: {defaultView: win}},
+ dataTransfer: {
+ dropEffect: action,
+ effectAllowed: action,
+ types: {
+ contains: function (type) {
+ return type == `zotero/${objectType}`;
+ }
+ },
+ getData: function (type) {
+ if (type == `zotero/${objectType}`) {
+ return ids.join(",");
+ }
}
}
- });
+ }, row);
// Add observer to wait for add
var result = yield promise;
- stub.restore();
+ Zotero.DragDrop.currentDragSource = null;
return result;
});
@@ -561,12 +567,9 @@ describe("Zotero.CollectionTreeView", function() {
var canDrop = Zotero.Promise.coroutine(function* (type, targetRowID, ids) {
var row = cv.getRowIndexByID(targetRowID);
- var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
- stub.returns(cv.getRow(row));
var dt = {
dropEffect: 'copy',
effectAllowed: 'copy',
- mozSourceNode: win.document.getElementById(`zotero-${type}s-tree`),
types: {
contains: function (type) {
return type == `zotero/${type}`;
@@ -582,7 +585,6 @@ describe("Zotero.CollectionTreeView", function() {
if (canDrop) {
canDrop = yield cv.canDropCheckAsync(row, 0, dt);
}
- stub.restore();
return canDrop;
});
@@ -604,14 +606,14 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'collection-item', 'test');
- yield drop('item', 'C' + collection.id, [item.id], deferred.promise);
+ yield onDrop('item', 'C' + collection.id, [item.id], deferred.promise);
Zotero.Notifier.unregisterObserver(observerID);
yield cv.selectCollection(collection.id);
yield waitForItemsLoad(win);
- var itemsView = win.ZoteroPane.itemsView
+ var itemsView = win.ZoteroPane.itemsView;
assert.equal(itemsView.rowCount, 1);
var treeRow = itemsView.getRow(0);
assert.equal(treeRow.ref.id, item.id);
@@ -636,7 +638,9 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'collection-item', 'test');
- yield drop('item', 'C' + collection2.id, [item.id], deferred.promise, 'move');
+ let promise = zp.itemsView.waitForSelect();
+ yield onDrop('item', 'C' + collection2.id, [item.id], deferred.promise, 'move');
+ yield promise;
Zotero.Notifier.unregisterObserver(observerID);
@@ -683,7 +687,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'item', 'test');
- yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
+ yield onDrop('item', 'P' + libraryID, [item.id], deferred.promise);
Zotero.Notifier.unregisterObserver(observerID);
stub.restore();
@@ -721,7 +725,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'item', 'test');
- yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
+ yield onDrop('item', 'P' + libraryID, [item.id], deferred.promise);
Zotero.Notifier.unregisterObserver(observerID);
stub.restore();
@@ -760,7 +764,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'item', 'test');
- yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
+ yield onDrop('item', 'P' + libraryID, [item.id], deferred.promise);
Zotero.Notifier.unregisterObserver(observerID);
stub.restore();
@@ -799,7 +803,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'item', 'test');
- yield drop('item', 'P' + libraryID, [item.id], deferred.promise);
+ yield onDrop('item', 'P' + libraryID, [item.id], deferred.promise);
Zotero.Notifier.unregisterObserver(observerID);
stub.restore();
@@ -821,7 +825,7 @@ describe("Zotero.CollectionTreeView", function() {
parentItemID: item.id
});
- var ids = (yield drop('item', 'L' + group.libraryID, [item.id])).ids;
+ var ids = (yield onDrop('item', 'L' + group.libraryID, [item.id])).ids;
yield cv.selectLibrary(group.libraryID);
yield waitForItemsLoad(win);
@@ -864,7 +868,7 @@ describe("Zotero.CollectionTreeView", function() {
attachment.setField('title', attachmentTitle);
yield attachment.saveTx();
- yield drop('item', 'L' + group.libraryID, [item.id]);
+ yield onDrop('item', 'L' + group.libraryID, [item.id]);
assert.isFalse(yield canDrop('item', 'L' + group.libraryID, [item.id]));
})
@@ -878,7 +882,7 @@ describe("Zotero.CollectionTreeView", function() {
await cv.selectLibrary(group1.libraryID);
await waitForItemsLoad(win);
- await drop('item', 'L' + group2.libraryID, [item.id]);
+ await onDrop('item', 'L' + group2.libraryID, [item.id]);
assert.isFalse(await item.getLinkedItem(group2.libraryID));
// New collection should link back to original
@@ -893,14 +897,14 @@ describe("Zotero.CollectionTreeView", function() {
var collection = await createDataObject('collection', { libraryID: group.libraryID });
var item = await createDataObject('item', false, { skipSelect: true });
- await drop('item', 'L' + group.libraryID, [item.id]);
+ await onDrop('item', 'L' + group.libraryID, [item.id]);
var droppedItem = await item.getLinkedItem(group.libraryID);
droppedItem.setCollections([collection.id]);
droppedItem.deleted = true;
await droppedItem.saveTx();
- await drop('item', 'L' + group.libraryID, [item.id]);
+ await onDrop('item', 'L' + group.libraryID, [item.id]);
var linkedItem = await item.getLinkedItem(group.libraryID);
assert.notEqual(linkedItem, droppedItem);
@@ -934,7 +938,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'collection', 'test');
- yield drop(
+ yield onDrop(
'collection',
{
row: 0,
@@ -990,7 +994,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'collection', 'test');
- yield drop(
+ yield onDrop(
'collection',
{
row: colIndexE,
@@ -1054,7 +1058,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'collection', 'test');
- yield drop(
+ yield onDrop(
'collection',
{
row: colIndexD,
@@ -1107,7 +1111,7 @@ describe("Zotero.CollectionTreeView", function() {
}
}, 'collection', 'test');
- await drop(
+ await onDrop(
'collection',
'L' + group.libraryID,
[collectionA.id],
@@ -1144,7 +1148,7 @@ describe("Zotero.CollectionTreeView", function() {
await cv.selectCollection(collection.id);
await waitForItemsLoad(win);
- await drop('collection', 'L' + group2.libraryID, [collection.id]);
+ await onDrop('collection', 'L' + group2.libraryID, [collection.id]);
assert.isFalse(await collection.getLinkedCollection(group2.libraryID));
// New collection should link back to original
@@ -1174,7 +1178,7 @@ describe("Zotero.CollectionTreeView", function() {
var deferred = Zotero.Promise.defer();
var itemIds;
- var ids = (yield drop('item', 'C' + collection.id, [feedItem.id])).ids;
+ var ids = (yield onDrop('item', 'C' + collection.id, [feedItem.id])).ids;
// Check that the translated item was the one that was created after drag
var item;
diff --git a/test/tests/duplicatesTest.js b/test/tests/duplicatesTest.js
index a52995b811..86cd071dce 100644
--- a/test/tests/duplicatesTest.js
+++ b/test/tests/duplicatesTest.js
@@ -38,8 +38,10 @@ describe("Duplicate Items", function () {
var iv = zp.itemsView;
var row = iv.getRowIndexByID(item1.id);
assert.isNumber(row);
- clickOnItemsRow(iv, row);
+ var promise = iv.waitForSelect();
+ clickOnItemsRow(win, iv, row);
assert.equal(iv.selection.count, 2);
+ yield promise;
// Click merge button
var button = win.document.getElementById('zotero-duplicates-merge-button');
@@ -75,7 +77,9 @@ describe("Duplicate Items", function () {
// Select the first item, which should select both
var iv = zp.itemsView;
var row = iv.getRowIndexByID(item1.id);
- clickOnItemsRow(iv, row);
+ var promise = iv.waitForSelect();
+ clickOnItemsRow(win, iv, row);
+ yield promise;
// Click merge button
var button = win.document.getElementById('zotero-duplicates-merge-button');
diff --git a/test/tests/itemTreeViewTest.js b/test/tests/itemTreeTest.js
similarity index 84%
rename from test/tests/itemTreeViewTest.js
rename to test/tests/itemTreeTest.js
index 9af7cfd102..817eec2d37 100644
--- a/test/tests/itemTreeViewTest.js
+++ b/test/tests/itemTreeTest.js
@@ -1,36 +1,35 @@
"use strict";
-describe("Zotero.ItemTreeView", function() {
+describe("Zotero.ItemTree", function() {
var win, zp, cv, itemsView;
- var userLibraryID;
var existingItemID;
var existingItemID2;
// Load Zotero pane and select library
- before(function* () {
- win = yield loadZoteroPane();
+ before(async function () {
+ win = await loadZoteroPane();
zp = win.ZoteroPane;
cv = zp.collectionsView;
- userLibraryID = Zotero.Libraries.userLibraryID;
- var item1 = yield createDataObject('item', { setTitle: true });
+ var item1 = await createDataObject('item', { setTitle: true });
existingItemID = item1.id;
- var item2 = yield createDataObject('item');
+ var item2 = await createDataObject('item');
existingItemID2 = item2.id;
});
- beforeEach(function* () {
- yield selectLibrary(win);
+ beforeEach(async function () {
+ await selectLibrary(win);
itemsView = zp.itemsView;
- })
+ itemsView._columnsId = null;
+ });
after(function () {
win.close();
});
- it("shouldn't show items in trash in library root", function* () {
- var item = yield createDataObject('item', { title: "foo" });
+ it("shouldn't show items in trash in library root", async function () {
+ var item = await createDataObject('item', { title: "foo" });
var itemID = item.id;
item.deleted = true;
- yield item.saveTx();
+ await item.saveTx();
assert.isFalse(itemsView.getRowIndexByID(itemID));
})
@@ -83,7 +82,7 @@ describe("Zotero.ItemTreeView", function() {
var toSelect = [note1.id, note2.id, note3.id];
itemsView.collapseAllRows();
-
+
var numSelected = await itemsView.selectItems(toSelect);
assert.equal(numSelected, 3);
var selected = itemsView.getSelectedItems(true);
@@ -106,9 +105,9 @@ describe("Zotero.ItemTreeView", function() {
var str = Zotero.Utilities.randomString();
var item = yield createDataObject('item', { title: str });
var row = itemsView.getRowIndexByID(item.id);
- assert.equal(itemsView.getCellText(row, { id: 'zotero-items-column-title' }), str);
+ assert.equal(itemsView.getCellText(row, 'title'), str);
yield modifyDataObject(item);
- assert.notEqual(itemsView.getCellText(row, { id: 'zotero-items-column-title' }), str);
+ assert.notEqual(itemsView.getCellText(row, 'title'), str);
})
})
@@ -121,15 +120,17 @@ describe("Zotero.ItemTreeView", function() {
win.ZoteroPane.itemSelected.restore();
})
- it("should select a new item", function* () {
+ it("should select a new item", async function () {
+ let selectPromise = itemsView.waitForSelect();
itemsView.selection.clearSelection();
assert.lengthOf(itemsView.getSelectedItems(), 0);
-
+
+ await selectPromise;
assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
// Create item
var item = new Zotero.Item('book');
- var id = yield item.saveTx();
+ var id = await item.saveTx();
// New item should be selected
var selected = itemsView.getSelectedItems();
@@ -138,7 +139,7 @@ describe("Zotero.ItemTreeView", function() {
// Item should have been selected once
assert.equal(win.ZoteroPane.itemSelected.callCount, 2);
- assert.ok(win.ZoteroPane.itemSelected.returnValues[1].value());
+ await assert.eventually.ok(win.ZoteroPane.itemSelected.returnValues[1]);
});
it("shouldn't select a new item if skipNotifier is passed", function* () {
@@ -166,9 +167,9 @@ describe("Zotero.ItemTreeView", function() {
assert.equal(selected[0], existingItemID);
});
- it("shouldn't select a new item if skipSelect is passed", function* () {
+ it("shouldn't select a new item if skipSelect is passed", async function () {
// Select existing item
- yield itemsView.selectItem(existingItemID);
+ await itemsView.selectItem(existingItemID);
var selected = itemsView.getSelectedItems(true);
assert.lengthOf(selected, 1);
assert.equal(selected[0], existingItemID);
@@ -178,14 +179,12 @@ describe("Zotero.ItemTreeView", function() {
// Create item with skipSelect flag
var item = new Zotero.Item('book');
- var id = yield item.saveTx({
+ var id = await item.saveTx({
skipSelect: true
});
- // itemSelected should have been called once (from 'selectEventsSuppressed = false'
- // in notify()) as a no-op
- assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
- assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
+ // No select events should have occurred
+ assert.equal(win.ZoteroPane.itemSelected.callCount, 0);
// Existing item should still be selected
selected = itemsView.getSelectedItems(true);
@@ -244,56 +243,6 @@ describe("Zotero.ItemTreeView", function() {
yield itemsView._refreshPromise;
});
- it.skip("shouldn't clear quicksearch in Unfiled Items when adding selected item to collection", async function () {
- var spy = sinon.spy(win.ZoteroPane, 'search');
-
- var collection = await createDataObject('collection');
- var title1 = Zotero.Utilities.randomString();
- var title2 = Zotero.Utilities.randomString();
- var item1 = await createDataObject('item', { title: title1 });
- var item2 = await createDataObject('item', { title: title1 });
- var item3 = await createDataObject('item', { title: title2 });
-
- await zp.setVirtual(userLibraryID, 'unfiled', true, true);
- itemsView = zp.itemsView;
- assert.equal(cv.selectedTreeRow.id, 'U' + userLibraryID);
-
- var searchString = title1;
- var quicksearch = win.document.getElementById('zotero-tb-search');
- quicksearch.value = searchString;
- quicksearch.doCommand();
- while (!spy.called) {
- Zotero.debug("Waiting for search");
- await Zotero.Promise.delay(50);
- }
- await spy.returnValues[0];
- spy.resetHistory();
-
- assert.equal(itemsView.rowCount, 2);
-
- await itemsView.selectItem(item1.id);
-
- // Move item1 to collection
- item1.setCollections([collection.id]);
- await item1.saveTx({
- skipSelect: true
- });
-
- assert.equal(itemsView.rowCount, 1);
- assert.equal(quicksearch.value, searchString);
-
- // Clear search
- quicksearch.value = "";
- quicksearch.doCommand();
- while (!spy.called) {
- Zotero.debug("Waiting for search");
- await Zotero.Promise.delay(50);
- }
- await spy.returnValues[0];
-
- spy.restore();
- });
-
it("shouldn't change selection outside of trash if new trashed item is created with skipSelect", function* () {
yield selectLibrary(win);
yield waitForItemsLoad(win);
@@ -325,10 +274,8 @@ describe("Zotero.ItemTreeView", function() {
item.setField('title', 'no select on modify');
yield item.saveTx();
- // itemSelected should have been called once (from 'selectEventsSuppressed = false'
- // in notify()) as a no-op
- assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
- assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
+ // No select events should have occurred
+ assert.equal(win.ZoteroPane.itemSelected.callCount, 0);
// Modified item should not be selected
assert.lengthOf(itemsView.getSelectedItems(), 0);
@@ -387,14 +334,14 @@ describe("Zotero.ItemTreeView", function() {
}.bind(this));
// Selection should stay on third row
- assert.equal(itemsView.selection.currentIndex, 2);
+ assert.equal(itemsView.selection.focused, 2);
// Delete item
var treeRow = itemsView.getRow(2);
yield treeRow.ref.eraseTx();
// Selection should stay on third row
- assert.equal(itemsView.selection.currentIndex, 2);
+ assert.equal(itemsView.selection.focused, 2);
yield Zotero.Items.erase(items.map(item => item.id));
});
@@ -581,33 +528,31 @@ describe("Zotero.ItemTreeView", function() {
assert.equal(zp.itemsView.getRowIndexByID(item.id), 0);
});
- it("should re-sort search results when an item is modified", function* () {
- var search = yield createDataObject('search');
+ it("should re-sort search results when an item is modified", async function () {
+ var search = await createDataObject('search');
itemsView = zp.itemsView;
var title = search.getConditions()[0].value;
- var item1 = yield createDataObject('item', { title: title + " 1" });
- var item2 = yield createDataObject('item', { title: title + " 3" });
- var item3 = yield createDataObject('item', { title: title + " 5" });
- var item4 = yield createDataObject('item', { title: title + " 7" });
-
- var col = itemsView._treebox.columns.getNamedColumn('zotero-items-column-title');
- col.element.click();
- if (col.element.getAttribute('sortDirection') == 'ascending') {
- col.element.click();
- }
+ var item1 = await createDataObject('item', { title: title + " 1" });
+ var item2 = await createDataObject('item', { title: title + " 3" });
+ var item3 = await createDataObject('item', { title: title + " 5" });
+ var item4 = await createDataObject('item', { title: title + " 7" });
+
+ const colIndex = itemsView.tree._getColumns().findIndex(column => column.dataKey == 'title');
+ await itemsView.tree._columns.toggleSort(colIndex);
// Check initial sort order
- assert.equal(itemsView.getRow(0).ref.getField('title'), title + " 7");
- assert.equal(itemsView.getRow(3).ref.getField('title'), title + " 1");
+ assert.equal(itemsView.getRow(0).ref.getField('title'), title + " 1");
+ assert.equal(itemsView.getRow(3).ref.getField('title'), title + " 7");
// Set first row to title that should be sorted in the middle
- itemsView.getRow(0).ref.setField('title', title + " 4");
- yield itemsView.getRow(0).ref.saveTx();
+ itemsView.getRow(3).ref.setField('title', title + " 4");
+ await itemsView.getRow(3).ref.saveTx();
- assert.equal(itemsView.getRow(0).ref.getField('title'), title + " 5");
- assert.equal(itemsView.getRow(1).ref.getField('title'), title + " 4");
- assert.equal(itemsView.getRow(3).ref.getField('title'), title + " 1");
+ assert.equal(itemsView.getRow(0).ref.getField('title'), title + " 1");
+ assert.equal(itemsView.getRow(1).ref.getField('title'), title + " 3");
+ assert.equal(itemsView.getRow(2).ref.getField('title'), title + " 4");
+ assert.equal(itemsView.getRow(3).ref.getField('title'), title + " 5");
});
it("should update search results when search conditions are changed", function* () {
@@ -643,16 +588,16 @@ describe("Zotero.ItemTreeView", function() {
assert.equal(zp.itemsView.rowCount, 1);
});
- it("should remove items from Unfiled Items when added to a collection", function* () {
+ it("should remove items from Unfiled Items when added to a collection", async function () {
var userLibraryID = Zotero.Libraries.userLibraryID;
- var collection = yield createDataObject('collection');
- var item = yield createDataObject('item', { title: "Unfiled Item" });
- yield zp.setVirtual(userLibraryID, 'unfiled', true, true);
- assert.equal(cv.selectedTreeRow.id, 'U' + userLibraryID);
- yield waitForItemsLoad(win);
+ var collection = await createDataObject('collection');
+ var item = await createDataObject('item', { title: "Unfiled Item" });
+ await zp.setVirtual(userLibraryID, 'unfiled', true, true);
+ assert.equal(zp.getCollectionTreeRow().id, 'U' + userLibraryID);
+ await waitForItemsLoad(win);
assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
- yield Zotero.DB.executeTransaction(function* () {
- yield collection.addItem(item.id);
+ await Zotero.DB.executeTransaction(async function () {
+ await collection.addItem(item.id);
});
assert.isFalse(zp.itemsView.getRowIndexByID(item.id));
});
@@ -675,38 +620,37 @@ describe("Zotero.ItemTreeView", function() {
});
describe("My Publications", function () {
- before(function* () {
+ before(async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var s = new Zotero.Search;
s.libraryID = libraryID;
s.addCondition('publications', 'true');
- var ids = yield s.search();
+ var ids = await s.search();
- yield Zotero.Items.erase(ids);
+ await Zotero.Items.erase(ids);
- yield zp.collectionsView.selectByID("P" + libraryID);
- yield waitForItemsLoad(win);
+ await zp.collectionsView.selectByID("P" + libraryID);
+ await waitForItemsLoad(win);
// Make sure we're showing the intro text
- var deck = win.document.getElementById('zotero-items-pane-content');
- assert.equal(deck.selectedIndex, 1);
+ var messageElem = win.document.querySelector('.items-tree-message');
+ assert.notEqual(messageElem.style.display, 'none');
});
- it("should replace My Publications intro text with items list on item add", function* () {
- var item = yield createDataObject('item');
+ it("should replace My Publications intro text with items list on item add", async function () {
+ var item = await createDataObject('item');
- yield zp.collectionsView.selectByID("P" + item.libraryID);
- yield waitForItemsLoad(win);
- var iv = zp.itemsView;
+ await zp.collectionsView.selectByID("P" + item.libraryID);
+ await waitForItemsLoad(win);
item.inPublications = true;
- yield item.saveTx();
+ await item.saveTx();
+
+ var messageElem = win.document.querySelector('.items-tree-message');
+ assert.equal(messageElem.style.display, 'none');
- var deck = win.document.getElementById('zotero-items-pane-content');
- assert.equal(deck.selectedIndex, 0);
-
- assert.isNumber(iv.getRowIndexByID(item.id));
+ assert.isNumber(itemsView.getRowIndexByID(item.id));
});
it("should add new item to My Publications items list", function* () {
@@ -716,16 +660,15 @@ describe("Zotero.ItemTreeView", function() {
yield zp.collectionsView.selectByID("P" + item1.libraryID);
yield waitForItemsLoad(win);
- var iv = zp.itemsView;
-
- var deck = win.document.getElementById('zotero-items-pane-content');
- assert.equal(deck.selectedIndex, 0);
+
+ var messageElem = win.document.querySelector('.items-tree-message');
+ assert.equal(messageElem.style.display, 'none');
var item2 = createUnsavedDataObject('item');
item2.inPublications = true;
yield item2.saveTx();
- assert.isNumber(iv.getRowIndexByID(item2.id));
+ assert.isNumber(itemsView.getRowIndexByID(item2.id));
});
it("should add modified item to My Publications items list", function* () {
@@ -736,17 +679,16 @@ describe("Zotero.ItemTreeView", function() {
yield zp.collectionsView.selectByID("P" + item1.libraryID);
yield waitForItemsLoad(win);
- var iv = zp.itemsView;
+
+ var messageElem = win.document.querySelector('.items-tree-message');
+ assert.equal(messageElem.style.display, 'none');
- var deck = win.document.getElementById('zotero-items-pane-content');
- assert.equal(deck.selectedIndex, 0);
-
- assert.isFalse(iv.getRowIndexByID(item2.id));
+ assert.isFalse(itemsView.getRowIndexByID(item2.id));
item2.inPublications = true;
yield item2.saveTx();
- assert.isNumber(iv.getRowIndexByID(item2.id));
+ assert.isNumber(itemsView.getRowIndexByID(item2.id));
});
it("should show Show/Hide button for imported file attachment", function* () {
@@ -755,9 +697,9 @@ describe("Zotero.ItemTreeView", function() {
yield zp.collectionsView.selectByID("P" + item.libraryID);
yield waitForItemsLoad(win);
- var iv = zp.itemsView;
- yield iv.selectItem(attachment.id);
+ yield itemsView.selectItem(attachment.id);
+ yield Zotero.Promise.delay();
var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
assert.isFalse(box.hidden);
@@ -772,9 +714,8 @@ describe("Zotero.ItemTreeView", function() {
yield zp.collectionsView.selectByID("P" + item.libraryID);
yield waitForItemsLoad(win);
- var iv = zp.itemsView;
- yield iv.selectItem(attachment.id);
+ yield itemsView.selectItem(attachment.id);
var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
assert.isTrue(box.hidden);
@@ -783,7 +724,7 @@ describe("Zotero.ItemTreeView", function() {
})
- describe("#drop()", function () {
+ describe("#onDrop()", function () {
var httpd;
var port = 16213;
var baseURL = `http://localhost:${port}/`;
@@ -791,6 +732,11 @@ describe("Zotero.ItemTreeView", function() {
var pdfURL = baseURL + pdfFilename;
var pdfPath;
+ function drop(index, orient, dataTransfer) {
+ Zotero.DragDrop.currentOrientation = orient;
+ return itemsView.onDrop({ dataTransfer: dataTransfer }, index);
+ }
+
// Serve a PDF to test URL dragging
before(function () {
Components.utils.import("resource://zotero-unit/httpd.js");
@@ -826,12 +772,11 @@ describe("Zotero.ItemTreeView", function() {
var item2 = yield createDataObject('item', { title: "B", collections: [collection.id] });
var item3 = yield createDataObject('item', { itemType: 'note', parentID: item1.id });
- let view = zp.itemsView;
- yield view.selectItem(item3.id);
+ yield itemsView.selectItem(item3.id);
- var promise = view.waitForSelect();
+ var promise = itemsView.waitForSelect();
- view.drop(view.getRowIndexByID(item2.id), 0, {
+ drop(itemsView.getRowIndexByID(item2.id), 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -845,17 +790,17 @@ describe("Zotero.ItemTreeView", function() {
}
},
mozItemCount: 1
- })
+ });
yield promise;
// Old parent should be empty
- assert.isFalse(view.isContainerOpen(view.getRowIndexByID(item1.id)));
- assert.isTrue(view.isContainerEmpty(view.getRowIndexByID(item1.id)));
+ assert.isFalse(itemsView.isContainerOpen(itemsView.getRowIndexByID(item1.id)));
+ assert.isTrue(itemsView.isContainerEmpty(itemsView.getRowIndexByID(item1.id)));
// New parent should be open
- assert.isTrue(view.isContainerOpen(view.getRowIndexByID(item2.id)));
- assert.isFalse(view.isContainerEmpty(view.getRowIndexByID(item2.id)));
+ assert.isTrue(itemsView.isContainerOpen(itemsView.getRowIndexByID(item2.id)));
+ assert.isFalse(itemsView.isContainerEmpty(itemsView.getRowIndexByID(item2.id)));
});
it("should move a child item from last item in list to another", function* () {
@@ -865,12 +810,11 @@ describe("Zotero.ItemTreeView", function() {
var item2 = yield createDataObject('item', { title: "B", collections: [collection.id] });
var item3 = yield createDataObject('item', { itemType: 'note', parentID: item2.id });
- let view = zp.itemsView;
- yield view.selectItem(item3.id);
+ yield itemsView.selectItem(item3.id);
- var promise = view.waitForSelect();
+ var promise = itemsView.waitForSelect();
- view.drop(view.getRowIndexByID(item1.id), 0, {
+ drop(itemsView.getRowIndexByID(item1.id), 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -884,17 +828,17 @@ describe("Zotero.ItemTreeView", function() {
}
},
mozItemCount: 1
- })
+ });
yield promise;
// Old parent should be empty
- assert.isFalse(view.isContainerOpen(view.getRowIndexByID(item2.id)));
- assert.isTrue(view.isContainerEmpty(view.getRowIndexByID(item2.id)));
+ assert.isFalse(itemsView.isContainerOpen(itemsView.getRowIndexByID(item2.id)));
+ assert.isTrue(itemsView.isContainerEmpty(itemsView.getRowIndexByID(item2.id)));
// New parent should be open
- assert.isTrue(view.isContainerOpen(view.getRowIndexByID(item1.id)));
- assert.isFalse(view.isContainerEmpty(view.getRowIndexByID(item1.id)));
+ assert.isTrue(itemsView.isContainerOpen(itemsView.getRowIndexByID(item1.id)));
+ assert.isFalse(itemsView.isContainerEmpty(itemsView.getRowIndexByID(item1.id)));
});
it("should create a stored top-level attachment when a file is dragged", function* () {
@@ -903,7 +847,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = itemsView.waitForSelect();
- itemsView.drop(0, -1, {
+ drop(0, -1, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -920,6 +864,8 @@ describe("Zotero.ItemTreeView", function() {
})
yield promise;
+ // Attachment add triggers multiple notifications and multiple select events
+ yield itemsView.waitForSelect();
var items = itemsView.getSelectedItems();
var path = yield items[0].getFilePathAsync();
assert.equal(
@@ -931,7 +877,7 @@ describe("Zotero.ItemTreeView", function() {
it("should create a stored top-level attachment when a URL is dragged", function* () {
var promise = itemsView.waitForSelect();
- itemsView.drop(0, -1, {
+ drop(0, -1, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -946,7 +892,7 @@ describe("Zotero.ItemTreeView", function() {
},
mozItemCount: 1,
})
-
+
yield promise;
var item = itemsView.getSelectedItems()[0];
assert.equal(item.getField('url'), pdfURL);
@@ -963,7 +909,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -1017,7 +963,7 @@ describe("Zotero.ItemTreeView", function() {
}
);
- itemsView.drop(0, -1, {
+ drop(0, -1, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -1074,7 +1020,7 @@ describe("Zotero.ItemTreeView", function() {
}
);
- itemsView.drop(0, -1, {
+ drop(0, -1, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -1118,7 +1064,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -1165,7 +1111,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'link',
effectAllowed: 'link',
types: {
@@ -1212,7 +1158,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'link',
effectAllowed: 'link',
types: {
@@ -1251,7 +1197,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -1293,7 +1239,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
@@ -1330,7 +1276,7 @@ describe("Zotero.ItemTreeView", function() {
var promise = waitForItemEvent('add');
- itemsView.drop(parentRow, 0, {
+ drop(parentRow, 0, {
dropEffect: 'copy',
effectAllowed: 'copy',
types: {
diff --git a/test/tests/libraryTest.js b/test/tests/libraryTest.js
index 98d56f91e6..2f3494c797 100644
--- a/test/tests/libraryTest.js
+++ b/test/tests/libraryTest.js
@@ -235,12 +235,11 @@ describe("Zotero.Library", function() {
let library = new Zotero.Library();
yield assert.isRejected(library.eraseTx());
});
- it("should throw when accessing erased library methods, except for #libraryID", function* () {
+ it("should throw when accessing erased library methods, except for #libraryID and #name", function* () {
let library = yield createGroup();
yield library.eraseTx();
assert.doesNotThrow(() => library.libraryID);
- assert.throws(() => library.name, /^Group \(\d+\) has been disabled$/);
assert.throws(() => library.editable = false, /^Group \(\d+\) has been disabled$/);
});
it("should clear child items from caches and DB", function* () {
diff --git a/test/tests/libraryTreeViewTest.js b/test/tests/libraryTreeTest.js
similarity index 96%
rename from test/tests/libraryTreeViewTest.js
rename to test/tests/libraryTreeTest.js
index 91d6b4c603..5c3272478c 100644
--- a/test/tests/libraryTreeViewTest.js
+++ b/test/tests/libraryTreeTest.js
@@ -1,6 +1,6 @@
"use strict";
-describe("Zotero.LibraryTreeView", function() {
+describe("Zotero.LibraryTree", function() {
var win, zp, cv, itemsView;
// Load Zotero pane and select library
diff --git a/test/tests/relatedboxTest.js b/test/tests/relatedboxTest.js
index 7bf1436992..8f89d03d0c 100644
--- a/test/tests/relatedboxTest.js
+++ b/test/tests/relatedboxTest.js
@@ -31,18 +31,16 @@ describe("Related Box", function () {
// wrappedJSObject isn't working on zotero-collections-tree for some reason, so
// just wait for the items tree to be created and select it directly
do {
- var view = selectWin.document.getElementById('zotero-items-tree').view.wrappedJSObject;
+ var selectItemsView = selectWin.itemsView;
+ var selectCollectionsView = selectWin.collectionsView;
yield Zotero.Promise.delay(50);
}
- while (!view);
- yield view.waitForLoad();
+ while (!selectItemsView || !selectCollectionsView);
+ yield selectCollectionsView.waitForLoad();
+ yield selectItemsView.waitForLoad();
// Select the other item
- for (let i = 0; i < view.rowCount; i++) {
- if (view.getRow(i).ref.id == item1.id) {
- view.selection.select(i);
- }
- }
+ yield selectItemsView.selectItem(item1.id);
selectWin.document.documentElement.acceptDialog();
// Wait for relations list to populate
diff --git a/test/tests/server_connectorTest.js b/test/tests/server_connectorTest.js
index 2cd9fa0bb9..c962cb2b2d 100644
--- a/test/tests/server_connectorTest.js
+++ b/test/tests/server_connectorTest.js
@@ -1678,6 +1678,7 @@ describe("Connector Server", function () {
});
it("should move item saved via /saveItems to another library", async function () {
+ let addItemsSpy = sinon.spy(Zotero.Server.Connector.SaveSession.prototype, 'addItems');
var group = await createGroup({ editable: true, filesEditable: false });
await selectLibrary(win);
await waitForItemsLoad(win);
@@ -1727,6 +1728,16 @@ describe("Connector Server", function () {
// Attachment
await waitForItemEvent('add');
+ // There's an additional addItems call in saveItems that is not async returned and runs
+ // after attachment notifier add event callbacks are run, so we have to do some
+ // hacky waiting here, otherwise we get some crazy race-conditions due to
+ // collection changing being debounced
+ let callCount = addItemsSpy.callCount;
+ while (addItemsSpy.callCount <= callCount) {
+ await Zotero.Promise.delay(50);
+ }
+ await addItemsSpy.lastCall.returnValue;
+
var req = await reqPromise;
assert.equal(req.status, 201);
@@ -1779,6 +1790,8 @@ describe("Connector Server", function () {
assert.isFalse(Zotero.Items.exists(item2.id));
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
assert.equal(item3.numAttachments(), 1);
+
+ addItemsSpy.restore();
});
it("should move item saved via /saveSnapshot to another library", async function () {
diff --git a/test/tests/tagSelectorTest.js b/test/tests/tagSelectorTest.js
index d78052d01c..2b3c38bc94 100644
--- a/test/tests/tagSelectorTest.js
+++ b/test/tests/tagSelectorTest.js
@@ -66,7 +66,6 @@ describe("Tag Selector", function () {
await Zotero.Tags.setColor(libraryID, "C", '#CCCCCC', 3);
var item = createUnsavedDataObject('item', { collections: [collection.id] });
- var item = createUnsavedDataObject('item');
await item.setTags(["A", "B"]);
var promise = waitForTagSelector(win);
await item.saveTx();
@@ -203,7 +202,7 @@ describe("Tag Selector", function () {
it("should add a tag when added to an item in the library root", async function () {
var promise;
- if (collectionsView.selection.currentIndex != 0) {
+ if (collectionsView.selection.pivot != 0) {
promise = waitForTagSelector(win);
await collectionsView.selectLibrary();
await promise;
diff --git a/test/tests/zoteroPaneTest.js b/test/tests/zoteroPaneTest.js
index d9ebe87bbe..4e45631591 100644
--- a/test/tests/zoteroPaneTest.js
+++ b/test/tests/zoteroPaneTest.js
@@ -455,7 +455,7 @@ describe("ZoteroPane", function() {
var selected = iv.selectItem(item.id);
assert.ok(selected);
- var tree = doc.getElementById('zotero-items-tree');
+ var tree = doc.getElementById(iv.id);
tree.focus();
yield Zotero.Promise.delay(1);
@@ -485,7 +485,7 @@ describe("ZoteroPane", function() {
var selected = iv.selectItem(item.id);
assert.ok(selected);
- var tree = doc.getElementById('zotero-items-tree');
+ var tree = doc.getElementById(iv.id);
tree.focus();
yield Zotero.Promise.delay(1);
@@ -566,27 +566,29 @@ describe("ZoteroPane", function() {
assert.isFalse(cv.getRowIndexByID(id));
yield zp.setVirtual(userLibraryID, 'duplicates', true, true);
// Duplicate Items should be selected
- assert.equal(cv.selectedTreeRow.id, id);
+ assert.equal(zp.getCollectionTreeRow().id, id);
// Should be missing from pref
assert.isUndefined(JSON.parse(Zotero.Prefs.get('duplicateLibraries'))[userLibraryID])
// Clicking should select both items
var row = cv.getRowIndexByID(id);
assert.ok(row);
- assert.equal(cv.selection.currentIndex, row);
+ assert.equal(cv.selection.pivot, row);
yield waitForItemsLoad(win);
var iv = zp.itemsView;
row = iv.getRowIndexByID(item1.id);
assert.isNumber(row);
- clickOnItemsRow(iv, row);
+ var promise = iv.waitForSelect();
+ clickOnItemsRow(win, iv, row);
assert.equal(iv.selection.count, 2);
+ yield promise;
// Show Unfiled Items
id = "U" + userLibraryID;
assert.isFalse(cv.getRowIndexByID(id));
yield zp.setVirtual(userLibraryID, 'unfiled', true, true);
// Unfiled Items should be selected
- assert.equal(cv.selectedTreeRow.id, id);
+ assert.equal(zp.getCollectionTreeRow().id, id);
// Should be missing from pref
assert.isUndefined(JSON.parse(Zotero.Prefs.get('unfiledLibraries'))[userLibraryID])
});
@@ -608,7 +610,7 @@ describe("ZoteroPane", function() {
// Library should have been expanded and Duplicate Items selected
assert.ok(cv.getRowIndexByID(id));
- assert.equal(cv.selectedTreeRow.id, id);
+ assert.equal(zp.getCollectionTreeRow().id, id);
});
it("should hide a virtual collection in My Library", function* () {
@@ -634,7 +636,7 @@ describe("ZoteroPane", function() {
var group = yield createGroup();
var groupRow = cv.getRowIndexByID(group.treeViewID);
- var rowCount = cv.rowCount;
+ var rowCount = cv._rows.length;
// Make sure group is open
if (!cv.isContainerOpen(groupRow)) {
@@ -658,7 +660,7 @@ describe("ZoteroPane", function() {
// Group should remain open
assert.isTrue(cv.isContainerOpen(groupRow));
// Row count should be 1 less
- assert.equal(cv.rowCount, --rowCount);
+ assert.equal(cv._rows.length, --rowCount);
// Hide Unfiled Items
id = "U" + group.libraryID;
@@ -674,7 +676,7 @@ describe("ZoteroPane", function() {
// Group should remain open
assert.isTrue(cv.isContainerOpen(groupRow));
// Row count should be 1 less
- assert.equal(cv.rowCount, --rowCount);
+ assert.equal(cv._rows.length, --rowCount);
});
});
@@ -712,47 +714,4 @@ describe("ZoteroPane", function() {
assert.lengthOf(Object.keys(conditions), 3);
});
});
-
- describe("#onCollectionSelected()", function() {
- var cv;
-
- beforeEach(function* () {
- cv = zp.collectionsView;
- yield cv.selectLibrary(Zotero.Libraries.userLibraryID);
- Zotero.Prefs.clear('itemsView.columnVisibility');
- yield clearFeeds();
- });
-
- it("should store column visibility settings when switching from default to feeds", function* () {
- doc.getElementById('zotero-items-column-dateAdded').setAttribute('hidden', false);
- var feed = yield createFeed();
- yield cv.selectLibrary(feed.libraryID);
- var settings = JSON.parse(Zotero.Prefs.get('itemsView.columnVisibility'));
- assert.isOk(settings.default.dateAdded);
- });
-
- it("should restore column visibility when switching between default and feeds", function* () {
- doc.getElementById('zotero-items-column-dateAdded').setAttribute('hidden', false);
- var feed = yield createFeed();
- yield cv.selectLibrary(feed.libraryID);
- assert.equal(doc.getElementById('zotero-items-column-dateAdded').getAttribute('hidden'), 'true');
- doc.getElementById('zotero-items-column-firstCreator').setAttribute('hidden', true);
- yield cv.selectLibrary(Zotero.Libraries.userLibraryID);
- assert.equal(doc.getElementById('zotero-items-column-dateAdded').getAttribute('hidden'), 'false');
- yield cv.selectLibrary(feed.libraryID);
- assert.equal(doc.getElementById('zotero-items-column-firstCreator').getAttribute('hidden'), 'true');
- });
-
- it("should restore column visibility settings on restart", function* () {
- doc.getElementById('zotero-items-column-dateAdded').setAttribute('hidden', false);
- assert.equal(doc.getElementById('zotero-items-column-dateAdded').getAttribute('hidden'), 'false');
-
- win.close();
- win = yield loadZoteroPane();
- doc = win.document;
- zp = win.ZoteroPane;
-
- assert.equal(doc.getElementById('zotero-items-column-dateAdded').getAttribute('hidden'), 'false');
- });
- });
})