Implement ItemPane custom section API

Unify properties as getter in ContextPane
Replace section `mode` with `editable`
Remove section `inTrash`
This commit is contained in:
windingwind 2024-02-01 21:08:03 +08:00 committed by Dan Stillman
parent b34859d882
commit 41acf4c737
41 changed files with 1077 additions and 462 deletions

View file

@ -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>

View file

@ -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') {

View file

@ -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'));
}
}

View file

@ -38,8 +38,6 @@
_annotation = null;
_mode = null;
_listenerAdded = false;
static get observedAttributes() {

View file

@ -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);

View file

@ -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() {

View file

@ -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));
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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();
};
}

View file

@ -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() {

View file

@ -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 {

View file

@ -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() {

View file

@ -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)());

View file

@ -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();
});
}
};

View file

@ -59,9 +59,7 @@
};
if (callback) callback({
doc: document,
append: (...args) => {
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
}
append,
});
}
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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();
}

View file

@ -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';

View file

@ -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 = "";
}

View file

@ -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,
});
}
}

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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 {

View 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();

View file

@ -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 = {};

View file

@ -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;
}

View file

@ -107,6 +107,7 @@ const xpcomFilesLocal = [
'id',
'integration',
'itemTreeManager',
'itemPaneManager',
'locale',
'locateManager',
'mime',

View file

@ -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) {

View file

@ -93,4 +93,5 @@
@import "elements/itemMessagePane";
@import "elements/itemDetails";
@import "elements/itemPane";
@import "elements/itemPaneCustomSection";
@import "elements/contextPane";

View file

@ -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 {

View file

@ -1,2 +1,3 @@
context-pane {
-moz-box-orient: vertical;
}

View 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)
}
}
}
}
}

View file

@ -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"] {

View file

@ -54,7 +54,7 @@ libraries-collections-box {
}
}
&:not([mode=edit]) {
&[readonly] {
.add {
display: none;
}

View file

@ -7,7 +7,7 @@ notes-box, related-box {
display: none;
}
&:not([mode=edit]) {
&[readonly] {
.add {
display: none;
}

View file

@ -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;
}

View file

@ -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;
}