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

View file

@ -1090,9 +1090,23 @@ class VirtualizedTable extends React.Component {
return this._getVisibleColumns().map((column, index) => {
let columnName = formatColumnName(column);
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) {
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
onDragStart={this._handleResizerDragStart.bind(this, index)}
onDrag={this._handleResizerDrag}
@ -1329,6 +1343,12 @@ class VirtualizedTable extends React.Component {
return row >= this._jsWindow.getFirstVisibleRow()
&& 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) {
let persistKeys = column.zoteroPersist;
let persistKeys = new Set(column.zoteroPersist);
if (!persistKeys) persistKeys = new Set();
// Always persist
['ordinal', 'hidden', 'sortDirection'].forEach(k => persistKeys.add(k));
@ -1585,7 +1605,7 @@ var Columns = class {
column.sortDirection *= -1;
}
else {
column.sortDirection = column.defaultSort || 1;
column.sortDirection = column.sortReverse ? -1 : 1;
}
}
});

View file

@ -173,7 +173,7 @@
}
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', '',
'chrome,dialog=no,centerscreen,resizable=yes', io);

View file

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

View file

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

View file

@ -32,10 +32,14 @@ const VirtualizedTable = require('components/virtualized-table');
const { renderCell, formatColumnName } = VirtualizedTable;
const Icons = require('components/icons');
const { getDOMElement } = Icons;
const { COLUMNS } = require('./itemTreeColumns');
const { COLUMNS } = require("zotero/itemTreeColumns");
const { Cc, Ci, Cu } = require('chrome');
Cu.import("resource://gre/modules/osfile.jsm");
/**
* @typedef {import("./itemTreeColumns.jsx").ItemTreeColumnOptions} ItemTreeColumnOptions
*/
const CHILD_INDENT = 12;
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");
@ -99,7 +103,7 @@ var ItemTree = class ItemTree extends LibraryTree {
this._unregisterID = Zotero.Notifier.registerObserver(
this,
['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search'],
['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search', 'itemtree'],
'itemTreeView',
50
);
@ -107,8 +111,7 @@ var ItemTree = class ItemTree extends LibraryTree {
this._itemsPaneMessage = null;
this._columnsId = null;
this.columns = null;
if (this.collectionTreeRow) {
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
* itemTreeColumns.js for available column fields.
* @returns {Array<Column>}
* Get global columns from ItemTreeColumns and local columns from this.columns
* @returns {ItemTreeColumnOptions[]}
*/
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;
}
// Reset columns on custom column change
if(type === "itemtree" && action === "refresh") {
await this._resetColumns();
return;
}
if (type == 'search' && action == 'modify') {
// TODO: Only refresh on condition change (not currently available in extraData)
await this.refresh();
@ -1300,7 +1317,8 @@ var ItemTree = class ItemTree extends LibraryTree {
return (row.ref.isFeedItem && Zotero.Feeds.get(row.ref.libraryID).name) || "";
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();
// Refresh columns from itemTreeColumns
const columns = this.getColumns();
let hasDefaultIn = columns.some(column => 'defaultIn' in column);
for (let column of columns) {
@ -3193,7 +3212,7 @@ var ItemTree = class ItemTree extends LibraryTree {
// Initial hidden value
if (!("hidden" in column)) {
if (hasDefaultIn) {
column.hidden = !(column.defaultIn && column.defaultIn.has(visibilityGroup));
column.hidden = !(column.defaultIn && column.defaultIn.includes(visibilityGroup));
}
else {
column.hidden = false;
@ -3573,7 +3592,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let columnMenuitemElements = {};
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
if (column.ignoreInColumnPicker === true) continue;
if (column.showInColumnPicker === false) continue;
let label = formatColumnName(column);
let menuitem = doc.createXULElement('menuitem');
menuitem.setAttribute('type', 'checkbox');
@ -3604,7 +3623,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let moreItems = [];
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
if (column.submenu) {
if (column.columnPickerSubMenu) {
moreItems.push(columnMenuitemElements[column.dataKey]);
}
}
@ -3815,6 +3834,15 @@ var ItemTree = class ItemTree extends LibraryTree {
}
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)
@ -3827,7 +3855,10 @@ var ItemTreeRow = function(ref, level, isOpen)
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() {

View file

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

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
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
// specify that item should be selected in window
io.select = this.citationItemIDs[citation][0];

View file

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

View file

@ -3181,6 +3181,7 @@ Zotero.Integration.Citation = class {
io.addBorder = Zotero.isWin;
io.singleSelection = true;
io.itemTreeID = "handle-missing-item-select-item-dialog";
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 = [
'collection', 'search', 'share', 'share-items', 'item', 'file',
'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 _queue = {};

View file

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