Implement ItemPane custom section API
Unify properties as getter in ContextPane Replace section `mode` with `editable` Remove section `inTrash`
This commit is contained in:
parent
b34859d882
commit
41acf4c737
41 changed files with 1077 additions and 462 deletions
|
@ -415,7 +415,7 @@
|
|||
</hbox>
|
||||
</toolbar>
|
||||
<hbox id="appcontent" flex="1">
|
||||
<tabbox id="left-tabbox" flex="2" width="300">
|
||||
<tabbox id="left-tabbox" flex="1" width="300">
|
||||
<tabs id="tabs">
|
||||
<tab id="tab-metadata" label="&scaffold.tabs.metadata.label;"/>
|
||||
<tab id="tab-code" label="&scaffold.tabs.code.label;"/>
|
||||
|
@ -550,8 +550,8 @@
|
|||
</tabbox>
|
||||
<splitter id="pane-splitter" resizeafter="farthest" />
|
||||
|
||||
<vbox id="right-pane" flex="1">
|
||||
<html:textarea style="-moz-box-flex: 1" id="output" readonly="true"/>
|
||||
<vbox id="right-pane">
|
||||
<html:textarea id="output" readonly="true"/>
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
let ZoteroContextPane = new function () {
|
||||
let _tabCover;
|
||||
var ZoteroContextPane = new function () {
|
||||
let _loadingMessageContainer;
|
||||
let _contextPane;
|
||||
let _contextPaneInner;
|
||||
let _contextPaneSplitter;
|
||||
|
@ -32,43 +32,42 @@ let ZoteroContextPane = new function () {
|
|||
let _librarySidenav;
|
||||
let _readerSidenav;
|
||||
|
||||
// Using attribute instead of property to set 'selectedIndex'
|
||||
// is more reliable
|
||||
|
||||
Object.defineProperty(this, 'activeEditor', {
|
||||
get: () => _contextPaneInner.activeEditor
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'sidenav', {
|
||||
get: () => (Zotero_Tabs.selectedType == "library"
|
||||
? _librarySidenav
|
||||
: _readerSidenav)
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'splitter', {
|
||||
get: () => (_isStacked()
|
||||
? _contextPaneSplitterStacked
|
||||
: _contextPaneSplitter)
|
||||
});
|
||||
|
||||
this.update = _update;
|
||||
|
||||
this.getActiveEditor = () => {
|
||||
return _contextPaneInner._getActiveEditor();
|
||||
};
|
||||
|
||||
this.focus = () => {
|
||||
return _contextPaneInner._focus();
|
||||
return _contextPaneInner.handleFocus();
|
||||
};
|
||||
|
||||
this.getSidenav = () => {
|
||||
return Zotero_Tabs.selectedType == "library"
|
||||
? _librarySidenav
|
||||
: _readerSidenav;
|
||||
};
|
||||
|
||||
this.getSplitter = () => {
|
||||
return _isStacked()
|
||||
? _contextPaneSplitterStacked
|
||||
: _contextPaneSplitter;
|
||||
};
|
||||
|
||||
this.showTabCover = (isShow) => {
|
||||
_tabCover.classList.toggle('hidden', !isShow);
|
||||
this.showLoadingMessage = (isShow) => {
|
||||
_loadingMessageContainer.classList.toggle('hidden', !isShow);
|
||||
};
|
||||
|
||||
this.updateAddToNote = _updateAddToNote;
|
||||
|
||||
this.togglePane = _togglePane;
|
||||
|
||||
this.init = function () {
|
||||
if (!Zotero) {
|
||||
return;
|
||||
}
|
||||
|
||||
_tabCover = document.getElementById('zotero-tab-cover');
|
||||
_loadingMessageContainer = document.getElementById('zotero-tab-cover');
|
||||
_contextPane = document.getElementById('zotero-context-pane');
|
||||
// <context-pane> CE
|
||||
_contextPaneInner = document.getElementById('zotero-context-pane-inner');
|
||||
|
@ -79,6 +78,8 @@ let ZoteroContextPane = new function () {
|
|||
|
||||
_contextPaneInner.sidenav = _readerSidenav;
|
||||
|
||||
this.context = _contextPaneInner;
|
||||
|
||||
window.addEventListener('resize', _update);
|
||||
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
|
||||
Zotero.Reader.onToggleSidebar = _updatePaneWidth;
|
||||
|
@ -93,7 +94,7 @@ let ZoteroContextPane = new function () {
|
|||
function _updateAddToNote() {
|
||||
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||
if (reader) {
|
||||
let editor = ZoteroContextPane.getActiveEditor();
|
||||
let editor = ZoteroContextPane.activeEditor;
|
||||
let libraryReadOnly = editor && editor.item && _isLibraryReadOnly(editor.item.libraryID);
|
||||
let noteReadOnly = editor && editor.item
|
||||
&& (editor.item.deleted || editor.item.parentItem && editor.item.parentItem.deleted);
|
||||
|
@ -157,17 +158,18 @@ let ZoteroContextPane = new function () {
|
|||
}
|
||||
|
||||
if (Zotero_Tabs.selectedIndex > 0) {
|
||||
let height = 0;
|
||||
if (_isStacked()
|
||||
&& _contextPane.getAttribute('collapsed') != 'true') {
|
||||
height = _contextPaneInner.getBoundingClientRect().height;
|
||||
var height = null;
|
||||
if (_isStacked()) {
|
||||
height = 0;
|
||||
if (_contextPane.getAttribute('collapsed') != 'true') {
|
||||
height = _contextPaneInner.getBoundingClientRect().height;
|
||||
}
|
||||
}
|
||||
Zotero.Reader.setBottomPlaceholderHeight(height);
|
||||
}
|
||||
|
||||
_updatePaneWidth();
|
||||
_updateAddToNote();
|
||||
_readerSidenav.container?.render();
|
||||
}
|
||||
|
||||
function _isLibraryReadOnly(libraryID) {
|
||||
|
@ -175,7 +177,7 @@ let ZoteroContextPane = new function () {
|
|||
}
|
||||
|
||||
function _togglePane() {
|
||||
var splitter = ZoteroContextPane.getSplitter();
|
||||
var splitter = ZoteroContextPane.splitter;
|
||||
|
||||
var open = true;
|
||||
if (splitter.getAttribute('state') != 'collapsed') {
|
||||
|
|
|
@ -35,19 +35,13 @@
|
|||
</collapsible-section>
|
||||
`);
|
||||
|
||||
showInFeeds = true;
|
||||
|
||||
_item = null;
|
||||
|
||||
_mode = null;
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
this.blurOpenField();
|
||||
this._item = item;
|
||||
super.item = item;
|
||||
if (item?.isRegularItem()) {
|
||||
this.hidden = false;
|
||||
}
|
||||
|
@ -56,16 +50,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
get editable() {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
set mode(mode) {
|
||||
if (this._mode === mode) {
|
||||
set editable(editable) {
|
||||
if (this._editable === editable) {
|
||||
return;
|
||||
}
|
||||
this.blurOpenField();
|
||||
this._mode = mode;
|
||||
super.editable = editable;
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -86,7 +80,7 @@
|
|||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +89,7 @@
|
|||
this.item.setField('abstractNote', this._abstractField.value);
|
||||
await this.item.saveTx();
|
||||
}
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
|
||||
async blurOpenField() {
|
||||
|
@ -105,9 +99,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
if (!this.item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
let abstract = this.item.getField('abstractNote');
|
||||
this._section.summary = abstract;
|
||||
|
@ -119,7 +113,7 @@
|
|||
else {
|
||||
this._abstractField.value = abstract;
|
||||
}
|
||||
this._abstractField.readOnly = this._mode == 'view';
|
||||
this._abstractField.readOnly = !this.editable;
|
||||
this._abstractField.setAttribute('aria-label', Zotero.ItemFields.getLocalizedString('abstractNote'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,6 @@
|
|||
|
||||
_annotation = null;
|
||||
|
||||
_mode = null;
|
||||
|
||||
_listenerAdded = false;
|
||||
|
||||
static get observedAttributes() {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
super.tabType = tabType;
|
||||
this._updateHidden();
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
set item(item) {
|
||||
this._item = item;
|
||||
super.item = item;
|
||||
this._updateHidden();
|
||||
}
|
||||
|
||||
|
@ -60,13 +60,13 @@
|
|||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
if (!this.initialized || !this.item?.isFileAttachment()) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
let annotations = this.item.getAnnotations();
|
||||
this._section.setCount(annotations.length);
|
||||
|
|
|
@ -77,7 +77,6 @@
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.editable = false;
|
||||
this.clickableLink = false;
|
||||
this.displayButton = false;
|
||||
this.displayNote = false;
|
||||
|
@ -104,7 +103,6 @@
|
|||
set mode(val) {
|
||||
Zotero.debug("Setting mode to '" + val + "'");
|
||||
|
||||
this.editable = false;
|
||||
this.synchronous = false;
|
||||
this.displayURL = false;
|
||||
this.displayFileName = false;
|
||||
|
@ -128,7 +126,6 @@
|
|||
break;
|
||||
|
||||
case 'edit':
|
||||
this.editable = true;
|
||||
this.displayURL = true;
|
||||
this.displayFileName = true;
|
||||
this.clickableLink = true;
|
||||
|
@ -150,7 +147,6 @@
|
|||
|
||||
case 'mergeedit':
|
||||
this.synchronous = true;
|
||||
this.editable = true;
|
||||
this.displayURL = true;
|
||||
this.displayFileName = true;
|
||||
this.displayAccessed = true;
|
||||
|
@ -171,6 +167,19 @@
|
|||
}
|
||||
|
||||
this._mode = val;
|
||||
|
||||
this._editable = ["edit", "mergeedit"].includes(this._mode);
|
||||
}
|
||||
|
||||
get editable() {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
set editable(editable) {
|
||||
// TODO: Replace `mode` with `editable`?
|
||||
this.mode = editable ? "edit" : "view";
|
||||
// Use the current `_editable` set by `mode`
|
||||
super.editable = this._editable;
|
||||
}
|
||||
|
||||
get usePreview() {
|
||||
|
@ -186,7 +195,7 @@
|
|||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
super.tabType = tabType;
|
||||
if (tabType == "reader") this.usePreview = false;
|
||||
}
|
||||
|
||||
|
@ -286,16 +295,16 @@
|
|||
continue;
|
||||
}
|
||||
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async render(force = false) {
|
||||
async asyncRender() {
|
||||
if (!this.item) return;
|
||||
if (this._isRendering) return;
|
||||
if (!this._section.open) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered("async")) return;
|
||||
|
||||
Zotero.debug('Refreshing attachment box');
|
||||
this._isRendering = true;
|
||||
|
@ -359,7 +368,13 @@
|
|||
}
|
||||
|
||||
if (this.displayFileName && !isLinkedURL) {
|
||||
let fileName = this.item.attachmentFilename;
|
||||
let fileName = "";
|
||||
try {
|
||||
fileName = this.item.attachmentFilename;
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.warn("Error getting attachment filename: " + e);
|
||||
}
|
||||
|
||||
if (fileName) {
|
||||
this._id("fileName").value = fileName;
|
||||
|
@ -528,7 +543,7 @@
|
|||
}
|
||||
// Don't allow empty filename
|
||||
if (!newFilename) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
return;
|
||||
}
|
||||
let newExt = getExtension(newFilename);
|
||||
|
@ -584,7 +599,7 @@
|
|||
Zotero.getString('pane.item.attachments.fileNotFound.text1')
|
||||
);
|
||||
}
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
|
||||
initAttachmentNoteEditor() {
|
||||
|
|
|
@ -48,7 +48,6 @@
|
|||
this._isDiscarding = false;
|
||||
this._failedCount = 0;
|
||||
|
||||
// this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this));
|
||||
this._resizeOb = new ResizeObserver(this._handleResize.bind(this));
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
// TODO: jump to annotations pane
|
||||
let pane;
|
||||
if (ZoteroContextPane) {
|
||||
pane = ZoteroContextPane.getSidenav()?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`);
|
||||
pane = ZoteroContextPane.sidenav?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`);
|
||||
}
|
||||
if (pane) {
|
||||
pane._section.open = true;
|
||||
|
|
|
@ -37,14 +37,8 @@
|
|||
<popupset/>
|
||||
`);
|
||||
|
||||
_item = null;
|
||||
|
||||
_attachmentIDs = [];
|
||||
|
||||
_mode = null;
|
||||
|
||||
_inTrash = false;
|
||||
|
||||
_preview = null;
|
||||
|
||||
get item() {
|
||||
|
@ -56,37 +50,18 @@
|
|||
return;
|
||||
}
|
||||
|
||||
this._item = item;
|
||||
super.item = item;
|
||||
let hidden = !item?.isRegularItem() || item?.isFeedItem;
|
||||
this.hidden = hidden;
|
||||
this._preview.disableResize = !!hidden;
|
||||
}
|
||||
|
||||
|
||||
get inTrash() {
|
||||
return this._inTrash;
|
||||
}
|
||||
|
||||
set inTrash(inTrash) {
|
||||
if (this._inTrash === inTrash) {
|
||||
return;
|
||||
if (this.tabType != "library") {
|
||||
return false;
|
||||
}
|
||||
this._inTrash = inTrash;
|
||||
if (!this._item?.isRegularItem()) {
|
||||
return;
|
||||
}
|
||||
for (let row of Array.from(this._attachments.querySelectorAll("attachment-row"))) {
|
||||
this._updateRowAttributes(row, row.attachment);
|
||||
}
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
get tabType() {
|
||||
return this._tabType;
|
||||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
this._updateHidden();
|
||||
return ZoteroPane.collectionsView.selectedTreeRow
|
||||
&& ZoteroPane.collectionsView.selectedTreeRow.isTrash();
|
||||
}
|
||||
|
||||
get usePreview() {
|
||||
|
@ -116,7 +91,7 @@
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener('add', this._handleAdd);
|
||||
this._section?.removeEventListener('add', this._handleAdd);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
|
@ -175,9 +150,15 @@
|
|||
return row;
|
||||
}
|
||||
|
||||
async render(force = false) {
|
||||
render() {
|
||||
if (!this._item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
async asyncRender() {
|
||||
if (!this._item) return;
|
||||
if (this._isAlreadyRendered("async")) return;
|
||||
|
||||
await this._updateAttachmentIDs();
|
||||
|
||||
|
@ -187,12 +168,11 @@
|
|||
for (let attachment of itemAttachments) {
|
||||
this.addRow(attachment);
|
||||
}
|
||||
this.updateCount();
|
||||
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
|
||||
}
|
||||
|
||||
updateCount() {
|
||||
let count = this._item.numAttachments(this._inTrash);
|
||||
let count = this._item.numAttachments(this.inTrash);
|
||||
this._section.setCount(count);
|
||||
}
|
||||
|
||||
|
@ -249,11 +229,9 @@
|
|||
};
|
||||
|
||||
_updateRowAttributes(row, attachment) {
|
||||
let hidden = !this._inTrash && attachment.deleted;
|
||||
let context = this._inTrash && !this._item.deleted && !attachment.deleted;
|
||||
let hidden = !this.inTrash && attachment.deleted;
|
||||
row.attachment = attachment;
|
||||
row.hidden = hidden;
|
||||
row.contextRow = context;
|
||||
}
|
||||
|
||||
async _updateAttachmentIDs() {
|
||||
|
@ -271,10 +249,6 @@
|
|||
}
|
||||
this._attachmentIDs = sortedAttachmentIDs;
|
||||
}
|
||||
|
||||
_updateHidden() {
|
||||
this.hidden = !this._item?.isRegularItem();
|
||||
}
|
||||
}
|
||||
customElements.define("attachments-box", AttachmentsBox);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ class XULElementBase extends XULElement {
|
|||
document.l10n.connectRoot(this.shadowRoot);
|
||||
}
|
||||
|
||||
window.addEventListener("unload", this._handleWindowUnload);
|
||||
|
||||
this.initialized = true;
|
||||
this.init();
|
||||
}
|
||||
|
@ -57,6 +59,11 @@ class XULElementBase extends XULElement {
|
|||
disconnectedCallback() {
|
||||
this.replaceChildren();
|
||||
this.destroy();
|
||||
window.removeEventListener("unload", this._handleWindowUnload);
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
_handleWindowUnload = () => {
|
||||
this.disconnectedCallback();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -401,9 +401,9 @@
|
|||
if (document.documentElement.getAttribute('windowtype') !== 'navigator:browser') {
|
||||
return null;
|
||||
}
|
||||
if (!ZoteroContextPane) return null;
|
||||
if (typeof ZoteroContextPane == "undefined") return null;
|
||||
// TODO: update this after unifying item pane & context pane
|
||||
return ZoteroContextPane.getSidenav();
|
||||
return ZoteroContextPane.sidenav;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -43,19 +43,24 @@
|
|||
sidenav.contextNotesPane = this._notesPaneDeck;
|
||||
}
|
||||
|
||||
get viewType() {
|
||||
get mode() {
|
||||
return ["item", "notes"][this._panesDeck.getAttribute('selectedIndex')];
|
||||
}
|
||||
|
||||
set viewType(viewType) {
|
||||
let viewTypeMap = {
|
||||
set mode(mode) {
|
||||
let modeMap = {
|
||||
item: "0",
|
||||
notes: "1",
|
||||
};
|
||||
if (!(viewType in viewTypeMap)) {
|
||||
throw new Error(`ContextPane.viewType must be one of ["item", "notes"], but got ${viewType}`);
|
||||
if (!(mode in modeMap)) {
|
||||
throw new Error(`ContextPane.mode must be one of ["item", "notes"], but got ${mode}`);
|
||||
}
|
||||
this._panesDeck.setAttribute("selectedIndex", viewTypeMap[viewType]);
|
||||
this._panesDeck.selectedIndex = modeMap[mode];
|
||||
}
|
||||
|
||||
get activeEditor() {
|
||||
let currentContext = this._getCurrentNotesContext();
|
||||
return currentContext?._getCurrentEditor();
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -153,7 +158,7 @@
|
|||
|
||||
_handleTabSelect(action, type, ids) {
|
||||
// TEMP: move these variables to ZoteroContextPane
|
||||
let _contextPaneSplitter = document.getElementById('zotero-context-splitter');
|
||||
let _contextPaneSplitter = ZoteroContextPane.splitter;
|
||||
let _contextPane = document.getElementById('zotero-context-pane');
|
||||
// It seems that changing `hidden` or `collapsed` values might
|
||||
// be related with significant slow down when there are too many
|
||||
|
@ -161,7 +166,7 @@
|
|||
if (Zotero_Tabs.selectedType == 'library') {
|
||||
_contextPaneSplitter.setAttribute('hidden', true);
|
||||
_contextPane.setAttribute('collapsed', true);
|
||||
ZoteroContextPane.showTabCover(false);
|
||||
ZoteroContextPane.showLoadingMessage(false);
|
||||
this._sidenav.hidden = true;
|
||||
}
|
||||
else if (Zotero_Tabs.selectedType == 'reader') {
|
||||
|
@ -191,9 +196,9 @@
|
|||
if (!reader) {
|
||||
return;
|
||||
}
|
||||
ZoteroContextPane.showTabCover(true);
|
||||
ZoteroContextPane.showLoadingMessage(true);
|
||||
await reader._initPromise;
|
||||
ZoteroContextPane.showTabCover(false);
|
||||
ZoteroContextPane.showLoadingMessage(false);
|
||||
// Focus reader pages view if context pane note editor is not selected
|
||||
if (Zotero_Tabs.selectedID == reader.tabID
|
||||
&& !Zotero_Tabs.isTabsMenuVisible()
|
||||
|
@ -209,7 +214,7 @@
|
|||
if (attachment) {
|
||||
this._selectNotesContext(attachment.libraryID);
|
||||
let notesContext = this._getNotesContext(attachment.libraryID);
|
||||
notesContext.updateFromCache();
|
||||
notesContext.updateNotesListFromCache();
|
||||
}
|
||||
|
||||
let currentNoteContext = this._getCurrentNotesContext();
|
||||
|
@ -217,7 +222,7 @@
|
|||
let selectedIndex = Array.from(tabNotesDeck.children).findIndex(x => x.getAttribute('data-tab-id') == reader.tabID);
|
||||
if (selectedIndex != -1) {
|
||||
tabNotesDeck.setAttribute('selectedIndex', selectedIndex);
|
||||
currentNoteContext.viewType = "childNote";
|
||||
currentNoteContext.mode = "childNote";
|
||||
}
|
||||
else {
|
||||
currentNoteContext._restoreViewType();
|
||||
|
@ -253,26 +258,23 @@
|
|||
context?.remove();
|
||||
}
|
||||
|
||||
_getActiveEditor() {
|
||||
let currentContext = this._getCurrentNotesContext();
|
||||
return currentContext?._getCurrentEditor();
|
||||
}
|
||||
|
||||
_getItemContext(tabID) {
|
||||
return this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`);
|
||||
}
|
||||
|
||||
_removeItemContext(tabID) {
|
||||
this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`).remove();
|
||||
this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`)?.remove();
|
||||
}
|
||||
|
||||
_selectItemContext(tabID) {
|
||||
let previousPinnedPane = this._sidenav.container?.pinnedPane || "";
|
||||
let previousContainer = this._sidenav.container;
|
||||
let selectedPanel = this._getItemContext(tabID);
|
||||
if (selectedPanel) {
|
||||
this._itemPaneDeck.selectedPanel = selectedPanel;
|
||||
selectedPanel.sidenav = this._sidenav;
|
||||
if (previousPinnedPane) selectedPanel.pinnedPane = previousPinnedPane;
|
||||
// Inherits previous pinned states
|
||||
if (previousContainer) selectedPanel.pinnedPane = previousContainer.pinnedPane;
|
||||
selectedPanel.render();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,7 +288,7 @@
|
|||
return;
|
||||
}
|
||||
libraryID = item.libraryID;
|
||||
let readOnly = !Zotero.Libraries.get(libraryID).editable;
|
||||
let editable = Zotero.Libraries.get(libraryID).editable;
|
||||
let parentID = item.parentID;
|
||||
|
||||
let previousPinnedPane = this._sidenav.container?.pinnedPane || "";
|
||||
|
@ -299,7 +301,8 @@
|
|||
itemDetails.className = 'zotero-item-pane-content';
|
||||
this._itemPaneDeck.appendChild(itemDetails);
|
||||
|
||||
itemDetails.mode = readOnly ? "view" : null;
|
||||
itemDetails.editable = editable;
|
||||
itemDetails.tabType = "reader";
|
||||
itemDetails.item = targetItem;
|
||||
// Manually cache parentID
|
||||
itemDetails.parentID = parentID;
|
||||
|
@ -313,14 +316,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
_focus() {
|
||||
let splitter = ZoteroContextPane.getSplitter();
|
||||
let node;
|
||||
handleFocus() {
|
||||
let splitter = ZoteroContextPane.splitter;
|
||||
|
||||
if (splitter.getAttribute('state') != 'collapsed') {
|
||||
if (this.viewType == "item") {
|
||||
node = this._itemPaneDeck.selectedPanel;
|
||||
node.focus();
|
||||
if (this.mode == "item") {
|
||||
let header = this._itemPaneDeck.selectedPanel.querySelector("pane-header editable-text");
|
||||
header.focus();
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
this._masterItem = item;
|
||||
itembox.item = item.clone();
|
||||
// The item.id is null which equals to _lastRenderItemID, so we need to force render it
|
||||
itembox.render(true);
|
||||
itembox._forceRenderAll();
|
||||
}
|
||||
|
||||
async merge() {
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
super();
|
||||
|
||||
this.clickable = false;
|
||||
this.editable = false;
|
||||
this.saveOnEdit = false;
|
||||
this.showTypeMenu = false;
|
||||
this.hideEmptyFields = false;
|
||||
|
@ -41,8 +40,6 @@
|
|||
this.eventHandlers = [];
|
||||
this.itemTypeMenu = null;
|
||||
|
||||
this.showInFeeds = true;
|
||||
|
||||
this._mode = 'view';
|
||||
this._visibleFields = [];
|
||||
this._hiddenFields = [];
|
||||
|
@ -231,7 +228,7 @@
|
|||
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox');
|
||||
Zotero.Prefs.registerObserver('fontSize', () => {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
});
|
||||
|
||||
this.style.setProperty('--comma-character',
|
||||
|
@ -253,7 +250,6 @@
|
|||
|
||||
set mode(val) {
|
||||
this.clickable = false;
|
||||
this.editable = false;
|
||||
this.saveOnEdit = false;
|
||||
this.showTypeMenu = false;
|
||||
this.hideEmptyFields = false;
|
||||
|
@ -266,7 +262,6 @@
|
|||
|
||||
case 'edit':
|
||||
this.clickable = true;
|
||||
this.editable = true;
|
||||
this.saveOnEdit = true;
|
||||
this.showTypeMenu = true;
|
||||
break;
|
||||
|
@ -282,6 +277,19 @@
|
|||
|
||||
this._mode = val;
|
||||
this.setAttribute('mode', val);
|
||||
|
||||
this._editable = this.mode == "edit";
|
||||
}
|
||||
|
||||
get editable() {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
set editable(editable) {
|
||||
// TODO: Replace `mode` with `editable`?
|
||||
this.mode = editable ? "edit" : "view";
|
||||
// Use the current `_editable` set by `mode`
|
||||
super.editable = this._editable;
|
||||
}
|
||||
|
||||
get item() {
|
||||
|
@ -464,12 +472,12 @@
|
|||
if (document.activeElement == this.itemTypeMenu) {
|
||||
this._selectField = "item-type-menu";
|
||||
}
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
Zotero.debug('Refreshing item box');
|
||||
|
||||
if (!this.item) {
|
||||
|
@ -481,9 +489,7 @@
|
|||
// Always update retraction status
|
||||
this.updateRetracted();
|
||||
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
this.updateRetracted();
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
// Init tab index to begin after all creator rows
|
||||
this._ztabindex = this._tabIndexMinCreators * (this.item.numCreators() || 1);
|
||||
|
@ -708,7 +714,7 @@
|
|||
menuitem.getAttribute('fieldname'),
|
||||
menuitem.getAttribute('originalValue')
|
||||
);
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
});
|
||||
popup.appendChild(menuitem);
|
||||
}
|
||||
|
@ -1141,7 +1147,7 @@
|
|||
// If the row is still hidden, no 'drop' event happened, meaning creator rows
|
||||
// were not reordered. To make sure everything is in correct order, just refresh.
|
||||
if (row.classList.contains("drag-hidden-creator")) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1186,12 +1192,12 @@
|
|||
rowData.setAttribute("ztabindex", ++this._ztabindex);
|
||||
rowData.addEventListener('click', () => {
|
||||
this._displayAllCreators = true;
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
});
|
||||
rowData.addEventListener('keypress', (e) => {
|
||||
if (["Enter", ' '].includes(e.key)) {
|
||||
this._displayAllCreators = true;
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
});
|
||||
rowData.textContent = Zotero.getString('general.numMore', num);
|
||||
|
@ -1407,7 +1413,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
else {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
|
||||
functionsToRun.forEach(f => f.bind(this)());
|
||||
|
|
|
@ -24,8 +24,6 @@
|
|||
*/
|
||||
|
||||
{
|
||||
const AsyncFunction = (async () => {}).constructor;
|
||||
|
||||
const waitFrame = async () => {
|
||||
return waitNoLongerThan(new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve);
|
||||
|
@ -93,12 +91,21 @@
|
|||
this._cachedParentID = parentID;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
get editable() {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
set editable(editable) {
|
||||
this._editable = editable;
|
||||
this.toggleAttribute('readonly', !editable);
|
||||
}
|
||||
|
||||
get tabType() {
|
||||
return this._tabType;
|
||||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
}
|
||||
|
||||
get pinnedPane() {
|
||||
|
@ -106,12 +113,12 @@
|
|||
}
|
||||
|
||||
set pinnedPane(val) {
|
||||
if (!val || !this.getPane(val)) {
|
||||
if (!val || !this.getEnabledPane(val)) {
|
||||
val = '';
|
||||
}
|
||||
this.setAttribute('pinnedPane', val);
|
||||
if (val) {
|
||||
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
|
||||
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getEnabledPane(val));
|
||||
}
|
||||
this.sidenav.updatePaneStatus(val);
|
||||
}
|
||||
|
@ -180,10 +187,12 @@
|
|||
});
|
||||
this._initIntersectionObserver();
|
||||
|
||||
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'ItemDetails');
|
||||
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item', 'itempane'], 'ItemDetails');
|
||||
|
||||
this._disableScrollHandler = false;
|
||||
this._pinnedPaneMinScrollHeight = 0;
|
||||
|
||||
this._lastUpdateCustomSection = 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -204,47 +213,22 @@
|
|||
Zotero.debug('Viewing item');
|
||||
this._isRendering = true;
|
||||
|
||||
this.renderCustomSections();
|
||||
|
||||
let panes = this.getPanes();
|
||||
let pendingBoxes = [];
|
||||
let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
|
||||
let tabType = Zotero_Tabs.selectedType;
|
||||
for (let box of [this._header, ...panes]) {
|
||||
if (!box.showInFeeds && item.isFeedItem) {
|
||||
box.style.display = 'none';
|
||||
box.hidden = true;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
box.style.display = '';
|
||||
box.hidden = false;
|
||||
}
|
||||
|
||||
if (this.mode) {
|
||||
box.mode = this.mode;
|
||||
|
||||
if (box.mode == 'view') {
|
||||
box.hideEmptyFields = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.mode = 'edit';
|
||||
}
|
||||
|
||||
box.editable = this.editable;
|
||||
box.tabType = this.tabType;
|
||||
box.item = item;
|
||||
box.inTrash = inTrash;
|
||||
box.tabType = tabType;
|
||||
// Render sync boxes immediately
|
||||
// Execute sync render immediately
|
||||
if (!box.hidden && box.render) {
|
||||
if (box.render instanceof AsyncFunction) {
|
||||
pendingBoxes.push(box);
|
||||
}
|
||||
else {
|
||||
if (box.render) {
|
||||
box.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pinnedPaneElem = this.getPane(this.pinnedPane);
|
||||
let pinnedPaneElem = this.getEnabledPane(this.pinnedPane);
|
||||
let pinnedIndex = panes.indexOf(pinnedPaneElem);
|
||||
|
||||
this._paneParent.style.paddingBottom = '';
|
||||
|
@ -257,28 +241,18 @@
|
|||
this._paneParent.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// Only render visible panes
|
||||
for (let box of pendingBoxes) {
|
||||
// Only execute async render for visible panes
|
||||
for (let box of panes) {
|
||||
if (!box.asyncRender) {
|
||||
continue;
|
||||
}
|
||||
if (pinnedIndex > -1 && panes.indexOf(box) < pinnedIndex) {
|
||||
continue;
|
||||
}
|
||||
if (!this.isPaneVisible(box.dataset.pane)) {
|
||||
continue;
|
||||
}
|
||||
await waitNoLongerThan(box.render(), 500);
|
||||
}
|
||||
// After all panes finish first rendering, try secondary rendering
|
||||
for (let box of panes) {
|
||||
if (!box.secondaryRender) {
|
||||
continue;
|
||||
}
|
||||
if (pinnedIndex > -1 && panes.indexOf(box) < pinnedIndex) {
|
||||
continue;
|
||||
}
|
||||
if (this.isPaneVisible(box.dataset.pane)) {
|
||||
continue;
|
||||
}
|
||||
await waitNoLongerThan(box.secondaryRender(), 500);
|
||||
await waitNoLongerThan(box.asyncRender(), 500);
|
||||
}
|
||||
if (this.item.id == item.id) {
|
||||
this._isRendering = false;
|
||||
|
@ -303,20 +277,20 @@
|
|||
// Create
|
||||
let currentPaneIDs = currentPaneElements.map(elem => elem.dataset.pane);
|
||||
for (let section of targetPanes) {
|
||||
let { paneID, head, sidenav, fragment,
|
||||
onInit, onDestroy, onDataChange, onRender, onSecondaryRender, onToggle,
|
||||
let { paneID, head, sidenav, bodyXHTML,
|
||||
onInit, onDestroy, onItemChange, onRender, onAsyncRender, onToggle,
|
||||
sectionButtons } = section;
|
||||
if (currentPaneIDs.includes(paneID)) continue;
|
||||
let elem = new (customElements.get("item-pane-custom-section"));
|
||||
elem.dataset.sidenavOptions = JSON.stringify(sidenav || {});
|
||||
elem.paneID = paneID;
|
||||
elem.fragment = fragment;
|
||||
elem.bodyXHTML = bodyXHTML;
|
||||
elem.registerSectionIcon({ icon: head.icon, darkIcon: head.darkIcon });
|
||||
elem.registerHook({ type: "init", callback: onInit });
|
||||
elem.registerHook({ type: "destroy", callback: onDestroy });
|
||||
elem.registerHook({ type: "dataChange", callback: onDataChange });
|
||||
elem.registerHook({ type: "itemChange", callback: onItemChange });
|
||||
elem.registerHook({ type: "render", callback: onRender });
|
||||
elem.registerHook({ type: "secondaryRender", callback: onSecondaryRender });
|
||||
elem.registerHook({ type: "asyncRender", callback: onAsyncRender });
|
||||
elem.registerHook({ type: "toggle", callback: onToggle });
|
||||
if (sectionButtons) {
|
||||
for (let buttonOptions of sectionButtons) {
|
||||
|
@ -335,13 +309,20 @@
|
|||
this._header.renderCustomHead(callback);
|
||||
}
|
||||
|
||||
notify = async (action, _type, _ids, _extraData) => {
|
||||
notify = async (action, type, _ids, _extraData) => {
|
||||
if (action == 'refresh' && this.item) {
|
||||
if (type == 'item-pane') {
|
||||
this.renderCustomSections();
|
||||
}
|
||||
await this.render();
|
||||
}
|
||||
};
|
||||
|
||||
getPane(id) {
|
||||
return this._paneParent.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]`);
|
||||
}
|
||||
|
||||
getEnabledPane(id) {
|
||||
return this._paneParent.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
|
||||
}
|
||||
|
||||
|
@ -368,8 +349,12 @@
|
|||
return visiblePanes;
|
||||
}
|
||||
|
||||
getCustomPanes() {
|
||||
return Array.from(this._paneParent.querySelectorAll(':scope > item-pane-custom-section[data-pane]'));
|
||||
}
|
||||
|
||||
isPaneVisible(paneID) {
|
||||
let paneElem = this.getPane(paneID);
|
||||
let paneElem = this.getEnabledPane(paneID);
|
||||
if (!paneElem) return false;
|
||||
let paneRect = paneElem.getBoundingClientRect();
|
||||
let containerRect = this._paneParent.getBoundingClientRect();
|
||||
|
@ -384,7 +369,7 @@
|
|||
}
|
||||
|
||||
async scrollToPane(paneID, behavior = 'smooth') {
|
||||
let pane = this.getPane(paneID);
|
||||
let pane = this.getEnabledPane(paneID);
|
||||
if (!pane) return null;
|
||||
|
||||
let scrollPromise;
|
||||
|
@ -486,7 +471,7 @@
|
|||
// Ignore overscroll (which generates scroll events on Windows 11, unlike on macOS)
|
||||
// and don't shrink below the pinned pane's min scroll height
|
||||
if (newMinScrollHeight > this._paneParent.scrollHeight
|
||||
|| this.getPane(this.pinnedPane) && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
|
||||
|| this.getEnabledPane(this.pinnedPane) && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
|
||||
return;
|
||||
}
|
||||
this._minScrollHeight = newMinScrollHeight;
|
||||
|
@ -539,6 +524,16 @@
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This function handles the intersection of panes with the viewport.
|
||||
* It triggers rendering and discarding of panes based on their visibility.
|
||||
* Panes are not rendered until they become visible in the viewport.
|
||||
* This approach prevents unnecessary rendering of all panes at once when switching items,
|
||||
* which can lead to slow performance and excessive battery usage,
|
||||
* especially for slow panes, e.g. attachment preview.
|
||||
* @param {IntersectionObserverEntry[]} entries
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_handleIntersection = async (entries) => {
|
||||
if (this._isRendering) return;
|
||||
let needsRefresh = [];
|
||||
|
@ -564,8 +559,8 @@
|
|||
if (needsCheckVisibility && !this.isPaneVisible(paneElem.dataset.pane)) {
|
||||
return;
|
||||
}
|
||||
await paneElem.render();
|
||||
if (paneElem.secondaryRender) await paneElem.secondaryRender();
|
||||
if (paneElem.render) paneElem.render();
|
||||
if (paneElem.asyncRender) await paneElem.asyncRender();
|
||||
});
|
||||
}
|
||||
if (needsDiscard.length > 0) {
|
||||
|
@ -573,7 +568,7 @@
|
|||
if (needsCheckVisibility && this.isPaneVisible(paneElem.dataset.pane)) {
|
||||
return;
|
||||
}
|
||||
paneElem.discard();
|
||||
if (paneElem.discard) paneElem.discard();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -59,9 +59,7 @@
|
|||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append: (...args) => {
|
||||
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
|
||||
}
|
||||
append,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,12 +67,20 @@
|
|||
this._data = data;
|
||||
}
|
||||
|
||||
get viewMode() {
|
||||
return this._viewMode;
|
||||
get collectionTreeRow() {
|
||||
return this._collectionTreeRow;
|
||||
}
|
||||
|
||||
set viewMode(mode) {
|
||||
this._viewMode = mode;
|
||||
set collectionTreeRow(val) {
|
||||
this._collectionTreeRow = val;
|
||||
}
|
||||
|
||||
get itemsView() {
|
||||
return this._itemsView;
|
||||
}
|
||||
|
||||
set itemsView(val) {
|
||||
this._itemsView = val;
|
||||
}
|
||||
|
||||
get editable() {
|
||||
|
@ -81,17 +89,18 @@
|
|||
|
||||
set editable(editable) {
|
||||
this._editable = editable;
|
||||
this.toggleAttribute('readonly', !editable);
|
||||
}
|
||||
|
||||
get viewType() {
|
||||
get mode() {
|
||||
return ["message", "item", "note", "duplicates"][this._deck.selectedIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set view type
|
||||
* Set mode of item pane
|
||||
* @param {"message" | "item" | "note" | "duplicates"} type view type
|
||||
*/
|
||||
set viewType(type) {
|
||||
set mode(type) {
|
||||
this.setAttribute("view-type", type);
|
||||
}
|
||||
|
||||
|
@ -121,14 +130,14 @@
|
|||
|
||||
notify(action, type) {
|
||||
if (type == 'item' && action == 'modify') {
|
||||
if (this.viewMode.isFeedsOrFeed) {
|
||||
if (this.collectionTreeRow.isFeedsOrFeed()) {
|
||||
this.updateReadLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderNoteEditor(item) {
|
||||
this.viewType = "note";
|
||||
this.mode = "note";
|
||||
|
||||
let noteEditor = document.getElementById('zotero-note-editor');
|
||||
noteEditor.mode = this.editable ? 'edit' : 'view';
|
||||
|
@ -139,9 +148,10 @@
|
|||
}
|
||||
|
||||
renderItemPane(item) {
|
||||
this.viewType = "item";
|
||||
this.mode = "item";
|
||||
|
||||
this._itemDetails.mode = this.editable ? null : "view";
|
||||
this._itemDetails.editable = this.editable;
|
||||
this._itemDetails.tabType = "library";
|
||||
this._itemDetails.item = item;
|
||||
|
||||
if (this.hasAttribute("collapsed")) {
|
||||
|
@ -180,7 +190,7 @@
|
|||
let count = this.data.length;
|
||||
|
||||
// Display duplicates merge interface in item pane
|
||||
if (this.viewMode.isDuplicates) {
|
||||
if (this.collectionTreeRow.isDuplicates()) {
|
||||
if (!this.editable) {
|
||||
if (count) {
|
||||
msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
|
||||
|
@ -191,11 +201,11 @@
|
|||
this.setItemPaneMessage(msg);
|
||||
}
|
||||
else if (count) {
|
||||
this.viewType = "duplicates";
|
||||
this.mode = "duplicates";
|
||||
|
||||
// On a Select All of more than a few items, display a row
|
||||
// count instead of the usual item type mismatch error
|
||||
let displayNumItemsOnTypeError = count > 5 && count == this.viewMode.rowCount;
|
||||
let displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
|
||||
|
||||
// Initialize the merge pane with the selected items
|
||||
this._duplicatesPane.setItems(this.data, displayNumItemsOnTypeError);
|
||||
|
@ -211,7 +221,7 @@
|
|||
msg = Zotero.getString('pane.item.selected.multiple', count);
|
||||
}
|
||||
else {
|
||||
let rowCount = this.viewMode.rowCount;
|
||||
let rowCount = this.itemsView.rowCount;
|
||||
let str = 'pane.item.unselected.';
|
||||
switch (rowCount) {
|
||||
case 0:
|
||||
|
@ -235,7 +245,7 @@
|
|||
}
|
||||
|
||||
setItemPaneMessage(msg) {
|
||||
this.viewType = "message";
|
||||
this.mode = "message";
|
||||
this._messagePane.render(msg);
|
||||
}
|
||||
|
||||
|
@ -258,7 +268,7 @@
|
|||
}
|
||||
|
||||
// My Publications buttons
|
||||
var isPublications = this.viewMode.isPublications;
|
||||
var isPublications = this.collectionTreeRow.isPublications();
|
||||
// Show in My Publications view if selected items are all notes or non-linked-file attachments
|
||||
var showMyPublicationsButtons = isPublications
|
||||
&& this.data.every((item) => {
|
||||
|
@ -274,13 +284,13 @@
|
|||
|
||||
// Trash button
|
||||
let nonDeletedItemsSelected = this.data.some(item => !item.deleted);
|
||||
if (this.viewMode.isTrash && !nonDeletedItemsSelected) {
|
||||
if (this.collectionTreeRow.isTrash() && !nonDeletedItemsSelected) {
|
||||
container.renderCustomHead(this.renderTrashHead.bind(this));
|
||||
return;
|
||||
}
|
||||
|
||||
// Feed buttons
|
||||
if (this.viewMode.isFeedsOrFeed) {
|
||||
if (this.collectionTreeRow.isFeedsOrFeed()) {
|
||||
container.renderCustomHead(this.renderFeedHead.bind(this));
|
||||
this.updateReadLabel();
|
||||
return;
|
||||
|
@ -476,6 +486,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
async handleBlur() {
|
||||
await this._itemDetails.blurOpenField();
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
if (this.getAttribute("collapsed")) {
|
||||
this.removeAttribute("width");
|
||||
|
@ -491,14 +505,14 @@
|
|||
if (!width || Number(width) < minWidth) this.setAttribute("width", String(minWidth));
|
||||
if (!height || Number(height) < minHeight) this.setAttribute("height", String(minHeight));
|
||||
// Render item pane after open
|
||||
if ((!width || !height) && this.viewType == "item") {
|
||||
if ((!width || !height) && this.mode == "item") {
|
||||
this._itemDetails.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleViewTypeChange(type) {
|
||||
let previousViewType = this.viewType;
|
||||
let previousViewType = this.mode;
|
||||
switch (type) {
|
||||
case "message": {
|
||||
this._deck.selectedIndex = 0;
|
||||
|
|
|
@ -25,6 +25,35 @@
|
|||
|
||||
|
||||
class ItemPaneSectionElementBase extends XULElementBase {
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
let success = this._handleDataChange("item", this._item, item);
|
||||
if (success === false) return;
|
||||
this._item = item;
|
||||
}
|
||||
|
||||
get editable() {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
set editable(editable) {
|
||||
this._editable = editable;
|
||||
this.toggleAttribute('readonly', !editable);
|
||||
}
|
||||
|
||||
get tabType() {
|
||||
return this._tabType;
|
||||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
let success = this._handleDataChange("tabType", this._tabType, tabType);
|
||||
if (success === false) return;
|
||||
this._tabType = tabType;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.render) {
|
||||
|
@ -58,21 +87,258 @@ class ItemPaneSectionElementBase extends XULElementBase {
|
|||
if (event.target !== this._section || !this._section.open) {
|
||||
return;
|
||||
}
|
||||
if (this.render) await this.render(true);
|
||||
if (this.secondaryRender) await this.secondaryRender(true);
|
||||
await this._forceRenderAll();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {"primary" | "secondary"} [type]
|
||||
* @param {"sync" | "async"} [type]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isAlreadyRendered(type = "primary") {
|
||||
_isAlreadyRendered(type = "sync") {
|
||||
let key = `_${type}RenderItemID`;
|
||||
let cachedFlag = this[key];
|
||||
if (cachedFlag && this.item?.id == cachedFlag) {
|
||||
return true;
|
||||
}
|
||||
this._lastRenderItemID = this.item.id;
|
||||
this[key] = this.item.id;
|
||||
return false;
|
||||
}
|
||||
|
||||
async _forceRenderAll() {
|
||||
if (this.hidden) return;
|
||||
// Clear cached flags to allow re-rendering
|
||||
delete this._syncRenderItemID;
|
||||
delete this._asyncRenderItemID;
|
||||
if (this.render) this.render();
|
||||
if (this.asyncRender) await this.asyncRender();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
class ItemPaneCustomSection extends ItemPaneSectionElementBase {
|
||||
_hooks = {};
|
||||
|
||||
_sectionButtons = {};
|
||||
|
||||
_refreshDisabled = true;
|
||||
|
||||
get content() {
|
||||
let extraButtons = Object.keys(this._sectionButtons).join(",");
|
||||
let content = `
|
||||
<collapsible-section custom="true" data-pane="${this.paneID}" extra-buttons="${extraButtons}">
|
||||
<html:div data-type="body">
|
||||
${this.bodyXHTML || ""}
|
||||
</html:div>
|
||||
</collapsible-section>
|
||||
<html:style class="custom-style"></html:style>
|
||||
`;
|
||||
return MozXULElement.parseXULToFragment(content);
|
||||
}
|
||||
|
||||
get paneID() {
|
||||
return this._paneID;
|
||||
}
|
||||
|
||||
set paneID(paneID) {
|
||||
this._paneID = paneID;
|
||||
if (this.initialized) {
|
||||
this._section.dataset.pane = paneID;
|
||||
this.dataset.pane = paneID;
|
||||
}
|
||||
}
|
||||
|
||||
get bodyXHTML() {
|
||||
return this._bodyXHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} bodyXHTML
|
||||
*/
|
||||
set bodyXHTML(bodyXHTML) {
|
||||
this._bodyXHTML = bodyXHTML;
|
||||
if (this.initialized) {
|
||||
this._body.replaceChildren(
|
||||
document.importNode(MozXULElement.parseXULToFragment(bodyXHTML), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector("collapsible-section");
|
||||
this._body = this._section.querySelector('[data-type="body"]');
|
||||
this._style = this.querySelector(".custom-style");
|
||||
|
||||
if (this.paneID) this.dataset.pane = this.paneID;
|
||||
if (this._label) this._section.label = this._label;
|
||||
this.updateSectionIcon();
|
||||
|
||||
this._sectionListeners = [];
|
||||
|
||||
let styles = [];
|
||||
for (let type of Object.keys(this._sectionButtons)) {
|
||||
let { icon, darkIcon, onClick } = this._sectionButtons[type];
|
||||
if (!darkIcon) {
|
||||
darkIcon = icon;
|
||||
}
|
||||
let listener = (event) => {
|
||||
let props = this._assembleProps(this._getHookProps());
|
||||
props.event = event;
|
||||
onClick(props);
|
||||
};
|
||||
this._section.addEventListener(type, listener);
|
||||
this._sectionListeners.push({ type, listener });
|
||||
let button = this._section.querySelector(`.${type}`);
|
||||
button.style = `--custom-button-icon-light: url('${icon}'); --custom-button-icon-dark: url('${darkIcon}');`;
|
||||
}
|
||||
|
||||
this._style.textContent = styles.join("\n");
|
||||
|
||||
this._section.addEventListener("toggle", this._handleToggle);
|
||||
this._sectionListeners.push({ type: "toggle", listener: this._handleToggle });
|
||||
|
||||
this._handleInit();
|
||||
|
||||
// Disable refresh until data is load
|
||||
this._refreshDisabled = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._sectionListeners.forEach(data => this._section?.removeEventListener(data.type, data.listener));
|
||||
|
||||
this._handleDestroy();
|
||||
this._hooks = null;
|
||||
}
|
||||
|
||||
setL10nID(l10nId) {
|
||||
this._section.dataset.l10nId = l10nId;
|
||||
}
|
||||
|
||||
setL10nArgs(l10nArgs) {
|
||||
this._section.dataset.l10nArgs = l10nArgs;
|
||||
}
|
||||
|
||||
registerSectionIcon(options) {
|
||||
let { icon, darkIcon } = options;
|
||||
if (!darkIcon) {
|
||||
darkIcon = icon;
|
||||
}
|
||||
this._lightIcon = icon;
|
||||
this._darkIcon = darkIcon;
|
||||
if (this.initialized) {
|
||||
this.updateSectionIcon();
|
||||
}
|
||||
}
|
||||
|
||||
updateSectionIcon() {
|
||||
this.style = `--custom-section-icon-light: url('${this._lightIcon}'); --custom-section-icon-dark: url('${this._darkIcon}')`;
|
||||
}
|
||||
|
||||
registerSectionButton(options) {
|
||||
let { type, icon, darkIcon, onClick } = options;
|
||||
if (!darkIcon) {
|
||||
darkIcon = icon;
|
||||
}
|
||||
if (this.initialized) {
|
||||
Zotero.warn(`ItemPaneCustomSection section button cannot be registered after initialization`);
|
||||
return;
|
||||
}
|
||||
this._sectionButtons[type.replace(/[^a-zA-Z0-9-_]/g, "-")] = {
|
||||
icon, darkIcon, onClick
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ type: "render" | "asyncRender" | "itemChange" | "init" | "destroy" | "toggle" }} options
|
||||
*/
|
||||
registerHook(options) {
|
||||
let { type, callback } = options;
|
||||
if (!callback) return;
|
||||
this._hooks[type] = callback;
|
||||
}
|
||||
|
||||
_getBasicHookProps() {
|
||||
return {
|
||||
paneID: this.paneID,
|
||||
doc: document,
|
||||
body: this._body,
|
||||
};
|
||||
}
|
||||
|
||||
_getUIHookProps() {
|
||||
return {
|
||||
item: this.item,
|
||||
tabType: this.tabType,
|
||||
editable: this.editable,
|
||||
setL10nArgs: l10nArgs => this.setL10nArgs(l10nArgs),
|
||||
setEnabled: enabled => this.hidden = !enabled,
|
||||
setSectionSummary: summary => this._section.summary = summary,
|
||||
setSectionButtonStatus: (type, options) => {
|
||||
let { disabled, hidden } = options;
|
||||
let button = this._section.querySelector(`.${type}`);
|
||||
if (!button) return;
|
||||
if (typeof disabled !== "undefined") button.disabled = disabled;
|
||||
if (typeof hidden !== "undefined") button.hidden = hidden;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_getHookProps() {
|
||||
return Object.assign({}, this._getBasicHookProps(), this._getUIHookProps());
|
||||
}
|
||||
|
||||
_assembleProps(...props) {
|
||||
return Object.freeze(Object.assign({}, ...props));
|
||||
}
|
||||
|
||||
_handleInit() {
|
||||
if (!this._hooks.init) return;
|
||||
let props = this._assembleProps(
|
||||
this._getHookProps(),
|
||||
{ refresh: async () => this._handleRefresh() },
|
||||
);
|
||||
this._hooks.init(props);
|
||||
}
|
||||
|
||||
_handleDestroy() {
|
||||
if (!this._hooks.destroy) return;
|
||||
let props = this._assembleProps(this._getBasicHookProps());
|
||||
this._hooks.destroy(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._hooks.render) return false;
|
||||
if (!this.initialized || this._isAlreadyRendered()) return false;
|
||||
return this._hooks.render(this._assembleProps(this._getHookProps()));
|
||||
}
|
||||
|
||||
async asyncRender() {
|
||||
if (!this._hooks.asyncRender) return false;
|
||||
if (!this.initialized || this._isAlreadyRendered("async")) return false;
|
||||
return this._hooks.asyncRender(this._assembleProps(this._getHookProps()));
|
||||
}
|
||||
|
||||
async _handleRefresh() {
|
||||
if (!this.initialized) return;
|
||||
await this._forceRenderAll();
|
||||
}
|
||||
|
||||
_handleToggle = (event) => {
|
||||
if (!this._hooks.toggle) return;
|
||||
let props = this._assembleProps(
|
||||
this._getHookProps(),
|
||||
{ event },
|
||||
);
|
||||
this._hooks.toggle(props);
|
||||
};
|
||||
|
||||
_handleDataChange(type, _oldValue, _newValue) {
|
||||
if (type == "item" && this._hooks.itemChange) {
|
||||
let props = this._assembleProps(this._getHookProps());
|
||||
this._hooks.itemChange(props);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("item-pane-custom-section", ItemPaneCustomSection);
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
data-l10n-id="sidenav-abstract"
|
||||
data-pane="abstract"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<html:div class="pin-wrapper" hidden="true">
|
||||
<toolbarbutton
|
||||
disabled="true"
|
||||
data-l10n-id="sidenav-attachment-preview"
|
||||
|
@ -64,13 +64,13 @@
|
|||
data-l10n-id="sidenav-notes"
|
||||
data-pane="notes"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<html:div class="pin-wrapper" hidden="true">
|
||||
<toolbarbutton
|
||||
disabled="true"
|
||||
data-l10n-id="sidenav-attachment-info"
|
||||
data-pane="attachment-info"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<html:div class="pin-wrapper" hidden="true">
|
||||
<toolbarbutton
|
||||
disabled="true"
|
||||
data-l10n-id="sidenav-attachment-annotations"
|
||||
|
@ -127,7 +127,7 @@
|
|||
set container(val) {
|
||||
if (this._container == val) return;
|
||||
this._container = val;
|
||||
this.render(true);
|
||||
this.render();
|
||||
}
|
||||
|
||||
get contextNotesPane() {
|
||||
|
@ -189,6 +189,7 @@
|
|||
}
|
||||
|
||||
init() {
|
||||
this._buttonContainer = this.querySelector('.inherit-flex');
|
||||
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
|
||||
let pane = toolbarbutton.dataset.pane;
|
||||
|
||||
|
@ -266,14 +267,59 @@
|
|||
this._contextNotesPane && !contextNotesPaneVisible);
|
||||
}
|
||||
|
||||
addPane(paneID) {
|
||||
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
|
||||
if (toolbarbutton) {
|
||||
toolbarbutton.parentElement.hidden = false;
|
||||
return;
|
||||
}
|
||||
let pane = this.container.getPane(paneID);
|
||||
if (!pane) return;
|
||||
let sidenavOptions = {};
|
||||
try {
|
||||
sidenavOptions = JSON.parse(pane.dataset.sidenavOptions);
|
||||
}
|
||||
catch (e) {}
|
||||
let { icon, darkIcon, l10nID, l10nArgs } = sidenavOptions;
|
||||
if (!darkIcon) darkIcon = icon;
|
||||
toolbarbutton = document.createXULElement("toolbarbutton");
|
||||
toolbarbutton.setAttribute("custom", "true");
|
||||
toolbarbutton.dataset.pane = paneID;
|
||||
toolbarbutton.dataset.l10nId = l10nID;
|
||||
toolbarbutton.dataset.l10nArgs = l10nArgs;
|
||||
toolbarbutton.style = `--custom-sidenav-icon-light: url('${icon}'); --custom-sidenav-icon-dark: url('${darkIcon}');`;
|
||||
toolbarbutton.addEventListener('contextmenu', (event) => {
|
||||
this._contextMenuTarget = paneID;
|
||||
this.querySelector('.zotero-menuitem-pin').hidden = this.pinnedPane == paneID;
|
||||
this.querySelector('.zotero-menuitem-unpin').hidden = this.pinnedPane != paneID;
|
||||
this.querySelector('.context-menu')
|
||||
.openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
});
|
||||
|
||||
let container = document.createElement("div");
|
||||
container.classList.add("pin-wrapper");
|
||||
container.classList.add("pinnable");
|
||||
container.append(toolbarbutton);
|
||||
if (this._defaultStatus) toolbarbutton.disabled = true;
|
||||
toolbarbutton.parentElement.hidden = this._defaultStatus || !this.container.getEnabledPane(paneID);
|
||||
this._buttonContainer.append(container);
|
||||
}
|
||||
|
||||
removePane(paneID) {
|
||||
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
|
||||
if (!toolbarbutton) return;
|
||||
toolbarbutton.parentElement.remove();
|
||||
}
|
||||
|
||||
updatePaneStatus(paneID) {
|
||||
if (!paneID) {
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
|
||||
if (!toolbarbutton) return;
|
||||
toolbarbutton.parentElement.hidden = !this.container.getPane(paneID);
|
||||
toolbarbutton.parentElement.hidden = !this.container.getEnabledPane(paneID);
|
||||
if (this.pinnedPane) {
|
||||
if (paneID == this.pinnedPane && !toolbarbutton.parentElement.classList.contains("pinned")) {
|
||||
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
|
||||
|
@ -303,7 +349,7 @@
|
|||
this.querySelectorAll('toolbarbutton').forEach((elem) => {
|
||||
elem.disabled = false;
|
||||
});
|
||||
this.render(true);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,18 +42,14 @@ import { getCSSIcon } from 'components/icons';
|
|||
</popupset>
|
||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
|
||||
_item = null;
|
||||
|
||||
_linkedItems = [];
|
||||
|
||||
_mode = null;
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
if (item?.isRegularItem()) {
|
||||
if (item?.isRegularItem() && !item?.isFeedItem) {
|
||||
this.hidden = false;
|
||||
}
|
||||
else {
|
||||
|
@ -64,15 +60,6 @@ import { getCSSIcon } from 'components/icons';
|
|||
this._linkedItems = [];
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
this.setAttribute('mode', mode);
|
||||
}
|
||||
|
||||
init() {
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'librariesCollectionsBox');
|
||||
this._body = this.querySelector('.body');
|
||||
|
@ -82,14 +69,14 @@ import { getCSSIcon } from 'components/icons';
|
|||
|
||||
destroy() {
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
this._section.removeEventListener('add', this._handleAdd);
|
||||
this._section?.removeEventListener('add', this._handleAdd);
|
||||
}
|
||||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify'
|
||||
&& this._item
|
||||
&& (ids.includes(this._item.id) || this._linkedItems.some(item => ids.includes(item.id)))) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +114,7 @@ import { getCSSIcon } from 'components/icons';
|
|||
|
||||
row.append(box);
|
||||
|
||||
if (this._mode == 'edit' && obj instanceof Zotero.Collection && !isContext) {
|
||||
if (this.editable && obj instanceof Zotero.Collection && !isContext) {
|
||||
let remove = document.createXULElement('toolbarbutton');
|
||||
remove.className = 'zotero-clicky zotero-clicky-minus';
|
||||
remove.setAttribute("tabindex", "0");
|
||||
|
@ -226,10 +213,10 @@ import { getCSSIcon } from 'components/icons';
|
|||
return row;
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
if (!this._item) return;
|
||||
if (!this._section.open) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
this._body.replaceChildren();
|
||||
|
||||
|
@ -239,15 +226,13 @@ import { getCSSIcon } from 'components/icons';
|
|||
this._addObject(collection, item);
|
||||
}
|
||||
}
|
||||
if (force) {
|
||||
this.secondaryRender();
|
||||
}
|
||||
}
|
||||
|
||||
async secondaryRender() {
|
||||
async asyncRender() {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
if (this._isAlreadyRendered("async")) return;
|
||||
// Skip if already rendered
|
||||
if (this._linkedItems.length > 0) {
|
||||
return;
|
||||
|
|
|
@ -330,9 +330,7 @@
|
|||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append: (...args) => {
|
||||
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
|
||||
}
|
||||
append,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -393,8 +391,8 @@
|
|||
|
||||
set mode(val) {
|
||||
this._mode = val;
|
||||
this._id('related').mode = val;
|
||||
this._id('tags').mode = val;
|
||||
this._id('related').editable = val == "edit";
|
||||
this._id('tags').editable = val == "edit";
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
`);
|
||||
|
||||
init() {
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
this._noteIDs = [];
|
||||
this.initCollapsibleSection();
|
||||
|
@ -45,35 +44,16 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener('add', this._handleAdd);
|
||||
this._section?.removeEventListener('add', this._handleAdd);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(val) {
|
||||
switch (val) {
|
||||
case 'view':
|
||||
case 'merge':
|
||||
case 'mergeedit':
|
||||
case 'edit':
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid mode '${val}'`);
|
||||
}
|
||||
this.setAttribute('mode', val);
|
||||
this._mode = val;
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(val) {
|
||||
if (val?.isRegularItem()) {
|
||||
if (val?.isRegularItem() && !val?.isFeedItem) {
|
||||
this.hidden = false;
|
||||
}
|
||||
else {
|
||||
|
@ -85,15 +65,15 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
|
||||
notify(event, type, ids, _extraData) {
|
||||
if (['modify', 'delete'].includes(event) && ids.some(id => this._noteIDs.includes(id))) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
this._noteIDs = this._item.getNotes();
|
||||
|
||||
|
@ -123,7 +103,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
|
||||
row.append(box);
|
||||
|
||||
if (this._mode == 'edit') {
|
||||
if (this.editable) {
|
||||
let remove = document.createXULElement("toolbarbutton");
|
||||
remove.addEventListener('command', () => this._handleRemove(id));
|
||||
remove.className = 'zotero-clicky zotero-clicky-minus';
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
|
||||
set editable(editable) {
|
||||
this._editable = editable;
|
||||
this.toggleAttribute('readonly', !editable);
|
||||
}
|
||||
|
||||
get libraryID() {
|
||||
|
@ -90,20 +91,21 @@
|
|||
this.node.selectedPanel = selectedPanel;
|
||||
}
|
||||
|
||||
get viewType() {
|
||||
get mode() {
|
||||
return ["notesList", "standaloneNote", "childNote"][this.node.selectedIndex];
|
||||
}
|
||||
|
||||
set viewType(viewType) {
|
||||
let viewTypeMap = {
|
||||
set mode(mode) {
|
||||
let modeMap = {
|
||||
notesList: "0",
|
||||
standaloneNote: "1",
|
||||
childNote: "2",
|
||||
};
|
||||
if (!(viewType in viewTypeMap)) {
|
||||
throw new Error(`NotesContext.viewType must be one of ["notesList", "standaloneNote", "childNote"], but got ${viewType}`);
|
||||
if (!(mode in modeMap)) {
|
||||
throw new Error(`NotesContext.mode must be one of ["notesList", "standaloneNote", "childNote"], but got ${mode}`);
|
||||
}
|
||||
this.node.setAttribute("selectedIndex", viewTypeMap[viewType]);
|
||||
// fx115: setting attribute doesn't work
|
||||
this.node.selectedIndex = modeMap[mode];
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
|
@ -124,8 +126,6 @@
|
|||
|
||||
affectedIDs = new Set();
|
||||
|
||||
updateFromCache = () => this._updateNotesList(true);
|
||||
|
||||
init() {
|
||||
this.node = this.querySelector(".context-node");
|
||||
this.editor = this.querySelector(".zotero-context-pane-pinned-note");
|
||||
|
@ -143,7 +143,7 @@
|
|||
}
|
||||
|
||||
focus() {
|
||||
if (this.viewType == "notesList") {
|
||||
if (this.mode == "notesList") {
|
||||
this.input.focus();
|
||||
return true;
|
||||
}
|
||||
|
@ -218,7 +218,7 @@
|
|||
}
|
||||
|
||||
_createNote(child) {
|
||||
this.viewType = "standaloneNote";
|
||||
this.mode = "standaloneNote";
|
||||
let item = new Zotero.Item('note');
|
||||
item.libraryID = this.libraryID;
|
||||
if (child) {
|
||||
|
@ -236,17 +236,17 @@
|
|||
}
|
||||
|
||||
_isNotesListVisible() {
|
||||
let splitter = ZoteroContextPane.getSplitter();
|
||||
let splitter = ZoteroContextPane.splitter;
|
||||
|
||||
return Zotero_Tabs.selectedID != 'zotero-pane'
|
||||
&& ZoteroContextPane.viewType == "notes"
|
||||
&& this.viewType == "notesList"
|
||||
&& ZoteroContextPane.context.mode == "notes"
|
||||
&& this.mode == "notesList"
|
||||
&& splitter.getAttribute('state') != 'collapsed';
|
||||
}
|
||||
|
||||
_getCurrentEditor() {
|
||||
let splitter = ZoteroContextPane.getSplitter();
|
||||
if (splitter.getAttribute('state') == 'collapsed' || ZoteroContextPane.viewType != "notes") return null;
|
||||
let splitter = ZoteroContextPane.splitter;
|
||||
if (splitter.getAttribute('state') == 'collapsed' || ZoteroContextPane.context.mode != "notes") return null;
|
||||
return this.node.selectedPanel.querySelector('note-editor');
|
||||
}
|
||||
|
||||
|
@ -289,13 +289,13 @@
|
|||
editor.item = item;
|
||||
editor.parentItem = null;
|
||||
|
||||
this.viewType = "childNote";
|
||||
this.mode = "childNote";
|
||||
tabNotesDeck.setAttribute('selectedIndex', tabNotesDeck.children.length - 1);
|
||||
|
||||
parentTitleContainer = this.querySelector('.context-note-child > .zotero-context-pane-editor-parent-line');
|
||||
}
|
||||
else {
|
||||
this.viewType = "standaloneNote";
|
||||
this.mode = "standaloneNote";
|
||||
editor.mode = this.editable ? 'edit' : 'view';
|
||||
editor.item = item;
|
||||
editor.parentItem = null;
|
||||
|
@ -313,8 +313,8 @@
|
|||
returnBtn.addEventListener("command", () => {
|
||||
// Immediately save note content before vbox with note-editor iframe is destroyed below
|
||||
editor.saveSync();
|
||||
ZoteroContextPane.viewType = "notes";
|
||||
this.viewType = "notesList";
|
||||
ZoteroContextPane.context.mode = "notes";
|
||||
this.mode = "notesList";
|
||||
vbox?.remove();
|
||||
ZoteroContextPane.updateAddToNote();
|
||||
this._preventViewTypeCache = true;
|
||||
|
@ -327,6 +327,10 @@
|
|||
ZoteroContextPane.updateAddToNote();
|
||||
}
|
||||
|
||||
updateNotesListFromCache() {
|
||||
this._updateNotesList(true);
|
||||
}
|
||||
|
||||
async _updateNotesList(useCached) {
|
||||
let query = this.input.value;
|
||||
let notes;
|
||||
|
@ -417,16 +421,15 @@
|
|||
}
|
||||
|
||||
_cacheViewType() {
|
||||
if (ZoteroContextPane.viewType == "notes"
|
||||
&& this.viewType != "childNote" && !this._preventViewTypeCache) {
|
||||
this._cachedViewType = this.viewType;
|
||||
if (ZoteroContextPane.context.mode == "notes"
|
||||
&& this.mode != "childNote" && !this._preventViewTypeCache) {
|
||||
this._cachedViewType = this.mode;
|
||||
}
|
||||
this._preventViewTypeCache = false;
|
||||
}
|
||||
|
||||
_restoreViewType() {
|
||||
if (!this._cachedViewType) return;
|
||||
this.viewType = this._cachedViewType;
|
||||
this.mode = this._cachedViewType || "notesList";
|
||||
this._cachedViewType = "";
|
||||
}
|
||||
|
||||
|
|
|
@ -54,8 +54,6 @@
|
|||
|
||||
_titleFieldID = null;
|
||||
|
||||
_mode = null;
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
@ -64,14 +62,6 @@
|
|||
this.blurOpenField();
|
||||
this._item = item;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
}
|
||||
|
||||
init() {
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'paneHeader');
|
||||
|
@ -103,7 +93,7 @@
|
|||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +112,7 @@
|
|||
this.item.setField(this._titleFieldID, this.titleField.value);
|
||||
await this.item.saveTx();
|
||||
}
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
|
||||
async blurOpenField() {
|
||||
|
@ -131,12 +121,12 @@
|
|||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
|
||||
render() {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
|
||||
|
||||
|
@ -149,7 +139,7 @@
|
|||
else {
|
||||
this.titleField.value = title;
|
||||
}
|
||||
this.titleField.readOnly = this._mode == 'view';
|
||||
this.titleField.readOnly = !this.editable;
|
||||
if (this._titleFieldID) {
|
||||
this.titleField.placeholder = Zotero.ItemFields.getLocalizedString(this._titleFieldID);
|
||||
}
|
||||
|
@ -164,9 +154,7 @@
|
|||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append: (...args) => {
|
||||
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
|
||||
}
|
||||
append,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
`);
|
||||
|
||||
init() {
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
|
||||
this.initCollapsibleSection();
|
||||
|
@ -44,34 +43,16 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener('add', this.add);
|
||||
this._section?.removeEventListener('add', this.add);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(val) {
|
||||
switch (val) {
|
||||
case 'view':
|
||||
case 'merge':
|
||||
case 'mergeedit':
|
||||
case 'edit':
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid mode '${val}'`);
|
||||
}
|
||||
this.setAttribute('mode', val);
|
||||
this._mode = val;
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(val) {
|
||||
this.hidden = val?.isFeedItem;
|
||||
this._item = val;
|
||||
}
|
||||
|
||||
|
@ -80,7 +61,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
|
||||
// Refresh if this item has been modified
|
||||
if (event == 'modify' && ids.includes(this._item.id)) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -90,16 +71,16 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
let relatedItemIDs = new Set(this._item.relatedItems.map(key => Zotero.Items.getIDFromLibraryAndKey(libraryID, key)));
|
||||
for (let id of ids) {
|
||||
if (relatedItemIDs.has(id)) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
if (!this.item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
let body = this.querySelector('.body');
|
||||
body.replaceChildren();
|
||||
|
@ -135,7 +116,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
box.appendChild(label);
|
||||
row.append(box);
|
||||
|
||||
if (this._mode == 'edit') {
|
||||
if (this.editable) {
|
||||
let remove = document.createXULElement("toolbarbutton");
|
||||
remove.addEventListener('command', () => this._handleRemove(id));
|
||||
remove.className = 'zotero-clicky zotero-clicky-minus';
|
||||
|
|
|
@ -48,7 +48,6 @@
|
|||
this._tabDirection = null;
|
||||
this._tagColors = [];
|
||||
this._notifierID = null;
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
|
||||
this.initCollapsibleSection();
|
||||
|
@ -90,41 +89,16 @@
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener('add', this._handleAddButtonClick);
|
||||
this._section?.removeEventListener('add', this._handleAddButtonClick);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(val) {
|
||||
this.clickable = false;
|
||||
this.editable = false;
|
||||
|
||||
switch (val) {
|
||||
case 'view':
|
||||
case 'merge':
|
||||
case 'mergeedit':
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
this.clickable = true;
|
||||
this.editable = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid mode ${val}`);
|
||||
}
|
||||
this.setAttribute('mode', val);
|
||||
this._mode = val;
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(val) {
|
||||
this.hidden = val?.isFeedItem;
|
||||
// Don't reload if item hasn't changed
|
||||
if (this._item == val) {
|
||||
return;
|
||||
|
@ -134,7 +108,7 @@
|
|||
|
||||
notify(event, type, ids, extraData) {
|
||||
if (type == 'setting' && ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
else if (type == 'item-tag') {
|
||||
let itemID, _tagID;
|
||||
|
@ -164,13 +138,13 @@
|
|||
this.updateCount();
|
||||
}
|
||||
else if (type == 'tag' && event == 'modify') {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
render() {
|
||||
if (!this.item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
if (this._isAlreadyRendered()) return;
|
||||
|
||||
Zotero.debug('Reloading tags box');
|
||||
|
||||
|
@ -297,7 +271,7 @@
|
|||
await item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -471,7 +445,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -488,7 +462,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +485,7 @@
|
|||
|
||||
tags.forEach(tag => this.item.addTag(tag));
|
||||
await this.item.saveTx();
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
}
|
||||
// Single tag at end
|
||||
else {
|
||||
|
@ -529,7 +503,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.render(true);
|
||||
this._forceRenderAll();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -753,7 +753,7 @@ var Zotero_Tabs = new function () {
|
|||
// Used to move focus back to itemTree or contextPane from the tabs.
|
||||
this.focusWrapAround = function () {
|
||||
// If no item is selected, focus items list.
|
||||
if (ZoteroPane.itemPane.viewType == "message") {
|
||||
if (ZoteroPane.itemPane.mode == "message") {
|
||||
document.getElementById("item-tree-main-default").focus();
|
||||
}
|
||||
else {
|
||||
|
|
335
chrome/content/zotero/xpcom/itemPaneManager.js
Normal file
335
chrome/content/zotero/xpcom/itemPaneManager.js
Normal file
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2024 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 *****
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @typedef SectionIcon
|
||||
* @type {object}
|
||||
* @property {string} icon - Icon URI
|
||||
* @property {string} [darkIcon] - Icon URI in dark mode. If not set, use `icon`
|
||||
* @typedef SectionL10n
|
||||
* @type {object}
|
||||
* @property {string} l10nID - data-l10n-id for localization
|
||||
* @property {string} [l10nArgs] - data-l10n-args for localization
|
||||
* @typedef SectionButton
|
||||
* @type {object}
|
||||
* @property {string} type - Button type, must be valid DOMString and without ","
|
||||
* @property {(props: SectionEventHookArgs) => void} onClick - Button click callback
|
||||
* @typedef SectionBasicHookArgs
|
||||
* @type {object}
|
||||
* @property {string} paneID - Registered pane id
|
||||
* @property {Document} doc - Document of section
|
||||
* @property {HTMLDivElement} body - Section body
|
||||
* @typedef SectionUIHookArgs
|
||||
* @type {object}
|
||||
* @property {Zotero.Item} item - Current item
|
||||
* @property {string} tabType - Current tab type
|
||||
* @property {boolean} editable - Whether the section is in edit mode
|
||||
* @property {(l10nArgs: string) => void} setL10nArgs - Set l10n args for section header
|
||||
* @property {(l10nArgs: string) => void} setEnabled - Set pane enabled state
|
||||
* @property {(summary: string) => void} setSectionSummary - Set pane section summary,
|
||||
* the text shown in the section header when the section is collapsed.
|
||||
*
|
||||
* See the Abstract section as an example
|
||||
* @property {(buttonType: string, options: {disabled?: boolean; hidden?: boolean}) => void} setSectionButtonStatus - Set pane section button status
|
||||
* @typedef SectionHookArgs
|
||||
* @type {SectionBasicHookArgs & SectionUIHookArgs}
|
||||
* @typedef {SectionHookArgs & { refresh: () => Promise<void> }} SectionInitHookArgs
|
||||
* A `refresh` is exposed to plugins to allows plugins to refresh the section when necessary,
|
||||
* e.g. item modify notifier callback. Note that calling `refresh` during initialization
|
||||
* have no effect.
|
||||
* @typedef {SectionHookArgs & { event: Event }} SectionEventHookArgs
|
||||
* @typedef ItemDetailsSectionOptions
|
||||
* @type {object}
|
||||
* @property {string} paneID - Unique pane ID
|
||||
* @property {string} pluginID - Set plugin ID to auto remove section when plugin is disabled/removed
|
||||
* @property {SectionL10n & SectionIcon} head - Header options. Icon should be 16*16 and `label` need to be localized
|
||||
* @property {SectionL10n & SectionIcon} sidenav - Sidenav options. Icon should be 20*20 and `tooltiptext` need to be localized
|
||||
* @property {string} [bodyXHTML] - Pane body's innerHTML, default to XUL namespace
|
||||
* @property {(props: SectionInitHookArgs) => void} [onInit]
|
||||
* Lifecycle hook called when section is initialized.
|
||||
* You can use destructuring assignment to get the props:
|
||||
* ```js
|
||||
* onInit({ paneID, doc, body, item, editable, tabType, setL10nArgs, setEnabled,
|
||||
* setSectionSummary, setSectionButtonStatus, refresh }) {
|
||||
* // Your code here
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Do:
|
||||
* 1. Initialize data if necessary
|
||||
* 2. Set up hooks, e.g. notifier callback
|
||||
*
|
||||
* Don't:
|
||||
* 1. Render/refresh UI
|
||||
* @property {(props: SectionBasicHookArgs) => void} [onDestroy]
|
||||
* Lifecycle hook called when section is destroyed
|
||||
*
|
||||
* Do:
|
||||
* 1. Remove data and release resource
|
||||
* 2. Remove hooks, e.g. notifier callback
|
||||
*
|
||||
* Don't:
|
||||
* 1. Render/refresh UI
|
||||
* @property {(props: SectionHookArgs) => boolean} [onItemChange]
|
||||
* Lifecycle hook called when section's item change received
|
||||
*
|
||||
* Do:
|
||||
* 1. Update data (no need to render or refresh);
|
||||
* 2. Update the section enabled state with `props.setEnabled`. For example, if the section
|
||||
* is only enabled in the readers, you can use:
|
||||
* ```js
|
||||
* onItemChange({ setEnabled }) {
|
||||
* setEnabled(newData.value === "reader");
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Don't:
|
||||
* 1. Render/refresh UI
|
||||
* @property {(props: SectionHookArgs) => void} onRender
|
||||
* Lifecycle hook called when section should do initial render. Cannot be async.
|
||||
*
|
||||
* Create elements and append them to `props.body`.
|
||||
*
|
||||
* If the rendering is slow, you should make the bottleneck async and move it to `onAsyncRender`.
|
||||
*
|
||||
* > Note that the rendering of section is fully controlled by Zotero to minimize resource usage.
|
||||
* > Only render UI things when you are told to.
|
||||
* @property {(props: SectionHookArgs) => void | Promise<void>} [onAsyncRender]
|
||||
* [Optional] Lifecycle hook called when section should do async render
|
||||
*
|
||||
* The best practice to time-consuming rendering with runtime decided section height is:
|
||||
* 1. Compute height and create a box in sync `onRender`;
|
||||
* 2. Render actual contents in async `onAsyncRender`.
|
||||
* @property {(props: SectionEventHookArgs) => void} [onToggle] - Called when section is toggled
|
||||
* @property {SectionButton[]} [sectionButtons] - Section button options
|
||||
*/
|
||||
|
||||
|
||||
class ItemPaneManager {
|
||||
_customSections = {};
|
||||
|
||||
_lastUpdateTime = 0;
|
||||
|
||||
/**
|
||||
* Register a custom section in item pane. All registered sections must be valid, and must have a unique paneID.
|
||||
* @param {ItemDetailsSectionOptions} options - section data
|
||||
* @returns {string | false} - The paneID or false if no section were added
|
||||
*/
|
||||
registerSection(options) {
|
||||
let registeredID = this._addSection(options);
|
||||
if (!registeredID) {
|
||||
return false;
|
||||
}
|
||||
this._addPluginShutdownObserver();
|
||||
this._notifyItemPane();
|
||||
return registeredID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a custom column.
|
||||
* @param {string} paneID - The paneID of the section(s) to unregister
|
||||
* @returns {boolean} true if the column(s) are unregistered
|
||||
*/
|
||||
unregisterSection(paneID) {
|
||||
const success = this._removeSection(paneID);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
this._notifyItemPane();
|
||||
return true;
|
||||
}
|
||||
|
||||
getUpdateTime() {
|
||||
return this._lastUpdateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ItemDetailsSectionOptions[]}
|
||||
*/
|
||||
getCustomSections() {
|
||||
return Object.values(this._customSections).map(opt => Object.assign({}, opt));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ItemDetailsSectionOptions} options
|
||||
* @returns {string | false}
|
||||
*/
|
||||
_addSection(options) {
|
||||
options = Object.assign({}, options);
|
||||
options.paneID = this._namespacedDataKey(options);
|
||||
if (!this._validateSectionOptions(options)) {
|
||||
return false;
|
||||
}
|
||||
this._customSections[options.paneID] = options;
|
||||
return options.paneID;
|
||||
}
|
||||
|
||||
_removeSection(paneID) {
|
||||
// If any check fails, return check results and do not remove any section
|
||||
if (!this._customSections[paneID]) {
|
||||
Zotero.warn(`ItemPaneManager section option with paneID ${paneID} does not exist.`);
|
||||
return false;
|
||||
}
|
||||
delete this._customSections[paneID];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ItemDetailsSectionOptions} options
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_validateSectionOptions(options) {
|
||||
let requiredParamsType = {
|
||||
paneID: "string",
|
||||
pluginID: "string",
|
||||
head: (val) => {
|
||||
if (typeof val != "object") {
|
||||
return "ItemPaneManager section options head must be object";
|
||||
}
|
||||
if (!val.l10nID || typeof val.l10nID != "string") {
|
||||
return "ItemPaneManager section options head l10nID must be non-empty string";
|
||||
}
|
||||
if (!val.icon || typeof val.icon != "string") {
|
||||
return "ItemPaneManager section options head icon must be non-empty string";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
sidenav: (val) => {
|
||||
if (typeof val != "object") {
|
||||
return "ItemPaneManager section options sidenav must be object";
|
||||
}
|
||||
if (!val.l10nID || typeof val.l10nID != "string") {
|
||||
return "ItemPaneManager section options sidenav l10nID must be non-empty string";
|
||||
}
|
||||
if (!val.icon || typeof val.icon != "string") {
|
||||
return "ItemPaneManager section options sidenav icon must be non-empty string";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
// Keep in sync with itemDetails.js
|
||||
let builtInPaneIDs = [
|
||||
"info",
|
||||
"abstract",
|
||||
"attachments",
|
||||
"notes",
|
||||
"attachment-info",
|
||||
"attachment-annotations",
|
||||
"libraries-collections",
|
||||
"tags",
|
||||
"related"
|
||||
];
|
||||
for (let key of Object.keys(requiredParamsType)) {
|
||||
let val = options[key];
|
||||
if (!val) {
|
||||
Zotero.warn(`ItemPaneManager section option must have ${key}`);
|
||||
return false;
|
||||
}
|
||||
let requiredType = requiredParamsType[key];
|
||||
if (typeof requiredType == "string" && typeof val != requiredType) {
|
||||
Zotero.warn(`ItemPaneManager section option ${key} must be ${requiredType}, but got ${typeof val}`);
|
||||
return false;
|
||||
}
|
||||
if (typeof requiredType == "function") {
|
||||
let result = requiredType(val);
|
||||
if (result !== true) {
|
||||
Zotero.warn(result);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (builtInPaneIDs.includes(options.paneID)) {
|
||||
Zotero.warn(`ItemPaneManager section option paneID must not conflict with built-in paneID, but got ${options.paneID}`);
|
||||
return false;
|
||||
}
|
||||
if (this._customSections[options.paneID]) {
|
||||
Zotero.warn(`ItemPaneManager section option paneID must be unique, but got ${options.paneID}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the dataKey is namespaced with the plugin ID
|
||||
* @param {ItemDetailsSectionOptions} options
|
||||
* @returns {string}
|
||||
*/
|
||||
_namespacedDataKey(options) {
|
||||
if (options.pluginID && options.paneID) {
|
||||
// Make sure the return value is valid as class name or element id
|
||||
return `${options.pluginID}-${options.paneID}`.replace(/[^a-zA-Z0-9-_]/g, "-");
|
||||
}
|
||||
return options.paneID;
|
||||
}
|
||||
|
||||
async _notifyItemPane() {
|
||||
this._lastUpdateTime = new Date().getTime();
|
||||
await Zotero.DB.executeTransaction(async function () {
|
||||
Zotero.Notifier.queue(
|
||||
'refresh',
|
||||
'itempane',
|
||||
[],
|
||||
{},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all columns registered by a plugin
|
||||
* @param {string} pluginID - Plugin ID
|
||||
*/
|
||||
async _unregisterSectionByPluginID(pluginID) {
|
||||
let paneIDs = Object.keys(this._customSections).filter(id => this._customSections[id].pluginID == pluginID);
|
||||
if (paneIDs.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
|
||||
paneIDs.forEach(id => this._removeSection(id));
|
||||
Zotero.debug(`ItemPaneManager sections registered by plugin ${pluginID} unregistered due to shutdown`);
|
||||
await this._notifyItemPane();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the shutdown observer is added
|
||||
* @returns {void}
|
||||
*/
|
||||
_addPluginShutdownObserver() {
|
||||
if (this._observerAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.Plugins.addObserver({
|
||||
shutdown: ({ id: pluginID }) => {
|
||||
this._unregisterSectionByPluginID(pluginID);
|
||||
}
|
||||
});
|
||||
this._observerAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.ItemPaneManager = new ItemPaneManager();
|
|
@ -34,7 +34,7 @@ Zotero.Notifier = new function(){
|
|||
'collection', 'search', 'share', 'share-items', 'item', 'file',
|
||||
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash',
|
||||
'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key', 'tab',
|
||||
'itemtree'
|
||||
'itemtree', 'itempane'
|
||||
];
|
||||
var _transactionID = false;
|
||||
var _queue = {};
|
||||
|
|
|
@ -907,8 +907,9 @@ class ReaderInstance {
|
|||
let rect = this._iframe.getBoundingClientRect();
|
||||
x += rect.left;
|
||||
y += rect.top;
|
||||
tagsbox.mode = 'edit';
|
||||
tagsbox.editable = true;
|
||||
tagsbox.item = item;
|
||||
tagsbox.render();
|
||||
menupopup.openPopup(null, 'before_start', x, y, true);
|
||||
setTimeout(() => {
|
||||
if (tagsbox.count == 0) {
|
||||
|
@ -1125,7 +1126,10 @@ class ReaderTab extends ReaderInstance {
|
|||
|
||||
_addToNote(annotations) {
|
||||
annotations = annotations.map(x => ({ ...x, attachmentItemID: this._item.id }));
|
||||
let noteEditor = this._window.ZoteroContextPane && this._window.ZoteroContextPane.getActiveEditor();
|
||||
if (!this._window.ZoteroContextPane) {
|
||||
return;
|
||||
}
|
||||
let noteEditor = this._window.ZoteroContextPane.activeEditor;
|
||||
if (!noteEditor) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ const xpcomFilesLocal = [
|
|||
'id',
|
||||
'integration',
|
||||
'itemTreeManager',
|
||||
'itemPaneManager',
|
||||
'locale',
|
||||
'locateManager',
|
||||
'mime',
|
||||
|
|
|
@ -1221,7 +1221,7 @@ var ZoteroPane = new function()
|
|||
if (this.itemsView && from == this.itemsView.id) {
|
||||
// Focus TinyMCE explicitly on tab key, since the normal focusing doesn't work right
|
||||
if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) {
|
||||
if (ZoteroPane.itemPane.viewType == "note") {
|
||||
if (ZoteroPane.itemPane.mode == "note") {
|
||||
document.getElementById('zotero-note-editor').focus();
|
||||
event.preventDefault();
|
||||
return;
|
||||
|
@ -1370,7 +1370,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
|
||||
yield this.itemPane._itemDetails.blurOpenField();
|
||||
yield this.itemPane.handleBlur();
|
||||
|
||||
if (row !== undefined && row !== null) {
|
||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
||||
|
@ -1868,14 +1868,8 @@ var ZoteroPane = new function()
|
|||
// Display buttons at top of item pane depending on context. This needs to run even if the
|
||||
// selection hasn't changed, because the selected items might have been modified.
|
||||
this.itemPane.data = selectedItems;
|
||||
let viewMode = {
|
||||
isFeedsOrFeed: collectionTreeRow.isFeedsOrFeed(),
|
||||
isDuplicates: collectionTreeRow.isDuplicates(),
|
||||
isPublications: collectionTreeRow.isPublications(),
|
||||
isTrash: collectionTreeRow.isTrash(),
|
||||
rowCount: this.itemsView.rowCount,
|
||||
};
|
||||
this.itemPane.viewMode = viewMode;
|
||||
this.itemPane.collectionTreeRow = collectionTreeRow;
|
||||
this.itemPane.itemsView = this.itemsView;
|
||||
this.itemPane.editable = this.collectionsView.editable;
|
||||
this.itemPane.updateItemPaneButtons(selectedItems);
|
||||
|
||||
|
@ -2245,11 +2239,11 @@ var ZoteroPane = new function()
|
|||
return;
|
||||
}
|
||||
|
||||
this.itemPane.viewType = "duplicates";
|
||||
this.itemPane.mode = "duplicates";
|
||||
|
||||
// Initialize the merge pane with the selected items
|
||||
this.itemPane._duplicatesPane.setItems(this.getSelectedItems());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.deleteSelectedCollection = function (deleteItems) {
|
||||
|
|
|
@ -93,4 +93,5 @@
|
|||
@import "elements/itemMessagePane";
|
||||
@import "elements/itemDetails";
|
||||
@import "elements/itemPane";
|
||||
@import "elements/itemPaneCustomSection";
|
||||
@import "elements/contextPane";
|
||||
|
|
|
@ -62,14 +62,6 @@ collapsible-section {
|
|||
|
||||
toolbarbutton.add {
|
||||
@include svgicon-menu("plus", "universal", "16");
|
||||
|
||||
&:hover {
|
||||
background: var(--fill-quinary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--fill-quarternary);
|
||||
}
|
||||
}
|
||||
|
||||
toolbarbutton.twisty .toolbarbutton-icon {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
context-pane {
|
||||
-moz-box-orient: vertical;
|
||||
}
|
||||
|
|
45
scss/elements/_itemPaneCustomSection.scss
Normal file
45
scss/elements/_itemPaneCustomSection.scss
Normal file
|
@ -0,0 +1,45 @@
|
|||
item-pane-custom-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
collapsible-section {
|
||||
&[custom] > .head {
|
||||
& .title::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
|
||||
fill: currentColor;
|
||||
stroke: currentColor;
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
background: var(--custom-section-icon-light) no-repeat center;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background: var(--custom-section-icon-dark) no-repeat center;
|
||||
}
|
||||
}
|
||||
|
||||
toolbarbutton.section-custom-button {
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
@media (prefers-color-scheme: light) {
|
||||
list-style-image: var(--custom-button-icon-light)
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
list-style-image: var(--custom-button-icon-dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,6 +107,18 @@ item-pane-sidenav {
|
|||
fill: var(--fill-secondary);
|
||||
stroke: var(--fill-secondary);
|
||||
}
|
||||
|
||||
&[custom] {
|
||||
@media (prefers-color-scheme: light) {
|
||||
list-style-image: var(--custom-sidenav-icon-light);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
list-style-image: var(--custom-sidenav-icon-dark);
|
||||
}
|
||||
fill: var(--fill-secondary);
|
||||
stroke: var(--fill-secondary);
|
||||
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
&.stacked toolbarbutton[data-pane="toggle-collapse"] {
|
||||
|
|
|
@ -54,7 +54,7 @@ libraries-collections-box {
|
|||
}
|
||||
}
|
||||
|
||||
&:not([mode=edit]) {
|
||||
&[readonly] {
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ notes-box, related-box {
|
|||
display: none;
|
||||
}
|
||||
|
||||
&:not([mode=edit]) {
|
||||
&[readonly] {
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
tags-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
|
@ -72,7 +76,7 @@ tags-box {
|
|||
}
|
||||
}
|
||||
|
||||
&:not([mode=edit]) {
|
||||
&[readonly] {
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -100,11 +100,6 @@ tab {
|
|||
-moz-box-align: center;
|
||||
}
|
||||
|
||||
#output {
|
||||
height:200px;
|
||||
background: var(--material-background);
|
||||
}
|
||||
|
||||
richlistbox {
|
||||
min-width:200px;
|
||||
}
|
||||
|
@ -129,11 +124,13 @@ vbox > splitter {
|
|||
|
||||
#left-tabbox {
|
||||
margin: 5px;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#checkboxes-translatorType checkbox {
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
#checkboxes-translatorType {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#tabpanel-metadata label:first-child {
|
||||
|
@ -143,7 +140,11 @@ vbox > splitter {
|
|||
#right-pane {
|
||||
margin: -16px -16px -16px 0px;
|
||||
border-left: var(--material-panedivider);
|
||||
textarea {
|
||||
|
||||
#output {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--material-background);
|
||||
outline: none;
|
||||
border: 0;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue