Add ItemTree column API (#3186)
This commit is contained in:
parent
676f820f87
commit
c89590c7b7
13 changed files with 586 additions and 121 deletions
|
@ -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');
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
361
chrome/content/zotero/xpcom/itemTreeManager.js
Normal file
361
chrome/content/zotero/xpcom/itemTreeManager.js
Normal 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();
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Add table
Reference in a new issue