@@ -102,12 +104,8 @@
`, ['chrome://zotero/locale/zotero.dtd']);
}
- connectedCallback() {
- this._destroyed = false;
- window.addEventListener("unload", this.destroy);
-
- this.appendChild(document.importNode(this.content, true));
-
+ init() {
+ this.initCollapsibleSection();
this._creatorTypeMenu.addEventListener('command', async (event) => {
var typeBox = document.popupNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
@@ -129,7 +127,7 @@
}
});
- this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (event) => {
+ this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
var row = document.popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
@@ -185,7 +183,6 @@
break;
}
this.moveCreator(index, dir);
- return;
}
else if (event.explicitOriginalTarget.id == "creator-transform-switch") {
// Switch creator field mode action
@@ -193,7 +190,6 @@
var lastName = creatorNameBox.firstChild;
let fieldMode = parseInt(lastName.getAttribute("fieldMode"));
this.switchCreatorMode(row, fieldMode == 1 ? 0 : 1, false, true, index);
- return;
}
});
@@ -235,7 +231,7 @@
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox');
Zotero.Prefs.registerObserver('fontSize', () => {
- this.refresh();
+ this.render(true);
});
this.style.setProperty('--comma-character',
@@ -243,24 +239,9 @@
}
destroy() {
- if (this._destroyed) {
- return;
- }
- window.removeEventListener("unload", this.destroy);
- this._destroyed = true;
-
Zotero.Notifier.unregisterObserver(this._notifierID);
}
- disconnectedCallback() {
- // Empty the DOM. We will rebuild if reconnected.
- while (this.lastChild) {
- this.removeChild(this.lastChild);
- }
- this.destroy();
- }
-
-
//
// Public properties
//
@@ -340,7 +321,6 @@
this._item = val;
this._lastTabIndex = null;
this.scrollToTop();
- this.refresh();
}
// .ref is an alias for .item
@@ -484,18 +464,24 @@
if (document.activeElement == this.itemTypeMenu) {
this._selectField = "item-type-menu";
}
- this.refresh();
+ this.render(true);
break;
}
}
- refresh() {
+ render(force = false) {
Zotero.debug('Refreshing item box');
if (!this.item) {
Zotero.debug('No item to refresh', 2);
return;
}
+ if (!this._section.open) return;
+
+ // Always update retraction status
+ this.updateRetracted();
+
+ if (!force && this._isAlreadyRendered()) return;
this.updateRetracted();
@@ -672,14 +658,14 @@
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
// eslint-disable-next-line no-loop-func
let triggerPopup = (e) => {
- let menupopup = ZoteroItemPane.buildFieldTransformMenu({
+ let menupopup = ZoteroPane.buildFieldTransformMenu({
target: valueElement,
onTransform: (newValue) => {
this._setFieldTransformedValue(valueElement, newValue);
}
});
this.querySelector('popupset').append(menupopup);
- menupopup.addEventListener('popuphidden', (e) => {
+ menupopup.addEventListener('popuphidden', () => {
menupopup.remove();
optionsButton.style.visibility = '';
});
@@ -722,7 +708,7 @@
menuitem.getAttribute('fieldname'),
menuitem.getAttribute('originalValue')
);
- this.refresh();
+ this.render(true);
});
popup.appendChild(menuitem);
}
@@ -879,7 +865,7 @@
}
this._refreshed = true;
// Add tabindex=0 to all focusable element
- this.querySelectorAll("[ztabindex]").forEach((node) =>{
+ this.querySelectorAll("[ztabindex]").forEach((node) => {
node.setAttribute("tabindex", 0);
});
// Make sure that any opened popup closes
@@ -1155,7 +1141,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.refresh();
+ this.render(true);
}
});
@@ -1200,12 +1186,12 @@
rowData.setAttribute("ztabindex", ++this._ztabindex);
rowData.addEventListener('click', () => {
this._displayAllCreators = true;
- this.refresh();
+ this.render(true);
});
rowData.addEventListener('keypress', (e) => {
if (["Enter", ' '].includes(e.key)) {
this._displayAllCreators = true;
- this.refresh();
+ this.render(true);
}
});
rowData.textContent = Zotero.getString('general.numMore', num);
@@ -1421,7 +1407,7 @@
await this.item.saveTx();
}
else {
- this.refresh();
+ this.render(true);
}
functionsToRun.forEach(f => f.bind(this)());
@@ -1512,7 +1498,7 @@
valueElement.setAttribute('tight', true);
valueElement.addEventListener("focus", e => this.updateLastFocused(e));
- valueElement.addEventListener("keypress", (e) => this.handleKeyPress(e));
+ valueElement.addEventListener("keypress", e => this.handleKeyPress(e));
switch (fieldName) {
case 'itemType':
valueElement.setAttribute('itemTypeID', valueText);
diff --git a/chrome/content/zotero/elements/itemDetails.js b/chrome/content/zotero/elements/itemDetails.js
new file mode 100644
index 0000000000..b975d22e1e
--- /dev/null
+++ b/chrome/content/zotero/elements/itemDetails.js
@@ -0,0 +1,575 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2024 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.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
.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+{
+ const AsyncFunction = (async () => {}).constructor;
+
+ const waitFrame = async () => {
+ return waitNoLongerThan(new Promise((resolve) => {
+ requestAnimationFrame(resolve);
+ }), 30);
+ };
+
+ const waitFrames = async (n) => {
+ for (let i = 0; i < n; i++) {
+ await waitFrame();
+ }
+ };
+
+ const waitNoLongerThan = async (promise, ms = 1000) => {
+ return Promise.race([
+ promise,
+ Zotero.Promise.delay(ms)
+ ]);
+ };
+
+ class ItemDetails extends XULElementBase {
+ content = MozXULElement.parseXULToFragment(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+
+ get item() {
+ return this._item;
+ }
+
+ set item(item) {
+ this._item = item;
+ }
+
+ get mode() {
+ return this._mode;
+ }
+
+ set mode(mode) {
+ this._mode = mode;
+ }
+
+ get pinnedPane() {
+ return this.getAttribute('pinnedPane');
+ }
+
+ set pinnedPane(val) {
+ if (!val || !this.getPane(val)) {
+ val = '';
+ }
+ this.setAttribute('pinnedPane', val);
+ if (val) {
+ this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
+ }
+ this.sidenav.updatePaneStatus(val);
+ }
+
+ get _minScrollHeight() {
+ return parseFloat(this._paneParent.style.getPropertyValue('--min-scroll-height') || 0);
+ }
+
+ set _minScrollHeight(val) {
+ this._paneParent.style.setProperty('--min-scroll-height', val + 'px');
+ }
+
+ get _collapsed() {
+ let collapsible = this.closest('splitter:not([hidden="true"]) + *');
+ if (!collapsible) return false;
+ return collapsible.getAttribute('collapsed') === 'true';
+ }
+
+ set _collapsed(val) {
+ let collapsible = this.closest('splitter:not([hidden="true"]) + *');
+ if (!collapsible) return;
+ let splitter = collapsible.previousElementSibling;
+ if (val) {
+ collapsible.setAttribute('collapsed', 'true');
+ collapsible.removeAttribute("width");
+ collapsible.removeAttribute("height");
+ splitter.setAttribute('state', 'collapsed');
+ splitter.setAttribute('substate', 'after');
+ }
+ else {
+ collapsible.removeAttribute('collapsed');
+ splitter.setAttribute('state', '');
+ splitter.setAttribute('substate', 'after');
+ }
+ window.dispatchEvent(new Event('resize'));
+ }
+
+ get sidenav() {
+ return this._sidenav;
+ }
+
+ set sidenav(sidenav) {
+ this._sidenav = sidenav;
+ sidenav.container = this;
+ }
+
+ static get observedAttributes() {
+ return ['pinnedPane'];
+ }
+
+ init() {
+ this._container = this.querySelector('#zotero-view-item-container');
+ this._header = this.querySelector('#zotero-item-pane-header');
+ this._paneParent = this.querySelector('#zotero-view-item');
+
+ this._container.addEventListener("keypress", this._handleKeypress);
+ this._paneParent.addEventListener('scroll', this._handleContainerScroll);
+
+ this._paneHiddenOb = new MutationObserver(this._handlePaneStatus);
+ this._paneHiddenOb.observe(this._paneParent, {
+ attributes: true,
+ attributeFilter: ["hidden"],
+ subtree: true,
+ });
+ this._initIntersectionObserver();
+
+ this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'ItemDetails');
+
+ this._disableScrollHandler = false;
+ this._pinnedPaneMinScrollHeight = 0;
+ }
+
+ destroy() {
+ this._container.removeEventListener("keypress", this._handleKeypress);
+ this._paneParent.removeEventListener('scroll', this._handleContainerScroll);
+
+ this._paneHiddenOb.disconnect();
+ this._intersectionOb.disconnect();
+
+ Zotero.Notifier.unregisterObserver(this._unregisterID);
+ }
+
+ async render() {
+ if (!this.initialized || !this.item) {
+ return;
+ }
+ let item = this.item;
+ Zotero.debug('Viewing item');
+ this._isRendering = true;
+
+ 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.item = item;
+ box.inTrash = inTrash;
+ box.tabType = tabType;
+ // Render sync boxes immediately
+ if (!box.hidden && box.render) {
+ if (box.render instanceof AsyncFunction) {
+ pendingBoxes.push(box);
+ }
+ else {
+ box.render();
+ }
+ }
+ }
+
+ let pinnedPaneElem = this.getPane(this.pinnedPane);
+ let pinnedIndex = panes.indexOf(pinnedPaneElem);
+
+ this._paneParent.style.paddingBottom = '';
+ if (pinnedPaneElem) {
+ let paneID = pinnedPaneElem.dataset.pane;
+ this.scrollToPane(paneID, 'instant');
+ this.pinnedPane = paneID;
+ }
+ else {
+ this._paneParent.scrollTo(0, 0);
+ }
+
+ // Only render visible panes
+ for (let box of pendingBoxes) {
+ 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);
+ }
+ if (this.item.id == item.id) {
+ this._isRendering = false;
+ }
+ }
+
+ renderCustomSections() {
+ let lastUpdate = Zotero.ItemPaneManager.getUpdateTime();
+ if (this._lastUpdateCustomSection == lastUpdate) return;
+ this._lastUpdateCustomSection = lastUpdate;
+
+ let targetPanes = Zotero.ItemPaneManager.getCustomSections();
+ let currentPaneElements = this.getCustomPanes();
+ // Remove
+ for (let elem of currentPaneElements) {
+ let elemPaneID = elem.dataset.pane;
+ if (targetPanes.find(pane => pane.paneID == elemPaneID)) continue;
+ this._intersectionOb.unobserve(elem);
+ elem.remove();
+ this.sidenav.removePane(elemPaneID);
+ }
+ // 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,
+ 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.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: "render", callback: onRender });
+ elem.registerHook({ type: "secondaryRender", callback: onSecondaryRender });
+ elem.registerHook({ type: "toggle", callback: onToggle });
+ if (sectionButtons) {
+ for (let buttonOptions of sectionButtons) {
+ elem.registerSectionButton(buttonOptions);
+ }
+ }
+ this._paneParent.append(elem);
+ elem.setL10nID(head.l10nID);
+ elem.setL10nArgs(head.l10nArgs);
+ this._intersectionOb.observe(elem);
+ this.sidenav.addPane(paneID);
+ }
+ }
+
+ renderCustomHead(callback) {
+ this._header.renderCustomHead(callback);
+ }
+
+ notify = async (action, _type, _ids, _extraData) => {
+ if (action == 'refresh' && this.item) {
+ await this.render();
+ }
+ };
+
+ getPane(id) {
+ return this._paneParent.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
+ }
+
+ getPanes() {
+ return Array.from(this._paneParent.querySelectorAll(':scope > [data-pane]'));
+ }
+
+ getEnabledPanes() {
+ return Array.from(this._paneParent.querySelectorAll(':scope > [data-pane]:not([hidden])'));
+ }
+
+ getVisiblePanes() {
+ let panes = this.getPanes();
+ let visiblePanes = [];
+ for (let paneElem of panes) {
+ if (this.isPaneVisible(paneElem.dataset.pane)) {
+ visiblePanes.push(paneElem);
+ }
+ else if (visiblePanes.length > 0) {
+ // Early stop at first invisible pane after some visible panes
+ break;
+ }
+ }
+ return visiblePanes;
+ }
+
+ isPaneVisible(paneID) {
+ let paneElem = this.getPane(paneID);
+ if (!paneElem) return false;
+ let paneRect = paneElem.getBoundingClientRect();
+ let containerRect = this._paneParent.getBoundingClientRect();
+ if (paneRect.top >= containerRect.bottom || paneRect.bottom <= containerRect.top) {
+ return false;
+ }
+ return true;
+ }
+
+ async scrollToPane(paneID, behavior = 'smooth') {
+ let pane = this.getPane(paneID);
+ if (!pane) return null;
+
+ let scrollPromise;
+
+ // If the itemPane is collapsed, just remember which pane needs to be scrolled to
+ // when itemPane is expanded.
+ if (this._collapsed || this.getAttribute("no-render")) {
+ return null;
+ }
+
+ // The pane should always be at the very top
+ // If there isn't enough stuff below it for it to be at the top, we add padding
+ // We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
+ this._makeSpaceForPane(pane);
+ if (behavior == 'smooth') {
+ this._disableScrollHandler = true;
+ scrollPromise = this._waitForScroll();
+ scrollPromise.then(() => this._disableScrollHandler = false);
+ }
+ pane.scrollIntoView({ block: 'start', behavior });
+ pane.focus();
+ return scrollPromise;
+ }
+
+ _makeSpaceForPane(pane) {
+ let oldMinScrollHeight = this._minScrollHeight;
+ let newMinScrollHeight = this._getMinScrollHeightForPane(pane);
+ if (newMinScrollHeight > oldMinScrollHeight) {
+ this._minScrollHeight = newMinScrollHeight;
+ }
+ }
+
+ _getMinScrollHeightForPane(pane) {
+ let paneRect = pane.getBoundingClientRect();
+ let containerRect = this._paneParent.getBoundingClientRect();
+ // No offsetTop property for XUL elements
+ let offsetTop = paneRect.top - containerRect.top + this._paneParent.scrollTop;
+ return offsetTop + containerRect.height;
+ }
+
+ async _waitForScroll() {
+ let scrollPromise = Zotero.Promise.defer();
+ let lastScrollTop = this._paneParent.scrollTop;
+ const checkScrollStart = () => {
+ // If the scrollTop is not changed, wait for scroll to happen
+ if (lastScrollTop === this._paneParent.scrollTop) {
+ requestAnimationFrame(checkScrollStart);
+ }
+ // Wait for scroll to end
+ else {
+ requestAnimationFrame(checkScrollEnd);
+ }
+ };
+ const checkScrollEnd = async () => {
+ // Wait for 3 frames to make sure not further scrolls
+ await waitFrames(3);
+ if (lastScrollTop === this._paneParent.scrollTop) {
+ scrollPromise.resolve();
+ }
+ else {
+ lastScrollTop = this._paneParent.scrollTop;
+ requestAnimationFrame(checkScrollEnd);
+ }
+ };
+ checkScrollStart();
+ // Abort after 3 seconds, which should be enough
+ return Promise.race([
+ scrollPromise.promise,
+ Zotero.Promise.delay(3000)
+ ]);
+ }
+
+ async blurOpenField() {
+ let panes = [this._header, ...this.getPanes()];
+ for (let pane of panes) {
+ if (pane.blurOpenField && pane.contains(document.activeElement)) {
+ await pane.blurOpenField();
+ break;
+ }
+ }
+ this._paneParent.focus();
+ }
+
+ _initIntersectionObserver() {
+ if (this._intersectionOb) {
+ this._intersectionOb.disconnect();
+ }
+ this._intersectionOb = new IntersectionObserver(this._handleIntersection);
+ this.getPanes().forEach(elem => this._intersectionOb.observe(elem));
+ }
+
+ _handleContainerScroll = () => {
+ // Don't scroll hidden pane
+ if (this.hidden || this._disableScrollHandler) return;
+
+ let minHeight = this._minScrollHeight;
+ if (minHeight) {
+ let newMinScrollHeight = this._paneParent.scrollTop + this._paneParent.clientHeight;
+ // 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) {
+ return;
+ }
+ this._minScrollHeight = newMinScrollHeight;
+ }
+ };
+
+ // Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
+ _handleKeypress = (event) => {
+ let stopEvent = () => {
+ event.preventDefault();
+ event.stopPropagation();
+ };
+ let isLibraryTab = Zotero_Tabs.selectedIndex == 0;
+ let sidenav = document.getElementById(
+ isLibraryTab ? 'zotero-view-item-sidenav' : 'zotero-context-pane-sidenav'
+ );
+
+ // Shift-tab from title when reader is opened focuses the last button in tabs toolbar
+ if (event.target.closest(".title") && event.key == "Tab"
+ && event.shiftKey && Zotero_Tabs.selectedType == "reader") {
+ let focusable = [...document.querySelectorAll("#zotero-tabs-toolbar toolbarbutton:not([disabled]):not([hidden])")];
+ let btn = focusable[focusable.length - 1];
+ btn.focus();
+ stopEvent();
+ return;
+ }
+ // Tab from the scrollable area focuses the pinned pane if it exists
+ if (event.target.classList.contains("zotero-view-item") && event.key == "Tab" && !event.shiftKey && sidenav.pinnedPane) {
+ let pane = sidenav.getPane(sidenav.pinnedPane);
+ pane.firstChild._head.focus();
+ stopEvent();
+ return;
+ }
+ // Tab tavigation between entries and buttons within library, related and notes boxes
+ if (event.key == "Tab" && event.target.closest(".box")) {
+ let next = null;
+ if (event.key == "Tab" && !event.shiftKey) {
+ next = event.target.nextElementSibling;
+ }
+ if (event.key == "Tab" && event.shiftKey) {
+ next = event.target.parentNode.previousElementSibling?.lastChild;
+ }
+ // Force the element to be visible before focusing
+ if (next) {
+ next.style.visibility = "visible";
+ next.focus();
+ next.style.removeProperty("visibility");
+ stopEvent();
+ }
+ }
+ };
+
+ _handlePaneStatus = (muts) => {
+ for (let mut of muts) {
+ let paneID = mut.target.dataset.pane;
+ if (paneID) {
+ this.sidenav.updatePaneStatus(paneID);
+ }
+ }
+ };
+
+ _handleIntersection = async (entries) => {
+ if (this._isRendering) return;
+ let needsRefresh = [];
+ let needsDiscard = [];
+ entries.forEach((entry) => {
+ let targetPaneElem = entry.target;
+ if (entry.isIntersecting && targetPaneElem.render) {
+ needsRefresh.push(targetPaneElem);
+ }
+ else if (targetPaneElem.discard) {
+ needsDiscard.push(targetPaneElem);
+ }
+ });
+ let needsCheckVisibility = false;
+ // Sidenav is in smooth scrolling mode
+ if (this._disableScrollHandler) {
+ // Wait for scroll to finish
+ await this._waitForScroll();
+ needsCheckVisibility = true;
+ }
+ if (needsRefresh.length > 0) {
+ needsRefresh.forEach(async (paneElem) => {
+ if (needsCheckVisibility && !this.isPaneVisible(paneElem.dataset.pane)) {
+ return;
+ }
+ await paneElem.render();
+ if (paneElem.secondaryRender) await paneElem.secondaryRender();
+ });
+ }
+ if (needsDiscard.length > 0) {
+ needsDiscard.forEach((paneElem) => {
+ if (needsCheckVisibility && this.isPaneVisible(paneElem.dataset.pane)) {
+ return;
+ }
+ paneElem.discard();
+ });
+ }
+ };
+ }
+
+ customElements.define("item-details", ItemDetails);
+}
diff --git a/chrome/content/zotero/elements/itemMessagePane.js b/chrome/content/zotero/elements/itemMessagePane.js
new file mode 100644
index 0000000000..216ab7520c
--- /dev/null
+++ b/chrome/content/zotero/elements/itemMessagePane.js
@@ -0,0 +1,70 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2024 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.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
.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+{
+ class ItemMessagePane extends XULElementBase {
+ content = MozXULElement.parseXULToFragment(`
+
+
+
+
+ `);
+
+ init() {
+ this._messageBox = this.querySelector('#zotero-item-pane-message-box');
+ }
+
+ render(content) {
+ this._messageBox.textContent = '';
+ if (typeof content == 'string') {
+ let contentParts = content.split("\n\n");
+ for (let part of contentParts) {
+ let desc = document.createXULElement('description');
+ desc.appendChild(document.createTextNode(part));
+ this._messageBox.appendChild(desc);
+ }
+ }
+ else {
+ this._messageBox.appendChild(content);
+ }
+ }
+
+ renderCustomHead(callback) {
+ let customHead = this.querySelector(".custom-head");
+ customHead.replaceChildren();
+ let append = (...args) => {
+ customHead.append(...args);
+ };
+ if (callback) callback({
+ doc: document,
+ append: (...args) => {
+ append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
+ }
+ });
+ }
+ }
+
+ customElements.define("item-message-pane", ItemMessagePane);
+}
diff --git a/chrome/content/zotero/elements/itemPane.js b/chrome/content/zotero/elements/itemPane.js
new file mode 100644
index 0000000000..63ba82d8b0
--- /dev/null
+++ b/chrome/content/zotero/elements/itemPane.js
@@ -0,0 +1,502 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2024 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.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
.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+{
+ class ItemPane extends XULElementBase {
+ content = MozXULElement.parseXULToFragment(`
+
+
+
+
+
+
+
+
+
+
+ `);
+
+ init() {
+ this._itemDetails = this.querySelector("#zotero-item-details");
+ this._noteEditor = this.querySelector("#zotero-note-editor");
+ this._duplicatesPane = this.querySelector("#zotero-duplicates-merge-pane");
+ this._messagePane = this.querySelector("#zotero-item-message");
+ this._sidenav = this.querySelector("#zotero-view-item-sidenav");
+ this._deck = this.querySelector("#zotero-item-pane-content");
+
+ this._itemDetails.sidenav = this._sidenav;
+
+ this._notifierID = Zotero.Notifier.registerObserver(this, ['item']);
+
+ this._translationTarget = null;
+ }
+
+ destroy() {
+ Zotero.Notifier.unregisterObserver(this._notifierID);
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ set data(data) {
+ this._data = data;
+ }
+
+ get viewMode() {
+ return this._viewMode;
+ }
+
+ set viewMode(mode) {
+ this._viewMode = mode;
+ }
+
+ get editable() {
+ return this._editable;
+ }
+
+ set editable(editable) {
+ this._editable = editable;
+ }
+
+ get viewType() {
+ return ["message", "item", "note", "duplicates"][this._deck.selectedIndex];
+ }
+
+ /**
+ * Set view type
+ * @param {"message" | "item" | "note" | "duplicates"} type view type
+ */
+ set viewType(type) {
+ switch (type) {
+ case "message": {
+ this._deck.selectedIndex = 0;
+ break;
+ }
+ case "item": {
+ this._deck.selectedIndex = 1;
+ break;
+ }
+ case "note": {
+ this._deck.selectedIndex = 2;
+ break;
+ }
+ case "duplicates": {
+ this._deck.selectedIndex = 3;
+ break;
+ }
+ }
+ // If item pane is no selected, do not render
+ this._itemDetails.toggleAttribute("no-render", type == "item");
+ this._itemDetails.sidenav.toggleDefaultStatus(type != "item");
+ }
+
+ render() {
+ if (!this.data) return false;
+ let hideSidenav = false;
+ let renderStatus = false;
+ // Single item selected
+ if (this.data.length == 1) {
+ let item = this.data[0];
+
+ if (item.isNote()) {
+ hideSidenav = true;
+ renderStatus = this.renderNoteEditor(item);
+ }
+ else {
+ renderStatus = this.renderItemPane(item);
+ }
+ }
+ // Zero or multiple items selected
+ else {
+ renderStatus = this.renderMessage();
+ }
+ this._sidenav.hidden = hideSidenav;
+ return renderStatus;
+ }
+
+ notify(action, type) {
+ if (type == 'item' && action == 'modify') {
+ if (this.viewMode.isFeedsOrFeed) {
+ this.updateReadLabel();
+ }
+ }
+ }
+
+ renderNoteEditor(item) {
+ this.viewType = "note";
+
+ let noteEditor = document.getElementById('zotero-note-editor');
+ noteEditor.mode = this.editable ? 'edit' : 'view';
+ noteEditor.viewMode = 'library';
+ noteEditor.parent = null;
+ noteEditor.item = item;
+ return true;
+ }
+
+ renderItemPane(item) {
+ this.viewType = "item";
+
+ this._itemDetails.mode = this.editable ? null : "view";
+ this._itemDetails.item = item;
+ this._itemDetails.render();
+
+ if (item.isFeedItem) {
+ let lastTranslationTarget = Zotero.Prefs.get('feeds.lastTranslationTarget');
+ if (lastTranslationTarget) {
+ let id = parseInt(lastTranslationTarget.substr(1));
+ if (lastTranslationTarget[0] == "L") {
+ this._translationTarget = Zotero.Libraries.get(id);
+ }
+ else if (lastTranslationTarget[0] == "C") {
+ this._translationTarget = Zotero.Collections.get(id);
+ }
+ }
+ if (!this._translationTarget) {
+ this._translationTarget = Zotero.Libraries.userLibrary;
+ }
+ this.setTranslateButton();
+ // Too slow for now
+ // if (!item.isTranslated) {
+ // item.translate();
+ // }
+ ZoteroPane.startItemReadTimeout(item.id);
+ }
+ return true;
+ }
+
+ renderMessage() {
+ let msg;
+
+ let count = this.data.length;
+
+ // Display duplicates merge interface in item pane
+ if (this.viewMode.isDuplicates) {
+ if (!this.editable) {
+ if (count) {
+ msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
+ }
+ else {
+ msg = Zotero.getString('pane.item.selected.zero');
+ }
+ this.setItemPaneMessage(msg);
+ }
+ else if (count) {
+ this.viewType = "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;
+
+ // Initialize the merge pane with the selected items
+ this._duplicatesPane.setItems(this.data, displayNumItemsOnTypeError);
+ }
+ else {
+ msg = Zotero.getString('pane.item.duplicates.selectToMerge');
+ this.setItemPaneMessage(msg);
+ }
+ }
+ // Display label in the middle of the item pane
+ else {
+ if (count) {
+ msg = Zotero.getString('pane.item.selected.multiple', count);
+ }
+ else {
+ let rowCount = this.viewMode.rowCount;
+ let str = 'pane.item.unselected.';
+ switch (rowCount) {
+ case 0:
+ str += 'zero';
+ break;
+ case 1:
+ str += 'singular';
+ break;
+ default:
+ str += 'plural';
+ break;
+ }
+ msg = Zotero.getString(str, [rowCount]);
+ }
+
+ this.setItemPaneMessage(msg);
+ // Return false for itemTreeTest#shouldn't select a modified item
+ return false;
+ }
+ return true;
+ }
+
+ setItemPaneMessage(msg) {
+ this.viewType = "message";
+ this._messagePane.render(msg);
+ }
+
+ /**
+ * Display buttons at top of item pane depending on context
+ */
+ updateItemPaneButtons() {
+ let container;
+ if (!this.data.length) {
+ return;
+ }
+ else if (this.data.length > 1) {
+ container = this._messagePane;
+ }
+ else if (this.data[0].isNote()) {
+ container = this._noteEditor;
+ }
+ else {
+ container = this._itemDetails;
+ }
+
+ // My Publications buttons
+ var isPublications = this.viewMode.isPublications;
+ // Show in My Publications view if selected items are all notes or non-linked-file attachments
+ var showMyPublicationsButtons = isPublications
+ && this.data.every((item) => {
+ return item.isNote()
+ || (item.isAttachment()
+ && item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
+ });
+
+ if (showMyPublicationsButtons) {
+ container.renderCustomHead(this.renderPublicationsHead.bind(this));
+ return;
+ }
+
+ // Trash button
+ let nonDeletedItemsSelected = this.data.some(item => !item.deleted);
+ if (this.viewMode.isTrash && !nonDeletedItemsSelected) {
+ container.renderCustomHead(this.renderTrashHead.bind(this));
+ return;
+ }
+
+ // Feed buttons
+ if (this.viewMode.isFeedsOrFeed) {
+ container.renderCustomHead(this.renderFeedHead.bind(this));
+ this.updateReadLabel();
+ return;
+ }
+
+ container.renderCustomHead();
+ }
+
+ renderPublicationsHead(data) {
+ let { doc, append } = data;
+ let button = doc.createXULElement("button");
+ button.id = 'zotero-item-pane-my-publications-button';
+
+ let hiddenItemsSelected = this.data.some(item => !item.inPublications);
+ let str, onclick;
+ if (hiddenItemsSelected) {
+ str = 'showInMyPublications';
+ onclick = () => Zotero.Items.addToPublications(this.data);
+ }
+ else {
+ str = 'hideFromMyPublications';
+ onclick = () => Zotero.Items.removeFromPublications(this.data);
+ }
+ button.label = Zotero.getString('pane.item.' + str);
+ button.onclick = onclick;
+ append(button);
+ }
+
+ renderTrashHead(data) {
+ let { doc, append } = data;
+ let restoreButton = doc.createXULElement("button");
+ restoreButton.id = "zotero-item-restore-button";
+ restoreButton.dataset.l10nId = "menu-restoreToLibrary";
+ restoreButton.addEventListener("command", () => {
+ ZoteroPane.restoreSelectedItems();
+ });
+
+ let deleteButton = doc.createXULElement("button");
+ deleteButton.id = "zotero-item-delete-button";
+ deleteButton.dataset.l10nId = "menu-deletePermanently";
+ deleteButton.addEventListener("command", () => {
+ ZoteroPane.deleteSelectedItems();
+ });
+
+ append(restoreButton, deleteButton);
+ }
+
+ renderFeedHead(data) {
+ let { doc, append } = data;
+
+ let toggleReadButton = doc.createXULElement("button");
+ toggleReadButton.id = "zotero-feed-item-toggleRead-button";
+ toggleReadButton.addEventListener("command", () => {
+ ZoteroPane.toggleSelectedItemsRead();
+ });
+
+ let addToButton = new (customElements.get('split-menu-button'));
+ addToButton.id = "zotero-feed-item-addTo-button";
+ addToButton.setAttribute("popup", "zotero-item-addTo-menu");
+ addToButton.addEventListener("command", () => this.translateSelectedItems());
+
+ append(toggleReadButton, addToButton);
+
+ this.setTranslateButton();
+ }
+
+ updateReadLabel() {
+ var items = this.data;
+ var isUnread = false;
+ for (let item of items) {
+ if (!item.isRead) {
+ isUnread = true;
+ break;
+ }
+ }
+ this.setReadLabel(!isUnread);
+ }
+
+ setReadLabel(isRead) {
+ var elem = document.getElementById('zotero-feed-item-toggleRead-button');
+ var label = Zotero.getString('pane.item.' + (isRead ? 'markAsUnread' : 'markAsRead'));
+ elem.label = label;
+
+ var key = Zotero.Keys.getKeyForCommand('toggleRead');
+ var tooltip = label + (Zotero.rtl ? ' \u202B' : ' ') + '(' + key + ')';
+ elem.title = tooltip;
+ }
+
+ async translateSelectedItems() {
+ var collectionID = this._translationTarget.objectType == 'collection' ? this._translationTarget.id : undefined;
+ var items = this.data;
+ for (let item of items) {
+ await item.translate(this._translationTarget.libraryID, collectionID);
+ }
+ }
+
+ buildTranslateSelectContextMenu(event) {
+ var menu = document.getElementById('zotero-item-addTo-menu');
+ // Don't trigger rebuilding on nested popupmenu open/close
+ if (event.target != menu) {
+ return;
+ }
+ // Clear previous items
+ while (menu.firstChild) {
+ menu.removeChild(menu.firstChild);
+ }
+
+ let target = Zotero.Prefs.get('feeds.lastTranslationTarget');
+ if (!target) {
+ target = "L" + Zotero.Libraries.userLibraryID;
+ }
+
+ var libraries = Zotero.Libraries.getAll();
+ for (let library of libraries) {
+ if (!library.editable || library.libraryType == 'publications') {
+ continue;
+ }
+ Zotero.Utilities.Internal.createMenuForTarget(
+ library,
+ menu,
+ target,
+ async (event, libraryOrCollection) => {
+ if (event.target.tagName == 'menu') {
+ // Simulate menuitem flash on OS X
+ if (Zotero.isMac) {
+ event.target.setAttribute('_moz-menuactive', false);
+ await Zotero.Promise.delay(50);
+ event.target.setAttribute('_moz-menuactive', true);
+ await Zotero.Promise.delay(50);
+ event.target.setAttribute('_moz-menuactive', false);
+ await Zotero.Promise.delay(50);
+ event.target.setAttribute('_moz-menuactive', true);
+ }
+ menu.hidePopup();
+
+ this.setTranslationTarget(libraryOrCollection);
+ event.stopPropagation();
+ }
+ else {
+ this.setTranslationTarget(libraryOrCollection);
+ event.stopPropagation();
+ }
+ }
+ );
+ }
+ }
+
+ setTranslateButton() {
+ if (!this._translationTarget) return;
+ var label = Zotero.getString('pane.item.addTo', this._translationTarget.name);
+ var elem = document.getElementById('zotero-feed-item-addTo-button');
+ elem.label = label;
+
+ var key = Zotero.Keys.getKeyForCommand('saveToZotero');
+
+ var tooltip = label
+ + (Zotero.rtl ? ' \u202B' : ' ') + '('
+ + (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
+ + key + ')';
+ elem.title = tooltip;
+ elem.image = this._translationTarget.treeViewImage;
+ }
+
+ setTranslationTarget(translationTarget) {
+ this._translationTarget = translationTarget;
+ Zotero.Prefs.set('feeds.lastTranslationTarget', translationTarget.treeViewID);
+ this.setTranslateButton();
+ }
+
+ static get observedAttributes() {
+ return ['collapsed'];
+ }
+
+ attributeChangedCallback(name) {
+ switch (name) {
+ case "collapsed": {
+ this.handleResize();
+ }
+ }
+ }
+
+ handleResize() {
+ if (this.getAttribute("collapsed")) {
+ this.removeAttribute("width");
+ this.removeAttribute("height");
+ }
+ else {
+ // Must have width or height to auto-resize when changing sidenav visibility
+ // Keep in sync with $min-width-item-pane and min-height + sidebar size
+ let minWidth = 337;
+ let minHeight = 205;
+ let width = this.getAttribute("width");
+ let height = this.getAttribute("height");
+ 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") {
+ this._itemDetails.render();
+ }
+ }
+ }
+ }
+ customElements.define("item-pane", ItemPane);
+}
diff --git a/chrome/content/zotero/elements/itemPaneSection.js b/chrome/content/zotero/elements/itemPaneSection.js
new file mode 100644
index 0000000000..f414e68dc5
--- /dev/null
+++ b/chrome/content/zotero/elements/itemPaneSection.js
@@ -0,0 +1,78 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2024 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.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
.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+class ItemPaneSectionElementBase extends XULElementBase {
+ connectedCallback() {
+ super.connectedCallback();
+ if (!this.render) {
+ Zotero.warn("Pane section must have method render().");
+ }
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ if (this._section) {
+ this._section.removeEventListener("toggle", this._handleSectionToggle);
+ this._section = null;
+ }
+ }
+
+ initCollapsibleSection() {
+ this._section = this.querySelector('collapsible-section');
+ if (this._section) {
+ this._section.addEventListener("toggle", this._handleSectionToggle);
+ }
+ }
+
+ /**
+ * @returns {boolean} if false, data change will not be saved
+ */
+ _handleDataChange(_type, _value) {
+ return true;
+ }
+
+ _handleSectionToggle = async (event) => {
+ if (event.target !== this._section || !this._section.open) {
+ return;
+ }
+ if (this.render) await this.render(true);
+ if (this.secondaryRender) await this.secondaryRender(true);
+ };
+
+ /**
+ * @param {"primary" | "secondary"} [type]
+ * @returns {boolean}
+ */
+ _isAlreadyRendered(type = "primary") {
+ let key = `_${type}RenderItemID`;
+ let cachedFlag = this[key];
+ if (cachedFlag && this.item?.id == cachedFlag) {
+ return true;
+ }
+ this._lastRenderItemID = this.item.id;
+ return false;
+ }
+}
diff --git a/chrome/content/zotero/elements/itemPaneSidenav.js b/chrome/content/zotero/elements/itemPaneSidenav.js
index e4bfba6f94..48f7a0e146 100644
--- a/chrome/content/zotero/elements/itemPaneSidenav.js
+++ b/chrome/content/zotero/elements/itemPaneSidenav.js
@@ -120,17 +120,13 @@
_disableScrollHandler = false;
- _pendingPane = null;
-
get container() {
return this._container;
}
set container(val) {
if (this._container == val) return;
- this._container?.removeEventListener('scroll', this._handleContainerScroll);
this._container = val;
- this._container.addEventListener('scroll', this._handleContainerScroll);
this.render(true);
}
@@ -145,25 +141,21 @@
}
get pinnedPane() {
- return this.getAttribute('pinnedPane');
+ return this.container?.pinnedPane;
}
set pinnedPane(val) {
- if (!val || !this.getPane(val)) {
- val = '';
- }
- this.setAttribute('pinnedPane', val);
- if (val) {
- this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
- }
+ if (!this.container) return;
+ this.container.pinnedPane = val;
}
-
- get _minScrollHeight() {
- return parseFloat(this._container.style.getPropertyValue('--min-scroll-height') || 0);
+
+ get _collapsed() {
+ return this.container?._collapsed;
}
-
- set _minScrollHeight(val) {
- this._container.style.setProperty('--min-scroll-height', val + 'px');
+
+ set _collapsed(val) {
+ if (!this.container) return;
+ this.container._collapsed = val;
}
get _contextNotesPaneVisible() {
@@ -191,213 +183,18 @@
get _showCollapseButton() {
return false;
}
-
- get _collapsed() {
- let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
- if (!collapsible) return false;
- return collapsible.getAttribute('collapsed') === 'true';
- }
-
- set _collapsed(val) {
- let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
- if (!collapsible) return;
- let splitter = collapsible.previousElementSibling;
- if (val) {
- collapsible.setAttribute('collapsed', 'true');
- splitter.setAttribute('state', 'collapsed');
- splitter.setAttribute('substate', 'after');
- }
- else {
- collapsible.removeAttribute('collapsed');
- splitter.setAttribute('state', '');
- splitter.setAttribute('substate', 'after');
- }
- window.dispatchEvent(new Event('resize'));
- this.render();
- }
- static get observedAttributes() {
- return ['pinnedPane'];
- }
-
- attributeChangedCallback() {
- this.render();
- }
-
- scrollToPane(id, behavior = 'smooth') {
- // If the itemPane is collapsed, just remember which pane needs to be scrolled to
- // when itemPane is expanded.
- if (this._collapsed) {
- this._pendingPane = id;
- return;
- }
- if (this._contextNotesPane && this._contextNotesPaneVisible) {
- this._contextNotesPaneVisible = false;
- behavior = 'instant';
- }
-
- let pane = this.getPane(id);
- if (!pane) return;
-
- // The pane should always be at the very top
- // If there isn't enough stuff below it for it to be at the top, we add padding
- // We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
- this._makeSpaceForPane(pane);
- if (behavior == 'smooth') {
- this._disableScrollHandler = true;
- this._waitForScroll().then(() => this._disableScrollHandler = false);
- }
- pane.scrollIntoView({ block: 'start', behavior });
- pane.focus();
- }
-
- _makeSpaceForPane(pane) {
- let oldMinScrollHeight = this._minScrollHeight;
- let newMinScrollHeight = this._getMinScrollHeightForPane(pane);
- if (newMinScrollHeight > oldMinScrollHeight) {
- this._minScrollHeight = newMinScrollHeight;
- }
- }
-
- _getMinScrollHeightForPane(pane) {
- let paneRect = pane.getBoundingClientRect();
- let containerRect = this._container.getBoundingClientRect();
- // No offsetTop property for XUL elements
- let offsetTop = paneRect.top - containerRect.top + this._container.scrollTop;
- return offsetTop + containerRect.height;
- }
-
- _handleContainerScroll = () => {
- // Don't scroll hidden pane
- if (this.hidden || this._disableScrollHandler) return;
- let minHeight = this._minScrollHeight;
- if (minHeight) {
- let newMinScrollHeight = this._container.scrollTop + this._container.clientHeight;
- // 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._container.scrollHeight
- || this.pinnedPane && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
- return;
- }
- this._minScrollHeight = newMinScrollHeight;
- }
- };
-
- async _waitForScroll() {
- let scrollPromise = Zotero.Promise.defer();
- let lastScrollTop = this._container.scrollTop;
- const waitFrame = async () => {
- return new Promise((resolve) => {
- requestAnimationFrame(resolve);
- });
- };
- const waitFrames = async (n) => {
- for (let i = 0; i < n; i++) {
- await waitFrame();
- }
- };
- const checkScrollStart = () => {
- // If the scrollTop is not changed, wait for scroll to happen
- if (lastScrollTop === this._container.scrollTop) {
- requestAnimationFrame(checkScrollStart);
- }
- // Wait for scroll to end
- else {
- requestAnimationFrame(checkScrollEnd);
- }
- };
- const checkScrollEnd = async () => {
- // Wait for 3 frames to make sure not further scrolls
- await waitFrames(3);
- if (lastScrollTop === this._container.scrollTop) {
- scrollPromise.resolve();
- }
- else {
- lastScrollTop = this._container.scrollTop;
- requestAnimationFrame(checkScrollEnd);
- }
- };
- checkScrollStart();
- // Abort after 3 seconds, which should be enough
- return Promise.race([
- scrollPromise.promise,
- Zotero.Promise.delay(3000)
- ]);
- }
-
- getPanes() {
- return Array.from(this.container.querySelectorAll(':scope > [data-pane]:not([hidden])'));
- }
-
- getPane(id) {
- return this.container.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
- }
-
isPanePinnable(id) {
return id !== 'info' && id !== 'context-all-notes' && id !== 'context-item-notes';
}
-
- showPendingPane() {
- if (!this._pendingPane || this._collapsed) return;
- this.scrollToPane(this._pendingPane, 'instant');
- this._pendingPane = null;
- }
init() {
- if (!this.container) {
- this.container = document.getElementById('zotero-view-item');
- }
-
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
let pane = toolbarbutton.dataset.pane;
- if (pane === 'context-notes') {
- toolbarbutton.addEventListener('click', (event) => {
- if (event.button !== 0) {
- return;
- }
- if (event.detail == 2) {
- this.pinnedPane = null;
- }
- this._contextNotesPaneVisible = true;
- });
- continue;
- }
- else if (pane === 'toggle-collapse') {
- toolbarbutton.addEventListener('click', (event) => {
- if (event.button !== 0) {
- return;
- }
- this._collapsed = !this._collapsed;
- });
- continue;
- }
-
let pinnable = this.isPanePinnable(pane);
toolbarbutton.parentElement.classList.toggle('pinnable', pinnable);
- toolbarbutton.addEventListener('click', (event) => {
- if (event.button !== 0) {
- return;
- }
-
- let scrollType = this._collapsed ? 'instant' : 'smooth';
- this._collapsed = false;
- switch (event.detail) {
- case 1:
- this.scrollToPane(pane, scrollType);
- break;
- case 2:
- if (this.pinnedPane == pane || !pinnable) {
- this.pinnedPane = null;
- }
- else {
- this.pinnedPane = pane;
- }
- break;
- }
- });
-
if (pinnable) {
toolbarbutton.addEventListener('contextmenu', (event) => {
this._contextMenuTarget = pane;
@@ -408,25 +205,24 @@
});
}
}
+
+ this.addEventListener('click', this.handleButtonClick);
this.querySelector('.zotero-menuitem-pin').addEventListener('command', () => {
- this.scrollToPane(this._contextMenuTarget, 'smooth');
+ this.container.scrollToPane(this._contextMenuTarget, 'smooth');
this.pinnedPane = this._contextMenuTarget;
});
this.querySelector('.zotero-menuitem-unpin').addEventListener('command', () => {
this.pinnedPane = null;
});
-
- this.render();
}
- render(force = false) {
- // TEMP: only render sidenav when pane is visible
- if (!force && this.container.id === "zotero-view-item"
- && document.querySelector("#zotero-item-pane-content").selectedIndex !== "1"
- ) {
- return;
- }
+ destroy() {
+ this.removeEventListener('click', this.handleButtonClick);
+ }
+
+ render() {
+ if (!this.container) return;
let contextNotesPaneVisible = this._contextNotesPaneVisible;
let pinnedPane = this.pinnedPane;
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
@@ -460,7 +256,7 @@
}
toolbarbutton.setAttribute('aria-selected', !contextNotesPaneVisible && pane == pinnedPane);
- toolbarbutton.parentElement.hidden = !this.getPane(pane);
+ toolbarbutton.parentElement.hidden = !this.container.getPane(pane);
// Set .pinned on the container, for pin styling
toolbarbutton.parentElement.classList.toggle('pinned', pane == pinnedPane);
@@ -470,6 +266,96 @@
this.querySelector('.highlight-notes-inactive').classList.toggle('highlight',
this._contextNotesPane && !contextNotesPaneVisible);
}
+
+ 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);
+ if (this.pinnedPane) {
+ if (paneID == this.pinnedPane && !toolbarbutton.parentElement.classList.contains("pinned")) {
+ this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
+ toolbarbutton.parentElement.classList.add('pinned');
+ }
+ }
+ else {
+ this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
+ }
+ }
+
+ toggleDefaultStatus(isDefault) {
+ this._defaultStatus = isDefault;
+ this.renderDefaultStatus();
+ }
+
+ renderDefaultStatus() {
+ if (this._defaultStatus) {
+ this.querySelectorAll('toolbarbutton').forEach((elem) => {
+ elem.disabled = true;
+ elem.parentElement.hidden = !(
+ ["info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"]
+ .includes(elem.dataset.pane));
+ });
+ }
+ else {
+ this.querySelectorAll('toolbarbutton').forEach((elem) => {
+ elem.disabled = false;
+ });
+ this.render(true);
+ }
+ }
+
+ handleButtonClick = (event) => {
+ let toolbarbutton = event.target;
+ let pane = toolbarbutton.dataset.pane;
+ if (!pane) return;
+ switch (pane) {
+ case "context-notes":
+ if (event.button !== 0) {
+ return;
+ }
+ if (event.detail == 2) {
+ this.pinnedPane = null;
+ }
+ this._contextNotesPaneVisible = true;
+ break;
+ case "toggle-collapse":
+ if (event.button !== 0) {
+ return;
+ }
+ this._collapsed = !this._collapsed;
+ break;
+ default: {
+ if (event.button !== 0) {
+ return;
+ }
+ let pinnable = this.isPanePinnable(pane);
+ let scrollType = this._collapsed ? 'instant' : 'smooth';
+ if (this._collapsed) this._collapsed = false;
+ switch (event.detail) {
+ case 1:
+ if (this._contextNotesPane && this._contextNotesPaneVisible) {
+ this._contextNotesPaneVisible = false;
+ scrollType = 'instant';
+ }
+ this.container.scrollToPane(pane, scrollType);
+ break;
+ case 2:
+ if (this.pinnedPane == pane || !pinnable) {
+ this.pinnedPane = null;
+ }
+ else {
+ this.pinnedPane = pane;
+ }
+ break;
+ }
+ }
+ }
+ this.render();
+ };
}
customElements.define("item-pane-sidenav", ItemPaneSidenav);
}
diff --git a/chrome/content/zotero/elements/librariesCollectionsBox.js b/chrome/content/zotero/elements/librariesCollectionsBox.js
index c0e3019f85..592ed15f29 100644
--- a/chrome/content/zotero/elements/librariesCollectionsBox.js
+++ b/chrome/content/zotero/elements/librariesCollectionsBox.js
@@ -28,7 +28,7 @@
import { getCSSIcon } from 'components/icons';
{
- class LibrariesCollectionsBox extends XULElementBase {
+ class LibrariesCollectionsBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
@@ -61,11 +61,7 @@ import { getCSSIcon } from 'components/icons';
return;
}
this._item = item;
- // Getting linked items is an async process, so start by rendering without them
this._linkedItems = [];
- this.render();
-
- this._updateLinkedItems();
}
get mode() {
@@ -75,33 +71,25 @@ import { getCSSIcon } from 'components/icons';
set mode(mode) {
this._mode = mode;
this.setAttribute('mode', mode);
- this.render();
}
init() {
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'librariesCollectionsBox');
this._body = this.querySelector('.body');
- this._section = this.querySelector('collapsible-section');
- this._section.addEventListener('add', (event) => {
- this.querySelector('.add-popup').openPopupAtScreen(
- event.detail.button.screenX,
- event.detail.button.screenY,
- true
- );
- this._section.open = true;
- });
- this.render();
+ this.initCollapsibleSection();
+ this._section.addEventListener('add', this._handleAdd);
}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
+ 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();
+ this.render(true);
}
}
@@ -238,27 +226,53 @@ import { getCSSIcon } from 'components/icons';
return row;
}
- async _updateLinkedItems() {
+ render(force = false) {
+ if (!this._item) return;
+ if (!this._section.open) return;
+ if (!force && this._isAlreadyRendered()) return;
+
+ this._body.replaceChildren();
+
+ for (let item of [this._item]) {
+ this._addObject(Zotero.Libraries.get(item.libraryID), item);
+ for (let collection of Zotero.Collections.get(item.getCollections())) {
+ this._addObject(collection, item);
+ }
+ }
+ if (force) {
+ this.secondaryRender();
+ }
+ }
+
+ async secondaryRender() {
+ if (!this._item) {
+ return;
+ }
+ // Skip if already rendered
+ if (this._linkedItems.length > 0) {
+ return;
+ }
+
this._linkedItems = (await Promise.all(Zotero.Libraries.getAll()
.filter(lib => lib.libraryID !== this._item.libraryID)
.map(lib => this._item.getLinkedItem(lib.libraryID, true))))
.filter(Boolean);
- this.render();
- }
-
- render() {
- if (!this._item) {
- return;
- }
-
- this._body.replaceChildren();
- for (let item of [this._item, ...this._linkedItems]) {
+ for (let item of this._linkedItems) {
this._addObject(Zotero.Libraries.get(item.libraryID), item);
for (let collection of Zotero.Collections.get(item.getCollections())) {
this._addObject(collection, item);
}
}
}
+
+ _handleAdd = (event) => {
+ this.querySelector('.add-popup').openPopupAtScreen(
+ event.detail.button.screenX,
+ event.detail.button.screenY,
+ true
+ );
+ this._section.open = true;
+ };
}
customElements.define("libraries-collections-box", LibrariesCollectionsBox);
}
diff --git a/chrome/content/zotero/elements/noteEditor.js b/chrome/content/zotero/elements/noteEditor.js
index 527649ad07..fc2ad110e5 100644
--- a/chrome/content/zotero/elements/noteEditor.js
+++ b/chrome/content/zotero/elements/noteEditor.js
@@ -40,6 +40,7 @@
this._destroyed = false;
this.content = MozXULElement.parseXULToFragment(`
+
@@ -321,6 +322,20 @@
}
}
+ renderCustomHead(callback) {
+ let customHead = this.querySelector(".custom-head");
+ customHead.replaceChildren();
+ let append = (...args) => {
+ customHead.append(...args);
+ };
+ if (callback) callback({
+ doc: document,
+ append: (...args) => {
+ append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
+ }
+ });
+ }
+
_id(id) {
return this.querySelector(`#${id}`);
}
@@ -394,6 +409,8 @@
}
refresh() {
+ this._id('related').render();
+ this._id('tags').render();
}
_id(id) {
diff --git a/chrome/content/zotero/elements/notesBox.js b/chrome/content/zotero/elements/notesBox.js
index 32be4dcdc0..7b97af7644 100644
--- a/chrome/content/zotero/elements/notesBox.js
+++ b/chrome/content/zotero/elements/notesBox.js
@@ -28,29 +28,24 @@
import { getCSSItemTypeIcon } from 'components/icons';
{
- class NotesBox extends XULElementBase {
+ class NotesBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
`);
- constructor() {
- super();
-
+ init() {
this._mode = 'view';
this._item = null;
this._noteIDs = [];
- }
-
- init() {
- this._section = this.querySelector('collapsible-section');
+ this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAdd);
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'notesBox');
}
destroy() {
- this._section = null;
+ this._section.removeEventListener('add', this._handleAdd);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
@@ -86,19 +81,19 @@ import { getCSSItemTypeIcon } from 'components/icons';
return;
}
this._item = val;
- this._refresh();
}
- notify(event, type, ids, extraData) {
+ notify(event, type, ids, _extraData) {
if (['modify', 'delete'].includes(event) && ids.some(id => this._noteIDs.includes(id))) {
- this._refresh();
+ this.render(true);
}
}
- _refresh() {
+ render(force = false) {
if (!this._item) {
return;
}
+ if (!force && this._isAlreadyRendered()) return;
this._noteIDs = this._item.getNotes();
diff --git a/chrome/content/zotero/elements/paneHeader.js b/chrome/content/zotero/elements/paneHeader.js
index 7bcca97930..a1f298d342 100644
--- a/chrome/content/zotero/elements/paneHeader.js
+++ b/chrome/content/zotero/elements/paneHeader.js
@@ -26,7 +26,7 @@
"use strict";
{
- class PaneHeader extends XULElementBase {
+ class PaneHeader extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
@@ -44,6 +44,8 @@
+
+
`, ['chrome://zotero/locale/zotero.dtd']);
showInFeeds = true;
@@ -61,7 +63,6 @@
set item(item) {
this.blurOpenField();
this._item = item;
- this.render();
}
get mode() {
@@ -70,7 +71,6 @@
set mode(mode) {
this._mode = mode;
- this.render();
}
init() {
@@ -85,7 +85,7 @@
if (!this._item) return;
event.preventDefault();
- let menupopup = ZoteroItemPane.buildFieldTransformMenu({
+ let menupopup = ZoteroPane.buildFieldTransformMenu({
target: this.titleField,
onTransform: (newValue) => {
this._setTransformedValue(newValue);
@@ -95,8 +95,6 @@
menupopup.addEventListener('popuphidden', () => menupopup.remove());
menupopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true);
});
-
- this.render();
}
destroy() {
@@ -105,7 +103,7 @@
notify(action, type, ids) {
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
- this.render();
+ this.render(true);
}
}
@@ -124,7 +122,7 @@
this.item.setField(this._titleFieldID, this.titleField.value);
await this.item.saveTx();
}
- this.render();
+ this.render(true);
}
async blurOpenField() {
@@ -134,10 +132,11 @@
}
}
- render() {
+ render(force = false) {
if (!this.item) {
return;
}
+ if (!force && this._isAlreadyRendered()) return;
this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
@@ -156,6 +155,20 @@
}
this.menuButton.hidden = !this.item.isRegularItem() && !this.item.isAttachment();
}
+
+ renderCustomHead(callback) {
+ let customHead = this.querySelector(".custom-head");
+ customHead.replaceChildren();
+ let append = (...args) => {
+ customHead.append(...args);
+ };
+ if (callback) callback({
+ doc: document,
+ append: (...args) => {
+ append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
+ }
+ });
+ }
}
customElements.define("pane-header", PaneHeader);
}
diff --git a/chrome/content/zotero/elements/relatedBox.js b/chrome/content/zotero/elements/relatedBox.js
index 9f43760dd0..e9389826b1 100644
--- a/chrome/content/zotero/elements/relatedBox.js
+++ b/chrome/content/zotero/elements/relatedBox.js
@@ -28,29 +28,24 @@
import { getCSSItemTypeIcon } from 'components/icons';
{
- class RelatedBox extends XULElementBase {
+ class RelatedBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
`);
-
- constructor() {
- super();
-
- this._mode = 'view';
- this._item = null;
- }
init() {
+ this._mode = 'view';
+ this._item = null;
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
- this._section = this.querySelector('collapsible-section');
+ this.initCollapsibleSection();
this._section.addEventListener('add', this.add);
}
destroy() {
+ this._section.removeEventListener('add', this.add);
Zotero.Notifier.unregisterObserver(this._notifierID);
- this._section = null;
}
get mode() {
@@ -78,7 +73,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
set item(val) {
this._item = val;
- this.refresh();
}
notify(event, type, ids, _extraData) {
@@ -86,7 +80,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
// Refresh if this item has been modified
if (event == 'modify' && ids.includes(this._item.id)) {
- this.refresh();
+ this.render(true);
return;
}
@@ -96,14 +90,17 @@ 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.refresh();
+ this.render(true);
return;
}
}
}
}
- refresh() {
+ render(force = false) {
+ if (!this.item) return;
+ if (!force && this._isAlreadyRendered()) return;
+
let body = this.querySelector('.body');
body.replaceChildren();
diff --git a/chrome/content/zotero/elements/splitMenuButton.js b/chrome/content/zotero/elements/splitMenuButton.js
index cea1fcdbd8..24bbef84f3 100644
--- a/chrome/content/zotero/elements/splitMenuButton.js
+++ b/chrome/content/zotero/elements/splitMenuButton.js
@@ -25,13 +25,16 @@
"use strict";
+/**
+ * A split menubutton with a clickable left side and a dropmarker that opens a menu.
+ */
{
- /**
- * A split menubutton with a clickable left side and a dropmarker that opens a menu.
- */
class SplitMenuButton extends HTMLButtonElement {
_image = null;
+
_label = null;
+
+ _commandListenerCache = [];
constructor() {
super();
@@ -54,23 +57,45 @@
}
connectedCallback() {
- this.append(this.constructor.contentFragment);
-
- // Prevent DOM-attached mouse handlers from running in the dropmarker area
- for (const eventType of ['mousedown', 'mouseup', 'click']) {
- const handler = this.getAttribute('on' + eventType);
- if (!handler) {
- continue;
- }
- this['on' + eventType] = null;
- this.addEventListener(eventType, (event) => {
- if (!this._isEventInDropmarkerBox(event)) {
- eval(handler).bind(this);
- }
- });
+ this.append(this.contentFragment);
+ }
+
+ disconnectedCallback() {
+ while (this._commandListenerCache.length) {
+ let cache = this._commandListenerCache.pop();
+ super.removeEventListener("click", cache.actual);
}
}
+ addEventListener(type, listener, options) {
+ if (type == "command") {
+ let newListener = (event) => {
+ if (!this._isEventInDropmarkerBox(event)) {
+ listener(event);
+ }
+ };
+ this._commandListenerCache.push({
+ original: listener,
+ actual: newListener
+ });
+ super.addEventListener("click", newListener, options);
+ return;
+ }
+ super.addEventListener(type, listener, options);
+ }
+
+ removeEventListener(type, listener, options) {
+ if (type == "command") {
+ let cacheIndex = this._commandListenerCache.findIndex(cache => cache.original == listener);
+ if (cacheIndex != -1) {
+ let cache = this._commandListenerCache.splice(cacheIndex, 1)[0];
+ super.removeEventListener("click", cache.actual, options);
+ return;
+ }
+ }
+ super.removeEventListener(type, listener, options);
+ }
+
get image() {
return this.querySelector('[anonid="button-image"]').src;
}
@@ -87,7 +112,7 @@
this.querySelector('[anonid="button-text"]').textContent = value;
}
- static get contentFragment() {
+ get contentFragment() {
// Zotero.hiDPI[Suffix] may not have been initialized yet, so calculate it ourselves
let hiDPISuffix = window.devicePixelRatio > 1 ? '@2x' : '';
let frag = document.importNode(
@@ -109,7 +134,7 @@
_isEventInDropmarkerBox(event) {
let rect = this.querySelector('[anonid="dropmarker-box"]').getBoundingClientRect();
- return !Zotero.rtl && event.clientX >= rect.left || Zotero.rtl && event.clientX <= rect.right
+ return !Zotero.rtl && event.clientX >= rect.left || Zotero.rtl && event.clientX <= rect.right;
}
}
diff --git a/chrome/content/zotero/elements/tagsBox.js b/chrome/content/zotero/elements/tagsBox.js
index aa42cac348..f51e6bb51b 100644
--- a/chrome/content/zotero/elements/tagsBox.js
+++ b/chrome/content/zotero/elements/tagsBox.js
@@ -26,20 +26,8 @@
"use strict";
{
- class TagsBox extends XULElement {
- constructor() {
- super();
-
- this.count = 0;
- this.clickHandler = null;
-
- this._tabDirection = null;
- this._tagColors = [];
- this._notifierID = null;
- this._mode = 'view';
- this._item = null;
-
- this.content = MozXULElement.parseXULToFragment(`
+ class TagsBox extends ItemPaneSectionElementBase {
+ content = MozXULElement.parseXULToFragment(`
@@ -52,16 +40,18 @@
`, ['chrome://zotero/locale/zotero.dtd']);
- }
- connectedCallback() {
- this._destroyed = false;
- window.addEventListener("unload", this.destroy);
+ init() {
+ this.count = 0;
+ this.clickHandler = null;
- let content = document.importNode(this.content, true);
- this.append(content);
+ this._tabDirection = null;
+ this._tagColors = [];
+ this._notifierID = null;
+ this._mode = 'view';
+ this._item = null;
- this._section = this.querySelector('collapsible-section');
+ this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAddButtonClick);
this.addEventListener('click', (event) => {
if (event.target === this) {
@@ -100,21 +90,10 @@
}
destroy() {
- if (this._destroyed) {
- return;
- }
- window.removeEventListener("unload", this.destroy);
- this._destroyed = true;
-
- this._section = null;
+ this._section.removeEventListener('add', this._handleAddButtonClick);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
- disconnectedCallback() {
- this.replaceChildren();
- this.destroy();
- }
-
get mode() {
return this._mode;
}
@@ -151,12 +130,11 @@
return;
}
this._item = val;
- this.reload();
}
notify(event, type, ids, extraData) {
if (type == 'setting' && ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
- this.reload();
+ this.render(true);
}
else if (type == 'item-tag') {
let itemID, _tagID;
@@ -186,11 +164,14 @@
this.updateCount();
}
else if (type == 'tag' && event == 'modify') {
- this.reload();
+ this.render(true);
}
}
- reload() {
+ render(force = false) {
+ if (!this.item) return;
+ if (!force && this._isAlreadyRendered()) return;
+
Zotero.debug('Reloading tags box');
// Cancel field focusing while we're updating
@@ -316,7 +297,7 @@
await item.saveTx();
}
catch (e) {
- this.reload();
+ this.render(true);
throw e;
}
}
@@ -490,7 +471,7 @@
await this.item.saveTx();
}
catch (e) {
- this.reload();
+ this.render(true);
throw e;
}
}
@@ -507,7 +488,7 @@
await this.item.saveTx();
}
catch (e) {
- this.reload();
+ this.render(true);
throw e;
}
}
@@ -530,7 +511,7 @@
tags.forEach(tag => this.item.addTag(tag));
await this.item.saveTx();
- this.reload();
+ this.render(true);
}
// Single tag at end
else {
@@ -548,7 +529,7 @@
await this.item.saveTx();
}
catch (e) {
- this.reload();
+ this.render(true);
throw e;
}
}
diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js
deleted file mode 100644
index 6422e5603b..0000000000
--- a/chrome/content/zotero/itemPane.js
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2009 Center for History and New Media
- George Mason University, Fairfax, Virginia, USA
- http://zotero.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
.
-
- ***** END LICENSE BLOCK *****
-*/
-
-var ZoteroItemPane = new function() {
- var _container;
- var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _tagsBox, _notesBox, _librariesCollectionsBox, _relatedBox, _boxes;
- var _deck;
- var _lastItem;
- var _selectedNoteID;
- var _translationTarget;
-
- this.onLoad = function () {
- if (!Zotero) {
- return;
- }
-
- _container = document.getElementById('zotero-view-item-container');
- _header = document.getElementById('zotero-item-pane-header');
- _sidenav = document.getElementById('zotero-view-item-sidenav');
- _scrollParent = document.getElementById('zotero-view-item');
- _itemBox = document.getElementById('zotero-editpane-item-box');
- _abstractBox = document.getElementById('zotero-editpane-abstract');
- _notesBox = document.getElementById('zotero-editpane-notes');
- _attachmentsBox = document.getElementById('zotero-editpane-attachments');
- _attachmentInfoBox = document.getElementById('zotero-attachment-box');
- _attachmentAnnotationsBox = document.getElementById('zotero-editpane-attachment-annotations');
- _tagsBox = document.getElementById('zotero-editpane-tags');
- _librariesCollectionsBox = document.getElementById('zotero-editpane-libraries-collections');
- _relatedBox = document.getElementById('zotero-editpane-related');
- _boxes = [_itemBox, _abstractBox, _notesBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _librariesCollectionsBox, _tagsBox, _relatedBox];
-
- _deck = document.getElementById('zotero-item-pane-content');
-
- this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
-
- _container.addEventListener("keypress", this.handleKeypress);
- };
-
-
- this.onUnload = function () {
- Zotero.Notifier.unregisterObserver(this._unregisterID);
- };
-
-
- /*
- * Load a top-level item
- */
- this.viewItem = Zotero.Promise.coroutine(function* (item, mode, pinnedPane) {
- Zotero.debug('Viewing item');
-
- _notesBox.parentItem = item;
-
- let isSameItem = _lastItem?.id === item.id;
-
- _lastItem = item;
-
- _container.classList.toggle('feed-item', !!item.isFeedItem);
- if (item.isFeedItem) {
- let lastTranslationTarget = Zotero.Prefs.get('feeds.lastTranslationTarget');
- if (lastTranslationTarget) {
- let id = parseInt(lastTranslationTarget.substr(1));
- if (lastTranslationTarget[0] == "L") {
- _translationTarget = Zotero.Libraries.get(id);
- }
- else if (lastTranslationTarget[0] == "C") {
- _translationTarget = Zotero.Collections.get(id);
- }
- }
- if (!_translationTarget) {
- _translationTarget = Zotero.Libraries.userLibrary;
- }
- this.setTranslateButton();
- }
-
- let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
- for (let box of [_header, ..._boxes]) {
- if (!box.showInFeeds && item.isFeedItem) {
- box.style.display = 'none';
- box.hidden = true;
- continue;
- }
- else {
- box.style.display = '';
- box.hidden = false;
- }
-
- if (mode) {
- box.mode = mode;
-
- if (box.mode == 'view') {
- box.hideEmptyFields = true;
- }
- }
- else {
- box.mode = 'edit';
- }
-
- box.item = item;
- box.inTrash = inTrash;
- }
-
- if (!isSameItem) {
- if (pinnedPane && !_sidenav.getPane(pinnedPane)) {
- pinnedPane = "";
- }
-
- _scrollParent.style.paddingBottom = '';
- if (pinnedPane) {
- _sidenav.scrollToPane(pinnedPane, 'instant');
- _sidenav.pinnedPane = pinnedPane;
- }
- else if (pinnedPane !== false) {
- _sidenav.scrollToPane(_sidenav.getPanes()[0]?.getAttribute('data-pane'), 'instant');
- }
- }
-
- _sidenav.render();
- });
-
-
- this.notify = Zotero.Promise.coroutine(function* (action, _type, _ids, _extraData) {
- if (action == 'refresh' && _lastItem) {
- yield this.viewItem(_lastItem, null, false);
- }
- });
-
-
- this.blurOpenField = async function () {
- if (_itemBox.contains(document.activeElement)) {
- await _itemBox.blurOpenField();
- }
- else if (_header.contains(document.activeElement)) {
- await _header.blurOpenField();
- }
- _scrollParent.focus();
- };
-
-
- this.onNoteSelected = function (item, editable) {
- _selectedNoteID = item.id;
-
- var noteEditor = document.getElementById('zotero-note-editor');
- noteEditor.mode = editable ? 'edit' : 'view';
- noteEditor.viewMode = 'library';
- noteEditor.parent = null;
- noteEditor.item = item;
-
- document.getElementById('zotero-item-pane-content').selectedIndex = 2;
- };
-
- // Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
- this.handleKeypress = function (event) {
- let stopEvent = () => {
- event.preventDefault();
- event.stopPropagation();
- };
- let isLibraryTab = Zotero_Tabs.selectedIndex == 0;
- let sidenav = document.getElementById(
- isLibraryTab ? 'zotero-view-item-sidenav' : 'zotero-context-pane-sidenav'
- );
-
- // Tab from the scrollable area focuses the pinned pane if it exists
- if (event.target.classList.contains("zotero-view-item") && event.key == "Tab" && !event.shiftKey && sidenav.pinnedPane) {
- let pane = sidenav.getPane(sidenav.pinnedPane);
- pane.firstChild._head.focus();
- stopEvent();
- return;
- }
- // Tab tavigation between entries and buttons within library, related and notes boxes
- if (event.key == "Tab" && event.target.closest(".box")) {
- let next = null;
- if (event.key == "Tab" && !event.shiftKey) {
- next = event.target.nextElementSibling;
- }
- if (event.key == "Tab" && event.shiftKey) {
- next = event.target.parentNode.previousElementSibling?.lastChild;
- }
- // Force the element to be visible before focusing
- if (next) {
- next.style.visibility = "visible";
- next.focus();
- next.style.removeProperty("visibility");
- stopEvent();
- }
- }
- };
-
-
- /**
- * Select the parent item and open the note editor
- */
- this.openNoteWindow = async function () {
- var selectedNote = Zotero.Items.get(_selectedNoteID);
- ZoteroPane.openNoteWindow(selectedNote.id);
- };
-
-
- this.translateSelectedItems = Zotero.Promise.coroutine(function* () {
- var collectionID = _translationTarget.objectType == 'collection' ? _translationTarget.id : undefined;
- var items = ZoteroPane_Local.itemsView.getSelectedItems();
- for (let item of items) {
- yield item.translate(_translationTarget.libraryID, collectionID);
- }
- });
-
-
- this.buildTranslateSelectContextMenu = function (event) {
- var menu = document.getElementById('zotero-item-addTo-menu');
- // Don't trigger rebuilding on nested popupmenu open/close
- if (event.target != menu) {
- return;
- }
- // Clear previous items
- while (menu.firstChild) {
- menu.removeChild(menu.firstChild);
- }
-
- let target = Zotero.Prefs.get('feeds.lastTranslationTarget');
- if (!target) {
- target = "L" + Zotero.Libraries.userLibraryID;
- }
-
- var libraries = Zotero.Libraries.getAll();
- for (let library of libraries) {
- if (!library.editable || library.libraryType == 'publications') {
- continue;
- }
- Zotero.Utilities.Internal.createMenuForTarget(
- library,
- menu,
- target,
- function(event, libraryOrCollection) {
- if (event.target.tagName == 'menu') {
- Zotero.Promise.coroutine(function* () {
- // Simulate menuitem flash on OS X
- if (Zotero.isMac) {
- event.target.setAttribute('_moz-menuactive', false);
- yield Zotero.Promise.delay(50);
- event.target.setAttribute('_moz-menuactive', true);
- yield Zotero.Promise.delay(50);
- event.target.setAttribute('_moz-menuactive', false);
- yield Zotero.Promise.delay(50);
- event.target.setAttribute('_moz-menuactive', true);
- }
- menu.hidePopup();
-
- ZoteroItemPane.setTranslationTarget(libraryOrCollection);
- event.stopPropagation();
- })();
- }
- else {
- ZoteroItemPane.setTranslationTarget(libraryOrCollection);
- event.stopPropagation();
- }
- }
- );
- }
- };
-
-
- this.setTranslateButton = function() {
- var label = Zotero.getString('pane.item.addTo', _translationTarget.name);
- var elem = document.getElementById('zotero-feed-item-addTo-button');
- elem.label = label;
-
- var key = Zotero.Keys.getKeyForCommand('saveToZotero');
-
- var tooltip = label
- + (Zotero.rtl ? ' \u202B' : ' ') + '('
- + (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
- + key + ')';
- elem.title = tooltip;
- elem.image = _translationTarget.treeViewImage;
- };
-
-
- this.setTranslationTarget = function(translationTarget) {
- _translationTarget = translationTarget;
- Zotero.Prefs.set('feeds.lastTranslationTarget', translationTarget.treeViewID);
- ZoteroItemPane.setTranslateButton();
- };
-
-
- this.setReadLabel = function (isRead) {
- var elem = document.getElementById('zotero-feed-item-toggleRead-button');
- var label = Zotero.getString('pane.item.' + (isRead ? 'markAsUnread' : 'markAsRead'));
- elem.textContent = label;
-
- var key = Zotero.Keys.getKeyForCommand('toggleRead');
- var tooltip = label + (Zotero.rtl ? ' \u202B' : ' ') + '(' + key + ')';
- elem.title = tooltip;
- };
-
-
- this.getPinnedPane = function () {
- return _sidenav.pinnedPane;
- };
-
-
- this.buildFieldTransformMenu = function ({ target, onTransform }) {
- let value = target.value;
- let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
- let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
-
- let menupopup = document.createXULElement('menupopup');
-
- let titleCase = document.createXULElement('menuitem');
- titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
- titleCase.addEventListener('command', () => {
- onTransform(valueTitleCased);
- });
- titleCase.disabled = valueTitleCased == value;
- menupopup.append(titleCase);
-
- let sentenceCase = document.createXULElement('menuitem');
- sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
- sentenceCase.addEventListener('command', () => {
- onTransform(valueSentenceCased);
- });
- sentenceCase.disabled = valueSentenceCased == value;
- menupopup.append(sentenceCase);
-
- Zotero.Utilities.Internal.updateEditContextMenu(menupopup, target);
-
- return menupopup;
- };
-};
-
-addEventListener("load", function(e) { ZoteroItemPane.onLoad(e); }, false);
-addEventListener("unload", function(e) { ZoteroItemPane.onUnload(e); }, false);
diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx
index 92d3b2d4aa..6328d7c68e 100644
--- a/chrome/content/zotero/itemTree.jsx
+++ b/chrome/content/zotero/itemTree.jsx
@@ -581,7 +581,7 @@ var ItemTree = class ItemTree extends LibraryTree {
}
}
else if (collectionTreeRow.isFeedsOrFeed()) {
- window.ZoteroPane.updateReadLabel();
+ // Moved to itemPane CE
}
// If not a search, process modifications manually
else {
@@ -1173,7 +1173,7 @@ var ItemTree = class ItemTree extends LibraryTree {
// Single item
if (rowsToSelect.length == 1) {
// this.selection.select() triggers the tree onSelect handler attribute, which calls
- // ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(), which refreshes the
+ // ZoteroPane.itemSelected(), which calls ZoteroPane.itemPane.render(), which refreshes the
// itembox. But since the 'onselect' doesn't handle promises, itemSelected() isn't waited for
// here, which means that 'yield selectItem(itemID)' continues before the itembox has been
// refreshed. To get around this, we wait for a select event that's triggered by
diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js
index 617b188cca..cf9f4b6563 100644
--- a/chrome/content/zotero/tabs.js
+++ b/chrome/content/zotero/tabs.js
@@ -753,8 +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.
- const pane = document.getElementById("zotero-item-pane-content");
- if (pane.selectedIndex === "0") {
+ if (ZoteroPane.itemPane.viewType == "message") {
document.getElementById("item-tree-main-default").focus();
}
else {
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 76f16e4e0d..c8e46682a4 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -33,6 +33,7 @@ var ZoteroPane = new function()
var _unserialized = false;
this.collectionsView = false;
this.itemsView = false;
+ this.itemPane = false;
this.progressWindow = false;
this._listeners = {};
this.__defineGetter__('loaded', function () { return _loaded; });
@@ -113,6 +114,7 @@ var ZoteroPane = new function()
this.itemsView?.updateFontSize();
});
Zotero.UIProperties.registerRoot(document.getElementById('zotero-context-pane'));
+ this.itemPane = document.querySelector("#zotero-item-pane");
ZoteroPane_Local.updateLayout();
ZoteroPane_Local.updateToolbarPosition();
this.updateWindow();
@@ -1219,8 +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) {
- var deck = document.getElementById('zotero-item-pane-content');
- if (deck.selectedPanel.id == 'zotero-view-note') {
+ if (ZoteroPane.itemPane.viewType == "note") {
document.getElementById('zotero-note-editor').focus();
event.preventDefault();
return;
@@ -1310,7 +1311,7 @@ var ZoteroPane = new function()
case 'saveToZotero':
var collectionTreeRow = this.getCollectionTreeRow();
if (collectionTreeRow.isFeedsOrFeed()) {
- ZoteroItemPane.translateSelectedItems();
+ this.itemPane.translateSelectedItems();
} else {
Zotero.debug(command + ' does not do anything in non-feed views')
}
@@ -1369,7 +1370,7 @@ var ZoteroPane = new function()
}
}
- yield ZoteroItemPane.blurOpenField();
+ yield this.itemPane._itemDetails.blurOpenField();
if (row !== undefined && row !== null) {
var collectionTreeRow = this.collectionsView.getRow(row);
@@ -1395,9 +1396,8 @@ var ZoteroPane = new function()
});
// Expand the item pane if it's closed
- var itemPane = document.getElementById("zotero-item-pane");
- if (itemPane.getAttribute("collapsed") == "true") {
- itemPane.setAttribute("collapsed", false)
+ if (this.itemPane.getAttribute("collapsed") == "true") {
+ this.itemPane.setAttribute("collapsed", false);
}
//set to Info tab
@@ -1857,12 +1857,27 @@ var ZoteroPane = new function()
Zotero.debug("Items view not available in itemSelected", 2);
return false;
}
+ let collectionTreeRow = this.getCollectionTreeRow();
+ // I don't think this happens in normal usage, but it can happen during tests
+ if (!collectionTreeRow) {
+ return false;
+ }
var selectedItems = this.itemsView.getSelectedItems();
// 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.updateItemPaneButtons(selectedItems);
+ 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.editable = this.collectionsView.editable;
+ this.itemPane.updateItemPaneButtons(selectedItems);
// Tab selection observer in standalone.js makes sure that
// updateQuickCopyCommands is called
@@ -1880,163 +1895,7 @@ var ZoteroPane = new function()
}
_lastSelectedItems = ids;
- var collectionTreeRow = this.getCollectionTreeRow();
- // I don't think this happens in normal usage, but it can happen during tests
- if (!collectionTreeRow) {
- return false;
- }
-
- let pane = document.getElementById('zotero-item-pane');
- let deck = document.getElementById('zotero-item-pane-content');
- let sidenav = document.getElementById('zotero-view-item-sidenav');
-
- let hideSidenav = false;
-
- // Single item selected
- if (selectedItems.length == 1) {
- var item = selectedItems[0];
- sidenav.querySelectorAll('toolbarbutton').forEach(button => button.disabled = false);
-
- if (item.isNote()) {
- hideSidenav = true;
- ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
- }
-
- // Regular item
- else {
- var isCommons = collectionTreeRow.isBucket();
-
- deck.selectedIndex = 1;
-
- let pane = ZoteroItemPane.getPinnedPane();
-
- var button = document.getElementById('zotero-item-show-original');
- if (isCommons) {
- button.hidden = false;
- button.disabled = !this.getOriginalItem();
- }
- else {
- button.hidden = true;
- }
-
- if (this.collectionsView.editable) {
- yield ZoteroItemPane.viewItem(item, null, pane);
- }
- else {
- yield ZoteroItemPane.viewItem(item, 'view', pane);
- }
-
- if (item.isFeedItem) {
- // Too slow for now
- // if (!item.isTranslated) {
- // item.translate();
- // }
- this.updateReadLabel();
- this.startItemReadTimeout(item.id);
- }
- }
- }
- // Zero or multiple items selected
- else {
- let defaultSidenavButtons = [
- "info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"
- ];
- sidenav.querySelectorAll('toolbarbutton').forEach((button) => {
- button.disabled = true;
- button.parentElement.hidden = !defaultSidenavButtons.includes(button.dataset.pane);
- });
- if (collectionTreeRow.isFeedsOrFeed()) {
- this.updateReadLabel();
- }
-
- let count = selectedItems.length;
-
- // Display duplicates merge interface in item pane
- if (collectionTreeRow.isDuplicates()) {
- if (!collectionTreeRow.editable) {
- if (count) {
- var msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
- }
- else {
- var msg = Zotero.getString('pane.item.selected.zero');
- }
- this.setItemPaneMessage(msg);
- }
- else if (count) {
- deck.selectedIndex = 3;
-
- // Load duplicates UI code
- if (typeof Zotero_Duplicates_Pane == 'undefined') {
- Zotero.debug("Loading duplicatesMerge.js");
- Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
- .getService(Components.interfaces.mozIJSSubScriptLoader)
- .loadSubScript("chrome://zotero/content/duplicatesMerge.js");
- }
-
- // On a Select All of more than a few items, display a row
- // count instead of the usual item type mismatch error
- var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
-
- // Initialize the merge pane with the selected items
- Zotero_Duplicates_Pane.setItems(selectedItems, displayNumItemsOnTypeError);
- }
- else {
- var msg = Zotero.getString('pane.item.duplicates.selectToMerge');
- this.setItemPaneMessage(msg);
- }
- }
- // Display label in the middle of the item pane
- else {
- if (count) {
- var msg = Zotero.getString('pane.item.selected.multiple', count);
- }
- else {
- var rowCount = this.itemsView.rowCount;
- var str = 'pane.item.unselected.';
- switch (rowCount){
- case 0:
- str += 'zero';
- break;
- case 1:
- str += 'singular';
- break;
- default:
- str += 'plural';
- break;
- }
- var msg = Zotero.getString(str, [rowCount]);
- }
-
- this.setItemPaneMessage(msg);
-
- return false;
- }
- }
-
- if (!document.querySelector("#zotero-items-splitter").collapsed) {
- let isStackedMode = Zotero.Prefs.get("layout") === "stacked";
- const sidenavSize = 37;
- if (hideSidenav && !sidenav.hidden) {
- sidenav.hidden = true;
- if (isStackedMode) {
- pane.height = `${(pane.clientHeight) + sidenavSize}`;
- }
- else {
- pane.width = `${(pane.clientWidth) + sidenavSize}`;
- }
- }
- else if (!hideSidenav && sidenav.hidden) {
- sidenav.hidden = false;
- if (isStackedMode) {
- pane.height = `${(pane.clientHeight) - sidenavSize}`;
- }
- else {
- pane.width = `${(pane.clientWidth) - sidenavSize}`;
- }
- }
- }
-
- return true;
+ return this.itemPane.render();
}.bind(this))()
.catch(function (e) {
Zotero.logError(e);
@@ -2048,56 +1907,6 @@ var ZoteroPane = new function()
}.bind(this));
};
-
- /**
- * Display buttons at top of item pane depending on context
- *
- * @param {Zotero.Item[]}
- */
- this.updateItemPaneButtons = function (selectedItems) {
- if (!selectedItems.length) {
- document.querySelectorAll('.zotero-item-pane-top-buttons').forEach(x => x.hidden = true);
- return;
- }
-
- // My Publications buttons
- var isPublications = this.getCollectionTreeRow().isPublications();
- // Show in My Publications view if selected items are all notes or non-linked-file attachments
- var showMyPublicationsButtons = isPublications
- && selectedItems.every((item) => {
- return item.isNote()
- || (item.isAttachment()
- && item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
- });
- var myPublicationsButtons = document.getElementById('zotero-item-pane-top-buttons-my-publications');
- myPublicationsButtons.hidden = !showMyPublicationsButtons;
- if (showMyPublicationsButtons) {
- let button = myPublicationsButtons.firstChild;
- let hiddenItemsSelected = selectedItems.some(item => !item.inPublications);
- let str, onclick;
- if (hiddenItemsSelected) {
- str = 'showInMyPublications';
- onclick = () => Zotero.Items.addToPublications(selectedItems);
- }
- else {
- str = 'hideFromMyPublications';
- onclick = () => Zotero.Items.removeFromPublications(selectedItems);
- }
- button.label = Zotero.getString('pane.item.' + str);
- button.onclick = onclick;
- }
-
- // Trash button
- let nonDeletedItemsSelected = selectedItems.some(item => !item.deleted);
- document.getElementById('zotero-item-pane-top-buttons-trash').hidden
- = !this.getCollectionTreeRow().isTrash() || nonDeletedItemsSelected;
-
- // Feed buttons
- document.getElementById('zotero-item-pane-top-buttons-feed').hidden
- = !this.getCollectionTreeRow().isFeedsOrFeed()
- };
-
-
this.updateAddAttachmentMenu = function (popup) {
if (!this.canEdit()) {
for (let node of popup.childNodes) {
@@ -2436,17 +2245,10 @@ var ZoteroPane = new function()
return;
}
- document.getElementById('zotero-item-pane-content').selectedIndex = 3;
-
- if (typeof Zotero_Duplicates_Pane == 'undefined') {
- Zotero.debug("Loading duplicatesMerge.js");
- Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
- .getService(Components.interfaces.mozIJSSubScriptLoader)
- .loadSubScript("chrome://zotero/content/duplicatesMerge.js");
- }
+ this.itemPane.viewType = "duplicates";
// Initialize the merge pane with the selected items
- Zotero_Duplicates_Pane.setItems(this.getSelectedItems());
+ this.itemPane._duplicatesPane.setItems(this.getSelectedItems());
}
@@ -2522,26 +2324,6 @@ var ZoteroPane = new function()
}
}
}
-
-
- // Currently used only for Commons to find original linked item
- this.getOriginalItem = function () {
- var item = this.getSelectedItems()[0];
- var collectionTreeRow = this.getCollectionTreeRow();
- // TEMP: Commons buckets only
- return collectionTreeRow.ref.getLocalItem(item);
- }
-
-
- this.showOriginalItem = function () {
- var item = this.getOriginalItem();
- if (!item) {
- Zotero.debug("Original item not found");
- return;
- }
- this.selectItem(item.id).done();
- }
-
/**
* Check whether every selected item can be restored from trash
@@ -4257,25 +4039,6 @@ var ZoteroPane = new function()
}
- this.setItemPaneMessage = function (content) {
- document.getElementById('zotero-item-pane-content').selectedIndex = 0;
-
- var elem = document.getElementById('zotero-item-pane-message-box');
- elem.textContent = '';
- if (typeof content == 'string') {
- let contentParts = content.split("\n\n");
- for (let part of contentParts) {
- let desc = document.createXULElement('description');
- desc.appendChild(document.createTextNode(part));
- elem.appendChild(desc);
- }
- }
- else {
- elem.appendChild(content);
- }
- }
-
-
/**
* @return {Promise
} - The id of the new note in non-popup mode, null in
* popup mode (where a note isn't created immediately), or false if library isn't editable
@@ -4862,11 +4625,10 @@ var ZoteroPane = new function()
}
}
else if (item.isNote()) {
- var type = Zotero.Libraries.get(item.libraryID).libraryType;
if (!this.collectionsView.editable) {
continue;
}
- ZoteroItemPane.openNoteWindow();
+ ZoteroPane.openNoteWindow(item.id);
}
else if (item.isAttachment()) {
yield this.viewAttachment(item.id, event);
@@ -6074,20 +5836,6 @@ var ZoteroPane = new function()
}
};
-
- this.updateReadLabel = function () {
- var items = this.getSelectedItems();
- var isUnread = false;
- for (let item of items) {
- if (!item.isRead) {
- isUnread = true;
- break;
- }
- }
- ZoteroItemPane.setReadLabel(!isUnread);
- };
-
-
var itemReadPromise;
this.startItemReadTimeout = function (feedItemID) {
if (itemReadPromise) {
@@ -6114,7 +5862,7 @@ var ZoteroPane = new function()
}
await feedItem.toggleRead(true);
- ZoteroItemPane.setReadLabel(true);
+ this.itemPane.setReadLabel(true);
}.bind(this))
.catch(function (e) {
if (e instanceof Zotero.Promise.CancellationError) {
@@ -6266,14 +6014,17 @@ var ZoteroPane = new function()
var itemsSplitter = document.getElementById("zotero-items-splitter");
var sidenav = document.getElementById("zotero-view-item-sidenav");
- if(Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane
+ if (Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane
layoutSwitcher.setAttribute("orient", "vertical");
itemsSplitter.setAttribute("orient", "vertical");
sidenav.classList.add("stacked");
- } else { // three-vertical-pane
+ this.itemPane.classList.add("stacked");
+ }
+ else { // three-vertical-pane
layoutSwitcher.setAttribute("orient", "horizontal");
itemsSplitter.setAttribute("orient", "horizontal");
sidenav.classList.remove("stacked");
+ this.itemPane.classList.remove("stacked");
}
this.updateToolbarPosition();
@@ -6381,7 +6132,6 @@ var ZoteroPane = new function()
var collectionsPane = document.getElementById("zotero-collections-pane");
var tagSelector = document.getElementById("zotero-tag-selector");
- var sidenav = document.getElementById("zotero-view-item-sidenav");
var collectionsPaneWidth = collectionsPane.getBoundingClientRect().width;
tagSelector.style.maxWidth = collectionsPaneWidth + 'px';
@@ -6403,10 +6153,8 @@ var ZoteroPane = new function()
}
this.handleTagSelectorResize();
-
- sidenav.render();
- // If the itemPane has just been expanded, scroll to the correct pane
- sidenav.showPendingPane();
+
+ this.itemPane.handleResize();
}
/**
@@ -6435,21 +6183,51 @@ var ZoteroPane = new function()
/**
* Implements nsIObserver for Zotero reload
*/
- var _reloadObserver = {
+ var _reloadObserver = {
+
/**
* Called when Zotero is reloaded (i.e., if it is switched into or out of connector mode)
*/
- "observe":function(aSubject, aTopic, aData) {
- if(aTopic == "zotero-reloaded") {
+ observe: function (aSubject, aTopic, aData) {
+ if (aTopic == "zotero-reloaded") {
Zotero.debug("Reloading Zotero pane");
for (let func of _reloadFunctions) func(aData);
- } else if(aTopic == "zotero-before-reload") {
+ }
+ else if (aTopic == "zotero-before-reload") {
Zotero.debug("Zotero pane caught before-reload event");
for (let func of _beforeReloadFunctions) func(aData);
}
}
};
-}
+
+ this.buildFieldTransformMenu = function ({ target, onTransform }) {
+ let value = target.value;
+ let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
+ let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
+
+ let menupopup = document.createXULElement('menupopup');
+
+ let titleCase = document.createXULElement('menuitem');
+ titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
+ titleCase.addEventListener('command', () => {
+ onTransform(valueTitleCased);
+ });
+ titleCase.disabled = valueTitleCased == value;
+ menupopup.append(titleCase);
+
+ let sentenceCase = document.createXULElement('menuitem');
+ sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
+ sentenceCase.addEventListener('command', () => {
+ onTransform(valueSentenceCased);
+ });
+ sentenceCase.disabled = valueSentenceCased == value;
+ menupopup.append(sentenceCase);
+
+ Zotero.Utilities.Internal.updateEditContextMenu(menupopup, target);
+
+ return menupopup;
+ };
+};
/**
* Keep track of which ZoteroPane was local (since ZoteroPane object might get swapped out for a
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index a49d831569..9f5c4df170 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -30,8 +30,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- &zotero.duplicatesMerge.versionSelect;
-
-
-
-
-
-
- &zotero.duplicatesMerge.fieldSelect;
-
-
-
-
-
-
-
-
-
+
@@ -1383,6 +1282,8 @@
oncommand="ZoteroPane.tagSelector.deleteAutomatic();
this.setAttribute('checked', false);"/>
+
+
diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl
index bc8caa1202..f033b24e11 100644
--- a/chrome/locale/en-US/zotero/zotero.ftl
+++ b/chrome/locale/en-US/zotero/zotero.ftl
@@ -37,6 +37,11 @@ menu-new-standalone-note =
menu-new-item-note =
.label = New Item Note
+menu-restoreToLibrary =
+ .label = Restore to Library
+menu-deletePermanently =
+ .label = Delete Permanently…
+
zotero-toolbar-tabs-menu =
.tooltiptext = List all tabs
filter-collections = Filter Collections
diff --git a/scss/_zotero.scss b/scss/_zotero.scss
index 68aad79c68..3eae0e56a9 100644
--- a/scss/_zotero.scss
+++ b/scss/_zotero.scss
@@ -60,7 +60,6 @@
@import "components/tabsMenu";
@import "components/newCollectionDialog";
@import "components/reader";
-@import "components/itemPane";
// Elements
// --------------------------------------------------
@@ -90,3 +89,8 @@
@import "elements/annotationRow";
@import "elements/noteRow";
@import "elements/librariesCollectionsBox";
+@import "elements/duplicatesMergePane";
+@import "elements/itemMessagePane";
+@import "elements/itemDetails";
+@import "elements/itemPane";
+@import "elements/contextPane";
diff --git a/scss/components/_itemPane.scss b/scss/components/_itemPane.scss
deleted file mode 100644
index 975a570744..0000000000
--- a/scss/components/_itemPane.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-#zotero-item-pane {
- width: $min-width-item-pane;
- min-width: $min-width-item-pane;
- /* Need a min height to prevent layout issues in stacked mode */
- min-height: 168px;
- background: var(--material-sidepane);
-}
diff --git a/scss/elements/_contextPane.scss b/scss/elements/_contextPane.scss
new file mode 100644
index 0000000000..4573bcff99
--- /dev/null
+++ b/scss/elements/_contextPane.scss
@@ -0,0 +1,2 @@
+context-pane {
+}
diff --git a/scss/elements/_duplicatesMergePane.scss b/scss/elements/_duplicatesMergePane.scss
new file mode 100644
index 0000000000..9f7930582b
--- /dev/null
+++ b/scss/elements/_duplicatesMergePane.scss
@@ -0,0 +1,24 @@
+duplicates-merge-pane {
+ -moz-box-orient: vertical;
+
+ groupbox {
+ margin: 8px 0 0 0;
+ }
+
+ #zotero-duplicates-merge-button
+ {
+ font-size: 13px;
+ }
+
+ #zotero-duplicates-merge-item-box-container {
+ overflow-y: auto;
+ padding: 0 8px;
+ }
+
+ /* Show duplicates date list item as selected even when not focused
+ (default behavior on other platforms) */
+ #zotero-duplicates-merge-original-date:not(:focus) > richlistitem[selected="true"] {
+ background-color: -moz-cellhighlight;
+ color: -moz-cellhighlighttext;
+ }
+}
diff --git a/chrome/skin/default/zotero/itemPane.css b/scss/elements/_itemDetails.scss
similarity index 66%
rename from chrome/skin/default/zotero/itemPane.css
rename to scss/elements/_itemDetails.scss
index dcbf4424b4..8f3abd46f6 100644
--- a/chrome/skin/default/zotero/itemPane.css
+++ b/scss/elements/_itemDetails.scss
@@ -11,6 +11,11 @@
.zotero-item-pane-content {
min-height: 0;
flex: 1;
+ width: $min-width-item-pane;
+ min-width: $min-width-item-pane;
+ /* Need a min height to prevent layout issues in stacked mode */
+ min-height: 168px;
+ background: var(--material-sidepane);
}
.zotero-view-item-container {
@@ -33,6 +38,9 @@
padding: 0 8px;
overflow-anchor: none; /* Work around tags box causing scroll to jump - figure this out */
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
+ display: flex;
+ flex-direction: column;
+ gap: 1px; /* Need gap for intersection computing */
}
.zotero-view-item::before {
@@ -54,33 +62,3 @@
{
margin-left: 5px;
}
-
-/* Buttons in trash and feed views */
-.zotero-item-pane-top-buttons > button {
- -moz-box-flex: 1
-}
-
-/* Merge pane in duplicates view */
-#zotero-duplicates-merge-button
-{
- font-size: 13px;
-}
-
-#zotero-duplicates-merge-pane > groupbox {
- margin: 8px 0 0 0;
-}
-
-#zotero-duplicates-merge-item-box-container {
- overflow-y: scroll;
-}
-
-#zotero-feed-item-toggleRead-button {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- max-width: 150px;
-}
-
-#zotero-feed-item-addTo-button {
- max-width: 250px;
-}
diff --git a/scss/elements/_itemMessagePane.scss b/scss/elements/_itemMessagePane.scss
new file mode 100644
index 0000000000..ebae40292b
--- /dev/null
+++ b/scss/elements/_itemMessagePane.scss
@@ -0,0 +1,32 @@
+item-message-pane {
+ -moz-box-orient: vertical;
+
+ .custom-head {
+ display: flex;
+ flex-direction: row;
+ align-self: stretch;
+ gap: 6px;
+ padding: 6px 8px;
+ background: var(--material-toolbar);
+ border-bottom: var(--material-panedivider);
+ height: 28px;
+
+ &:empty {
+ display: none;
+ }
+
+ button {
+ height: 26px;
+ margin: 0;
+ flex-grow: 1;
+ }
+ }
+
+ #zotero-item-pane-groupbox {
+ -moz-box-flex: 1;
+ -moz-box-pack: center;
+ -moz-box-align: center;
+ -moz-appearance: none !important;
+ border-width: 0;
+ }
+}
diff --git a/scss/elements/_itemPane.scss b/scss/elements/_itemPane.scss
new file mode 100644
index 0000000000..78b3173861
--- /dev/null
+++ b/scss/elements/_itemPane.scss
@@ -0,0 +1,13 @@
+item-pane {
+ &[collapsed="true"] {
+ visibility: inherit;
+
+ #zotero-item-pane-content {
+ visibility: collapse;
+ }
+ }
+
+ &.stacked {
+ -moz-box-orient: vertical;
+ }
+}
diff --git a/scss/elements/_noteEditor.scss b/scss/elements/_noteEditor.scss
index 272475cc4a..78e73ef787 100644
--- a/scss/elements/_noteEditor.scss
+++ b/scss/elements/_noteEditor.scss
@@ -1,3 +1,28 @@
+note-editor {
+ -moz-box-orient: vertical;
+
+ .custom-head {
+ display: flex;
+ flex-direction: row;
+ align-self: stretch;
+ gap: 6px;
+ padding: 6px 8px;
+ background: var(--material-toolbar);
+ border-bottom: var(--material-panedivider);
+ height: 28px;
+
+ &:empty {
+ display: none;
+ }
+
+ button {
+ height: 26px;
+ margin: 0;
+ flex-grow: 1;
+ }
+ }
+}
+
links-box {
display: flex;
flex-direction: column;
diff --git a/scss/elements/_paneHeader.scss b/scss/elements/_paneHeader.scss
index 6d52ff205e..b4c5046ae1 100644
--- a/scss/elements/_paneHeader.scss
+++ b/scss/elements/_paneHeader.scss
@@ -2,27 +2,34 @@ pane-header {
display: flex;
flex-direction: column;
align-items: flex-start;
- padding: 6px 8px 0 8px;
+ padding: 6px 8px;
+ gap: 6px;
+ border-bottom: 1px solid var(--fill-quinary);
.head {
display: flex;
align-self: stretch;
gap: 4px;
- padding-bottom: 6px;
- border-bottom: 1px solid var(--fill-quinary);
- }
-
- .title {
- align-self: center;
- margin-top: calc(0px - var(--editable-text-padding-block));
- padding: 2px 0px 1px 0px;
- flex: 1 1 0;
- font-weight: 600;
- line-height: 1.333;
+ .title {
+ align-self: center;
+ margin-top: calc(0px - var(--editable-text-padding-block));
+ padding: 2px 0px 1px 0px;
+ flex: 1 1 0;
+ font-weight: 600;
+ line-height: 1.333;
+
+ editable-text {
+ flex: 1;
+ }
+ }
- editable-text {
- flex: 1;
+ .menu-button {
+ align-self: start;
+ }
+
+ .menu-button toolbarbutton {
+ @include svgicon-menu("go-to", "universal", "20");
}
}
@@ -35,4 +42,21 @@ pane-header {
--width-focus-border: 2px;
@include focus-ring;
}
+
+ .custom-head {
+ display: flex;
+ flex-direction: row;
+ align-self: stretch;
+ gap: 6px;
+
+ &:empty {
+ display: none;
+ }
+
+ button {
+ height: 26px;
+ margin: 0;
+ flex-grow: 1;
+ }
+ }
}
diff --git a/test/tests/itemPaneTest.js b/test/tests/itemPaneTest.js
index bd13cfed4c..2fab81c051 100644
--- a/test/tests/itemPaneTest.js
+++ b/test/tests/itemPaneTest.js
@@ -403,9 +403,11 @@ describe("Item pane", function () {
let button = doc.getElementById('zotero-feed-item-toggleRead-button');
- assert.equal(button.textContent, Zotero.getString('pane.item.markAsUnread'));
+ assert.equal(button.label, Zotero.getString('pane.item.markAsUnread'));
yield item.toggleRead(false);
- assert.equal(button.textContent, Zotero.getString('pane.item.markAsRead'));
+ // Button is re-created
+ button = doc.getElementById('zotero-feed-item-toggleRead-button');
+ assert.equal(button.label, Zotero.getString('pane.item.markAsRead'));
});
});
});
diff --git a/test/tests/itemTreeTest.js b/test/tests/itemTreeTest.js
index 5e33f9a36c..f2e549c75e 100644
--- a/test/tests/itemTreeTest.js
+++ b/test/tests/itemTreeTest.js
@@ -856,7 +856,7 @@ describe("Zotero.ItemTree", function() {
yield itemsView.selectItem(attachment.id);
yield Zotero.Promise.delay();
- var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
+ var box = win.document.getElementById('zotero-item-pane-my-publications-button');
assert.isFalse(box.hidden);
});
@@ -872,8 +872,9 @@ describe("Zotero.ItemTree", function() {
yield itemsView.selectItem(attachment.id);
- var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
- assert.isTrue(box.hidden);
+ var box = win.document.getElementById('zotero-item-pane-my-publications-button');
+ // box is not created if it shouldn't show
+ assert.isNull(box);
});
});
})