From e522f6185e7972924069c531a442fe6f99c882e9 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Fri, 19 Dec 2025 05:49:42 +0100 Subject: [PATCH] Add note as tab (#5528) --- chrome/content/zotero/components/tabBar.jsx | 6 +- chrome/content/zotero/contextPane.js | 156 ++++- chrome/content/zotero/customElements.js | 2 + chrome/content/zotero/elements/contextPane.js | 69 ++- chrome/content/zotero/elements/itemDetails.js | 7 + chrome/content/zotero/elements/itemPane.js | 2 +- .../content/zotero/elements/itemPaneHeader.js | 27 +- .../zotero/elements/itemPaneSidenav.js | 59 +- chrome/content/zotero/elements/noteBox.js | 210 +++++++ chrome/content/zotero/elements/noteEditor.js | 39 +- .../content/zotero/elements/notesContext.js | 2 +- chrome/content/zotero/elements/tabContent.js | 115 ++++ chrome/content/zotero/locateMenu.js | 112 ++-- chrome/content/zotero/note.js | 6 + .../preferences/preferences_general.xhtml | 9 + .../content/zotero/standalone/standalone.js | 23 +- chrome/content/zotero/tabs.js | 540 +++++++++++++----- chrome/content/zotero/xpcom/data/item.js | 7 +- chrome/content/zotero/xpcom/data/notes.js | 158 +++++ chrome/content/zotero/xpcom/editorInstance.js | 45 +- chrome/content/zotero/xpcom/prefs.js | 5 + chrome/content/zotero/xpcom/reader.js | 127 ++-- chrome/content/zotero/zoteroPane.js | 108 ++-- chrome/content/zotero/zoteroPane.xhtml | 29 +- chrome/locale/en-US/zotero/note-editor.ftl | 1 + chrome/locale/en-US/zotero/preferences.ftl | 4 + chrome/locale/en-US/zotero/zotero.ftl | 34 ++ .../default/zotero/16/universal/note-info.svg | 3 + .../default/zotero/itempane/16/note-info.svg | 3 + .../default/zotero/itempane/20/note-info.svg | 3 + .../default/zotero/itempane/20/notes-1.svg | 3 + .../default/zotero/itempane/20/notes-3.svg | 4 + .../skin/default/zotero/itempane/20/notes.svg | 5 +- defaults/preferences/zotero.js | 3 + note-editor | 2 +- resource/allfaz.mjs | 206 +++++++ scss/_zotero.scss | 1 + scss/abstracts/_variables.scss | 2 + scss/components/_contextPane.scss | 2 +- scss/components/_mainWindow.scss | 1 + scss/elements/_noteBox.scss | 41 ++ scss/elements/_noteEditor.scss | 6 + test/tests/noteTabTest.js | 55 ++ 43 files changed, 1863 insertions(+), 379 deletions(-) create mode 100644 chrome/content/zotero/elements/noteBox.js create mode 100644 chrome/content/zotero/elements/tabContent.js create mode 100644 chrome/skin/default/zotero/16/universal/note-info.svg create mode 100644 chrome/skin/default/zotero/itempane/16/note-info.svg create mode 100644 chrome/skin/default/zotero/itempane/20/note-info.svg create mode 100644 chrome/skin/default/zotero/itempane/20/notes-1.svg create mode 100644 chrome/skin/default/zotero/itempane/20/notes-3.svg create mode 100644 resource/allfaz.mjs create mode 100644 scss/elements/_noteBox.scss create mode 100644 test/tests/noteTabTest.js diff --git a/chrome/content/zotero/components/tabBar.jsx b/chrome/content/zotero/components/tabBar.jsx index c9fcd34676..7ed80cdbd3 100644 --- a/chrome/content/zotero/components/tabBar.jsx +++ b/chrome/content/zotero/components/tabBar.jsx @@ -274,8 +274,8 @@ const TabBar = forwardRef(function (props, ref) { const handleDragEnd = useCallback(() => { setDragging(false); - props.refocusReader(); - }, [props.refocusReader]); + props.onRefocus(); + }, [props.onRefocus]); const handleTabBarDragOver = useCallback((event) => { event.preventDefault(); @@ -485,7 +485,7 @@ TabBar.propTypes = { onTabClose: PropTypes.func.isRequired, onLoad: PropTypes.func.isRequired, onTabMove: PropTypes.func.isRequired, - refocusReader: PropTypes.func.isRequired, + onRefocus: PropTypes.func.isRequired, onContextMenu: PropTypes.func.isRequired, tabs: PropTypes.arrayOf( PropTypes.shape({ diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js index dfb8a2139a..cb27177a7d 100644 --- a/chrome/content/zotero/contextPane.js +++ b/chrome/content/zotero/contextPane.js @@ -31,6 +31,7 @@ var ZoteroContextPane = new function () { let _contextPaneSplitterStacked; let _librarySidenav; let _readerSidenav; + let _sidePaneState; Object.defineProperty(this, 'activeEditor', { get: () => _contextPaneInner.activeEditor @@ -69,6 +70,42 @@ var ZoteroContextPane = new function () { _loadingMessageContainer.classList.toggle('hidden', !isShow); }; + this.getSidePaneState = (tabType) => { + if (!_sidePaneState) { + _loadSidePaneState(); + } + if (!_sidePaneState[tabType]) { + _sidePaneState[tabType] = { + width: 0, + open: false, + }; + } + return _sidePaneState[tabType]; + }; + + this.updateSidePaneState = (tabType, state) => { + if (!_sidePaneState) { + _loadSidePaneState(); + } + if (!_sidePaneState[tabType]) { + _sidePaneState[tabType] = {}; + } + state = state || {}; + let hasChanges = false; + for (let key in state) { + if (_sidePaneState[tabType][key] !== state[key]) { + hasChanges = true; + break; + } + } + if (!hasChanges) { + return _sidePaneState[tabType]; + } + Object.assign(_sidePaneState[tabType], state); + _saveSidePaneState(); + return _sidePaneState[tabType]; + }; + this.init = function () { if (!Zotero) { return; @@ -83,6 +120,8 @@ var ZoteroContextPane = new function () { _librarySidenav = document.querySelector("#zotero-view-item-sidenav"); _readerSidenav = document.getElementById('zotero-context-pane-sidenav'); + _loadSidePaneState(); + // Never use default status for the reader sidenav _readerSidenav.toggleDefaultStatus(false); @@ -91,14 +130,12 @@ var ZoteroContextPane = new function () { this.context = _contextPaneInner; window.addEventListener('resize', this.update); - Zotero.Reader.onChangeSidebarWidth = this._updatePaneWidth; - Zotero.Reader.onToggleSidebar = this._updatePaneWidth; }; this.destroy = function () { window.removeEventListener('resize', this.update); - Zotero.Reader.onChangeSidebarWidth = () => {}; - Zotero.Reader.onToggleSidebar = () => {}; + + _saveSidePaneState(); }; this.updateAddToNote = () => { @@ -111,28 +148,68 @@ var ZoteroContextPane = new function () { reader.enableAddToNote(!!editor && !libraryReadOnly && !noteReadOnly); } }; - - this._updatePaneWidth = () => { + + /** + * Update the layout of the context pane and side pane. + * @param {Object} options - Options for updating the layout. + * @param {number | boolean} [options.sidePaneWidth] - The width of the side pane in pixels. + * If boolean, it indicates whether the side pane is open (true) or collapsed (false). + * @param {number} [options.contextPaneWidth] - The width of the context pane in pixels. + * @returns {Object} An object containing the updated layout state. + */ + this.updateLayout = ({ sidePaneWidth, contextPaneWidth } = {}) => { let stacked = _isStacked(); - let readerSidebarWidth = (Zotero.Reader.getSidebarOpen() ? Zotero.Reader.getSidebarWidth() : 0) - + 'px'; - let contextPaneWidth = _contextPane.getAttribute("width"); + let { tabContentType: tabType } = Zotero_Tabs.parseTabType(); + let sidePaneState; + if (typeof sidePaneWidth === 'number') { + // If sidePaneWidth is a number, update the width and open state + sidePaneState = this.updateSidePaneState(tabType, { width: sidePaneWidth, open: sidePaneWidth > 0 }); + } + else if (typeof sidePaneWidth === 'boolean') { + // If sidePaneWidth is a boolean, update the open state only + sidePaneState = this.updateSidePaneState(tabType, { open: sidePaneWidth }); + sidePaneWidth = sidePaneState.width || 0; + } + else { + // If sidePaneWidth is not provided, use the saved state + sidePaneState = this.getSidePaneState(tabType); + sidePaneWidth = sidePaneState.width || 0; + if (sidePaneState.open === false) { + sidePaneWidth = 0; + } + } + + if (typeof contextPaneWidth !== 'number') { + contextPaneWidth = _contextPane.getAttribute("width"); + } + + let sidebarWidth = `${sidePaneWidth}px`; if (contextPaneWidth && !_contextPane.style.width) { _contextPane.style.width = `${contextPaneWidth}px`; } if (Zotero.rtl) { _contextPane.style.left = 0; - _contextPane.style.right = stacked ? readerSidebarWidth : 'unset'; + _contextPane.style.right = stacked ? sidebarWidth : 'unset'; } else { - _contextPane.style.left = stacked ? readerSidebarWidth : 'unset'; + _contextPane.style.left = stacked ? sidebarWidth : 'unset'; _contextPane.style.right = 0; } + + let placeholder = document.getElementById('zotero-reader-sidebar-pane'); + placeholder.setAttribute('collapsed', sidebarWidth ? 'false' : 'true'); + // Don't set width if 0 to prevent layout issues in older versions + if (sidePaneWidth) { + placeholder.setAttribute('width', sidebarWidth); + } + + return { sidePaneState }; }; this.update = () => { + let updatedState = {}; if (Zotero_Tabs.selectedType === 'library') { - return; + return updatedState; } if (_isStacked()) { _contextPaneSplitterStacked.setAttribute('hidden', false); @@ -171,8 +248,6 @@ var ZoteroContextPane = new function () { _contextPaneSplitterStacked.setAttribute('state', this.collapsed ? 'collapsed' : 'open'); } - Zotero.Reader.setContextPaneOpen(!this.collapsed); - var height = null; if (_isStacked()) { height = 0; @@ -180,18 +255,65 @@ var ZoteroContextPane = new function () { height = _contextPaneInner.getBoundingClientRect().height; } } - Zotero.Reader.setBottomPlaceholderHeight(height); - - this._updatePaneWidth(); + + _contextPaneInner.setAttribute('collapsed', this.collapsed ? 'true' : 'false'); + + let tabContent = _getTabContent(); + if (tabContent) { + tabContent.setBottomPlaceholderHeight(height); + tabContent.setContextPaneOpen(!this.collapsed); + } + + Object.assign(updatedState, this.updateLayout()); this.updateAddToNote(); ZoteroPane.updateLayoutConstraints(); + return updatedState; }; this.togglePane = () => { this.collapsed = !this.collapsed; }; + function _loadSidePaneState() { + let sidePaneState = Zotero.Prefs.get('sidePaneState') || "{}"; + try { + sidePaneState = JSON.parse(sidePaneState); + } + catch { + sidePaneState = {}; + } + _sidePaneState = sidePaneState; + } + + function _saveSidePaneState() { + let sidePaneState; + try { + sidePaneState = JSON.stringify(_sidePaneState); + } + catch { + // Default status if serialization fails + sidePaneState = JSON.stringify({ + reader: { + width: 0, + open: false, + }, + note: { + width: 0, + open: false, + }, + }); + } + Zotero.Prefs.set('sidePaneState', sidePaneState); + } + + function _getTabContent(tabID) { + if (!tabID) { + tabID = Zotero_Tabs.selectedID; + } + return document.querySelector(`#${tabID}`); + } + function _isStacked() { return Zotero.Prefs.get('layout') == 'stacked'; } diff --git a/chrome/content/zotero/customElements.js b/chrome/content/zotero/customElements.js index cab62b5010..06cb02c65d 100644 --- a/chrome/content/zotero/customElements.js +++ b/chrome/content/zotero/customElements.js @@ -47,6 +47,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemTreeMe ['item-message-pane', 'chrome://zotero/content/elements/itemMessagePane.js'], ['merge-group', 'chrome://zotero/content/elements/mergeGroup.js'], ['menulist-item-types', 'chrome://zotero/content/elements/menulistItemTypes.js'], + ['note-box', 'chrome://zotero/content/elements/noteBox.js'], ['note-editor', 'chrome://zotero/content/elements/noteEditor.js'], ['notes-box', 'chrome://zotero/content/elements/notesBox.js'], ['quick-search-textbox', 'chrome://zotero/content/elements/quickSearchTextbox.js'], @@ -54,6 +55,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemTreeMe ['shadow-autocomplete-input', 'chrome://zotero/content/elements/shadowAutocompleteInput.js'], ['split-menu-button', 'chrome://zotero/content/elements/splitMenuButton.js'], ['tabs-menu-panel', 'chrome://zotero/content/elements/tabsMenuPanel.js'], + ['tab-content', 'chrome://zotero/content/elements/tabContent.js'], ['tags-box', 'chrome://zotero/content/elements/tagsBox.js'], ['zotero-text-link', 'chrome://zotero/content/elements/textLink.js'], ['zoterosearch', 'chrome://zotero/content/elements/zoteroSearch.js'], diff --git a/chrome/content/zotero/elements/contextPane.js b/chrome/content/zotero/elements/contextPane.js index 4bf8314432..412a7c26c7 100644 --- a/chrome/content/zotero/elements/contextPane.js +++ b/chrome/content/zotero/elements/contextPane.js @@ -75,6 +75,26 @@ setPaneCollapsed(this, val); } + static get observedAttributes() { + return ['collapsed']; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "collapsed": { + this.handleCollapse(oldValue, newValue); + break; + } + } + } + + handleCollapse(prevState, newState) { + if (prevState === "true" && (!newState || newState === "false")) { + let itemContext = this._getItemContext(Zotero_Tabs.selectedID); + itemContext?.render(); + } + } + init() { this._panesDeck = this.querySelector('#zotero-context-pane-deck'); // Item pane deck @@ -118,11 +138,12 @@ if (action === 'modify') { for (let itemDetails of Array.from(this._itemPaneDeck.children)) { let tabID = itemDetails.tabID; - let item = Zotero.Items.get(Zotero_Tabs._getTab(tabID)?.tab.data.itemID); + let tab = Zotero_Tabs._getTab(tabID).tab; + let item = Zotero.Items.get(tab?.data.itemID); if ((item.parentID || itemDetails.parentID) && item.parentID !== itemDetails.parentID) { this._removeItemContext(tabID); - this._addItemContext(tabID, item.itemID); + this._addItemContext(tabID, item.itemID, tab?.type); } } } @@ -190,23 +211,28 @@ ZoteroContextPane.showLoadingMessage(false); this._sidenav.hidden = true; } - else if (tabType == 'reader' + else if (Zotero_Tabs.hasContextPane(tabType) // The reader tab load event is triggered asynchronously. // If the tab is no longer selected by the time the event is triggered, // we don't need to update the context pane, since it must already be // updated by another select tab event. && (action === 'select' || (action === 'load' && Zotero_Tabs.selectedID == tabID))) { - this._handleReaderReady(tabID); - this._setupNotesContext(tabID); + this._handleTabReady(tabID); + if (Zotero_Tabs.hasNoteContext(tabType)) { + this._setupNotesContext(tabID); + } + else { + this._disableNotesContext(); + } _contextPaneSplitter.setAttribute('hidden', false); _contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed')); this._sidenav.hidden = false; - let data = Zotero_Tabs._tabs.find(tab => tab.id === ids[0]).data; - await this._addItemContext(ids[0], data.itemID, data.type); + let tab = Zotero_Tabs._getTab(tabID).tab; + await this._addItemContext(ids[0], tab.data.itemID, tab.type); this._selectItemContext(tabID); } @@ -226,15 +252,20 @@ let currentNoteContext = this._getCurrentNotesContext(); // Always switch to the current selected tab, since the selection might have changed currentNoteContext.switchToTab(Zotero_Tabs.selectedID); + this.sidenav.contextNotesPaneEnabled = true; } - async _handleReaderReady(tabID) { - let reader = Zotero.Reader.getByTabID(tabID); - if (!reader) { + _disableNotesContext() { + this.sidenav.contextNotesPaneEnabled = false; + } + + async _handleTabReady(tabID) { + let tabContent = Zotero_Tabs.getTabContent(tabID); + if (!tabContent) { return; } - // Focus reader pages view if context pane note editor is not selected - if (Zotero_Tabs.selectedID == reader.tabID + // Focus tab content (e.g. reader pages view) if context pane note editor is not selected + if (Zotero_Tabs.selectedID == tabID && !Zotero_Tabs.tabsMenuPanel.visible && (!document.activeElement || !document.activeElement.closest('.context-node iframe[id="editor-view"]'))) { @@ -243,7 +274,7 @@ setTimeout(() => { // Timeout to make sure focus does not stick to the tab // after click on windows - reader.focus(); + tabContent.setFocus(); }); } } @@ -298,7 +329,7 @@ } } - async _addItemContext(tabID, itemID, _tabType = "") { + async _addItemContext(tabID, itemID, tabType = "") { if (this._getItemContext(tabID)) { return; } @@ -316,7 +347,13 @@ let previousPinnedPane = this._sidenav.container?.pinnedPane || ""; - let targetItem = parentID ? Zotero.Items.get(parentID) : item; + let targetItem; + if (item.isNote()) { + targetItem = item; + } + else { + targetItem = parentID ? Zotero.Items.get(parentID) : item; + } let editable = Zotero.Libraries.get(libraryID).editable // If the parent item or the attachment itself is in trash, itemPane is not editable @@ -330,7 +367,7 @@ itemDetails.editable = editable; itemDetails.tabID = tabID; - itemDetails.tabType = "reader"; + itemDetails.tabType = Zotero_Tabs.parseTabType(tabType).tabContentType; itemDetails.item = targetItem; // Manually cache parentID itemDetails.parentID = parentID; diff --git a/chrome/content/zotero/elements/itemDetails.js b/chrome/content/zotero/elements/itemDetails.js index 71b7365a03..2c6ea9c1e5 100644 --- a/chrome/content/zotero/elements/itemDetails.js +++ b/chrome/content/zotero/elements/itemDetails.js @@ -64,6 +64,8 @@ +