diff --git a/.eslintrc b/.eslintrc index 0a9a253ce1..e3b25a9f0e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,6 +23,7 @@ "XRegExp": false, "XULElement": false, "XULElementBase": false, + "ItemPaneSectionElementBase": false, "Cu": false, "ChromeWorker": false, "Localization": false, @@ -31,7 +32,6 @@ "ZoteroPane_Local": false, "ZoteroPane": false, "Zotero_Tabs": false, - "ZoteroItemPane": false, "IOUtils": false, "NetUtil": false, "FileUtils": false, diff --git a/chrome/content/zotero-platform/mac/itemPane.css b/chrome/content/zotero-platform/mac/itemPane.css deleted file mode 100644 index ca05aa7bcb..0000000000 --- a/chrome/content/zotero-platform/mac/itemPane.css +++ /dev/null @@ -1,15 +0,0 @@ -#zotero-feed-item-toggleRead-button { - margin: 5px 0 3px 6px; -} - -#zotero-feed-item-addTo-button { - margin: 5px 6px 3px; - padding-left: 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/content/zotero-platform/win/overlay.css b/chrome/content/zotero-platform/win/overlay.css index a602ca7596..6ac37edd03 100644 --- a/chrome/content/zotero-platform/win/overlay.css +++ b/chrome/content/zotero-platform/win/overlay.css @@ -17,11 +17,6 @@ tree { padding-left: 0.05em; } -#zotero-item-pane-groupbox { - -moz-appearance: none !important; - border-width: 0; -} - .zotero-editpane-item-box > scrollbox, .zotero-view-item > tabpanel > vbox, #zotero-editpane-tags > scrollbox, .zotero-editpane-related { padding-top: 5px; diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js index 710c8cb71a..56705de90f 100644 --- a/chrome/content/zotero/contextPane.js +++ b/chrome/content/zotero/contextPane.js @@ -82,12 +82,12 @@ var ZoteroContextPane = new function () { window.addEventListener('resize', _update); Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth; Zotero.Reader.onToggleSidebar = _updatePaneWidth; - _contextPaneInner.addEventListener("keypress", ZoteroItemPane.handleKeypress); + _contextPaneInner.addEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress); }; this.destroy = function () { window.removeEventListener('resize', _update); - _contextPaneInner.removeEventListener("keypress", ZoteroItemPane.handleKeypress); + _contextPaneInner.removeEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress); Zotero.Notifier.unregisterObserver(this._notifierID); Zotero.Reader.onChangeSidebarWidth = () => {}; Zotero.Reader.onToggleSidebar = () => {}; @@ -227,10 +227,6 @@ var ZoteroContextPane = new function () { _selectItemContext(ids[0]); _update(); - // When a loaded tab is selected, scroll to the pinned pane, if any - if (_sidenav.pinnedPane) { - _sidenav.scrollToPane(_sidenav.pinnedPane, 'instant'); - } } } }; @@ -265,10 +261,12 @@ var ZoteroContextPane = new function () { } } } + return null; } function _focus() { var splitter; + let node; if (Zotero.Prefs.get('layout') == 'stacked') { splitter = _contextPaneSplitterStacked; } @@ -284,7 +282,7 @@ var ZoteroContextPane = new function () { return true; } else { - var node = _notesPaneDeck.selectedPanel; + node = _notesPaneDeck.selectedPanel; if (node.selectedIndex == 0) { node.querySelector('search-textbox').focus(); return true; @@ -375,12 +373,13 @@ var ZoteroContextPane = new function () { _updatePaneWidth(); _updateAddToNote(); - _sidenav.showPendingPane(); + _sidenav.container?.render(); } function _togglePane() { var splitter = Zotero.Prefs.get('layout') == 'stacked' - ? _contextPaneSplitterStacked : _contextPaneSplitter; + ? _contextPaneSplitterStacked + : _contextPaneSplitter; var open = true; if (splitter.getAttribute('state') != 'collapsed') { @@ -396,6 +395,7 @@ var ZoteroContextPane = new function () { if (reader) { return Zotero.Items.get(reader.itemID); } + return null; } function _addNotesContext(libraryID) { @@ -522,7 +522,7 @@ var ZoteroContextPane = new function () { if (item) { document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', readOnly); var popup = document.getElementById('context-pane-list-popup'); - let handleCommand = (event) => _handleListPopupClick(id, event); + let handleCommand = event => _handleListPopupClick(id, event); popup.addEventListener('popupshowing', () => { popup.addEventListener('command', handleCommand, { once: true }); popup.addEventListener('popuphiding', () => { @@ -549,7 +549,8 @@ var ZoteroContextPane = new function () { function _isVisible() { let splitter = Zotero.Prefs.get('layout') == 'stacked' - ? _contextPaneSplitterStacked : _contextPaneSplitter; + ? _contextPaneSplitterStacked + : _contextPaneSplitter; return Zotero_Tabs.selectedID != 'zotero-pane' && _panesDeck.selectedIndex == 1 @@ -604,7 +605,7 @@ var ZoteroContextPane = new function () { for (let cachedNote of context.cachedNotes) { cachedNotesIndex.set(cachedNote.id, cachedNote); } - notes = notes.map(note => { + notes = notes.map((note) => { var parentItem = note.parentItem; // If neither note nor parent item is affected try to return the cached note if (!context.affectedIDs.has(note.id) @@ -760,8 +761,9 @@ var ZoteroContextPane = new function () { var tabNotesDeck = _notesPaneDeck.selectedPanel.querySelector('.zotero-context-pane-tab-notes-deck'); var parentTitleContainer; + let vbox; if (isChild) { - var vbox = document.createXULElement('vbox'); + vbox = document.createXULElement('vbox'); vbox.setAttribute('data-tab-id', Zotero_Tabs.selectedID); vbox.style.display = 'flex'; @@ -821,24 +823,16 @@ var ZoteroContextPane = new function () { } function _selectItemContext(tabID) { + let previousPinnedPane = _sidenav.container?.pinnedPane || ""; let selectedPanel = Array.from(_itemPaneDeck.children).find(x => x.id == tabID + '-context'); if (selectedPanel) { _itemPaneDeck.selectedPanel = selectedPanel; - let div = selectedPanel.querySelector('.zotero-view-item'); - // _addItemContext() awaits, so the div may not have been created yet. We'll set _sidenav.container - // below even if we don't set it here. - if (div) { - _sidenav.container = div; - } + selectedPanel.sidenav = _sidenav; + if (previousPinnedPane) selectedPanel.pinnedPane = previousPinnedPane; } } async function _addItemContext(tabID, itemID) { - var container = document.createXULElement('vbox'); - container.id = tabID + '-context'; - container.className = 'zotero-item-pane-content'; - _itemPaneDeck.appendChild(container); - var { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); var library = Zotero.Libraries.get(libraryID); await library.waitForDataLoad('item'); @@ -859,86 +853,22 @@ var ZoteroContextPane = new function () { update: () => {} }; _itemContexts.push(context); + + let previousPinnedPane = _sidenav.container?.pinnedPane || ""; let targetItem = parentID ? Zotero.Items.get(parentID) : item; - - // Dynamically create item pane tabs and panels as in zoteroPane.xhtml. - // Keep the code below in sync with zoteroPane.xhtml - // hbox - var hbox = document.createXULElement('hbox'); - hbox.setAttribute('flex', '1'); - hbox.className = 'zotero-view-item-container'; - container.append(hbox); - - // main - var main = document.createElement('div'); - main.className = 'zotero-view-item-main'; - hbox.append(main); - - // pane-header - var paneHeader = document.createXULElement('pane-header'); - main.append(paneHeader); + let itemDetails = document.createXULElement('item-details'); + itemDetails.id = tabID + '-context'; + itemDetails.className = 'zotero-item-pane-content'; + _itemPaneDeck.appendChild(itemDetails); - // div - var div = document.createElement('div'); - div.className = 'zotero-view-item'; - div.setAttribute("tabindex", "0"); - main.append(div); - - // Info - var itemBox = new (customElements.get('item-box')); - itemBox.setAttribute('data-pane', 'info'); - div.append(itemBox); - - // Abstract - var abstractBox = new (customElements.get('abstract-box')); - abstractBox.className = 'zotero-editpane-abstract'; - abstractBox.setAttribute('data-pane', 'abstract'); - div.append(abstractBox); - - // Attachment info - var attachmentBox = new (customElements.get('attachment-box')); - attachmentBox.className = 'zotero-editpane-attachment'; - attachmentBox.setAttribute('data-pane', 'attachment-info'); - div.append(attachmentBox); + itemDetails.mode = readOnly ? "view" : null; + itemDetails.item = targetItem; + itemDetails.sidenav = _sidenav; + if (previousPinnedPane) itemDetails.pinnedPane = previousPinnedPane; - // Tags - var tagsBox = new (customElements.get('tags-box')); - tagsBox.className = 'zotero-editpane-tags'; - tagsBox.setAttribute('data-pane', 'tags'); - div.append(tagsBox); - - // Related - var relatedBox = new (customElements.get('related-box')); - relatedBox.className = 'zotero-editpane-related'; - relatedBox.setAttribute('data-pane', 'related'); - div.append(relatedBox); - - paneHeader.mode = readOnly ? 'view' : 'edit'; - paneHeader.item = targetItem; - - itemBox.mode = readOnly ? 'view' : 'edit'; - itemBox.item = targetItem; - - abstractBox.mode = readOnly ? 'view' : 'edit'; - abstractBox.item = targetItem; - - attachmentBox.mode = readOnly ? 'view' : 'edit'; - attachmentBox.item = targetItem; - - tagsBox.mode = readOnly ? 'view' : 'edit'; - tagsBox.item = targetItem; - - relatedBox.mode = readOnly ? 'view' : 'edit'; - relatedBox.item = targetItem; - - if (_itemPaneDeck.selectedPanel === container) { - _sidenav.container = div; - } - // When a tab is loaded, scroll to the pinned pane, if any - if (_sidenav.pinnedPane) { - _sidenav.scrollToPane(_sidenav.pinnedPane, 'instant'); - } + _selectItemContext(tabID); + await itemDetails.render(); } }; diff --git a/chrome/content/zotero/customElements.js b/chrome/content/zotero/customElements.js index f8a8099f2c..d7cf4ad6b4 100644 --- a/chrome/content/zotero/customElements.js +++ b/chrome/content/zotero/customElements.js @@ -31,13 +31,18 @@ Services.scriptloader.loadSubScript("resource://zotero/require.js", this); Services.scriptloader.loadSubScript("chrome://global/content/customElements.js", this); Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js", this); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPaneSection.js', this); // Load our custom elements Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentBox.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentPreview.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentPreviewBox.js', this); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/duplicatesMergePane.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/guidancePanel.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemBox.js', this); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemDetails.js', this); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPane.js', this); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemMessagePane.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/mergeGroup.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/menulistItemTypes.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteEditor.js', this); diff --git a/chrome/content/zotero/duplicatesMerge.js b/chrome/content/zotero/duplicatesMerge.js deleted file mode 100644 index 0388f4acdd..0000000000 --- a/chrome/content/zotero/duplicatesMerge.js +++ /dev/null @@ -1,157 +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 ***** -*/ - -"use strict"; - -var Zotero_Duplicates_Pane = new function () { - var _masterItem; - var _items = []; - var _otherItems = []; - var _ignoreFields = ['dateAdded', 'dateModified', 'accessDate']; - - this.setItems = function (items, displayNumItemsOnTypeError) { - var itemTypeID, oldestItem, otherItems = []; - for (let item of items) { - // Find the oldest item - if (!oldestItem) { - oldestItem = item; - } - else if (item.dateAdded < oldestItem.dateAdded) { - otherItems.push(oldestItem); - oldestItem = item; - } - else { - otherItems.push(item); - } - - if (!item.isRegularItem() || ['annotation', 'attachment', 'note'].includes(item.itemType)) { - var msg = Zotero.getString('pane.item.duplicates.onlyTopLevel'); - ZoteroPane_Local.setItemPaneMessage(msg); - return false; - } - - // Make sure all items are of the same type - if (itemTypeID) { - if (itemTypeID != item.itemTypeID) { - if (displayNumItemsOnTypeError) { - var msg = Zotero.getString('pane.item.selected.multiple', items.length); - } - else { - var msg = Zotero.getString('pane.item.duplicates.onlySameItemType'); - } - ZoteroPane_Local.setItemPaneMessage(msg); - return false; - } - } - else { - itemTypeID = item.itemTypeID; - } - } - - _items = items; - - _items.sort(function (a, b) { - return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1; - }); - - // - // Update the UI - // - - var button = document.getElementById('zotero-duplicates-merge-button'); - var versionSelect = document.getElementById('zotero-duplicates-merge-version-select'); - var itembox = document.getElementById('zotero-duplicates-merge-item-box'); - var fieldSelect = document.getElementById('zotero-duplicates-merge-field-select'); - - var alternatives = oldestItem.multiDiff(otherItems, _ignoreFields); - if (alternatives) { - // Populate menulist with Date Added values from all items - var dateList = document.getElementById('zotero-duplicates-merge-original-date'); - dateList.innerHTML = ''; - - var numRows = 0; - for (let item of items) { - var date = Zotero.Date.sqlToDate(item.dateAdded, true); - dateList.appendItem(date.toLocaleString()); - numRows++; - } - - dateList.setAttribute('rows', numRows); - - // If we set this inline, the selection doesn't take on the first - // selection after unhiding versionSelect (when clicking - // from a set with no differences) -- tested in Fx5.0.1 - setTimeout(function () { - dateList.selectedIndex = 0; - }, 0); - } - - button.label = Zotero.getString('pane.item.duplicates.mergeItems', (otherItems.length + 1)); - versionSelect.hidden = fieldSelect.hidden = !alternatives; - itembox.hiddenFields = alternatives ? [] : ['dateAdded', 'dateModified']; - - this.setMaster(0); - - return true; - } - - - this.setMaster = function (pos) { - var itembox = document.getElementById('zotero-duplicates-merge-item-box'); - itembox.mode = 'fieldmerge'; - - _otherItems = _items.concat(); - var item = _otherItems.splice(pos, 1)[0]; - - // Add master item's values to the beginning of each set of - // alternative values so that they're still available if the item box - // modifies the item - var alternatives = item.multiDiff(_otherItems, _ignoreFields); - if (alternatives) { - let itemValues = item.toJSON(); - for (let i in alternatives) { - alternatives[i].unshift(itemValues[i] !== undefined ? itemValues[i] : ''); - } - itembox.fieldAlternatives = alternatives; - } - - _masterItem = item; - itembox.item = item.clone(); - } - - - this.merge = Zotero.Promise.coroutine(function* () { - var itembox = document.getElementById('zotero-duplicates-merge-item-box'); - Zotero.CollectionTreeCache.clear(); - // Update master item with any field alternatives from the item box - var json = _masterItem.toJSON(); - // Exclude certain properties that are empty in the cloned object, so we don't clobber them - const { relations, collections, tags, ...keep } = itembox.item.toJSON(); - Object.assign(json, keep); - - _masterItem.fromJSON(json); - Zotero.Items.merge(_masterItem, _otherItems); - }); -} diff --git a/chrome/content/zotero/elements/abstractBox.js b/chrome/content/zotero/elements/abstractBox.js index ad4b78a2a9..84b877e26a 100644 --- a/chrome/content/zotero/elements/abstractBox.js +++ b/chrome/content/zotero/elements/abstractBox.js @@ -26,7 +26,7 @@ "use strict"; { - class AbstractBox extends XULElementBase { + class AbstractBox extends ItemPaneSectionElementBase { content = MozXULElement.parseXULToFragment(` @@ -50,7 +50,6 @@ this._item = item; if (item?.isRegularItem()) { this.hidden = false; - this.render(); } else { this.hidden = true; @@ -67,13 +66,12 @@ } this.blurOpenField(); this._mode = mode; - this.render(); } init() { this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'abstractBox'); - this._section = this.querySelector('collapsible-section'); + this.initCollapsibleSection(); this._abstractField = this.querySelector('editable-text'); this._abstractField.addEventListener('change', () => this.save()); @@ -88,7 +86,7 @@ notify(action, type, ids) { if (action == 'modify' && this.item && ids.includes(this.item.id)) { - this.render(); + this.render(true); } } @@ -97,7 +95,7 @@ this.item.setField('abstractNote', this._abstractField.value); await this.item.saveTx(); } - this.render(); + this.render(true); } async blurOpenField() { @@ -107,10 +105,9 @@ } } - render() { - if (!this.item) { - return; - } + render(force = false) { + if (!this.item) return; + if (!force && this._isAlreadyRendered()) return; let abstract = this.item.getField('abstractNote'); this._section.summary = abstract; diff --git a/chrome/content/zotero/elements/attachmentAnnotationsBox.js b/chrome/content/zotero/elements/attachmentAnnotationsBox.js index 7f3e0dbba5..6d5f01186e 100644 --- a/chrome/content/zotero/elements/attachmentAnnotationsBox.js +++ b/chrome/content/zotero/elements/attachmentAnnotationsBox.js @@ -24,13 +24,22 @@ */ { - class AttachmentAnnotationsBox extends XULElementBase { + class AttachmentAnnotationsBox extends ItemPaneSectionElementBase { content = MozXULElement.parseXULToFragment(` `); + + get tabType() { + return this._tabType; + } + + set tabType(tabType) { + this._tabType = tabType; + this._updateHidden(); + } get item() { return this._item; @@ -38,35 +47,26 @@ set item(item) { this._item = item; - if (item?.isFileAttachment()) { - this.hidden = false; - this.render(); - } - else { - this.hidden = true; - } + this._updateHidden(); } init() { - this._section = this.querySelector('collapsible-section'); - this._section.addEventListener("toggle", this._handleSectionOpen); + this.initCollapsibleSection(); + this._body = this.querySelector('.body'); - - this.render(); } - destroy() { - this._section.removeEventListener("toggle", this._handleSectionOpen); - } + destroy() {} notify(action, type, ids) { if (action == 'modify' && this.item && ids.includes(this.item.id)) { - this.render(); + this.render(true); } } - render() { + render(force = false) { if (!this.initialized || !this.item?.isFileAttachment()) return; + if (!force && this._isAlreadyRendered()) return; let annotations = this.item.getAnnotations(); this._section.setCount(annotations.length); @@ -91,12 +91,9 @@ } } - _handleSectionOpen = (event) => { - if (event.target !== this._section || !this._section.open) { - return; - } - this.render(); - }; + _updateHidden() { + this.hidden = !this.item?.isFileAttachment() || this.tabType == "reader"; + } } customElements.define("attachment-annotations-box", AttachmentAnnotationsBox); } diff --git a/chrome/content/zotero/elements/attachmentBox.js b/chrome/content/zotero/elements/attachmentBox.js index ea50e72a5f..9063bedd53 100644 --- a/chrome/content/zotero/elements/attachmentBox.js +++ b/chrome/content/zotero/elements/attachmentBox.js @@ -28,7 +28,7 @@ { - class AttachmentBox extends XULElementBase { + class AttachmentBox extends ItemPaneSectionElementBase { content = MozXULElement.parseXULToFragment(` @@ -181,6 +181,15 @@ this.toggleAttribute('data-use-preview', val); } + get tabType() { + return this._tabType; + } + + set tabType(tabType) { + this._tabType = tabType; + if (tabType == "reader") this.usePreview = false; + } + get item() { return this._item; } @@ -192,15 +201,16 @@ if (val.isAttachment()) { this._item = val; this.hidden = false; - this.render(); + this._preview.disableResize = false; } else { this.hidden = true; + this._preview.disableResize = true; } } init() { - this._section = this.querySelector('collapsible-section'); + this.initCollapsibleSection(); this._id('url').addEventListener('contextmenu', (event) => { this._id('url-menu').openPopupAtScreen(event.screenX, event.screenY, true); @@ -230,12 +240,6 @@ this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentbox'); - this._section.addEventListener("toggle", (ev) => { - if (ev.target.open && this.usePreview) { - this._preview.render(); - } - }); - // Work around the reindex toolbarbutton not wanting to properly receive focus on tab. // Make focusable. On focus of the image, bounce the focus to the toolbarbutton. // Temporarily remove tabindex from the so that the focus can move past the @@ -282,15 +286,17 @@ continue; } - this.render(); + this.render(true); break; } } - async render() { - if (this._isRendering) { - return; - } + async render(force = false) { + if (!this.item) return; + if (this._isRendering) return; + if (!this._section.open) return; + if (!force && this._isAlreadyRendered()) return; + Zotero.debug('Refreshing attachment box'); this._isRendering = true; // Cancel editing filename when refreshing @@ -298,6 +304,7 @@ if (this.usePreview) { this._preview.item = this.item; + this._preview.render(); } let fileNameRow = this._id('fileNameRow'); @@ -521,7 +528,7 @@ } // Don't allow empty filename if (!newFilename) { - this.render(); + this.render(true); return; } let newExt = getExtension(newFilename); @@ -577,7 +584,7 @@ Zotero.getString('pane.item.attachments.fileNotFound.text1') ); } - this.render(); + this.render(true); } initAttachmentNoteEditor() { diff --git a/chrome/content/zotero/elements/attachmentPreview.js b/chrome/content/zotero/elements/attachmentPreview.js index ed0df9d7ee..d0fc7fa285 100644 --- a/chrome/content/zotero/elements/attachmentPreview.js +++ b/chrome/content/zotero/elements/attachmentPreview.js @@ -24,8 +24,7 @@ */ { - // eslint-disable-next-line no-undef - class AttachmentPreview extends XULElementBase { + class AttachmentPreview extends ItemPaneSectionElementBase { static fileTypeMap = { // TODO: support video and audio // 'video/mp4': 'video', @@ -49,7 +48,7 @@ this._isDiscarding = false; this._failedCount = 0; - this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this)); + // this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this)); this._resizeOb = new ResizeObserver(this._handleResize.bind(this)); } @@ -97,14 +96,6 @@ set item(val) { this._item = (val instanceof Zotero.Item && val.isFileAttachment()) ? val : null; - if (this.isVisible) { - this.render(); - } - } - - setItemAndRender(item) { - this._item = item; - this.render(); } get previewType() { @@ -140,7 +131,16 @@ } get hasPreview() { - return this.getAttribute("data-preview-status") === "success"; + return this.dataset.previewStatus === "success"; + } + + get disableResize() { + return this.dataset.disableResize !== "false"; + } + + set disableResize(val) { + this.dataset.disableResize = val ? "true" : "false"; + this._handleResize(); } setPreviewStatus(val) { @@ -151,32 +151,9 @@ this.setAttribute("data-preview-status", val); } - get isVisible() { - const rect = this.getBoundingClientRect(); - // Sample per 20 px - const samplePeriod = 20; - let x = rect.left + rect.width / 2; - let yStart = rect.top; - let yEnd = rect.bottom; - let elAtPos; - // Check visibility from top/bottom to center - for (let dy = 1; dy < Math.floor((yEnd - yStart) / 2); dy += samplePeriod) { - elAtPos = document.elementFromPoint(x, yStart + dy); - if (this.contains(elAtPos)) { - return true; - } - elAtPos = document.elementFromPoint(x, yEnd - dy); - if (this.contains(elAtPos)) { - return true; - } - } - return false; - } - init() { this.setPreviewStatus("loading"); this._dragImageContainer = this.querySelector(".drag-container"); - this._intersectionOb.observe(this); this._resizeOb.observe(this); this.addEventListener("dblclick", (event) => { this.openAttachment(event); @@ -193,7 +170,6 @@ destroy() { this._reader?.uninit(); - this._intersectionOb.disconnect(); this._resizeOb.disconnect(); this.removeEventListener("DOMContentLoaded", this._handleReaderLoad); this.removeEventListener("mouseenter", this.updateGoto); @@ -401,41 +377,8 @@ } } - async _handleIntersection(entries) { - const DISCARD_TIMEOUT = 60000; - let needsRefresh = false; - let needsDiscard = false; - entries.forEach((entry) => { - if (entry.isIntersecting) { - needsRefresh = true; - } - else { - needsDiscard = true; - } - }); - if (needsRefresh) { - let sidenav = this._getSidenav(); - // Sidenav is in smooth scrolling mode - if (sidenav?._disableScrollHandler) { - // Wait for scroll to finish - await sidenav._waitForScroll(); - // If the preview is not visible, do not render - if (!this.isVisible) { - return; - } - } - // Try to render the preview when the preview enters viewport - this.render(); - } - else if (!this._isDiscardPlanned && needsDiscard) { - this._isDiscardPlanned = true; - setTimeout(() => { - this.discard(); - }, DISCARD_TIMEOUT); - } - } - _handleResize() { + if (this.disableResize) return; this.style.setProperty("--preview-width", `${this.clientWidth}px`); } @@ -475,14 +418,6 @@ this.style.setProperty("--width-height-ratio", scaleRatio); } - _getSidenav() { - // TODO: update this after unifying item pane & context pane - return document.querySelector( - Zotero_Tabs.selectedType === 'library' - ? "#zotero-view-item-sidenav" - : "#zotero-context-pane-sidenav"); - } - _id(id) { return this.querySelector(`#${id}`); } diff --git a/chrome/content/zotero/elements/attachmentPreviewBox.js b/chrome/content/zotero/elements/attachmentPreviewBox.js index d988e03823..14a1b371af 100644 --- a/chrome/content/zotero/elements/attachmentPreviewBox.js +++ b/chrome/content/zotero/elements/attachmentPreviewBox.js @@ -25,7 +25,7 @@ { - class AttachmentPreviewBox extends XULElementBase { + class AttachmentPreviewBox extends ItemPaneSectionElementBase { content = MozXULElement.parseXULToFragment(` @@ -64,19 +64,14 @@ } init() { - this._section = this.querySelector('collapsible-section'); + this.initCollapsibleSection(); this._preview = this.querySelector("#attachment-preview"); - - this._section.addEventListener("toggle", (ev) => { - if (ev.target.open && this.usePreview) { - this._preview.render(); - } - }); } destroy() {} async render() { + if (!this._section.open) return; let bestAttachment = await this.item.getBestAttachment(); if (bestAttachment) { this._preview.item = bestAttachment; diff --git a/chrome/content/zotero/elements/attachmentRow.js b/chrome/content/zotero/elements/attachmentRow.js index 315705cf06..56cafaa5dc 100644 --- a/chrome/content/zotero/elements/attachmentRow.js +++ b/chrome/content/zotero/elements/attachmentRow.js @@ -93,7 +93,6 @@ import { getCSSItemTypeIcon } from 'components/icons'; _handleAnnotationClick = () => { // TODO: jump to annotations pane - // ZoteroItemPane.setNextPane("attachment-annotations"); let pane = this._getSidenav()?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`); if (pane) { pane._section.open = true; diff --git a/chrome/content/zotero/elements/attachmentsBox.js b/chrome/content/zotero/elements/attachmentsBox.js index c3f95a88d6..8fd8392c3a 100644 --- a/chrome/content/zotero/elements/attachmentsBox.js +++ b/chrome/content/zotero/elements/attachmentsBox.js @@ -26,7 +26,7 @@ "use strict"; { - class AttachmentsBox extends XULElementBase { + class AttachmentsBox extends ItemPaneSectionElementBase { content = MozXULElement.parseXULToFragment(` @@ -52,22 +52,14 @@ } set item(item) { - let isRegularItem = item?.isRegularItem(); - this.hidden = !isRegularItem; - if (!isRegularItem || this._item === item) { + if (this._item === item) { return; } this._item = item; - this.refresh(); - } - - get mode() { - return this._mode; - } - - set mode(mode) { - this._mode = mode; + let hidden = !item?.isRegularItem() || item?.isFeedItem; + this.hidden = hidden; + this._preview.disableResize = !!hidden; } get inTrash() { @@ -88,17 +80,28 @@ this.updateCount(); } + get tabType() { + return this._tabType; + } + + set tabType(tabType) { + this._tabType = tabType; + this._updateHidden(); + } + get usePreview() { + if (this.tabType == "reader") return false; return this.hasAttribute('data-use-preview'); } set usePreview(val) { + if (this.tabType == "reader") return; this.toggleAttribute('data-use-preview', val); this.updatePreview(); } init() { - this._section = this.querySelector('collapsible-section'); + this.initCollapsibleSection(); this._section.addEventListener('add', this._handleAdd); // this._section.addEventListener('togglePreview', this._handleTogglePreview); @@ -112,16 +115,11 @@ this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentsBox'); - this._section.addEventListener("toggle", (ev) => { - if (ev.target.open) { - this._preview.render(); - } - }); - this._section._contextMenu.addEventListener('popupshowing', this._handleContextMenu, { once: true }); } destroy() { + this._section.removeEventListener('add', this._handleAdd); Zotero.Notifier.unregisterObserver(this._notifierID); } @@ -180,8 +178,9 @@ return row; } - async refresh() { + async render(force = false) { if (!this._item) return; + if (!force && this._isAlreadyRendered()) return; this.usePreview = Zotero.Prefs.get('showAttachmentPreview'); @@ -202,15 +201,12 @@ } async updatePreview() { - if (!this.usePreview) { + if (!this.usePreview || !this._section.open) { return; } let attachment = await this._item.getBestAttachment(); - if (!this._preview.hasPreview) { - this._preview.setItemAndRender(attachment); - return; - } this._preview.item = attachment; + await this._preview.render(); } _handleAdd = (event) => { @@ -232,6 +228,7 @@ }; _handleContextMenu = () => { + if (this.tabType == "reader") return; let contextMenu = this._section._contextMenu; let menu = document.createXULElement("menuitem"); menu.classList.add('menuitem-iconic', 'zotero-menuitem-toggle-preview'); @@ -264,6 +261,10 @@ } this._attachmentIDs = sortedAttachmentIDs; } + + _updateHidden() { + this.hidden = !this._item?.isRegularItem(); + } } customElements.define("attachments-box", AttachmentsBox); } diff --git a/chrome/content/zotero/elements/collapsibleSection.js b/chrome/content/zotero/elements/collapsibleSection.js index 0d5af71755..50430773f3 100644 --- a/chrome/content/zotero/elements/collapsibleSection.js +++ b/chrome/content/zotero/elements/collapsibleSection.js @@ -212,7 +212,7 @@ pinSection.setAttribute('data-l10n-id', 'pin-section'); pinSection.addEventListener('command', () => { let sidenav = this._getSidenav(); - sidenav.scrollToPane(this.dataset.pane, 'smooth'); + sidenav.container.scrollToPane(this.dataset.pane, 'smooth'); sidenav.pinnedPane = this.dataset.pane; }); contextMenu.append(pinSection); @@ -401,6 +401,7 @@ if (document.documentElement.getAttribute('windowtype') !== 'navigator:browser') { return null; } + if (typeof Zotero_Tabs == "undefined") return null; // TODO: update this after unifying item pane & context pane return document.querySelector( Zotero_Tabs.selectedType === 'library' diff --git a/chrome/content/zotero/elements/duplicatesMergePane.js b/chrome/content/zotero/elements/duplicatesMergePane.js new file mode 100644 index 0000000000..eda9981466 --- /dev/null +++ b/chrome/content/zotero/elements/duplicatesMergePane.js @@ -0,0 +1,186 @@ +/* + ***** 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 DuplicatesMergePane extends XULElementBase { + content = MozXULElement.parseXULToFragment(` + +