Add ItemTree column API (#3186)

This commit is contained in:
windingwind 2023-07-25 12:47:12 +08:00 committed by GitHub
parent 676f820f87
commit c89590c7b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 586 additions and 121 deletions

View file

@ -26,7 +26,7 @@
Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/Services.jsm");
import ItemTree from 'zotero/itemTree'; import ItemTree from 'zotero/itemTree';
import { getDefaultColumnsByDataKeys } from 'zotero/itemTreeColumns'; import { getColumnDefinitionsByDataKey } from 'zotero/itemTreeColumns'
var ZoteroAdvancedSearch = new function() { var ZoteroAdvancedSearch = new function() {
@ -60,13 +60,14 @@ var ZoteroAdvancedSearch = new function() {
id: "advanced-search", id: "advanced-search",
dragAndDrop: true, dragAndDrop: true,
onActivate: this.onItemActivate.bind(this), onActivate: this.onItemActivate.bind(this),
columns: getDefaultColumnsByDataKeys(['title', 'firstCreator']), columns: getColumnDefinitionsByDataKey(["title", "firstCreator"]),
}); });
// A minimal implementation of Zotero.CollectionTreeRow // A minimal implementation of Zotero.CollectionTreeRow
var collectionTreeRow = { var collectionTreeRow = {
view: {}, view: {},
ref: _searchBox.search, ref: _searchBox.search,
visibilityGroup: 'default',
isSearchMode: () => true, isSearchMode: () => true,
getItems: async () => [], getItems: async () => [],
isLibrary: () => false, isLibrary: () => false,
@ -96,6 +97,7 @@ var ZoteroAdvancedSearch = new function() {
var collectionTreeRow = { var collectionTreeRow = {
view: {}, view: {},
ref: _searchBox.search, ref: _searchBox.search,
visibilityGroup: 'default',
isSearchMode: () => true, isSearchMode: () => true,
getItems: async function () { getItems: async function () {
await Zotero.Libraries.get(_libraryID).waitForDataLoad('item'); await Zotero.Libraries.get(_libraryID).waitForDataLoad('item');

View file

@ -1090,9 +1090,23 @@ class VirtualizedTable extends React.Component {
return this._getVisibleColumns().map((column, index) => { return this._getVisibleColumns().map((column, index) => {
let columnName = formatColumnName(column); let columnName = formatColumnName(column);
let label = columnName; let label = columnName;
// Allow custom icons to be used in column headers
if (column.iconPath) {
column.iconLabel = <span
className="icon icon-bg"
style={{backgroundImage: `url("${column.iconPath}")`}}>
</span>;
}
if (column.iconLabel) { if (column.iconLabel) {
label = column.iconLabel; label = column.iconLabel;
} }
if (column.htmlLabel) {
if (React.isValidElement(column.htmlLabel)) {
label = column.htmlLabel;
} else if (typeof column.htmlLabel === "string") {
label = <span dangerouslySetInnerHTML={{ __html: column.htmlLabel }} />;
}
}
let resizer = (<Draggable let resizer = (<Draggable
onDragStart={this._handleResizerDragStart.bind(this, index)} onDragStart={this._handleResizerDragStart.bind(this, index)}
onDrag={this._handleResizerDrag} onDrag={this._handleResizerDrag}
@ -1329,6 +1343,12 @@ class VirtualizedTable extends React.Component {
return row >= this._jsWindow.getFirstVisibleRow() return row >= this._jsWindow.getFirstVisibleRow()
&& row <= this._jsWindow.getLastVisibleRow(); && row <= this._jsWindow.getLastVisibleRow();
} }
async _resetColumns() {
this.invalidate();
this._columns = new Columns(this);
await new Promise((resolve) => {this.forceUpdate(resolve)});
}
} }
/** /**
@ -1452,7 +1472,7 @@ var Columns = class {
} }
_getColumnPrefsToPersist(column) { _getColumnPrefsToPersist(column) {
let persistKeys = column.zoteroPersist; let persistKeys = new Set(column.zoteroPersist);
if (!persistKeys) persistKeys = new Set(); if (!persistKeys) persistKeys = new Set();
// Always persist // Always persist
['ordinal', 'hidden', 'sortDirection'].forEach(k => persistKeys.add(k)); ['ordinal', 'hidden', 'sortDirection'].forEach(k => persistKeys.add(k));
@ -1585,7 +1605,7 @@ var Columns = class {
column.sortDirection *= -1; column.sortDirection *= -1;
} }
else { else {
column.sortDirection = column.defaultSort || 1; column.sortDirection = column.sortReverse ? -1 : 1;
} }
} }
}); });

View file

@ -173,7 +173,7 @@
} }
add = async () => { add = async () => {
let io = { dataIn: null, dataOut: null, deferred: Zotero.Promise.defer() }; let io = { dataIn: null, dataOut: null, deferred: Zotero.Promise.defer(), itemTreeID: 'related-box-select-item-dialog' };
window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '', window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '',
'chrome,dialog=no,centerscreen,resizable=yes', io); 'chrome,dialog=no,centerscreen,resizable=yes', io);

View file

@ -138,6 +138,10 @@ var Zotero_Citation_Dialog = new function () {
i++; i++;
} }
menu.selectedIndex = pageLocatorIndex; menu.selectedIndex = pageLocatorIndex;
if (!io.itemTreeID) {
io.itemTreeID = "add-citation-select-item-dialog";
}
// load (from selectItemsDialog.js) // load (from selectItemsDialog.js)
yield doLoad(); yield doLoad();

View file

@ -57,6 +57,10 @@ var Zotero_Bibliography_Dialog = new function () {
window.addEventListener('dialogcancel', () => Zotero_Bibliography_Dialog.close()); window.addEventListener('dialogcancel', () => Zotero_Bibliography_Dialog.close());
_editor = document.querySelector('#editor').contentWindow.editor; _editor = document.querySelector('#editor').contentWindow.editor;
if (!io.itemTreeID) {
io.itemTreeID = "edit-bib-select-item-dialog";
}
// load (from selectItemsDialog.js) // load (from selectItemsDialog.js)
await doLoad(); await doLoad();

View file

@ -32,10 +32,14 @@ const VirtualizedTable = require('components/virtualized-table');
const { renderCell, formatColumnName } = VirtualizedTable; const { renderCell, formatColumnName } = VirtualizedTable;
const Icons = require('components/icons'); const Icons = require('components/icons');
const { getDOMElement } = Icons; const { getDOMElement } = Icons;
const { COLUMNS } = require('./itemTreeColumns'); const { COLUMNS } = require("zotero/itemTreeColumns");
const { Cc, Ci, Cu } = require('chrome'); const { Cc, Ci, Cu } = require('chrome');
Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/osfile.jsm");
/**
* @typedef {import("./itemTreeColumns.jsx").ItemTreeColumnOptions} ItemTreeColumnOptions
*/
const CHILD_INDENT = 12; const CHILD_INDENT = 12;
const COLORED_TAGS_RE = new RegExp("^(?:Numpad|Digit)([0-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1})$"); const COLORED_TAGS_RE = new RegExp("^(?:Numpad|Digit)([0-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1})$");
const COLUMN_PREFS_FILEPATH = OS.Path.join(Zotero.Profile.dir, "treePrefs.json"); const COLUMN_PREFS_FILEPATH = OS.Path.join(Zotero.Profile.dir, "treePrefs.json");
@ -99,7 +103,7 @@ var ItemTree = class ItemTree extends LibraryTree {
this._unregisterID = Zotero.Notifier.registerObserver( this._unregisterID = Zotero.Notifier.registerObserver(
this, this,
['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search'], ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search', 'itemtree'],
'itemTreeView', 'itemTreeView',
50 50
); );
@ -107,8 +111,7 @@ var ItemTree = class ItemTree extends LibraryTree {
this._itemsPaneMessage = null; this._itemsPaneMessage = null;
this._columnsId = null; this._columnsId = null;
this.columns = null;
if (this.collectionTreeRow) { if (this.collectionTreeRow) {
this.collectionTreeRow.view.itemTreeView = this; this.collectionTreeRow.view.itemTreeView = this;
} }
@ -142,12 +145,20 @@ var ItemTree = class ItemTree extends LibraryTree {
/** /**
* Extension developers: use this to monkey-patch additional columns. See * Get global columns from ItemTreeColumns and local columns from this.columns
* itemTreeColumns.js for available column fields. * @returns {ItemTreeColumnOptions[]}
* @returns {Array<Column>}
*/ */
getColumns() { getColumns() {
return Array.from(this.props.columns); const extraColumns = Zotero.ItemTreeManager.getCustomColumns(this.props.id);
/** @type {ItemTreeColumnOptions[]} */
const currentColumns = this.props.columns.map(col => Object.assign({}, col));
extraColumns.forEach((column) => {
if (!currentColumns.find(c => c.dataKey === column.dataKey)) {
currentColumns.push(column);
}
});
return currentColumns;
} }
/** /**
@ -349,6 +360,12 @@ var ItemTree = class ItemTree extends LibraryTree {
return; return;
} }
// Reset columns on custom column change
if(type === "itemtree" && action === "refresh") {
await this._resetColumns();
return;
}
if (type == 'search' && action == 'modify') { if (type == 'search' && action == 'modify') {
// TODO: Only refresh on condition change (not currently available in extraData) // TODO: Only refresh on condition change (not currently available in extraData)
await this.refresh(); await this.refresh();
@ -1300,7 +1317,8 @@ var ItemTree = class ItemTree extends LibraryTree {
return (row.ref.isFeedItem && Zotero.Feeds.get(row.ref.libraryID).name) || ""; return (row.ref.isFeedItem && Zotero.Feeds.get(row.ref.libraryID).name) || "";
default: default:
return row.ref.getField(field, false, true); // Get from row.getField() to allow for custom fields
return row.getField(field, false, true);
} }
} }
@ -3165,6 +3183,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let columnsSettings = this._getColumnPrefs(); let columnsSettings = this._getColumnPrefs();
// Refresh columns from itemTreeColumns
const columns = this.getColumns(); const columns = this.getColumns();
let hasDefaultIn = columns.some(column => 'defaultIn' in column); let hasDefaultIn = columns.some(column => 'defaultIn' in column);
for (let column of columns) { for (let column of columns) {
@ -3193,7 +3212,7 @@ var ItemTree = class ItemTree extends LibraryTree {
// Initial hidden value // Initial hidden value
if (!("hidden" in column)) { if (!("hidden" in column)) {
if (hasDefaultIn) { if (hasDefaultIn) {
column.hidden = !(column.defaultIn && column.defaultIn.has(visibilityGroup)); column.hidden = !(column.defaultIn && column.defaultIn.includes(visibilityGroup));
} }
else { else {
column.hidden = false; column.hidden = false;
@ -3573,7 +3592,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let columnMenuitemElements = {}; let columnMenuitemElements = {};
for (let i = 0; i < columns.length; i++) { for (let i = 0; i < columns.length; i++) {
const column = columns[i]; const column = columns[i];
if (column.ignoreInColumnPicker === true) continue; if (column.showInColumnPicker === false) continue;
let label = formatColumnName(column); let label = formatColumnName(column);
let menuitem = doc.createXULElement('menuitem'); let menuitem = doc.createXULElement('menuitem');
menuitem.setAttribute('type', 'checkbox'); menuitem.setAttribute('type', 'checkbox');
@ -3604,7 +3623,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let moreItems = []; let moreItems = [];
for (let i = 0; i < columns.length; i++) { for (let i = 0; i < columns.length; i++) {
const column = columns[i]; const column = columns[i];
if (column.submenu) { if (column.columnPickerSubMenu) {
moreItems.push(columnMenuitemElements[column.dataKey]); moreItems.push(columnMenuitemElements[column.dataKey]);
} }
} }
@ -3815,6 +3834,15 @@ var ItemTree = class ItemTree extends LibraryTree {
} }
return span; return span;
} }
async _resetColumns(){
this._columnsId = null;
return new Promise((resolve) => this.forceUpdate(async () => {
await this.tree._resetColumns();
await this.refreshAndMaintainSelection();
resolve();
}));
}
}; };
var ItemTreeRow = function(ref, level, isOpen) var ItemTreeRow = function(ref, level, isOpen)
@ -3827,7 +3855,10 @@ var ItemTreeRow = function(ref, level, isOpen)
ItemTreeRow.prototype.getField = function(field, unformatted) ItemTreeRow.prototype.getField = function(field, unformatted)
{ {
return this.ref.getField(field, unformatted, true); if (!Zotero.ItemTreeManager.isCustomColumn(field)) {
return this.ref.getField(field, unformatted, true);
}
return Zotero.ItemTreeManager.getCustomCellData(this.ref, field);
} }
ItemTreeRow.prototype.numNotes = function() { ItemTreeRow.prototype.numNotes = function() {

View file

@ -2,7 +2,7 @@
***** BEGIN LICENSE BLOCK ***** ***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship Copyright © 2020 Corporation for Digital Scholarship
Vienna, Virginia, USA Vienna, Virginia, USA
http://zotero.org http://zotero.org
This file is part of Zotero. This file is part of Zotero.
@ -23,318 +23,358 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
*/ */
(function() {
const React = require('react'); const React = require('react');
const Icons = require('components/icons'); const Icons = require('components/icons');
/** /**
* @type Column { * @typedef ItemTreeColumnOptions
* dataKey: string, // Required, see use in ItemTree#_getRowData() * @type {object}
* * @property {string} dataKey - Required, see use in ItemTree#_getRowData()
* defaultIn: Set<string>, // Types of trees the column is default in. Can be [default, feed]; * @property {string} label - The column label. Either a string or the id to an i18n string.
* disabledIn: Set<string>, // Types of trees where the column is not available * @property {string} [pluginID] - Set plugin ID to auto remove column when plugin is removed.
* defaultSort: number // Default: 1. -1 for descending sort * @property {string[]} [enabledTreeIDs=[]] - Which tree ids the column should be enabled in. If undefined, enabled in main tree. If ["*"], enabled in all trees.
* * @property {string[]} [defaultIn] - Will be deprecated. Types of trees the column is default in. Can be [default, feed];
* flex: number, // Default: 1. When the column is added to the tree how much space it should occupy as a flex ratio * @property {string[]} [disabledIn] - Will be deprecated. Types of trees where the column is not available
* width: string, // A column width instead of flex ratio. See above. * @property {boolean} [sortReverse=false] - Default: false. Set to true to reverse the sort order
* fixedWidth: boolean // Default: false. Set to true to disable column resizing * @property {number} [flex=1] - Default: 1. When the column is added to the tree how much space it should occupy as a flex ratio
* staticWidth: boolean // Default: false. Set to true to prevent columns from changing width when * @property {string} [width] - A column width instead of flex ratio. See above.
* // the width of the tree increases or decreases * @property {boolean} [fixedWidth] - Default: false. Set to true to disable column resizing
* minWidth: number, // Override the default [20px] column min-width for resizing * @property {boolean} [staticWidth] - Default: false. Set to true to prevent columns from changing width when the width of the tree increases or decreases
* * @property {number} [minWidth] - Override the default [20px] column min-width for resizing
* label: string, // The column label. Either a string or the id to an i18n string. * @property {React.Component} [iconLabel] - Set an Icon label instead of a text-based one
* iconLabel: React.Component, // Set an Icon label instead of a text-based one * @property {string} [iconPath] - Set an Icon path, overrides {iconLabel}
* * @property {string | React.Component} [htmlLabel] - Set an HTML label, overrides {iconLabel} and {label}. Can be a HTML string or a React component.
* ignoreInColumnPicker: boolean // Default: false. Set to true to not display in column picker. * @property {boolean} [showInColumnPicker=true] - Default: true. Set to true to show in column picker.
* submenu: boolean, // Default: false. Set to true to display the column in "More Columns" submenu of column picker. * @property {boolean} [columnPickerSubMenu=false] - Default: false. Set to true to display the column in "More Columns" submenu of column picker.
* * @property {boolean} [primary] - Should only be one column at the time. Title is the primary column
* primary: boolean, // Should only be one column at the time. Title is the primary column * @property {boolean} [custom] - Set automatically to true when the column is added by the user
* zoteroPersist: Set<string>, // Which column properties should be persisted between zotero close * @property {(item: Zotero.Item, dataKey: string) => string} [dataProvider] - Custom data provider that is called when rendering cells
* } * @property {string[]} [zoteroPersist] - Which column properties should be persisted between zotero close
*/
/**
* @type {ItemTreeColumnOptions[]}
* @constant
*/ */
const COLUMNS = [ const COLUMNS = [
{ {
dataKey: "title", dataKey: "title",
primary: true, primary: true,
defaultIn: new Set(["default", "feeds", "feed"]), defaultIn: ["default", "feeds", "feed"],
label: "itemFields.title", label: "itemFields.title",
ignoreInColumnPicker: true, showInColumnPicker: false,
flex: 4, flex: 4,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "firstCreator", dataKey: "firstCreator",
defaultIn: new Set(["default", "feeds", "feed"]), defaultIn: ["default", "feeds", "feed"],
label: "zotero.items.creator_column", label: "zotero.items.creator_column",
showInColumnPicker: true,
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "itemType", dataKey: "itemType",
label: "zotero.items.itemType", label: "zotero.items.itemType",
showInColumnPicker: true,
width: "40", width: "40",
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "date", dataKey: "date",
defaultIn: new Set(["feeds", "feed"]), defaultIn: ["feeds", "feed"],
defaultSort: -1, sortReverse: true,
label: "itemFields.date", label: "itemFields.date",
showInColumnPicker: true,
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "year", dataKey: "year",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
defaultSort: -1, sortReverse: true,
label: "zotero.items.year_column", label: "zotero.items.year_column",
showInColumnPicker: true,
flex: 1, flex: 1,
staticWidth: true, staticWidth: true,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "publisher", dataKey: "publisher",
label: "itemFields.publisher", label: "itemFields.publisher",
showInColumnPicker: true,
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "publicationTitle", dataKey: "publicationTitle",
label: "itemFields.publicationTitle", label: "itemFields.publicationTitle",
showInColumnPicker: true,
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "journalAbbreviation", dataKey: "journalAbbreviation",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.journalAbbreviation", label: "itemFields.journalAbbreviation",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "language", dataKey: "language",
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.language", label: "itemFields.language",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "accessDate", dataKey: "accessDate",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
defaultSort: -1, sortReverse: true,
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.accessDate", label: "itemFields.accessDate",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "libraryCatalog", dataKey: "libraryCatalog",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.libraryCatalog", label: "itemFields.libraryCatalog",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "callNumber", dataKey: "callNumber",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.callNumber", label: "itemFields.callNumber",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "rights", dataKey: "rights",
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.rights", label: "itemFields.rights",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "dateAdded", dataKey: "dateAdded",
defaultSort: -1, sortReverse: true,
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
showInColumnPicker: true,
label: "itemFields.dateAdded", label: "itemFields.dateAdded",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "dateModified", dataKey: "dateModified",
defaultSort: -1, sortReverse: true,
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
showInColumnPicker: true,
label: "zotero.items.dateModified_column", label: "zotero.items.dateModified_column",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "archive", dataKey: "archive",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.archive", label: "itemFields.archive",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "archiveLocation", dataKey: "archiveLocation",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.archiveLocation", label: "itemFields.archiveLocation",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "place", dataKey: "place",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.place", label: "itemFields.place",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "volume", dataKey: "volume",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.volume", label: "itemFields.volume",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "edition", dataKey: "edition",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.edition", label: "itemFields.edition",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "number", dataKey: "number",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.number", label: "itemFields.number",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "pages", dataKey: "pages",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.pages", label: "itemFields.pages",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "issue", dataKey: "issue",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.issue", label: "itemFields.issue",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "series", dataKey: "series",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.series", label: "itemFields.series",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "seriesTitle", dataKey: "seriesTitle",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.seriesTitle", label: "itemFields.seriesTitle",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "court", dataKey: "court",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.court", label: "itemFields.court",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "medium", dataKey: "medium",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.medium", label: "itemFields.medium",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "genre", dataKey: "genre",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.genre", label: "itemFields.genre",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "system", dataKey: "system",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.system", label: "itemFields.system",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "shortTitle", dataKey: "shortTitle",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
submenu: true, showInColumnPicker: true,
columnPickerSubMenu: true,
label: "itemFields.shortTitle", label: "itemFields.shortTitle",
flex: 2, flex: 2,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "extra", dataKey: "extra",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
showInColumnPicker: true,
label: "itemFields.extra", label: "itemFields.extra",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "hasAttachment", dataKey: "hasAttachment",
defaultIn: new Set(["default"]), defaultIn: ["default"],
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
showInColumnPicker: true,
label: "zotero.tabs.attachments.label", label: "zotero.tabs.attachments.label",
iconLabel: <Icons.IconAttachSmall />, iconLabel: <Icons.IconAttachSmall />,
fixedWidth: true, fixedWidth: true,
width: "16", width: "16",
zoteroPersist: new Set(["hidden", "sortDirection"]) zoteroPersist: ["hidden", "sortDirection"]
}, },
{ {
dataKey: "numNotes", dataKey: "numNotes",
disabledIn: ["feeds", "feed"], disabledIn: ["feeds", "feed"],
showInColumnPicker: true,
label: "zotero.tabs.notes.label", label: "zotero.tabs.notes.label",
iconLabel: <Icons.IconTreeitemNoteSmall />, iconLabel: <Icons.IconTreeitemNoteSmall />,
width: "14", width: "14",
minWidth: 14, minWidth: 14,
staticWidth: true, staticWidth: true,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
}, },
{ {
dataKey: "feed", dataKey: "feed",
disabledIn: ["default", "feed"], disabledIn: ["default", "feed"],
showInColumnPicker: true,
label: "itemFields.feed", label: "itemFields.feed",
flex: 1, flex: 1,
zoteroPersist: new Set(["width", "hidden", "sortDirection"]) zoteroPersist: ["width", "hidden", "sortDirection"]
} }
]; ];
function getDefaultColumnByDataKey(dataKey) { /**
return Object.assign({}, COLUMNS.find(col => col.dataKey == dataKey), {hidden: false}); * Returns the columns that match the given data keys from the COLUMNS constant.
} * @param {string | string[]} dataKeys - The data key(s) to match.
* @returns {ItemTreeColumnOptions | ItemTreeColumnOptions[]} - The matching columns.
function getDefaultColumnsByDataKeys(dataKeys) { */
return COLUMNS.filter(column => dataKeys.includes(column.dataKey)).map(column => Object.assign({}, column, {hidden: false})); function getColumnDefinitionsByDataKey(dataKeys) {
const isSingle = !Array.isArray(dataKeys);
if (isSingle) {
dataKeys = [dataKeys];
}
const matches = COLUMNS.filter(column => dataKeys.includes(column.dataKey)).map(column => Object.assign({}, column, { hidden: false }));
return isSingle ? matches[0] : matches;
} }
module.exports = { module.exports = {
COLUMNS, COLUMNS,
getDefaultColumnByDataKey, getColumnDefinitionsByDataKey,
getDefaultColumnsByDataKeys,
}; };
})();

View file

@ -341,7 +341,7 @@ const Zotero_RTFScan = { // eslint-disable-line no-unused-vars, camelcase
} }
else { // mapped or unmapped citation, or ambiguous citation parent else { // mapped or unmapped citation, or ambiguous citation parent
var citation = row.rtf; var citation = row.rtf;
var io = { singleSelection: true }; var io = { singleSelection: true, itemTreeID: 'rtf-scan-select-item-dialog' };
if (this.citationItemIDs[citation] && this.citationItemIDs[citation].length == 1) { // mapped citation if (this.citationItemIDs[citation] && this.citationItemIDs[citation].length == 1) { // mapped citation
// specify that item should be selected in window // specify that item should be selected in window
io.select = this.citationItemIDs[citation][0]; io.select = this.citationItemIDs[citation][0];

View file

@ -60,7 +60,7 @@ var doLoad = async function () {
onItemSelected(); onItemSelected();
} }
}, },
id: "select-items-dialog", id: io.itemTreeID || "select-items-dialog",
dragAndDrop: false, dragAndDrop: false,
persistColumns: true, persistColumns: true,
columnPicker: true, columnPicker: true,

View file

@ -3181,6 +3181,7 @@ Zotero.Integration.Citation = class {
io.addBorder = Zotero.isWin; io.addBorder = Zotero.isWin;
io.singleSelection = true; io.singleSelection = true;
io.itemTreeID = "handle-missing-item-select-item-dialog";
await Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xhtml', 'resizable', io); await Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xhtml', 'resizable', io);

View file

@ -0,0 +1,361 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
const { COLUMNS: ITEMTREE_COLUMNS } = require("zotero/itemTreeColumns");
/**
* @typedef {import("../itemTreeColumns.jsx").ItemTreeColumnOptions} ItemTreeColumnOptions
* @typedef {"dataKey" | "label" | "pluginID"} RequiredCustomColumnOptionKeys
* @typedef {Required<Pick<ItemTreeColumnOptions, RequiredCustomColumnOptionKeys>>} RequiredCustomColumnOptionsPartial
* @typedef {Omit<ItemTreeColumnOptions, RequiredCustomColumnOptionKeys>} CustomColumnOptionsPartial
* @typedef {RequiredCustomColumnOptionsPartial & CustomColumnOptionsPartial} ItemTreeCustomColumnOptions
* @typedef {Partial<Omit<ItemTreeCustomColumnOptions, "enabledTreeIDs">>} ItemTreeCustomColumnFilters
*/
class ItemTreeManager {
_observerAdded = false;
/** @type {Record<string, ItemTreeCustomColumnOptions}} */
_customColumns = {};
/**
* Register a custom column. All registered columns must be valid, and must have a unique dataKey.
* Although it's async, resolving does not promise the item trees are updated.
*
* Note that the `dataKey` you use here may be different from the one returned by the function.
* This is because the `dataKey` is prefixed with the `pluginID` to avoid conflicts after the column is registered.
* @param {ItemTreeCustomColumnOptions | ItemTreeCustomColumnOptions[]} options - An option or array of options to register
* @returns {string | string[] | false} - The dataKey(s) of the added column(s) or false if no columns were added
* @example
* A minimal custom column:
* ```js
* // You can unregister the column later with Zotero.ItemTreeManager.unregisterColumns(registeredDataKey);
* const registeredDataKey = await Zotero.ItemTreeManager.registerColumns(
* {
* dataKey: 'rtitle',
* label: 'Reversed Title',
* pluginID: 'make-it-red@zotero.org', // Replace with your plugin ID
* dataProvider: (item, dataKey) => {
* return item.getField('title').split('').reverse().join('');
* },
* });
* ```
* @example
* A custom column using all available options.
* Note that the column will only be shown in the main item tree.
* ```js
* const registeredDataKey = await Zotero.ItemTreeManager.registerColumns(
* {
* dataKey: 'rtitle',
* label: 'Reversed Title',
* enabledTreeIDs: ['main'], // only show in the main item tree
* sortReverse: true, // sort by increasing order
* flex: 0, // don't take up all available space
* width: 100, // assign fixed width in pixels
* fixedWidth: true, // don't allow user to resize
* staticWidth: true, // don't allow column to be resized when the tree is resized
* minWidth: 50, // minimum width in pixels
* iconPath: 'chrome://zotero/skin/tick.png', // icon to show in the column header
* htmlLabel: '<span style="color: red;">reversed title</span>', // use HTML in the label. This will override the label and iconPath property
* showInColumnPicker: true, // show in the column picker
* columnPickerSubMenu: true, // show in the column picker submenu
* pluginID: 'make-it-red@zotero.org', // plugin ID, which will be used to unregister the column when the plugin is unloaded
* dataProvider: (item, dataKey) => {
* // item: the current item in the row
* // dataKey: the dataKey of the column
* // return: the data to display in the column
* return item.getField('title').split('').reverse().join('');
* },
* zoteroPersist: ['width', 'hidden', 'sortDirection'], // persist the column properties
* });
* ```
* @example
* Register multiple custom columns:
* ```js
* const registeredDataKeys = await Zotero.ItemTreeManager.registerColumns(
* [
* {
* dataKey: 'rtitle',
* iconPath: 'chrome://zotero/skin/tick.png',
* label: 'Reversed Title',
* pluginID: 'make-it-red@zotero.org', // Replace with your plugin ID
* dataProvider: (item, dataKey) => {
* return item.getField('title').split('').reverse().join('');
* },
* },
* {
* dataKey: 'utitle',
* label: 'Uppercase Title',
* pluginID: 'make-it-red@zotero.org', // Replace with your plugin ID
* dataProvider: (item, dataKey) => {
* return item.getField('title').toUpperCase();
* },
* },
* ]);
* ```
*/
async registerColumns(options) {
const registeredDataKeys = this._addColumns(options);
if (!registeredDataKeys) {
return false;
}
this._addPluginShutdownObserver();
await this._notifyItemTrees();
return registeredDataKeys;
}
/**
* Unregister a custom column.
* Although it's async, resolving does not promise the item trees are updated.
* @param {string | string[]} dataKeys - The dataKey of the column to unregister
* @returns {boolean} true if the column(s) are unregistered
* @example
* ```js
* Zotero.ItemTreeManager.unregisterColumns('rtitle');
* ```
*/
async unregisterColumns(dataKeys) {
const success = this._removeColumns(dataKeys);
if (!success) {
return false;
}
await this._notifyItemTrees();
return true;
}
// TODO: add cell renderer registration
/**
* Get column(s) that matches the properties of option
* @param {string | string[]} [filterTreeIDs] - The tree IDs to match
* @param {ItemTreeCustomColumnFilters} [options] - An option or array of options to match
* @returns {ItemTreeCustomColumnOptions[]}
*/
getCustomColumns(filterTreeIDs, options) {
const allColumns = Object.values(this._customColumns).map(opt => Object.assign({}, opt));
if (!filterTreeIDs && !options) {
return allColumns;
}
let filteredColumns = allColumns;
if (typeof filterTreeIDs === "string") {
filterTreeIDs = [filterTreeIDs];
}
if (filterTreeIDs && !filterTreeIDs.includes("*")) {
const filterTreeIDsSet = new Set(filterTreeIDs);
filteredColumns = filteredColumns.filter((column) => {
if (column.enabledTreeIDs[0] == "*") return true;
for (const treeID of column.enabledTreeIDs) {
if (filterTreeIDsSet.has(treeID)) return true;
}
return false;
});
}
if (options) {
filteredColumns = filteredColumns.filter((col) => {
return Object.keys(options).every((key) => {
// Ignore undefined properties
if (options[key] === undefined) {
return true;
}
return options[key] === col[key];
});
});
}
return filteredColumns;
}
/**
* Check if a column is registered as a custom column
* @param {string} dataKey - The dataKey of the column
* @returns {boolean} true if the column is registered as a custom column
*/
isCustomColumn(dataKey) {
return !!this._customColumns[dataKey];
}
/**
* A centralized data source for custom columns. This is used by the ItemTreeRow to get data.
* @param {Zotero.Item} item - The item to get data from
* @param {string} dataKey - The dataKey of the column
* @returns {string}
*/
getCustomCellData(item, dataKey) {
const options = this._customColumns[dataKey];
if (options && options.dataProvider) {
return options.dataProvider(item, dataKey);
}
return "";
}
/**
* Check if column options is valid.
* All its children must be valid. Otherwise, the validation fails.
* @param {ItemTreeCustomColumnOptions[]} options - An array of options to validate
* @returns {boolean} true if the options are valid
*/
_validateColumnOption(options) {
// Check if the input option has duplicate dataKeys
const noInputDuplicates = !options.find((opt, i, arr) => arr.findIndex(o => o.dataKey === opt.dataKey) !== i);
if (!noInputDuplicates) {
Zotero.warn(`ItemTree Column options have duplicate dataKey.`);
}
const noDeniedProperties = options.every((option) => {
const valid = !option.primary;
return valid;
});
const requiredProperties = options.every((option) => {
const valid = option.dataKey && option.label && option.pluginID;
if (!valid) {
Zotero.warn(`ItemTree Column option ${JSON.stringify(option)} must have dataKey, label, and pluginID.`);
}
return valid;
});
const noRegisteredDuplicates = options.every((option) => {
const valid = !this._customColumns[option.dataKey] && !ITEMTREE_COLUMNS.find(col => col.dataKey === option.dataKey);
if (!valid) {
Zotero.warn(`ItemTree Column option ${JSON.stringify(option)} with dataKey ${option.dataKey} already exists.`);
}
return valid;
});
return noInputDuplicates && noDeniedProperties && requiredProperties && noRegisteredDuplicates;
}
/**
* Add a new column or new columns.
* If the options is an array, all its children must be valid.
* Otherwise, no columns are added.
* @param {ItemTreeCustomColumnOptions | ItemTreeCItemTreeCustomColumnOptionsolumnOptions[]} options - An option or array of options to add
* @returns {string | string[] | false} - The dataKey(s) of the added column(s) or false if no columns were added
*/
_addColumns(options) {
const isSingle = !Array.isArray(options);
if (isSingle) {
options = [options];
}
options.forEach((o) => {
o.dataKey = this._namespacedDataKey(o);
o.enabledTreeIDs = o.enabledTreeIDs || ["main"];
if (o.enabledTreeIDs.includes("*")) {
o.enabledTreeIDs = ["*"];
}
o.showInColumnPicker = o.showInColumnPicker === undefined ? true : o.showInColumnPicker;
});
// If any check fails, return check results
if (!this._validateColumnOption(options)) {
return false;
}
for (const opt of options) {
this._customColumns[opt.dataKey] = Object.assign({}, opt, { custom: true });
}
return isSingle ? options[0].dataKey : options.map(opt => opt.dataKey);
}
/**
* Remove a column option
* @param {string | string[]} dataKeys - The dataKey of the column to remove
* @returns {boolean} - True if column(s) were removed, false if not
*/
_removeColumns(dataKeys) {
if (!Array.isArray(dataKeys)) {
dataKeys = [dataKeys];
}
// If any check fails, return check results and do not remove any columns
for (const key of dataKeys) {
if (!this._customColumns[key]) {
Zotero.warn(`ItemTree Column option with dataKey ${key} does not exist.`);
return false;
}
}
for (const key of dataKeys) {
delete this._customColumns[key];
}
return true;
}
/**
* Make sure the dataKey is namespaced with the plugin ID
* @param {ItemTreeCustomColumnOptions} options
* @returns {string}
*/
_namespacedDataKey(options) {
if (options.pluginID && options.dataKey) {
// Make sure the return value is valid as class name or element id
return `${options.pluginID}-${options.dataKey}`.replace(/[^a-zA-Z0-9-_]/g, "-");
}
return options.dataKey;
}
/**
* Reset the item trees to update the columns
*/
async _notifyItemTrees() {
await Zotero.DB.executeTransaction(async function () {
Zotero.Notifier.queue(
'refresh',
'itemtree',
[],
{},
);
});
}
/**
* Unregister all columns registered by a plugin
* @param {string} pluginID - Plugin ID
*/
async _unregisterColumnByPluginID(pluginID) {
const columns = this.getCustomColumns(undefined, { pluginID });
if (columns.length === 0) {
return;
}
// Remove the columns one by one
// This is to ensure that the columns are removed and not interrupted by any non-existing columns
columns.forEach(column => this._removeColumns(column.dataKey));
Zotero.debug(`ItemTree columns registered by plugin ${pluginID} unregistered due to shutdown`);
await this._notifyItemTrees();
}
/**
* Ensure that the shutdown observer is added
* @returns {void}
*/
_addPluginShutdownObserver() {
if (this._observerAdded) {
return;
}
Zotero.Plugins.addObserver({
shutdown: ({ id: pluginID }) => {
this._unregisterColumnByPluginID(pluginID);
}
});
this._observerAdded = true;
}
}
Zotero.ItemTreeManager = new ItemTreeManager();

View file

@ -33,7 +33,8 @@ Zotero.Notifier = new function(){
var _types = [ var _types = [
'collection', 'search', 'share', 'share-items', 'item', 'file', 'collection', 'search', 'share', 'share-items', 'item', 'file',
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash',
'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key', 'tab' 'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key', 'tab',
'itemtree'
]; ];
var _transactionID = false; var _transactionID = false;
var _queue = {}; var _queue = {};

View file

@ -154,6 +154,7 @@ const xpcomFilesLocal = [
'connector/httpIntegrationClient', 'connector/httpIntegrationClient',
'connector/server_connector', 'connector/server_connector',
'connector/server_connectorIntegration', 'connector/server_connectorIntegration',
'itemTreeManager',
]; ];
Components.utils.import("resource://gre/modules/ComponentUtils.jsm"); Components.utils.import("resource://gre/modules/ComponentUtils.jsm");