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 @@
+
+
@@ -556,6 +558,11 @@
return null;
}
+ // If the pane is already at the top, no need to scroll
+ if (Math.abs(pane.getBoundingClientRect().top - this._paneParent.getBoundingClientRect().top) < 1) {
+ return true;
+ }
+
// Temporarily disable intersection observer to prevent unwanted rendering
this._toggleIntersectionObserver(false);
diff --git a/chrome/content/zotero/elements/itemPane.js b/chrome/content/zotero/elements/itemPane.js
index 1d9d0a1119..21aff91a44 100644
--- a/chrome/content/zotero/elements/itemPane.js
+++ b/chrome/content/zotero/elements/itemPane.js
@@ -43,7 +43,7 @@
-
+
`);
init() {
diff --git a/chrome/content/zotero/elements/itemPaneHeader.js b/chrome/content/zotero/elements/itemPaneHeader.js
index 7e93bf643e..c492b152d2 100644
--- a/chrome/content/zotero/elements/itemPaneHeader.js
+++ b/chrome/content/zotero/elements/itemPaneHeader.js
@@ -62,6 +62,8 @@
_item = null;
_titleFieldID = null;
+
+ _editable = true;
get item() {
return this._item;
@@ -70,6 +72,18 @@
set item(item) {
this.blurOpenField();
this._item = item;
+ this.titleField.readOnly = !this.editable;
+ }
+
+ get editable() {
+ return this._editable && !this.item.isNote();
+ }
+
+ set editable(editable) {
+ this._editable = editable;
+ if (this.titleField) {
+ this.titleField.readOnly = !editable;
+ }
}
init() {
@@ -243,9 +257,16 @@
}
if (headerMode === 'title' || headerMode === 'titleCreatorYear') {
- this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this._item.itemTypeID, 'title');
-
- let title = this.item.getField(this._titleFieldID);
+ let title = "";
+ if (!this._item.isNote()) {
+ this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this._item.itemTypeID, 'title');
+ title = this.item.getField(this._titleFieldID);
+ }
+ else {
+ this._titleFieldID = "";
+ title = this._item.getDisplayTitle() || Zotero.getString("item-title-empty-note");
+ }
+
// If focused, update the value that will be restored on Escape;
// otherwise, update the displayed value
if (this.titleField.focused) {
diff --git a/chrome/content/zotero/elements/itemPaneSidenav.js b/chrome/content/zotero/elements/itemPaneSidenav.js
index 1f0a331728..ee7fc5a217 100644
--- a/chrome/content/zotero/elements/itemPaneSidenav.js
+++ b/chrome/content/zotero/elements/itemPaneSidenav.js
@@ -80,7 +80,7 @@
_container = null;
_contextNotesPane = null;
-
+
_contextMenuTarget = null;
_draggedWrapper = null;
@@ -89,12 +89,20 @@
_prefObserverID = null;
+ get observedAttributes() {
+ return ["no-context-notes"];
+ }
+
+ attributeChangedCallback() {
+ this.render();
+ }
+
get _defaultPanes() {
return ["info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"];
}
get _builtInPanes() {
- return ["info", "abstract", "attachments", "notes", "attachment-info", "attachment-annotations", "libraries-collections", "tags", "related"];
+ return ["info", "abstract", "attachments", "notes", "note-info", "attachment-info", "attachment-annotations", "libraries-collections", "tags", "related"];
}
get container() {
@@ -116,6 +124,20 @@
this._contextNotesPane = val;
this.render();
}
+
+ get contextNotesPaneEnabled() {
+ return !this.hasAttribute("no-context-notes");
+ }
+
+ set contextNotesPaneEnabled(val) {
+ if (this.contextNotesPaneEnabled === val) return;
+ if (val) {
+ this.removeAttribute("no-context-notes");
+ }
+ else {
+ this.setAttribute("no-context-notes", "true");
+ }
+ }
get pinnedPane() {
return this.container?.pinnedPane;
@@ -177,9 +199,9 @@
}
isPaneOrderable(paneID) {
- let orderable =
+ let orderable
// Built-in or orderable custom sections
- this._builtInPanes.includes(paneID) || Zotero.ItemPaneManager.isSectionOrderable(paneID);
+ = this._builtInPanes.includes(paneID) || Zotero.ItemPaneManager.isSectionOrderable(paneID);
return orderable;
}
@@ -199,6 +221,7 @@
else if (direction === 'down') {
return isOrderable && isNextOrderable && !isLast;
}
+ return false;
}
isOrderChanged() {
@@ -235,7 +258,19 @@
}
event.preventDefault();
let menu = button.querySelector('menupopup');
- await Zotero_LocateMenu.buildLocateMenu(menu);
+ let locateMode;
+ // If the container is not set, assume it's a standalone window
+ if (!this.container?.tabType) {
+ locateMode = "window";
+ }
+ // If it's library tab, we can open in either tab or window
+ else if (this.container.tabType === "library") {
+ locateMode = "library";
+ }
+ else {
+ locateMode = "tab";
+ }
+ await Zotero_LocateMenu.buildLocateMenu(menu, { locateMode });
Zotero.MenuManager.updateMenuPopup(menu, "sidenav/locate", {
event: undefined,
@@ -329,7 +364,7 @@
}
if (pane == 'context-notes') {
- let hidden = !this._contextNotesPane;
+ let hidden = !this.contextNotesPaneEnabled;
let selected = contextNotesPaneVisible;
button.parentElement.hidden = hidden;
@@ -383,7 +418,7 @@
currentOrder = [...currentOrder];
// Restore the order from installed plugins but not registered in the current order
let prevOrder = this.getPersistedOrder();
- let installedPluginIDs = undefined;
+ let installedPluginIDs;
for (let paneID of prevOrder) {
if (currentOrder.includes(paneID)) {
continue;
@@ -416,7 +451,7 @@
try {
return value.split(",");
}
- catch(e) {
+ catch {
return this._builtInPanes;
}
}
@@ -450,7 +485,7 @@
try {
sidenavOptions = JSON.parse(pane.dataset.sidenavOptions);
}
- catch (e) {}
+ catch {}
let { icon, darkIcon, l10nID, l10nArgs } = sidenavOptions;
if (!darkIcon) darkIcon = icon;
button.setAttribute("custom", "true");
@@ -823,7 +858,7 @@
}
// Insert at the index of the previous wrapper
this.changePaneOrder(paneID, actualIndex);
- }
+ };
handleButtonDragStart = (event) => {
let wrapper = event.target.closest('.pin-wrapper');
@@ -922,7 +957,7 @@
}
};
- handleButtonDragLeave = (event) => {
+ handleButtonDragLeave = (_event) => {
if (this._dropIndicator) {
this._dropIndicator.setAttribute("hidden", "true");
}
@@ -950,7 +985,7 @@
}
this.container?.render();
}
- }
+ };
}
customElements.define("item-pane-sidenav", ItemPaneSidenav);
}
diff --git a/chrome/content/zotero/elements/noteBox.js b/chrome/content/zotero/elements/noteBox.js
new file mode 100644
index 0000000000..ac0c7ae539
--- /dev/null
+++ b/chrome/content/zotero/elements/noteBox.js
@@ -0,0 +1,210 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2022 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 *****
+*/
+
+
+{
+ let { countWords } = ChromeUtils.importESModule("resource://zotero/allfaz.mjs").default;
+
+ class NoteBox extends ItemPaneSectionElementBase {
+ content = MozXULElement.parseXULToFragment(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+
+ constructor() {
+ super();
+
+ this._item = null;
+ this._section = null;
+ }
+
+ get item() {
+ return this._item;
+ }
+
+ set item(val) {
+ if (!(val instanceof Zotero.Item)) {
+ throw new Error("'item' must be a Zotero.Item");
+ }
+
+ if (val.isNote()) {
+ this._item = val;
+ this.hidden = false;
+ }
+ else {
+ this.hidden = true;
+ }
+ }
+
+ init() {
+ this.initCollapsibleSection();
+ this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteBox');
+
+ for (let label of this.querySelectorAll(".meta-label")) {
+ // Prevent default focus/blur behavior - we implement our own below
+ label.addEventListener("mousedown", this._handleMetaLabelMousedown);
+ label.addEventListener("click", this._handleMetaLabelClick);
+ }
+
+ this._id("parentItem").addEventListener("click", this._handleViewParentItem);
+ }
+
+ destroy() {
+ Zotero.Notifier.unregisterObserver(this._notifierID);
+
+ for (let label of this.querySelectorAll(".meta-label")) {
+ label.removeEventListener("mousedown", this._handleMetaLabelMousedown);
+ label.removeEventListener("click", this._handleMetaLabelClick);
+ }
+
+ this._id("parentItem")?.removeEventListener("click", this._handleViewParentItem);
+ }
+
+ notify(event, _type, ids, _extraData) {
+ if (event != 'modify' || !this.item?.id) return;
+
+ if (ids.includes(this.item.id)) {
+ this._forceRenderAll();
+ return;
+ }
+
+ if (ids.includes(this.item.parentItemID)) {
+ this._updateParentItemInfo();
+ }
+ }
+
+ render() {
+ if (!this.item) return;
+ if (!this._section.open) return;
+ if (this._isAlreadyRendered("sync")) return;
+
+ this.updateInfo();
+ }
+
+ updateInfo() {
+ if (!this._item || !this._item.isNote()) return;
+
+ let dateCreatedField = this._id('dateCreated');
+ let dateModifiedField = this._id('dateModified');
+ let wordCountField = this._id('wordCount');
+
+ this._updateParentItemInfo();
+
+ // Note word counts
+ let noteContent = this._item.getNote();
+ let wordCount = this._calculateWordCounts(noteContent);
+
+ wordCountField.value = wordCount.toLocaleString();
+
+ // Date created
+ let dateAdded = this._item.getField('dateAdded');
+ if (dateAdded) {
+ let date = Zotero.Date.sqlToDate(dateAdded, true);
+ dateCreatedField.value = date.toLocaleString();
+ }
+
+ // Date modified
+ let dateModified = this._item.getField('dateModified');
+ if (dateModified) {
+ let date = Zotero.Date.sqlToDate(dateModified, true);
+ dateModifiedField.value = date.toLocaleString();
+ }
+ }
+
+ _updateParentItemInfo() {
+ let parentItemButton = this._id("parentItem");
+ let parentItemButtonL10nArgs;
+ if (this._item.parentItemID) {
+ parentItemButtonL10nArgs = `{"hasParentItem":true,"parentItemTitle":"${this._item.parentItem.getDisplayTitle()}"}`;
+ }
+ else {
+ parentItemButtonL10nArgs = '{"hasParentItem":false}';
+ }
+ parentItemButton.setAttribute("data-l10n-args", parentItemButtonL10nArgs);
+ }
+
+ _calculateWordCounts(noteContent) {
+ if (!noteContent) {
+ return 0;
+ }
+
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(noteContent, "text/html");
+ const text = doc.body.textContent || "";
+ return countWords(text);
+ }
+
+ _handleMetaLabelClick = (event) => {
+ event.preventDefault();
+
+ let labelWrapper = event.target.closest(".meta-label");
+ if (labelWrapper.nextSibling.contains(document.activeElement)) {
+ ZoteroPane.itemsView.focus();
+ }
+ else if (!labelWrapper.nextSibling.firstChild.readOnly) {
+ labelWrapper.nextSibling.firstChild.focus();
+ }
+ };
+
+ _handleMetaLabelMousedown = (event) => {
+ event.preventDefault();
+ };
+
+ _handleViewParentItem = (event) => {
+ event.preventDefault();
+ if (!this._item) return;
+ if (!this._item.parentItemID) {
+ ZoteroPane.selectItem(this._item.id);
+ }
+ ZoteroPane.selectItem(this._item.id);
+ };
+
+ _id(id) {
+ return this.querySelector(`#${id}`);
+ }
+ }
+
+ customElements.define("note-box", NoteBox);
+}
diff --git a/chrome/content/zotero/elements/noteEditor.js b/chrome/content/zotero/elements/noteEditor.js
index 59b56ebde8..070bd671b1 100644
--- a/chrome/content/zotero/elements/noteEditor.js
+++ b/chrome/content/zotero/elements/noteEditor.js
@@ -38,6 +38,8 @@
this._initialized = false;
this._editorInstance = null;
this._destroyed = false;
+ this._bottomPlaceholder = null;
+ this._contextPaneOpen = null;
this.content = MozXULElement.parseXULToFragment(`
@@ -140,6 +142,7 @@
popup: this._id('editor-menu'),
onNavigate: this._navigateHandler,
viewMode: this.viewMode,
+ tabID: this.tabID,
readOnly: this._mode != 'edit',
disableUI: this._mode == 'merge',
onReturn: this._returnHandler,
@@ -148,6 +151,7 @@
if (this._onInitCallback) {
this._onInitCallback();
}
+ requestIdleCallback(() => this.setToggleContextPaneButtonMode());
};
onInit = (callback) => {
@@ -301,6 +305,9 @@
this._id('links-box').parentItem = val;
}
+ // TODO: implement this
+ async navigate(_location) {}
+
async focus() {
let n = 0;
while (!this._editorInstance && n++ < 100) {
@@ -321,10 +328,12 @@
this._iframe.focus();
this._editorInstance._iframeWindow.document.querySelector('.toolbar-button-return').focus();
}
- catch (e) {
+ catch {
}
}
+ refresh() {}
+
renderCustomHead(callback) {
let customHead = this.querySelector(".custom-head");
customHead.replaceChildren();
@@ -335,6 +344,34 @@
doc: document,
append,
});
+ };
+
+ setBottomPlaceholderHeight(height) {
+ // Store raw value as null/number for toggle contextPane button update
+ this._bottomPlaceholder = height;
+ if (typeof height !== 'number') {
+ height = 0;
+ }
+ this.style.height = `calc(100% - ${height}px)`;
+ this.setToggleContextPaneButtonMode();
+ };
+
+ setContextPaneOpen(open) {
+ this._contextPaneOpen = open;
+ this.setToggleContextPaneButtonMode();
+ }
+
+ setToggleContextPaneButtonMode() {
+ if (!this._editorInstance) return;
+ if (this._bottomPlaceholder === null && this._contextPaneOpen === null) return;
+ let mode = null;
+ if (this._bottomPlaceholder !== null) {
+ mode = "stacked";
+ }
+ else if (!this._contextPaneOpen) {
+ mode = "standard";
+ }
+ this._editorInstance.setToggleContextPaneButtonMode(mode);
}
_id(id) {
diff --git a/chrome/content/zotero/elements/notesContext.js b/chrome/content/zotero/elements/notesContext.js
index f697c73089..9b9218dc5b 100644
--- a/chrome/content/zotero/elements/notesContext.js
+++ b/chrome/content/zotero/elements/notesContext.js
@@ -517,7 +517,7 @@
break;
case 'context-pane-list-edit-in-window':
- ZoteroPane_Local.openNoteWindow(id);
+ ZoteroPane.openNote(id, { openInWindow: true });
break;
case 'context-pane-list-move-to-trash':
diff --git a/chrome/content/zotero/elements/tabContent.js b/chrome/content/zotero/elements/tabContent.js
new file mode 100644
index 0000000000..5a1cb265e3
--- /dev/null
+++ b/chrome/content/zotero/elements/tabContent.js
@@ -0,0 +1,115 @@
+/*
+ ***** 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 TabContent extends XULElementBase {
+ content = MozXULElement.parseXULToFragment("");
+
+ get tabID() {
+ return this.getAttribute("id");
+ }
+
+ set tabID(id) {
+ this.setAttribute("id", id);
+ }
+
+ get tabData() {
+ return Zotero_Tabs._getTab(this.tabID);
+ }
+
+ /**
+ * @returns {number | null}
+ * @description The width of the sidebar in pixels.
+ */
+ get sidePaneWidth() {
+ let state = ZoteroContextPane.getSidePaneState(this.tabData.type);
+ if (state) {
+ return state.width || 0;
+ }
+ return null;
+ }
+
+ set sidePaneWidth(width) {
+ ZoteroContextPane.updateLayout({ sidePaneWidth: width });
+ }
+
+ async init() {
+ }
+
+ async destroy() {
+ }
+
+ /**
+ * Notify the tab content that the tab has been selected or deselected.
+ * Triggered by the Zotero_Tabs when a tab is selected or deselected.
+ * @param {boolean} selected - Whether this tab is currently selected.
+ */
+ onTabSelectionChanged(selected) {
+ this.dispatchEvent(new CustomEvent("tab-selection-change", {
+ detail: {
+ selected
+ }
+ }));
+ }
+
+ /**
+ * Notify the tab content that the bottom placeholder height has changed.
+ * @param {number} height - The new height in pixels.
+ */
+ setBottomPlaceholderHeight(height) {
+ this.dispatchEvent(new CustomEvent("tab-bottom-placeholder-resize", {
+ detail: {
+ height,
+ }
+ }));
+ }
+
+ /**
+ * Notify the tab content that the context pane has been toggled.
+ * @param {boolean} open - Whether the context pane is open or not.
+ */
+ setContextPaneOpen(open) {
+ this.dispatchEvent(new CustomEvent("tab-context-pane-toggle", {
+ detail: {
+ open,
+ }
+ }));
+ }
+
+ /**
+ * Notify the tab content that it has received focus.
+ * Used by the context pane to move focus.
+ */
+ setFocus() {
+ this.dispatchEvent(new CustomEvent("tab-focus", {
+ detail: {
+ tabID: this.tabID
+ }
+ }));
+ }
+ }
+
+ customElements.define("tab-content", TabContent);
+};
diff --git a/chrome/content/zotero/locateMenu.js b/chrome/content/zotero/locateMenu.js
index 61ddaf49f4..35aa69fb69 100644
--- a/chrome/content/zotero/locateMenu.js
+++ b/chrome/content/zotero/locateMenu.js
@@ -34,7 +34,7 @@ var Zotero_LocateMenu = new function () {
/**
* Clear and build the locate menu
*/
- this.buildLocateMenu = async function (locateMenu) {
+ this.buildLocateMenu = async function (locateMenu, { locateMode } = {}) {
// clear menu
while(locateMenu.childElementCount > 0) {
locateMenu.removeChild(locateMenu.firstChild);
@@ -43,7 +43,9 @@ var Zotero_LocateMenu = new function () {
var selectedItems = await _getSelectedItems();
if(selectedItems.length) {
- await _addViewOptions(locateMenu, selectedItems, true, true, true);
+ await _addViewOptions(locateMenu, selectedItems, true, true, {
+ locateMode, isToolbarMenu: true
+ });
var availableEngines = _getAvailableLocateEngines(selectedItems);
// add engines that are available for selected items
@@ -158,8 +160,10 @@ var Zotero_LocateMenu = new function () {
* @param {Boolean} addExtraOptions Whether to add options that start with "_" below the separator
* @param {Boolean} isToolbarMenu Whether the menu being populated is displayed in the toolbar
* (and not the item tree context menu)
+ * @param {"tab" | "window"} locateMode Whether the menu being populated is displayed in a tab or window
*/
- var _addViewOptions = async function (locateMenu, selectedItems, showIcons, addExtraOptions, isToolbarMenu) {
+ var _addViewOptions = async function (locateMenu, selectedItems, showIcons, addExtraOptions, options = {}) {
+ let { isToolbarMenu, locateMode } = options;
var optionsToShow = {};
// check which view options are available
@@ -167,7 +171,7 @@ var Zotero_LocateMenu = new function () {
for(var viewOption in ViewOptions) {
if (!optionsToShow[viewOption]
&& (!isToolbarMenu || !ViewOptions[viewOption].hideInToolbar)) {
- optionsToShow[viewOption] = await ViewOptions[viewOption].canHandleItem(item);
+ optionsToShow[viewOption] = await ViewOptions[viewOption].canHandleItem(item, { locateMode });
}
}
}
@@ -358,9 +362,7 @@ var Zotero_LocateMenu = new function () {
selectedItems.push(attachment);
}
}
- else if (!item.isNote()) {
- selectedItems.push(item);
- }
+ selectedItems.push(item);
}
return selectedItems;
}
@@ -368,23 +370,24 @@ var Zotero_LocateMenu = new function () {
var ViewOptions = {};
/**
- * "Open PDF" option
+ * "Open * in "
*
- * Should appear only when the item is a PDF, or a linked or attached file or web attachment is
- * a PDF
+ * Only for built-in tab item types: PDF, EPUB, Snapshot, Note
*/
- function ViewAttachment(alternateWindowBehavior) {
- this._attachmentType = "mixed";
- this._numAttachments = 0;
+ function ViewItem(alternateWindowBehavior) {
+ this._viewItemType = "mixed";
+ this._numItems = 0;
Object.defineProperty(this, "className", {
get() {
- switch (this._attachmentType) {
+ switch (this._viewItemType) {
case "pdf":
return "zotero-menuitem-attachments-pdf";
case "epub":
return "zotero-menuitem-attachments-epub";
case "snapshot":
return "zotero-menuitem-attachments-snapshot";
+ case "note":
+ return "zotero-menuitem-attach-note";
default: {
let openInNewWindow = Zotero.Prefs.get("openReaderInNewWindow");
if (alternateWindowBehavior) {
@@ -395,16 +398,12 @@ var Zotero_LocateMenu = new function () {
}
},
});
-
- // Don't show alternate-behavior option ("in New Window" when openReaderInNewWindow is false,
- // "in New Tab" when it's true) in toolbar Locate menu
- this.hideInToolbar = alternateWindowBehavior;
this.l10nId = "item-menu-viewAttachment";
Object.defineProperty(this, "l10nArgs", {
get: () => {
let openIn;
- if (this._attachmentType !== "mixed" && Zotero.Prefs.get(`fileHandler.${this._attachmentType}`)) {
+ if (this._viewItemType !== "mixed" && Zotero.Prefs.get(`fileHandler.${this._viewItemType}`)) {
openIn = "external";
}
else {
@@ -415,56 +414,77 @@ var Zotero_LocateMenu = new function () {
openIn = openInNewWindow ? "window" : "tab";
}
return {
- attachmentType: this._attachmentType,
- numAttachments: this._numAttachments,
+ attachmentType: this._viewItemType,
+ numAttachments: this._numItems,
openIn,
};
}
});
- this.canHandleItem = async function (item) {
- const attachment = await _getFirstUsableAttachment(item);
+ this.canHandleItem = async function (item, { locateMode } = {}) {
+ const usableItem = await _getFirstUsableItem(item);
+ if (!usableItem) {
+ return false;
+ }
// Don't show alternate-behavior option when using an external PDF viewer
- return attachment
- && !(alternateWindowBehavior && Zotero.Prefs.get(`fileHandler.${attachment.attachmentReaderType}`));
+ if (!item.isNote()
+ && Zotero.Prefs.get(`fileHandler.${usableItem.attachmentReaderType}`)
+ && alternateWindowBehavior) {
+ return false;
+ }
+ if ((locateMode === "tab" && !alternateWindowBehavior)
+ || (locateMode === "window" && alternateWindowBehavior)) {
+ // Don't show option if it would open in the same type of the current context
+ return false;
+ }
+ return usableItem;
};
this.updateMenuItem = async function (items) {
- let attachmentType = null;
- let numAttachments = 0;
+ let viewItemType = null;
+ let numItems = 0;
for (let item of items) {
- let attachment = await _getFirstUsableAttachment(item);
- let thisAttachmentType = attachment?.attachmentReaderType;
- if (!thisAttachmentType) {
+ let usableItem = await _getFirstUsableItem(item);
+ if (!usableItem) {
+ continue;
+ }
+ let thisViewItemType = usableItem.isNote() ? "note" : usableItem?.attachmentReaderType;
+ if (!thisViewItemType) {
continue;
}
- if (attachmentType === null) {
- attachmentType = thisAttachmentType;
+ if (viewItemType === null) {
+ viewItemType = thisViewItemType;
}
- else if (attachmentType !== thisAttachmentType) {
- attachmentType = "mixed";
+ else if (viewItemType !== thisViewItemType) {
+ viewItemType = "mixed";
}
- numAttachments++;
+ numItems++;
}
- this._attachmentType = attachmentType;
- this._numAttachments = numAttachments;
+ this._viewItemType = viewItemType;
+ this._numItems = numItems;
};
this.handleItems = async function (items, event) {
- var attachments = [];
+ let usableItems = [];
for (let item of items) {
- var attachment = await _getFirstUsableAttachment(item);
- if (attachment) attachments.push(attachment.id);
+ let usableItem = await _getFirstUsableItem(item);
+ if (usableItem) usableItems.push(usableItem);
}
- ZoteroPane_Local.viewAttachment(attachments, event, false,
- { forceAlternateWindowBehavior: alternateWindowBehavior });
+ ZoteroPane.viewItems(usableItems, event,
+ {
+ noLocateOnMissing: false,
+ forceAlternateWindowBehavior: alternateWindowBehavior
+ });
};
- var _getFirstUsableAttachment = async function (item) {
- var attachments = item.isAttachment() ? [item] : ((await item.getBestAttachments()));
+ var _getFirstUsableItem = async function (item) {
+ if (item.isNote()) {
+ return item;
+ }
+ let attachments = item.isAttachment() ? [item] : ((await item.getBestAttachments()));
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
if (attachment.attachmentReaderType
@@ -476,8 +496,8 @@ var Zotero_LocateMenu = new function () {
};
}
- ViewOptions.viewAttachmentInTab = new ViewAttachment(false);
- ViewOptions.viewAttachmentInWindow = new ViewAttachment(true);
+ ViewOptions.viewItemInTab = new ViewItem(false);
+ ViewOptions.viewItemInWindow = new ViewItem(true);
/**
* "View Online" option
diff --git a/chrome/content/zotero/note.js b/chrome/content/zotero/note.js
index e89397b8fb..8abdd7ea89 100644
--- a/chrome/content/zotero/note.js
+++ b/chrome/content/zotero/note.js
@@ -35,6 +35,9 @@ function showInLibrary() {
async function onLoad() {
if (window.arguments) {
var io = window.arguments[0];
+ if (io.wrappedJSObject) {
+ io = io.wrappedJSObject;
+ }
}
let itemID = parseInt(io.itemID);
@@ -70,6 +73,9 @@ async function onLoad() {
noteEditor.focus();
notifierUnregisterID = Zotero.Notifier.registerObserver(NotifyCallback, 'item', 'noteWindow');
+
+ io.noteEditor = noteEditor;
+ io._initPromise?.resolve();
}
// If there's an error saving a note, close the window and crash the app
diff --git a/chrome/content/zotero/preferences/preferences_general.xhtml b/chrome/content/zotero/preferences/preferences_general.xhtml
index d62826c7ce..f4368631b7 100644
--- a/chrome/content/zotero/preferences/preferences_general.xhtml
+++ b/chrome/content/zotero/preferences/preferences_general.xhtml
@@ -266,6 +266,15 @@
native="true"
/>
+
+
+
+
+
diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js
index 36f628ed2c..e61d1e6229 100644
--- a/chrome/content/zotero/standalone/standalone.js
+++ b/chrome/content/zotero/standalone/standalone.js
@@ -45,6 +45,7 @@ const ZoteroStandalone = new function () {
//const NOTE_FONT_SIZES = ["11", "12", "13", "14", "18", "24", "36", "48", "64", "72", "96"];
const NOTE_FONT_SIZE_DEFAULT = "14";
+ const NOTE_TAB_FONT_SIZE_DEFAULT = "16";
Object.defineProperty(this, 'currentReader', {
get: () => Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)
@@ -495,7 +496,21 @@ const ZoteroStandalone = new function () {
'view-menuitem-note-font-size-reset',
noteFontSize != NOTE_FONT_SIZE_DEFAULT
);
-
+
+ let noteTabFontSize = Zotero.Prefs.get('note.tabFontSize');
+ for (let menuitem of document.querySelectorAll(`#note-tab-font-size-menu menuitem`)) {
+ if (parseInt(menuitem.getAttribute('label')) == noteTabFontSize) {
+ menuitem.setAttribute('checked', true);
+ }
+ else {
+ menuitem.removeAttribute('checked');
+ }
+ }
+ this.updateMenuItemEnabled(
+ 'view-menuitem-note-tab-font-size-reset',
+ noteTabFontSize != NOTE_TAB_FONT_SIZE_DEFAULT
+ );
+
// Recursive collections
this.updateMenuItemCheckmark(
'view-menuitem-recursive-collections',
@@ -694,7 +709,11 @@ const ZoteroStandalone = new function () {
var size = event.originalTarget.getAttribute('label');
Zotero.Prefs.set('note.fontSize', size);
};
-
+
+ this.updateNoteTabFontSize = function (event) {
+ var size = event.originalTarget.getAttribute('label');
+ Zotero.Prefs.set('note.tabFontSize', size);
+ };
this.promptForRestart = function () {
// Prompt to restart
diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js
index 6f7039a4c0..be733737c8 100644
--- a/chrome/content/zotero/tabs.js
+++ b/chrome/content/zotero/tabs.js
@@ -76,6 +76,252 @@ var Zotero_Tabs = new function () {
this._history = [];
this._focusOptions = {};
+ this._loadableTypes = ['reader', 'note'];
+
+ this._hasContextPaneTypes = ['reader', 'note'];
+
+ this._hasNoteContextTypes = ['reader', 'note'];
+
+ this.hasContextPane = function (type) {
+ return this._hasContextPaneTypes.includes(type);
+ };
+
+ this.hasNoteContext = function (type) {
+ return this._hasNoteContextTypes.includes(type);
+ };
+
+ this.tabHooks = {
+ load: {
+ reader: async (tab, tabIndex, options) => {
+ let reader = await Zotero.Reader.open(tab.data.itemID, options && options.location, {
+ tabID: tab.id,
+ title: tab.title,
+ tabIndex,
+ allowDuplicate: true,
+ secondViewState: tab.data.secondViewState,
+ preventJumpback: true
+ });
+ await reader._initPromise;
+ },
+ note: async (tab, tabIndex, options) => {
+ let noteEditor = await Zotero.Notes.open(tab.data.itemID, options && options.location, {
+ tabID: tab.id,
+ title: tab.title,
+ tabIndex,
+ allowDuplicate: true,
+ preventJumpback: true
+ });
+ await noteEditor;
+ }
+ },
+ focus: {
+ library: async () => {
+ let collectionsPane = document.getElementById("zotero-collections-pane");
+ if (collectionsPane.getAttribute("collapsed")) {
+ document.getElementById('zotero-tb-add').focus();
+ return;
+ }
+ document.getElementById('zotero-tb-collection-add').focus();
+ },
+ reader: async (tab) => {
+ let reader = Zotero.Reader.getByTabID(tab.id);
+ if (reader) {
+ // Move focus to the reader and focus the toolbar
+ reader.focusFirst();
+ reader.focusToolbar();
+ }
+ },
+ note: async (tab) => {
+ let noteEditor = Zotero.Notes.getByTabID(tab.id);
+ if (noteEditor) {
+ // Move focus to the note editor and focus the toolbar
+ noteEditor.focusToolbar();
+ }
+ },
+ },
+ refocus: {
+ library: async (tab, tabIndex, options) => {
+ // Move focus to the last focused element of zoteroPane if any or itemTree otherwise
+ if (options.focusElementID) {
+ tab.lastFocusedElement = document.getElementById(options.focusElementID);
+ }
+ // Small delay to make sure the focus does not remain on the actual
+ // tab after mouse click
+ setTimeout(() => {
+ if (tab.lastFocusedElement) {
+ tab.lastFocusedElement.focus();
+ }
+ if (document.activeElement !== tab.lastFocusedElement) {
+ ZoteroPane.itemsView.focus();
+ }
+ tab.lastFocusedElement = null;
+ });
+ },
+ reader: async (tab, _tabIndex, _options) => {
+ let reader = Zotero.Reader.getByTabID(tab.id);
+ if (reader) {
+ reader.focus();
+ }
+ },
+ note: async (tab, _tabIndex, _options) => {
+ let noteEditor = Zotero.Notes.getByTabID(tab.id);
+ if (noteEditor) {
+ noteEditor.focus();
+ }
+ }
+ },
+ moveToNewWindow: {
+ reader: async (tab, _tabIndex) => {
+ Zotero_Tabs.close(tab.id);
+ let { itemID, secondViewState } = tab.data;
+ await Zotero.Reader.open(itemID, null, { openInWindow: true, secondViewState });
+ },
+ note: async (tab, _tabIndex) => {
+ Zotero_Tabs.close(tab.id);
+ let { itemID } = tab.data;
+ await Zotero.Notes.open(itemID, null, { openInWindow: true });
+ }
+ },
+ duplicate: {
+ reader: async (tab, tabIndex) => {
+ if (tab.data.itemID) {
+ let { secondViewState } = tab.data;
+ await Zotero.Reader.open(tab.data.itemID, null, { tabIndex: tabIndex + 1, allowDuplicate: true, secondViewState });
+ }
+ },
+ note: async (tab, tabIndex) => {
+ if (tab.data.itemID) {
+ await Zotero.Notes.open(tab.data.itemID, null, { tabIndex: tabIndex + 1, allowDuplicate: true });
+ }
+ }
+ },
+ undoClose: {
+ reader: async (tab, _tabIndex) => {
+ if (Zotero.Items.exists(tab.data.itemID)) {
+ await Zotero.Reader.open(tab.data.itemID,
+ null,
+ {
+ tabIndex: tab.index,
+ openInBackground: true,
+ allowDuplicate: true
+ }
+ );
+ return true;
+ }
+ return false;
+ },
+ note: async (tab, _tabIndex) => {
+ if (Zotero.Items.exists(tab.data.itemID)) {
+ await Zotero.Notes.open(tab.data.itemID,
+ null,
+ {
+ tabIndex: tab.index,
+ openInBackground: true,
+ allowDuplicate: true
+ }
+ );
+ return true;
+ }
+ return false;
+ }
+ },
+ restoreState: {
+ library: async (tab, _tabIndex) => {
+ this.rename('zotero-pane', tab.title);
+ // At first, library tab is added without the icon data. We set it here once we know what it is
+ let libraryTab = this._getTab('zotero-pane');
+ libraryTab.tab.data = tab.data || {};
+ return {
+ itemID: null,
+ };
+ },
+ reader: async (tab, tabIndex) => {
+ if (Zotero.Items.exists(tab.data.itemID)) {
+ // Strip non-printable characters, which can result in DOM syntax errors
+ // ("An invalid or illegal string was specified") -- reproduced with "\u0001"
+ // in a title in session.json
+ let title = tab.title.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
+ this.add({
+ type: 'reader-unloaded',
+ title,
+ index: tabIndex,
+ data: tab.data,
+ select: tab.selected
+ });
+ return {
+ itemID: tab.data.itemID,
+ };
+ }
+ return {
+ itemID: null,
+ };
+ },
+ note: async (tab, tabIndex) => {
+ if (Zotero.Items.exists(tab.data.itemID)) {
+ let title = tab.title.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
+ this.add({
+ type: 'note-unloaded',
+ title,
+ index: tabIndex,
+ data: tab.data,
+ select: tab.selected
+ });
+ return {
+ itemID: tab.data.itemID,
+ };
+ }
+ return {
+ itemID: null,
+ };
+ }
+ },
+ getTitle: {
+ reader: async (tab) => {
+ let item = Zotero.Items.get(tab.data.itemID);
+ return item ? item.getTabTitle() : "";
+ },
+ note: async (tab) => {
+ let item = Zotero.Items.get(tab.data.itemID);
+ if (!item) {
+ return "";
+ }
+ let title = await item.getTabTitle();
+ if (!title) {
+ return Zotero.getString("item-title-empty-note");
+ }
+ return title;
+ }
+ }
+ };
+
+ this._getHook = function (type, action) {
+ if (this.tabHooks[action] && this.tabHooks[action][type]) {
+ return this.tabHooks[action][type];
+ }
+ return async () => {};
+ };
+
+ this._hasHook = function (type, action) {
+ return !!(this.tabHooks[action] && this.tabHooks[action][type]);
+ };
+
+ this.parseTabType = function (type) {
+ if (!type) {
+ type = this.selectedType;
+ }
+ if (type === 'zotero-pane') {
+ return {
+ tabContentType: 'library',
+ tabState: '',
+ };
+ }
+ let [tabContentType, tabState] = type.split('-');
+ return {
+ tabContentType,
+ tabState
+ };
+ };
+
// Keep track of item modifications to update the title
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'tabs');
@@ -83,9 +329,7 @@ var Zotero_Tabs = new function () {
this._prefsObserverID = Zotero.Prefs.registerObserver('tabs.title.reader', async () => {
for (let tab of this._tabs) {
if (!tab.data.itemID) continue;
- let item = Zotero.Items.get(tab.data.itemID);
- let title = await item.getTabTitle();
- this.rename(tab.id, title);
+ this.rename(tab.id);
}
});
@@ -103,6 +347,14 @@ var Zotero_Tabs = new function () {
return { tab: this._tabs[tabIndex], tabIndex };
};
+ this.getTabContent = function (id) {
+ if (!id) {
+ id = this._selectedID;
+ }
+ return document.getElementById(id);
+ };
+
+ // TODO: update this
this._update = function () {
// Go through all tabs and try to save their icons to tab.data
for (let tab of this._tabs) {
@@ -120,18 +372,19 @@ var Zotero_Tabs = new function () {
let item = Zotero.Items.get(tab.data.itemID);
tab.data.icon = item.getItemTypeIconName(true);
}
- catch (e) {
+ catch {
// item might not yet be loaded, we will get the right icon on the next update
}
}
}
this._tabBarRef.current.setTabs(this._tabs.map((tab) => {
+ let { tabContentType } = this.parseTabType(tab.type);
return {
id: tab.id,
type: tab.type,
title: tab.title,
- renderTitle: tab.type === 'reader' || tab.type === 'reader-unloaded',
+ renderTitle: tabContentType === 'reader',
selected: tab.id == this._selectedID,
isItemType: tab.id !== 'zotero-pane',
icon: tab.data?.icon || null
@@ -157,9 +410,9 @@ var Zotero_Tabs = new function () {
return tab && tab.id;
};
- this.setSecondViewState = function (tabID, state) {
+ this.setTabData = function (tabID, data) {
let { tab } = this._getTab(tabID);
- tab.data.secondViewState = state;
+ Object.assign(tab.data, data);
Zotero.Session.debounceSave();
};
@@ -171,7 +424,7 @@ var Zotero_Tabs = new function () {
onTabMove={this.move.bind(this)}
onTabClose={this.close.bind(this)}
onContextMenu={this._openMenu.bind(this)}
- refocusReader={this.refocusReader.bind(this)}
+ onRefocus={this.refocus.bind(this)}
onLoad={this._update.bind(this)}
/>
);
@@ -182,16 +435,23 @@ var Zotero_Tabs = new function () {
if (event !== "modify") return;
for (let id of ids) {
let item = Zotero.Items.get(id);
- // If a top-level item is updated, update all tabs that have its attachments
+ // If a top-level item is updated, update all tabs that have its attachments and notes
// Otherwise, just update the tab with the updated attachment
- let attachmentIDs = item.isAttachment() ? [id] : item.getAttachments();
- for (let attachmentID of attachmentIDs) {
- let attachment = Zotero.Items.get(attachmentID);
- let relevantTabs = this._tabs.filter(tab => tab.data.itemID == attachmentID);
+ let itemIDs = [];
+ if (item.isAttachment() || item.isNote()) {
+ itemIDs.push(id);
+ }
+ else if (item.isRegularItem()) {
+ itemIDs.push(
+ ...item.getAttachments(),
+ ...item.getNotes()
+ );
+ }
+ for (let itemID of itemIDs) {
+ let relevantTabs = this._tabs.filter(tab => tab.data.itemID == itemID);
if (!relevantTabs.length) continue;
for (let tab of relevantTabs) {
- let title = await attachment.getTabTitle();
- this.rename(tab.id, title);
+ this.rename(tab.id);
}
}
}
@@ -200,8 +460,9 @@ var Zotero_Tabs = new function () {
this.getState = function () {
return this._tabs.map((tab) => {
let type = tab.type;
- if (type === 'reader-unloaded') {
- type = 'reader';
+ // If type matches *-unloaded, use the base type
+ if (type.endsWith('-unloaded')) {
+ type = type.replace(/-unloaded$/, '');
}
var o = {
type,
@@ -222,27 +483,11 @@ var Zotero_Tabs = new function () {
let itemIDs = [];
for (let i = 0; i < tabs.length; i++) {
let tab = tabs[i];
- if (tab.type === 'library') {
- this.rename('zotero-pane', tab.title);
- // At first, library tab is added without the icon data. We set it here once we know what it is
- let libraryTab = this._getTab('zotero-pane');
- libraryTab.tab.data = tab.data || {};
- }
- else if (tab.type === 'reader') {
- if (Zotero.Items.exists(tab.data.itemID)) {
- // Strip non-printable characters, which can result in DOM syntax errors
- // ("An invalid or illegal string was specified") -- reproduced with "\u0001"
- // in a title in session.json
- let title = tab.title.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
- this.add({
- type: 'reader-unloaded',
- title,
- index: i,
- data: tab.data,
- select: tab.selected
- });
- itemIDs.push(tab.data.itemID);
- }
+ let { tabContentType } = this.parseTabType(tab.type);
+ let restoreStateHook = this._getHook(tabContentType, 'restoreState');
+ let { itemID } = await restoreStateHook(tab, i);
+ if (itemID) {
+ itemIDs.push(itemID);
}
}
// Unset the previously selected tab id, because it was set when restoring tabs
@@ -258,7 +503,7 @@ var Zotero_Tabs = new function () {
* Add a new tab
*
* @param {String} type
- * @param {String} title
+ * @param {String} [title] - Tab title. If empty and data.itemID is set, the title will be fetched automatically
* @param {String} data - Extra data about the tab to pass to notifier and session
* @param {Integer} index
* @param {Boolean} select
@@ -268,8 +513,11 @@ var Zotero_Tabs = new function () {
this.add = function ({ id, type, data, title, index, select, onClose, preventJumpback }) {
if (typeof type != 'string') {
}
- if (typeof title != 'string') {
- throw new Error(`'title' should be a string (was ${typeof title})`);
+ if (title && typeof title != 'string') {
+ throw new Error(`'title' should be string or undefined (was ${typeof title})`);
+ }
+ if (!title) {
+ title = "";
}
if (index !== undefined && (!Number.isInteger(index) || index < 1)) {
throw new Error(`'index' should be an integer > 0 (was ${index} (${typeof index})`);
@@ -278,7 +526,7 @@ var Zotero_Tabs = new function () {
throw new Error(`'onClose' should be a function (was ${typeof onClose})`);
}
id = id || 'tab-' + Zotero.Utilities.randomString();
- var container = document.createXULElement('vbox');
+ var container = document.createXULElement('tab-content');
container.id = id;
this.deck.appendChild(container);
var tab = { id, type, title, data, onClose };
@@ -293,15 +541,9 @@ var Zotero_Tabs = new function () {
this._prevSelectedID = previousID;
}
}
- // When a new tab is opened synchronously by ReaderTab constructor, the title is empty.
- // However, { id, container } needs to return immediately, so do not wait for the new title
- // and construct it in async manner below.
if (!title && data.itemID) {
- (async () => {
- let item = Zotero.Items.get(data.itemID);
- title = await item.getTabTitle();
- this.rename(tab.id, title);
- })();
+ // Not awaited as the id and container should be returned synchronously
+ this.rename(tab.id);
}
return { id, container };
};
@@ -312,14 +554,21 @@ var Zotero_Tabs = new function () {
* @param {String} id
* @param {String} title
*/
- this.rename = function (id, title) {
- if (typeof title != 'string') {
- throw new Error(`'title' should be a string (was ${typeof title})`);
+ this.rename = async function (id, title) {
+ if (title && typeof title != 'string') {
+ throw new Error(`'title' should be string or undefined (was ${typeof title})`);
}
- var { tab } = this._getTab(id);
+ let { tab } = this._getTab(id);
if (!tab) {
return;
}
+ if (!title) {
+ let { tabContentType } = this.parseTabType(tab.type);
+ if (this._hasHook(tabContentType, 'getTitle')) {
+ title = await this._getHook(tabContentType, 'getTitle')(tab);
+ }
+ }
+
tab.title = title;
this._update();
};
@@ -366,7 +615,7 @@ var Zotero_Tabs = new function () {
if (tab.onClose) {
tab.onClose();
}
- historyEntry.push({ index: tmpTabs.indexOf(tab), data: tab.data });
+ historyEntry.push({ index: tmpTabs.indexOf(tab), data: tab.data, type: tab.type });
closedIDs.push(id);
setTimeout(() => {
@@ -399,19 +648,14 @@ var Zotero_Tabs = new function () {
let maxIndex = -1;
let openPromises = [];
for (let tab of historyEntry) {
- if (Zotero.Items.exists(tab.data.itemID)) {
- openPromises.push(Zotero.Reader.open(tab.data.itemID,
- null,
- {
- tabIndex: tab.index,
- openInBackground: true,
- allowDuplicate: true
+ let { tabContentType } = this.parseTabType(tab.type);
+ let undoCloseHook = this._getHook(tabContentType, 'undoClose');
+ openPromises.push(undoCloseHook({ data: tab.data }, tab.index)
+ .then((opened) => {
+ if (opened && tab.index > maxIndex) {
+ maxIndex = tab.index;
}
- ));
- if (tab.index > maxIndex) {
- maxIndex = tab.index;
- }
- }
+ }));
}
await Promise.all(openPromises);
// Select last reopened tab
@@ -453,39 +697,15 @@ var Zotero_Tabs = new function () {
* @param {Boolean} reopening
*/
this.select = function (id, reopening, options = {}) {
- var { tab, tabIndex } = this._getTab(id);
- // Move focus to the last focused element of zoteroPane if any or itemTree otherwise
- let focusZoteroPane = () => {
- if (tab.id !== 'zotero-pane') return;
- if (options.focusElementID) {
- tab.lastFocusedElement = document.getElementById(options.focusElementID);
- }
- // Small delay to make sure the focus does not remain on the actual
- // tab after mouse click
- setTimeout(() => {
- if (this.tabsMenuPanel.visible) {
- this.tabsMenuPanel.resetFocus();
- }
- else if (tab.lastFocusedElement) {
- tab.lastFocusedElement.focus();
- }
- else if (document.activeElement !== tab.lastFocusedElement) {
- ZoteroPane_Local.itemsView.focus();
- }
- tab.lastFocusedElement = null;
- });
- };
+ let { tab, tabIndex } = this._getTab(id);
+ let { tabContentType, tabState } = this.parseTabType(tab.type);
+
if (!tab || tab.id === this._selectedID) {
// Focus on reader or zotero pane when keepTabFocused is explicitly false
// E.g. when a tab is selected via Space or Enter
if (options.keepTabFocused === false && tab?.id === this._selectedID) {
- var reader = Zotero.Reader.getByTabID(this._selectedID);
- if (reader) {
- reader.focus();
- }
- if (tab.id == 'zotero-pane') {
- focusZoteroPane();
- }
+ let focusHook = this._getHook(tabContentType, 'focus');
+ focusHook(tab, tabIndex, options);
}
return;
}
@@ -525,32 +745,29 @@ var Zotero_Tabs = new function () {
// never return focus to another tab or
selectedTab.lastFocusedElement = document.activeElement;
}
- if (tab.type === 'reader-unloaded') {
- tab.type = "reader-loading";
+
+ if (tabState === 'unloaded') {
+ tab.type = `${tabContentType}-loading`;
// Make sure the loading message is displayed first.
// Then, open reader and hide the loading message once it has loaded.
ZoteroContextPane.showLoadingMessage(true);
- let hideMessageWhenReaderLoaded = async () => {
- let reader = await Zotero.Reader.open(tab.data.itemID, options && options.location, {
- tabID: tab.id,
- title: tab.title,
- tabIndex,
- allowDuplicate: true,
- secondViewState: tab.data.secondViewState,
- preventJumpback: true
- });
- await reader._initPromise;
+ let loadHook = this._getHook(tabContentType, 'load');
+ loadHook(tab, tabIndex, options).then(() => {
ZoteroContextPane.showLoadingMessage(false);
- };
- hideMessageWhenReaderLoaded();
+ this.markAsLoaded(tab.id);
+ });
}
+ // Notify previously selected tab content about selection change
+ this.getTabContent(this._selectedID)?.onTabSelectionChanged(false);
+
this._prevSelectedID = reopening ? this._selectedID : null;
this._selectedID = id;
this.deck.selectedIndex = Array.from(this.deck.children).findIndex(x => x.id == id);
this._update();
Zotero.Notifier.trigger('select', 'tab', [tab.id], { [tab.id]: { type: tab.type } }, true);
- if (tab.id === 'zotero-pane' && (options.keepTabFocused !== true)) {
- focusZoteroPane();
+ if (options.keepTabFocused !== true) {
+ let focusHook = this._getHook(tabContentType, 'focus');
+ focusHook(tab, tabIndex, options);
}
let tabNode = document.querySelector(`#tab-bar-container .tab[data-id="${tab.id}"]`);
if (this._focusOptions.keepTabFocused && document.activeElement.getAttribute('data-id') != tabNode.getAttribute('data-id')) {
@@ -569,17 +786,20 @@ var Zotero_Tabs = new function () {
// tabs deck selection index bigger than the deck children count. It feels like something
// isn't update synchronously
setTimeout(() => this.unloadUnusedTabs());
+
+ // Notify tab content about selection change
+ this.getTabContent(this._selectedID)?.onTabSelectionChanged(true);
};
this.unload = function (id) {
var { tab, tabIndex } = this._getTab(id);
- if (!tab || tab.id === this._selectedID || tab.type !== 'reader') {
+ if (!tab || tab.id === this._selectedID || !this._loadableTypes.includes(tab.type)) {
return;
}
this.close(tab.id);
this.add({
id: tab.id,
- type: 'reader-unloaded',
+ type: `${tab.type}-unloaded`,
title: tab.title,
index: tabIndex,
data: tab.data
@@ -589,9 +809,13 @@ var Zotero_Tabs = new function () {
// Mark a tab as loaded
this.markAsLoaded = function (id) {
let { tab } = this._getTab(id);
- if (!tab || tab.type == "reader") return;
+ if (!tab) return;
+ let { tabContentType, tabState } = this.parseTabType(tab.type);
+ if (tabState !== 'loading') {
+ return;
+ }
let prevType = tab.type;
- tab.type = "reader";
+ tab.type = tabContentType;
Zotero.Notifier.trigger("load", "tab", [id], { [id]: Object.assign({}, tab, { prevType }) }, true);
};
@@ -601,6 +825,7 @@ var Zotero_Tabs = new function () {
this.unload(tab.id);
}
}
+ // TODO: also unload note tabs
let tabs = this._tabs.slice().filter(x => x.type === 'reader');
tabs.sort((a, b) => b.timeUnselected - a.timeUnselected);
tabs = tabs.slice(MAX_LOADED_TABS);
@@ -633,15 +858,49 @@ var Zotero_Tabs = new function () {
};
/**
- * Return focus into the reader of the selected tab.
- * Required to move focus from the tab into the reader after drag.
+ * Return focus into the content of the selected tab.
+ * Required to move focus from the tab into the content after drag.
*/
- this.refocusReader = function () {
- var reader = Zotero.Reader.getByTabID(this._selectedID);
- if (!reader) return;
- setTimeout(() => {
- reader.focus();
- });
+ this.refocus = function (id) {
+ if (!id) id = this._selectedID;
+ let { tab, tabIndex } = this._getTab(id);
+ if (!tab) return;
+ let { tabContentType } = this.parseTabType(tab.type);
+ let refocusHook = this._getHook(tabContentType, 'refocus');
+ refocusHook(tab, tabIndex, this._focusOptions);
+ };
+
+ /**
+ * Move focus into the first element in content of the selected tab.
+ * Required to move focus from the outside into the tab content.
+ */
+ this.focusContent = function (id) {
+ if (!id) id = this._selectedID;
+ let { tab, tabIndex } = this._getTab(id);
+ if (!tab) return;
+ let { tabContentType } = this.parseTabType(tab.type);
+ let focusHook = this._getHook(tabContentType, 'focus');
+ focusHook(tab, tabIndex);
+ };
+
+ /**
+ * Move focus back from the tab content,
+ * e.g. shift-tab from the first focusable element.
+ */
+ this.focusBack = function () {
+ document.getElementById("zotero-tb-sync").focus();
+ };
+
+ /**
+ * Move focus to the next focusable element after the tab content,
+ * e.g. tab from the last focusable element.
+ */
+ this.focusForward = function () {
+ let focused = ZoteroContextPane.focus();
+ // If context pane wasn't focused (e.g. it's collapsed), focus the tab bar
+ if (!focused) {
+ this.moveFocus("current");
+ }
};
/**
@@ -697,7 +956,7 @@ var Zotero_Tabs = new function () {
}
}
};
-
+
/**
* Jump to the tab at a particular index. If the index points beyond the array, jump to the last
* tab.
@@ -709,7 +968,8 @@ var Zotero_Tabs = new function () {
};
this._openMenu = function (x, y, id) {
- var { tab, tabIndex } = this._getTab(id);
+ let { tab, tabIndex } = this._getTab(id);
+ let { tabContentType } = this.parseTabType(tab.type);
let menuitem;
let popup = document.createXULElement('menupopup');
document.querySelector('popupset').appendChild(popup);
@@ -754,30 +1014,26 @@ var Zotero_Tabs = new function () {
this.move(id, this._tabs.length);
});
menupopup.appendChild(menuitem);
-
- if (tab.type === 'reader' || tab.type === 'reader-unloaded') {
- // Move to new window
+ // Move to new window
+ if (this._hasHook(tabContentType, 'moveToNewWindow')) {
menuitem = document.createXULElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.moveToWindow'));
menuitem.setAttribute('disabled', false);
menuitem.addEventListener('command', () => {
- let { tab } = this._getTab(id);
- if (tab && (tab.type === 'reader' || tab.type === 'reader-unloaded')) {
- this.close(id);
- let { itemID, secondViewState } = tab.data;
- Zotero.Reader.open(itemID, null, { openInWindow: true, secondViewState });
- }
+ let { tabContentType } = this.parseTabType(tab.type);
+ let moveHook = this._getHook(tabContentType, 'moveToNewWindow');
+ moveHook(tab, tabIndex);
});
menupopup.appendChild(menuitem);
- // Duplicate tab
+ }
+ // Duplicate tab
+ if (this._hasHook(tabContentType, 'duplicate')) {
menuitem = document.createXULElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('tabs.duplicate'));
menuitem.addEventListener('command', () => {
- if (tab.data.itemID) {
- tabIndex++;
- let { secondViewState } = tab.data;
- Zotero.Reader.open(tab.data.itemID, null, { tabIndex, allowDuplicate: true, secondViewState });
- }
+ let { tabContentType } = this.parseTabType(tab.type);
+ let duplicateHook = this._getHook(tabContentType, 'duplicate');
+ duplicateHook(tab, tabIndex);
});
popup.appendChild(menuitem);
}
@@ -803,7 +1059,7 @@ var Zotero_Tabs = new function () {
popup.appendChild(menuitem);
}
// Undo close
- if (['reader', 'reader-unloaded'].includes(tab.type)) {
+ if (this._hasHook(tabContentType, 'undoClose')) {
menuitem = document.createXULElement('menuitem');
menuitem.setAttribute(
'label',
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
index 6a71edf4eb..a7f6e97e3f 100644
--- a/chrome/content/zotero/xpcom/data/item.js
+++ b/chrome/content/zotero/xpcom/data/item.js
@@ -1026,8 +1026,11 @@ Zotero.Item.prototype.updateDisplayTitle = function () {
* @returns {String} title for the tab of this item
*/
Zotero.Item.prototype.getTabTitle = async function () {
- if (!this.isAttachment()) {
- throw new Error("Can only get tab title for attachments");
+ if (!this.isAttachment() && !this.isNote()) {
+ throw new Error("Can only get tab title for attachments and notes");
+ }
+ if (this.isNote()) {
+ return this.getDisplayTitle();
}
let type = Zotero.Prefs.get('tabs.title.reader');
let readerTitle = this.getDisplayTitle();
diff --git a/chrome/content/zotero/xpcom/data/notes.js b/chrome/content/zotero/xpcom/data/notes.js
index 0afe0ce09a..bc5cd2ce4a 100644
--- a/chrome/content/zotero/xpcom/data/notes.js
+++ b/chrome/content/zotero/xpcom/data/notes.js
@@ -34,6 +34,164 @@ Zotero.Notes = new function () {
this._editorInstances = [];
this._downloadInProgressPromise = null;
+
+ this.open = async function (itemID, location, { title, tabIndex, tabID, openInBackground, openInWindow, allowDuplicate, preventJumpback, parentItemKey } = {}) {
+ let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
+ let library = Zotero.Libraries.get(libraryID);
+ let win = Zotero.getMainWindow();
+
+ if (!win) {
+ openInWindow = true;
+ }
+
+ await library.waitForDataLoad('item');
+
+ let item = Zotero.Items.get(itemID);
+ if (!item) {
+ throw new Error('Item does not exist');
+ }
+
+ let noteEditor;
+ if (!openInWindow && !allowDuplicate && !this._editorInstances.find(r => r.itemID === itemID)) {
+ if (win) {
+ let existingTabID = win.Zotero_Tabs.getTabIDByItemID(itemID);
+ if (existingTabID) {
+ win.Zotero_Tabs.select(existingTabID, false, { location });
+ return win.Zotero_Tabs.getTabContent(existingTabID).querySelector('note-editor.note-tab');
+ }
+ }
+ }
+
+ if (openInWindow) {
+ noteEditor = this._editorInstances.find(r => r.itemID === itemID && r.viewMode === 'window');
+ }
+ else if (!allowDuplicate) {
+ noteEditor = this._editorInstances.find(r => r.itemID === itemID && r.viewMode === 'tab');
+ }
+
+ if (noteEditor) {
+ if (noteEditor.viewMode === 'tab') {
+ win.Zotero_Tabs.select(noteEditor.tabID, true);
+ }
+
+ if (location) {
+ noteEditor.navigate(location);
+ }
+ }
+ else if (openInWindow) {
+ let name = null;
+
+ if (itemID) {
+ // Create a name for this window so we can focus it later
+ //
+ // Collection is only used on new notes, so we don't need to
+ // include it in the name
+ name = 'zotero-note-' + itemID;
+ }
+
+ let io = { itemID, parentItemKey, location, _initPromise: Zotero.Promise.defer() };
+ Services.ww.openWindow(
+ win,
+ 'chrome://zotero/content/note.xhtml',
+ name,
+ 'chrome,resizable,centerscreen,dialog=no',
+ io
+ );
+ await io._initPromise.promise;
+ noteEditor = io.noteEditor;
+ }
+ else {
+ let id;
+ let container;
+ let select = !openInBackground;
+ if (tabID) {
+ id = tabID;
+ container = win.document.getElementById(tabID);
+ noteEditor = container.querySelector('note-editor.note-tab');
+ }
+ else {
+ ({ id, container } = win.Zotero_Tabs.add({
+ id: tabID,
+ type: 'note-unloaded',
+ title,
+ index: tabIndex,
+ data: {
+ itemID,
+ },
+ select,
+ preventJumpback,
+ }));
+ }
+
+ if (!noteEditor && !openInBackground) {
+ noteEditor = win.document.createXULElement('note-editor');
+ noteEditor.classList.add('note-tab');
+ container.appendChild(noteEditor);
+
+ noteEditor.mode = item.isEditable() ? 'edit' : 'view';
+ noteEditor.viewMode = 'tab';
+ noteEditor.item = item;
+ noteEditor.tabID = id;
+ noteEditor._id('links-container').hidden = true;
+
+ container.addEventListener('tab-bottom-placeholder-resize', (event) => {
+ this.setBottomPlaceholderHeight(noteEditor, event.detail.height);
+ });
+
+ container.addEventListener('tab-context-pane-toggle', (event) => {
+ this.setContextPaneOpen(noteEditor, event.detail.open);
+ });
+
+ container.addEventListener('tab-focus', () => {
+ noteEditor.focus();
+ });
+
+ container.addEventListener('tab-selection-change', (event) => {
+ if (event.detail.selected) {
+ this._updateLayout();
+ }
+ });
+ }
+
+ if (select) {
+ this._updateLayout();
+ }
+ }
+ return noteEditor;
+ };
+
+ this.setBottomPlaceholderHeight = function (noteEditor, height) {
+ noteEditor.setBottomPlaceholderHeight(height);
+ };
+
+ this.toggleSidePane = function (_open) {
+ // TODO: Implement this once the note editor supports side pane
+ };
+
+ this.setSidePaneWidth = function () {
+ // TODO: Implement this once the note editor supports side pane
+ };
+
+ this.setContextPaneOpen = function (noteEditor, open) {
+ noteEditor.setContextPaneOpen(open);
+ };
+
+ this._updateLayout = function () {
+ let { sidePaneState } = Zotero.getMainWindow().ZoteroContextPane.update();
+ this.toggleSidePane(sidePaneState.open);
+ this.setSidePaneWidth(sidePaneState.width);
+ };
+
+ this.getByTabID = function (tabID) {
+ if (!tabID) {
+ return null;
+ }
+ let noteEditor = this._editorInstances.find(x => x._tabID === tabID);
+ if (noteEditor) {
+ return noteEditor;
+ }
+ return null;
+ };
this.noteToTitle = function (text) {
Zotero.debug(`Zotero.Note.noteToTitle() is deprecated -- use Zotero.Utilities.Item.noteToTitle() instead`);
diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js
index 8197e061e3..30b7efee18 100644
--- a/chrome/content/zotero/xpcom/editorInstance.js
+++ b/chrome/content/zotero/xpcom/editorInstance.js
@@ -56,6 +56,7 @@ class EditorInstance {
this._item = options.item;
this._reloaded = options.reloaded;
this._viewMode = options.viewMode;
+ this._tabID = options.tabID;
this._readOnly = options.readOnly || this._isReadOnly();
this._filesReadOnly = !Zotero.Libraries.get(this._item.libraryID).filesEditable;
this._disableUI = options.disableUI;
@@ -73,6 +74,7 @@ class EditorInstance {
});
this._prefObserverIDs = [
Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange),
+ Zotero.Prefs.registerObserver('note.tabFontSize', this._handleFontChange),
Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange),
Zotero.Prefs.registerObserver('note.css', this._handleStyleChange),
Zotero.Prefs.registerObserver('layout.spellcheckDefault', this._handleSpellCheckChange, true)
@@ -244,7 +246,13 @@ class EditorInstance {
}
}
}
-
+
+ if (type === 'item' && ['delete', 'trash'].includes(event) && this._tabID) {
+ if (this._item && (ids.includes(this._item.id) || ids.includes(this._item.parentItemID))) {
+ Zotero.getMainWindow().Zotero_Tabs.close(this._tabID);
+ }
+ }
+
if (this._readOnly || !this._item) {
return;
}
@@ -288,7 +296,14 @@ class EditorInstance {
}
_getFont() {
- let fontSize = Zotero.Prefs.get('note.fontSize');
+ let fontSizePrefKey;
+ if (this._tabID) {
+ fontSizePrefKey = `note.tabFontSize`;
+ }
+ else {
+ fontSizePrefKey = `note.fontSize`;
+ }
+ let fontSize = Zotero.Prefs.get(fontSizePrefKey);
// Fix empty old font prefs before a value was enforced
if (fontSize < 6) {
fontSize = 11;
@@ -317,7 +332,7 @@ class EditorInstance {
catch (e) {
Zotero.logError(e);
}
- }
+ };
_showInLibrary(ids) {
if (!Array.isArray(ids)) {
@@ -330,6 +345,15 @@ class EditorInstance {
}
}
+ setToggleContextPaneButtonMode(mode) {
+ this._postMessage({ action: 'setToggleContextPaneButtonMode', mode });
+ }
+
+ focusToolbar() {
+ this._iframeWindow.focus();
+ this._postMessage({ action: 'focusToolbar' });
+ }
+
async importImages(annotations) {
for (let annotation of annotations) {
if (annotation.image && !this._filesReadOnly) {
@@ -666,6 +690,21 @@ class EditorInstance {
this._openPopup(x, y, pos, itemGroups);
return;
}
+ case 'toggleContextPane': {
+ let win = Zotero.getMainWindow();
+ win.ZoteroContextPane.togglePane();
+ return;
+ }
+ case 'focusBack': {
+ let win = Zotero.getMainWindow();
+ win.Zotero_Tabs.focusBack();
+ return;
+ }
+ case 'focusForward': {
+ let win = Zotero.getMainWindow();
+ win.Zotero_Tabs.focusForward();
+ return;
+ }
case 'return': {
this._onReturn();
return;
diff --git a/chrome/content/zotero/xpcom/prefs.js b/chrome/content/zotero/xpcom/prefs.js
index 7b341ef81f..54d12ac054 100644
--- a/chrome/content/zotero/xpcom/prefs.js
+++ b/chrome/content/zotero/xpcom/prefs.js
@@ -358,6 +358,11 @@ Zotero.Prefs = new function () {
Zotero.Prefs.set('note.fontSize', 11);
}
}],
+ [ "note.tabFontSize", function (val) {
+ if (val < 6) {
+ Zotero.Prefs.set('note.tabFontSize', 11);
+ }
+ }],
[ "sync.autoSync", function (val) {
if (val) {
Zotero.Sync.EventListeners.AutoSyncListener.register();
diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js
index aabbeded68..e493ba0263 100644
--- a/chrome/content/zotero/xpcom/reader.js
+++ b/chrome/content/zotero/xpcom/reader.js
@@ -336,7 +336,7 @@ class ReaderInstance {
else if (this.tabID) {
let win = Zotero.getMainWindow();
if (win) {
- win.Zotero_Tabs.setSecondViewState(this.tabID, state);
+ win.Zotero_Tabs.setTabData(this.tabID, { secondViewState: state });
}
}
},
@@ -536,17 +536,13 @@ class ReaderInstance {
// Shift-tab from the toolbar focuses the sync button (if reader instance is opened in a tab)
if (!this.tabID) return;
let win = Zotero.getMainWindow();
- win.document.getElementById("zotero-tb-sync").focus();
+ win.Zotero_Tabs.focusBack();
},
onIframeTab: () => {
// Tab after the last tabstop will focus the contextPane (if reader instance is opened in a tab)
if (!this.tabID) return;
let win = Zotero.getMainWindow();
- let focused = win.ZoteroContextPane.focus();
- // If context pane wasn't focused (e.g. it's collapsed), focus the tab bar
- if (!focused) {
- win.Zotero_Tabs.moveFocus("current");
- }
+ win.Zotero_Tabs.focusForward();
},
onSetZoom: (iframe, zoom) => {
iframe.browsingContext.textZoom = 1;
@@ -1370,7 +1366,7 @@ class ReaderInstance {
if (this.tabID) {
let win = Zotero.getMainWindow();
if (win) {
- win.Zotero_Tabs.setSecondViewState(this.tabID, this.getSecondViewState());
+ win.Zotero_Tabs.setTabData(this.tabID, { secondViewState: this.getSecondViewState() });
}
}
}
@@ -1425,10 +1421,9 @@ class ReaderTab extends ReaderInstance {
this._contextPaneOpen = options.contextPaneOpen;
this._bottomPlaceholderHeight = options.bottomPlaceholderHeight;
this._showContextPaneToggle = true;
- this._onToggleSidebarCallback = options.onToggleSidebar;
- this._onChangeSidebarWidthCallback = options.onChangeSidebarWidth;
this._window = Services.wm.getMostRecentWindow('navigator:browser');
let existingTabID = options.tabID;
+ let select = !options.background;
// If an unloaded tab for this item already exists, load the reader in it.
// Otherwise, create a new tab
if (existingTabID) {
@@ -1444,7 +1439,7 @@ class ReaderTab extends ReaderInstance {
data: {
itemID: this._item.id
},
- select: !options.background,
+ select,
preventJumpback: options.preventJumpback
});
this.tabID = id;
@@ -1469,7 +1464,49 @@ class ReaderTab extends ReaderInstance {
this._iframe.setAttribute('tooltip', 'html-tooltip');
- this._open({ location: options.location, secondViewState: options.secondViewState });
+ this._onToggleSidebarCallback = (open) => {
+ if (open) {
+ this._window.ZoteroContextPane.updateLayout({ sidePaneWidth: true });
+ }
+ else {
+ this._window.ZoteroContextPane.updateLayout({ sidePaneWidth: false });
+ }
+
+ if (options.onToggleSidebar) {
+ options.onToggleSidebar(open);
+ }
+ };
+
+ this._onChangeSidebarWidthCallback = (width) => {
+ this._window.ZoteroContextPane.updateLayout({ sidePaneWidth: width });
+
+ if (options.onChangeSidebarWidth) {
+ options.onChangeSidebarWidth(width);
+ }
+ };
+
+ this._open({ location: options.location, secondViewState: options.secondViewState }).then(() => {
+ this._tabContainer.addEventListener('tab-bottom-placeholder-resize', (event) => {
+ this.setBottomPlaceholderHeight(event.detail.height);
+ });
+
+ this._tabContainer.addEventListener('tab-context-pane-toggle', (event) => {
+ this.setContextPaneOpen(event.detail.open);
+ });
+
+ this._tabContainer.addEventListener('tab-focus', () => {
+ this.focus();
+ });
+
+ this._tabContainer.addEventListener('tab-selection-change', (event) => {
+ if (event.detail.selected) {
+ this._updateLayout();
+ }
+ });
+ if (select) {
+ this._updateLayout();
+ }
+ });
}
close() {
@@ -1552,6 +1589,12 @@ class ReaderTab extends ReaderInstance {
editorInstance.insertAnnotations(annotations);
}
}
+
+ _updateLayout() {
+ let { sidePaneState } = this._window.ZoteroContextPane.updateLayout();
+ this.toggleSidebar(sidePaneState.open);
+ this.setSidebarWidth(sidePaneState.width);
+ }
}
@@ -2099,62 +2142,14 @@ class Reader {
_loadSidebarState() {
let win = Zotero.getMainWindow();
if (win) {
- let pane = win.document.getElementById('zotero-reader-sidebar-pane');
- this._sidebarOpen = pane.getAttribute('collapsed') == 'false';
- let width = pane.getAttribute('width');
- if (width) {
- this._sidebarWidth = parseInt(width);
+ let state = win.ZoteroContextPane.getSidePaneState('reader');
+ this._sidebarOpen = state.open;
+ if (state.width) {
+ this._sidebarWidth = parseInt(state.width);
}
}
}
-
- _setSidebarState() {
- let win = Zotero.getMainWindow();
- if (win) {
- let pane = win.document.getElementById('zotero-reader-sidebar-pane');
- pane.setAttribute('collapsed', this._sidebarOpen ? 'false' : 'true');
- pane.setAttribute('width', this._sidebarWidth);
- }
- }
- getSidebarOpen() {
- return this._sidebarOpen;
- }
-
- setSidebarWidth(width) {
- this._sidebarWidth = width;
- let readers = this._readers.filter(r => r instanceof ReaderTab);
- for (let reader of readers) {
- reader.setSidebarWidth(width);
- }
- this._setSidebarState();
- }
-
- toggleSidebar(open) {
- this._sidebarOpen = open;
- let readers = this._readers.filter(r => r instanceof ReaderTab);
- for (let reader of readers) {
- reader.toggleSidebar(open);
- }
- this._setSidebarState();
- }
-
- setContextPaneOpen(open) {
- this._contextPaneOpen = open;
- let readers = this._readers.filter(r => r instanceof ReaderTab);
- for (let reader of readers) {
- reader.setContextPaneOpen(open);
- }
- }
-
- setBottomPlaceholderHeight(height) {
- this._bottomPlaceholderHeight = height;
- let readers = this._readers.filter(r => r instanceof ReaderTab);
- for (let reader of readers) {
- reader.setBottomPlaceholderHeight(height);
- }
- }
-
notify(event, type, ids, extraData) {
if (type === 'tab') {
if (event === 'close') {
@@ -2333,22 +2328,18 @@ class Reader {
preventJumpback: preventJumpback,
onToggleSidebar: (open) => {
this._sidebarOpen = open;
- this.toggleSidebar(open);
if (this.onToggleSidebar) {
this.onToggleSidebar(open);
}
},
onChangeSidebarWidth: (width) => {
this._sidebarWidth = width;
- this._debounceSidebarWidthUpdate();
if (this.onChangeSidebarWidth) {
this.onChangeSidebarWidth(width);
}
}
});
this._readers.push(reader);
- // Change tab's type from "reader-unloaded" to "reader" after reader loaded
- win.Zotero_Tabs.markAsLoaded(tabID);
}
if (!openInBackground
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index e8f92151b8..920ecb44bc 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -262,19 +262,8 @@ var ZoteroPane = new function () {
ArrowNext: () => null,
ArrowPrevious: () => null,
Tab: () => {
- if (Zotero_Tabs.selectedIndex > 0) {
- let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
- if (reader) {
- // Move focus to the reader and focus the toolbar
- reader.focusFirst();
- reader.focusToolbar();
- }
- return null;
- }
- if (collectionsPane.getAttribute("collapsed")) {
- return document.getElementById('zotero-tb-add');
- }
- return document.getElementById('zotero-tb-collection-add');
+ Zotero_Tabs.focusContent();
+ return null;
},
ShiftTab: () => document.getElementById('zotero-tb-sync-error')
},
@@ -3183,6 +3172,14 @@ var ZoteroPane = new function () {
}
return [];
}
+ case 'note': {
+ let tab = Zotero_Tabs.getTabInfo(Zotero_Tabs.selectedID);
+ if (tab) {
+ let item = Zotero.Items.get(tab.data.itemID);
+ return asIDs ? [item.id] : [item];
+ }
+ return [];
+ }
default:
return [];
}
@@ -4533,10 +4530,10 @@ var ZoteroPane = new function () {
// TODO: _text_
var c = this.getSelectedCollection();
if (c) {
- this.openNoteWindow(null, c.id, parentKey);
+ this.openNote(null, { col: c.id, parentKey });
}
else {
- this.openNoteWindow(null, null, parentKey);
+ this.openNote(null, { parentKey });
}
return null;
}
@@ -4628,34 +4625,32 @@ var ZoteroPane = new function () {
};
+ this.openNote = function (itemID, options = {
+ col: undefined,
+ parentKey: undefined,
+ openInWindow: undefined
+ }) {
+ let {
+ col,
+ parentKey,
+ openInWindow,
+ } = options;
+ if (openInWindow === undefined) {
+ openInWindow = Zotero.Prefs.get('openNoteInNewWindow');
+ }
+
+ return Zotero.Notes.open(itemID, {}, {
+ openInWindow,
+ });
+ };
+
+ /**
+ * Opens a note in a new window
+ * @deprecated - use openNote() with openInWindow option
+ */
this.openNoteWindow = function (itemID, col, parentKey) {
- var item = Zotero.Items.get(itemID);
- var type = Zotero.Libraries.get(item.libraryID).libraryType;
- if (!this.canEdit()) {
- this.displayCannotEditLibraryMessage();
- return;
- }
-
- var name = null;
-
- if (itemID) {
- let w = this.findNoteWindow(itemID);
- if (w) {
- w.focus();
- return;
- }
-
- // Create a name for this window so we can focus it later
- //
- // Collection is only used on new notes, so we don't need to
- // include it in the name
- name = 'zotero-note-' + itemID;
- }
-
- var io = { itemID: itemID, collectionID: col, parentItemKey: parentKey };
- window.openDialog('chrome://zotero/content/note.xhtml', name, 'chrome,resizable,centerscreen,dialog=false', io);
- }
-
+ return this.openNote(itemID, { col, parentKey, openInWindow: true });
+ };
this.findNoteWindow = function (itemID) {
var name = 'zotero-note-' + itemID;
@@ -4976,14 +4971,15 @@ var ZoteroPane = new function () {
};
- this.viewItems = async function (items, event) {
+ this.viewItems = async function (items, event, options = {}) {
+ let { noLocateOnMissing } = options;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.isRegularItem()) {
// Prefer local file attachments
let attachment = await item.getBestAttachment();
if (attachment) {
- await this.viewAttachment(attachment.id, event);
+ await this.viewAttachment(attachment.id, event, noLocateOnMissing, options);
continue;
}
@@ -5017,13 +5013,17 @@ var ZoteroPane = new function () {
if (!this.collectionsView.editable) {
continue;
}
- ZoteroPane.openNoteWindow(item.id);
+ let openInWindow = event?.shiftKey || options.forceAlternateWindowBehavior;
+ ZoteroPane.openNote(item.id, { openInWindow });
}
else if (item.isAttachment()) {
- await this.viewAttachment(item.id, event);
+ await this.viewAttachment(item.id, event, noLocateOnMissing, options);
}
else if (item.isAnnotation()) {
- this.viewAttachment(item.parentItemID, event, false, { location: { annotationID: item.key } });
+ this.viewAttachment(item.parentItemID, event, false,
+ Object.assign(
+ { location: { annotationID: item.key } }, options
+ ));
}
}
};
@@ -5472,20 +5472,22 @@ var ZoteroPane = new function () {
this.canEdit = function (row) {
switch (Zotero_Tabs.selectedType) {
case 'library':
+ {
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
}
return this.collectionsView.getRow(row).editable;
- case 'reader': {
- let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
- if (!itemID) {
- throw new Error('Reader tab has no itemID');
- }
- return Zotero.Items.get(itemID).library.editable;
}
default:
+ {
+ let tabInfo = Zotero_Tabs.getTabInfo();
+ let item = Zotero.Items.get(tabInfo.data?.itemID);
+ if (item) {
+ return item.isEditable();
+ }
return false;
+ }
}
};
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 05f44865e2..46d3e3ef1b 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -622,8 +622,7 @@
-