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 == 'a' && (Zotero.isMac ? event.metaKey : event.ctrlKey)) {
+ if (!this.collectionTreeRow.isPublications()) {
+ this.expandMatchParents(this._searchParentIDs);
+ }
+ }
+ 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'),
+ defaultRowHeight: 18, // px
+ alternatingRowColors: Zotero.isMac ? ['-moz-OddTreeRow', '-moz-EvenTreeRow'] : null,
+
+ showHeader: true,
+ columns: this._getColumns(),
+ onColumnPickerMenu: this._displayColumnPickerMenu,
+ onColumnSort: this.collectionTreeRow.isFeed() ? null : this._handleColumnSort,
+ getColumnPrefs: this._getColumnPrefs,
+ storeColumnPrefs: this._storeColumnPrefs,
+ containerWidth: this.domEl.clientWidth,
+
+ multiSelect: true,
+
+ onSelectionChange: this._handleSelectionChange,
+ isSelectable: this.isSelectable,
+ 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) {
+ if (this._locked) return;
+ if (!collectionTreeRow) {
+ this.tree = null;
+ this._treebox = null;
+ return this.clearItemsPaneMessage();
+ }
+ Zotero.debug(`itemTree.changeCollectionTreeRow(): ${collectionTreeRow.id}`);
+ this._itemTreeLoadingDeferred = Zotero.Promise.defer();
+ this.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
+ let newId = "item-tree-" + this.props.id + "-" + collectionTreeRow.visibilityGroup;
+ if (this.id != newId && this.props.persistColumns) {
+ await this._writeColumnPrefsToFile(true);
+ this.id = newId;
+ await this._loadColumnPrefsFromFile();
+ }
+ this.id = newId;
+ this.collectionTreeRow = collectionTreeRow;
+ this.selection.selectEventsSuppressed = true;
+ this.collectionTreeRow.view.itemTreeView = this;
+ // 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 = this._getSortDirection(sortFields);
+ 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._refreshRowMap();
+
+ this._rememberOpenState(openItemIDs);
+ this._restoreSelection(savedSelection);
+
+ if (this.tree && !this.selection.selectEventsSuppressed) {
+ this.tree.invalidate();
+ }
+
+ 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) {
+ if (this._locked) return;
+ 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);
+ }
+ if (!skipRowMapRefresh) {
+ var savedSelection = this.getSelectedItems(true);
+ }
+
+ 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) {
+ Zotero.debug('Refreshing item row map');
+ this._refreshRowMap();
+
+ await this._refreshPromise;
+ this._restoreSelection(savedSelection);
+ this.tree.invalidate();
+ }
+ }
+
+ 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;
+ }
+ }
+
+ isSelectable = (index) => {
+ if (!this._searchMode || this.collectionTreeRow.isPublications()) return true;
+ let row = this.getRow(index);
+ return row && this._searchItemIDs.has(row.id);
+ }
+
+ 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();
+ var previousOrientation = Zotero.DragDrop.currentOrientation;
+ 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 || previousOrientation != Zotero.DragDrop.currentOrientation) {
+ 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 {
+ // 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]);
+ this.tree.invalidateRow(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;
+
+ if (!skipRowMapRefresh) {
+ var savedSelection = this.getSelectedItems(true);
+ }
+
+ 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) {
+ Zotero.debug('Refreshing item row map');
+ this._refreshRowMap();
+
+ await this._refreshPromise;
+ this._restoreSelection(savedSelection, false);
+ this.tree.invalidate();
+ }
+ }
+
+ _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 {};
+ return this._columnPrefs || {};
+ }
+
+ _storeColumnPrefs = (prefs) => {
+ if (!this.props.persistColumns) return;
+ Zotero.debug(`Storing itemTree ${this.id} column prefs`, 2);
+ this._columnPrefs = prefs;
+ if (!this._columns) {
+ Zotero.debug(new Error(), 1);;
+ }
+ this._columns = this._columns.map(column => Object.assign(column, prefs[column.dataKey]))
+ .sort((a, b) => a.ordinal - b.ordinal);
+
+ this._writeColumnPrefsToFile();
+ }
+
+ _loadColumnPrefsFromFile = async () => {
+ if (!this.props.persistColumns) return;
+ try {
+ let columnPrefs = await Zotero.File.getContentsAsync(COLUMN_PREFS_FILEPATH);
+ let persistSettings = JSON.parse(columnPrefs);
+ this._columnPrefs = persistSettings[this.id] || {};
+ }
+ catch (e) {
+ this._columnPrefs = {};
+ }
+ }
+
+ /**
+ * Writes column prefs to file, but is throttled to not do it more often than
+ * every 60s. Can use the force param to force write to file immediately.
+ * @param force {Boolean} force an immediate write to file without throttling
+ * @returns {Promise}
+ */
+ _writeColumnPrefsToFile = async (force=false) => {
+ if (!this.props.persistColumns) return;
+ var writeToFile = async () => {
+ try {
+ let persistSettingsString = await Zotero.File.getContentsAsync(COLUMN_PREFS_FILEPATH);
+ var persistSettings = JSON.parse(persistSettingsString);
+ }
+ catch {
+ persistSettings = {};
+ }
+ persistSettings[this.id] = this._columnPrefs;
+
+ let prefString = JSON.stringify(persistSettings);
+ Zotero.debug(`Writing column prefs of length ${prefString.length} to file ${COLUMN_PREFS_FILEPATH}`);
+
+ return Zotero.File.putContentsAsync(COLUMN_PREFS_FILEPATH, prefString);
+ };
+ if (this._writeColumnsTimeout) {
+ clearTimeout(this._writeColumnsTimeout);
+ }
+ if (force) {
+ return writeToFile();
+ }
+ else {
+ this._writeColumnsTimeout = setTimeout(writeToFile, 60000);
+ }
+ };
+
+ _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;
+ 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;
+ }
+
+ if (!this.selection.selectEventsSuppressed) {
+ var unsuppress = this.selection.selectEventsSuppressed = true;
+ }
+
+ this.selection.clearSelection();
+
+ let focusedSet = false;
+ var toggleSelect = (function (itemID) {
+ if (!focusedSet) {
+ this.selection.select(this._rowMap[itemID]);
+ focusedSet = true;
+ }
+ else {
+ this.selection.toggleSelect(this._rowMap[itemID]);
+ }
+ }).bind(this);
+ try {
+ for (let i = 0; i < selection.length; i++) {
+ if (this._rowMap[selection[i]] != null) {
+ toggleSelect(selection[i]);
+ }
+ // Try the parent
+ else {
+ var item = Zotero.Items.get(selection[i]);
+ if (!item) {
+ continue;
+ }
+
+ var parent = item.parentItemID;
+ if (!parent) {
+ continue;
+ }
+
+ if (this._rowMap[parent] != null) {
+ if (expandCollapsedParents) {
+ await this._closeContainer(this._rowMap[parent]);
+ await this.toggleOpenState(this._rowMap[parent]);
+ toggleSelect(selection[i]);
+ }
+ else {
+ !this.selection.isSelected(this._rowMap[parent]) &&
+ toggleSelect(parent);
+ }
+ }
+ }
+ }
+ }
+ // 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));
+
+ if (unsuppress) {
+ this.selection.selectEventsSuppressed = false;
+ }
+ }
+
+ _handleColumnSort = async (index, sortDirection) => {
+ let columnSettings = this._getColumnPrefs();
+ let column = this._getColumn(index);
+ 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;
+ }
+ }
+
+ await this._refreshPromise;
+ this.selection.selectEventsSuppressed = true;
+ await this.sort();
+ this.forceUpdate(() => {
+ this.selection.selectEventsSuppressed = false;
+ // Store column prefs as a final action because it freezes the UI momentarily
+ // and makes the column sorting look laggy
+ this._storeColumnPrefs(columnSettings);
+ });
+ }
+
+ _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];
+ if (column.inMenu === false) continue;
+ 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);
+
+ //
+ // Restore Default Column Order
+ //
+ let menuitem = doc.createElementNS(ns, 'menuitem');
+ menuitem.setAttribute('label', Zotero.Intl.strings['zotero.items.restoreColumnOrder.label']);
+ menuitem.setAttribute('anonid', prefix + 'restore-order');
+ menuitem.addEventListener('command', () => this.tree._columns.restoreDefaultOrder());
+ menupopup.appendChild(menuitem);
+
+ document.children[0].appendChild(menupopup);
+ menupopup.openPopup(null, null, event.clientX + 2, event.clientY + 2);
+ }
+
+ _getSortDirection(sortFields) {
+ if (this.collectionTreeRow.isFeed()) {
+ return Zotero.Prefs.get('feeds.sortAscending') ? 1 : -1;
+ }
+ const columns = this._getColumns();
+ for (const field of sortFields) {
+ const col = columns.find(c => c.dataKey == field);
+ if (col) {
+ return col.sortDirection;
+ }
+ }
+ return 1;
+ }
+
+ _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/itemTreeColumns.jsx b/chrome/content/zotero/itemTreeColumns.jsx
new file mode 100644
index 0000000000..413308d277
--- /dev/null
+++ b/chrome/content/zotero/itemTreeColumns.jsx
@@ -0,0 +1,290 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 Corporation for Digital Scholarship
+ Vienna, 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,
+ inMenu: false,
+ 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/libraryTree.js b/chrome/content/zotero/libraryTree.js
new file mode 100644
index 0000000000..c799ae0fcc
--- /dev/null
+++ b/chrome/content/zotero/libraryTree.js
@@ -0,0 +1,249 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 Corporation for Digital Scholarship
+ Vienna, 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);
+
+ if (index <= this.selection.focused) {
+ this.selection.select(this.selection.focused - 1);
+ }
+
+ 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/locateManager.jsx b/chrome/content/zotero/locateManager.jsx
new file mode 100644
index 0000000000..db23c20236
--- /dev/null
+++ b/chrome/content/zotero/locateManager.jsx
@@ -0,0 +1,141 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 Corporation for Digital Scholarship
+ Vienna, 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_general.js b/chrome/content/zotero/preferences/preferences_general.js
index 366fb43f7e..2d851ffe90 100644
--- a/chrome/content/zotero/preferences/preferences_general.js
+++ b/chrome/content/zotero/preferences/preferences_general.js
@@ -27,7 +27,7 @@
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/osfile.jsm");
-import FilePicker from 'zotero/filePicker';
+import FilePicker from 'zotero/modules/filePicker';
Zotero_Preferences.General = {
init: function () {
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..006a77a98c
--- /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 57%
rename from chrome/content/zotero/rtfScan.js
rename to chrome/content/zotero/rtfScan.jsx
index 18fc2c1856..f9f4164a92 100644
--- a/chrome/content/zotero/rtfScan.js
+++ b/chrome/content/zotero/rtfScan.jsx
@@ -27,7 +27,12 @@
* @fileOverview Tools for automatically retrieving a citation for the given PDF
*/
-import FilePicker from 'zotero/filePicker';
+import FilePicker from 'zotero/modules/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/build_typeSchemaData.html b/chrome/content/zotero/tools/build_typeSchemaData.html
index b75652d460..8821b1c24f 100644
--- a/chrome/content/zotero/tools/build_typeSchemaData.html
+++ b/chrome/content/zotero/tools/build_typeSchemaData.html
@@ -7,7 +7,7 @@