Add note as tab (#5528)

This commit is contained in:
windingwind 2025-12-19 05:49:42 +01:00 committed by GitHub
parent f1c7b7939c
commit e522f6185e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1863 additions and 379 deletions

View file

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

View file

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

View file

@ -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'],

View file

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

View file

@ -64,6 +64,8 @@
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" data-pane="notes"/>
<note-box id="zotero-note-box" data-pane="note-info" hidden="true"/>
<attachment-box id="zotero-attachment-box" data-pane="attachment-info" data-use-preview="true" hidden="true"/>
<attachment-annotations-box id="zotero-editpane-attachment-annotations" data-pane="attachment-annotations" hidden="true"/>
@ -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);

View file

@ -43,7 +43,7 @@
<annotation-items-pane id="zotero-annotations-pane" />
</deck>
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
<item-pane-sidenav id="zotero-view-item-sidenav" no-context-notes="true" class="zotero-view-item-sidenav"/>
`);
init() {

View file

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

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
{
let { countWords } = ChromeUtils.importESModule("resource://zotero/allfaz.mjs").default;
class NoteBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-note-info" data-pane="note-info">
<html:div class="body">
<html:div class="metadata-table">
<html:div id="parentItemRow" class="meta-row">
<html:div class="meta-label"><html:label id="parentItem-label" class="key" data-l10n-id="note-info-parent-item"/></html:div>
<html:div class="meta-data clicky-item" tabindex="0"><html:span id="parentItem" class="clicky-text" data-l10n-id="note-info-parent-item-button" aria-labelledby="parentItem-label"></html:span></html:div>
</html:div>
<html:div id="wordCountRow" class="meta-row">
<html:div class="meta-label"><html:label id="wordCount-label" class="key" data-l10n-id="note-info-word-count"/></html:div>
<html:div class="meta-data"><editable-text id="wordCount" aria-labelledby="wordCount-label" nowrap="true" tight="true" readonly="true"/></html:div>
</html:div>
<html:div id="dateCreatedRow" class="meta-row">
<html:div class="meta-label"><html:label id="dateCreated-label" class="key" data-l10n-id="note-info-date-created"/></html:div>
<html:div class="meta-data"><editable-text id="dateCreated" aria-labelledby="dateCreated-label" nowrap="true" tight="true" readonly="true"/></html:div>
</html:div>
<html:div id="dateModifiedRow" class="meta-row">
<html:div class="meta-label"><html:label id="dateModified-label" class="key" data-l10n-id="note-info-date-modified"/></html:div>
<html:div class="meta-data"><editable-text id="dateModified" aria-labelledby="dateModified-label" nowrap="true" tight="true" readonly="true"/></html:div>
</html:div>
</html:div>
</html:div>
</collapsible-section>
`);
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);
}

View file

@ -38,6 +38,8 @@
this._initialized = false;
this._editorInstance = null;
this._destroyed = false;
this._bottomPlaceholder = null;
this._contextPaneOpen = null;
this.content = MozXULElement.parseXULToFragment(`
<html:div class="custom-head empty"></html:div>
@ -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) {

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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);
};

View file

@ -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 <tab/window>"
*
* 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

View file

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

View file

@ -266,6 +266,15 @@
native="true"
/>
</groupbox>
<groupbox id="zotero-prefpane-note-groupbox" aria-labelledby="preferences-note-title">
<label><html:h2 id="preferences-note-title" data-l10n-id="preferences-note-title"/></label>
<checkbox id="open-note-in-new-window"
data-l10n-id="preferences-note-open-in-new-window"
preference="extensions.zotero.openNoteInNewWindow"
native="true"
/>
</groupbox>
<groupbox id="zotero-prefpane-locate-groupbox" aria-label="&zotero.preferences.prefpane.locate;" aria-describedby="preferences-locate-library-lookup-intro">
<label><html:h2>&zotero.preferences.prefpane.locate;</html:h2></label>

View file

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

View file

@ -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 <window>
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',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -622,8 +622,7 @@
<menuitem id="view-menuitem-font-size-reset" label="&zotero.general.reset;"/>
</menupopup>
</menu>
<menu id="note-font-size-menu"
label="&noteFontSize.label;">
<menu id="note-font-size-menu" data-l10n-id="menu-view-note-font-size">
<!-- TODO: Maybe switch to Bigger/Smaller once we can update without restarting -->
<!--<menupopup oncommand="ZoteroStandalone.onViewMenuItemClick(event)">
<menuitem id="view-menuitem-note-font-size-bigger" label="&zotero.general.bigger;"/>
@ -651,6 +650,28 @@
oncommand="ZoteroStandalone.onViewMenuItemClick(event); event.stopPropagation();"/>
</menupopup>
</menu>
<menu id="note-tab-font-size-menu" data-l10n-id="menu-view-note-tab-font-size">
<menupopup oncommand="ZoteroStandalone.updateNoteTabFontSize(event)">
<menuitem label="11" type="checkbox"/>
<menuitem label="12" type="checkbox"/>
<menuitem label="13" type="checkbox"/>
<menuitem label="14" type="checkbox"/>
<menuitem label="15" type="checkbox"/>
<menuitem label="16" type="checkbox"/>
<menuitem label="18" type="checkbox"/>
<menuitem label="24" type="checkbox"/>
<menuitem label="36" type="checkbox"/>
<menuitem label="48" type="checkbox"/>
<menuitem label="64" type="checkbox"/>
<menuitem label="72" type="checkbox"/>
<menuitem label="96" type="checkbox"/>
<menuseparator/>
<menuitem
id="view-menuitem-note-tab-font-size-reset"
label="&zotero.general.reset;"
oncommand="ZoteroStandalone.onViewMenuItemClick(event); event.stopPropagation();"/>
</menupopup>
</menu>
<menuseparator class="menu-type-library" />
<menu id="column-picker-submenu"
class="menu-type-library"
@ -1070,7 +1091,7 @@
<label>&zotero.general.loading;</label>
</div>
<deck id="tabs-deck">
<vbox id="zotero-pane"
<tab-content id="zotero-pane"
onkeydown="ZoteroPane_Local.handleKeyDown(event, this.id)"
onkeyup="ZoteroPane_Local.handleKeyUp(event, this.id)"
onkeypress="ZoteroPane_Local.handleKeyPress(event)">
@ -1346,7 +1367,7 @@
<item-pane id="zotero-item-pane" zotero-persist="width height"/>
</box>
</hbox>
</vbox>
</tab-content>
</deck>
<splitter id="zotero-context-splitter"

View file

@ -63,3 +63,4 @@ note-editor-delete-column = Delete Column
note-editor-delete-table = Delete Table
note-editor-link-popup-appeared = Link popup appeared. Use Shift-Tab to navigate it.
note-editor-citation-popup-appeared = Citation popup appeared. Use Shift-Tab to navigate it.
note-editor-toggle-context-pane = Toggle Context Pane

View file

@ -50,6 +50,10 @@ preferences-reader-ebook-font = Ebook font:
preferences-reader-ebook-hyphenate =
.label = Enable automatic hyphenation
preferences-note-title = Notes
preferences-note-open-in-new-window =
.label = Open notes in new windows instead of tabs
preferences-color-scheme = Color Scheme:
preferences-color-scheme-auto =
.label = Automatic

View file

@ -134,6 +134,12 @@ menu-view-columns-move-left =
menu-view-columns-move-right =
.label = Move Column Right
menu-view-note-font-size =
.label = Note Font Size
menu-view-note-tab-font-size =
.label = Note Tab Font Size
menu-show-tabs-menu =
.label = Show Tabs Menu
@ -189,12 +195,14 @@ item-menu-viewAttachment =
[pdf] PDF
[epub] EPUB
[snapshot] Snapshot
[note] Note
*[other] Attachment
}
*[other] { $attachmentType ->
[pdf] PDFs
[epub] EPUBs
[snapshot] Snapshots
[note] Notes
*[other] Attachments
}
} {
@ -488,6 +496,7 @@ pane-info = Info
pane-abstract = Abstract
pane-attachments = Attachments
pane-notes = Notes
pane-note-info = Note Info
pane-libraries-collections = Libraries and Collections
pane-tags = Tags
pane-related = Related
@ -563,6 +572,8 @@ sidenav-attachments =
.tooltiptext = { pane-attachments }
sidenav-notes =
.tooltiptext = { pane-notes }
sidenav-note-info =
.tooltiptext = { pane-note-info }
sidenav-attachment-info =
.tooltiptext = { pane-attachment-info }
sidenav-attachment-preview =
@ -643,6 +654,29 @@ attachment-info-convert-note =
} Note
.tooltiptext = Adding notes to attachments is no longer supported, but you can edit this note by migrating it to a separate note.
section-note-info =
.label = { pane-note-info }
note-info-title = Title
note-info-parent-item = Parent Item
note-info-parent-item-button = {
$hasParentItem ->
[true] { $parentItemTitle }
*[false] None
}
.title = {
$hasParentItem ->
[true] View parent item in library
*[false] View note item in library
}
note-info-date-created = Created
note-info-date-modified = Modified
note-info-size = Size
note-info-word-count = Word Count
note-info-character-count = Character Count
item-title-empty-note = Untitled Note
attachment-preview-placeholder = No attachment to preview
attachment-rename-from-parent =
.tooltiptext = Rename File to Match Parent Item

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7.25684C12.989 7.25296 12.9778 7.24989 12.9668 7.24609C14.7316 7.85452 16 9.52847 16 11.5C16 13.9853 13.9853 16 11.5 16C10.4288 16 9.44558 15.625 8.67285 15H5.29297L1 10.707V0H13V7.25684ZM11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8ZM2 10H6V14H7.75879C7.27996 13.2849 7 12.4253 7 11.5C7 9.01472 9.01472 7 11.5 7C11.669 7 11.8358 7.0092 12 7.02734V3H2V10ZM12 14H11V11H12V14ZM5 13.293V11H2.70703L5 13.293ZM11.5 9C11.8452 9 12.125 9.27982 12.125 9.625C12.125 9.97018 11.8452 10.25 11.5 10.25C11.1548 10.25 10.875 9.97018 10.875 9.625C10.875 9.27982 11.1548 9 11.5 9ZM2 2H12V1H2V2Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7.25684C12.989 7.25296 12.9778 7.24989 12.9668 7.24609C14.7316 7.85452 16 9.52847 16 11.5C16 13.9853 13.9853 16 11.5 16C10.4288 16 9.44558 15.625 8.67285 15H5.29297L1 10.707V0H13V7.25684ZM11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8ZM2 10H6V14H7.75879C7.27996 13.2849 7 12.4253 7 11.5C7 9.01472 9.01472 7 11.5 7C11.669 7 11.8358 7.0092 12 7.02734V3H2V10ZM12 14H11V11H12V14ZM5 13.293V11H2.70703L5 13.293ZM11.5 9C11.8452 9 12.125 9.27982 12.125 9.625C12.125 9.97018 11.8452 10.25 11.5 10.25C11.1548 10.25 10.875 9.97018 10.875 9.625C10.875 9.27982 11.1548 9 11.5 9ZM2 2H12V1H2V2Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 11.5674L18.1426 11.6689C19.2702 12.5124 20 13.8585 20 15.375C20 17.9293 17.9293 20 15.375 20C14.29 20 13.2928 19.6256 12.5039 19H8.36621L8.18262 18.8174L3.18262 13.8174L3 13.6338V1H18V11.5674ZM15.375 12C13.511 12 12 13.511 12 15.375C12 17.239 13.511 18.75 15.375 18.75C17.239 18.75 18.75 17.239 18.75 15.375C18.75 13.511 17.239 12 15.375 12ZM4.25 12.75H9.25V17.75H11.4062C10.9899 17.0557 10.75 16.2435 10.75 15.375C10.75 12.8207 12.8207 10.75 15.375 10.75C15.8538 10.75 16.3156 10.8229 16.75 10.958V5H4.25V12.75ZM16 17.75H14.75V15H16V17.75ZM8 16.8652V14H5.13477L8 16.8652ZM15.375 13C15.7892 13 16.125 13.3358 16.125 13.75C16.125 14.1642 15.7892 14.5 15.375 14.5C14.9608 14.5 14.625 14.1642 14.625 13.75C14.625 13.3358 14.9608 13 15.375 13ZM4.25 3.75H16.75V2.25H4.25V3.75Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 911 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 13.6338L15.8174 13.8174L10.8174 18.8174L10.6338 19H2V1H16V13.6338ZM18.25 14.6338L18.0674 14.8174L13.8848 19H12.1152L17 14.1152V2H18.25V14.6338ZM3.25 17.75H9.75V12.75H14.75V5H3.25V17.75ZM11 16.8652L13.8652 14H11V16.8652ZM3.25 3.75H14.75V2.25H3.25V3.75Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 391 B

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 13.8838L17.8174 14.0674L13.0674 18.8174L12.8838 19H5V3.75H18V13.8838ZM6.25 7.25V17.75H12V13H16.75V7.25H6.25ZM13.25 16.8652L15.8652 14.25H13.25V16.8652ZM6.25 6H16.75V5H6.25V6Z" fill="context-fill"/>
<path d="M14.4854 2.24512L2.78027 3.26953L4 17.2158L2.75488 17.3252L1.42578 2.13281L14.376 1L14.4854 2.24512Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View file

@ -1,5 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="note">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M3.625 1H3V1.625V3.75V5V18.375V19H3.625H12.375H12.6339L12.8169 18.8169L17.8169 13.8169L18 13.6339V13.375V1.625V1H17.375H3.625ZM4.25 5V17.75H11.75V13.375V12.75H12.375H16.75V5H4.25ZM16.75 3.75V2.25H4.25V3.75H16.75ZM13 14H15.8661L13 16.8661V14Z" fill="context-fill"/>
</g>
<path d="M16.0002 2.99988V19.2499H7.11639L6.9328 19.0673L1.9328 14.0673L1.75018 13.8837V2.99988H16.0002ZM3.00018 6.99988V12.9999H8.00018V17.9999H14.7502V6.99988H3.00018ZM3.88495 14.2499L6.75018 17.1151V14.2499H3.88495ZM3.00018 4.24988V5.74988H14.7502V4.24988H3.00018Z" fill="context-fill"/>
<path d="M5 1.375H17.625V17" stroke="context-fill" stroke-width="1.25"/>
</svg>

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 467 B

View file

@ -111,6 +111,7 @@ pref("extensions.zotero.search.useLeftBound", true);
// Notes
pref("extensions.zotero.note.fontFamily", "-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Helvetica, Arial, sans-serif");
pref("extensions.zotero.note.fontSize", "14");
pref("extensions.zotero.note.tabFontSize", "16");
pref("extensions.zotero.note.css", "");
pref("extensions.zotero.note.smartQuotes", true);
@ -202,6 +203,8 @@ pref("extensions.zotero.fileHandler.epub", "");
pref("extensions.zotero.fileHandler.snapshot", "");
pref("extensions.zotero.openReaderInNewWindow", false);
pref("extensions.zotero.openNoteInNewWindow", false);
// File/URL opening executable if launch() fails
pref("extensions.zotero.fallbackLauncher.unix", "/usr/bin/xdg-open");
pref("extensions.zotero.fallbackLauncher.windows", "");

@ -1 +1 @@
Subproject commit 1f1e9bfca8d7640f321046693616034d702c8151
Subproject commit e3a088dfce14fa08c357bcd13c2220528d20779b

206
resource/allfaz.mjs Normal file
View file

@ -0,0 +1,206 @@
/*!
Copyright (c) 2025 Abdullah Atta.
Licensed under the MIT License (MIT), see
https://github.com/thecodrr/alfaaz
Build on commit a132fdb
*/
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// dist/languages/burmese.js
var require_burmese = __commonJS({
"dist/languages/burmese.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BURMESE_UNICODE_RANGE = void 0;
exports.BURMESE_UNICODE_RANGE = [[4096, 4255]];
}
});
// dist/languages/cjk.js
var require_cjk = __commonJS({
"dist/languages/cjk.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CJK_UNICODE_RANGES = void 0;
exports.CJK_UNICODE_RANGES = [
[19968, 40959],
[13312, 19903],
[131072, 173791],
[173824, 177983],
[177984, 178207],
[178208, 183983],
[183984, 191471],
[196608, 201551],
[201552, 205743],
[63744, 64255],
[194560, 195103],
[12032, 12255],
[11904, 12031],
[12288, 12351],
[13056, 13311],
[65072, 65103]
// CJK Compatibility Forms FE30-FE4F
];
}
});
// dist/languages/javanese.js
var require_javanese = __commonJS({
"dist/languages/javanese.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JAVANESE_UNICODE_RANGE = void 0;
exports.JAVANESE_UNICODE_RANGE = [[43392, 43487]];
}
});
// dist/languages/khmer.js
var require_khmer = __commonJS({
"dist/languages/khmer.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KHMER_UNICODE_RANGE = void 0;
exports.KHMER_UNICODE_RANGE = [[6016, 6143]];
}
});
// dist/languages/lao.js
var require_lao = __commonJS({
"dist/languages/lao.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LAO_UNICODE_RANGE = void 0;
exports.LAO_UNICODE_RANGE = [[3712, 3839]];
}
});
// dist/languages/thai.js
var require_thai = __commonJS({
"dist/languages/thai.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.THAI_UNICODE_RANGE = void 0;
exports.THAI_UNICODE_RANGE = [[3584, 3711]];
}
});
// dist/languages/vai.js
var require_vai = __commonJS({
"dist/languages/vai.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VAI_UNICODE_RANGE = void 0;
exports.VAI_UNICODE_RANGE = [[42240, 42559]];
}
});
// dist/languages/index.js
var require_languages = __commonJS({
"dist/languages/index.js"(exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UNICODE_RANGES = void 0;
var burmese_1 = require_burmese();
var cjk_1 = require_cjk();
var javanese_1 = require_javanese();
var khmer_1 = require_khmer();
var lao_1 = require_lao();
var thai_1 = require_thai();
var vai_1 = require_vai();
exports.UNICODE_RANGES = [
...thai_1.THAI_UNICODE_RANGE,
...lao_1.LAO_UNICODE_RANGE,
...burmese_1.BURMESE_UNICODE_RANGE,
...khmer_1.KHMER_UNICODE_RANGE,
...javanese_1.JAVANESE_UNICODE_RANGE,
...vai_1.VAI_UNICODE_RANGE,
...cjk_1.CJK_UNICODE_RANGES
];
}
});
// dist/index.js
var require_index = __commonJS({
"dist/index.js"(exports) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.countLines = exports.countWords = void 0;
var languages_1 = require_languages();
var CHINESE_MAX_CODE_POINT = 205743;
var BYTE_SIZE = 8;
var BITMAP = new Uint8Array(CHINESE_MAX_CODE_POINT / BYTE_SIZE + 1);
function insertCharsIntoMap(...chars) {
for (const char of chars) {
const charCode = char.charCodeAt(0);
const byteIndex = Math.floor(charCode / BYTE_SIZE);
const bitIndex = charCode % BYTE_SIZE;
BITMAP[byteIndex] = BITMAP[byteIndex] ^ 1 << bitIndex;
}
}
function insertRangeIntoMap(from, to) {
for (let i = from / BYTE_SIZE; i < Math.ceil(to / BYTE_SIZE); i++) {
BITMAP[i] = 255;
}
}
var NEWLINE = "\n";
insertCharsIntoMap(
" ",
"\n",
" ",
"\v",
"*",
"/",
"&",
":",
";",
".",
",",
"?",
"=",
"\u0F0B",
// Tibetan uses [U+0F0B TIBETAN MARK INTERSYLLABIC TSHEG] (pronounced tsek) to signal the end of a syllable.
"\u1361",
// Ethiopic text uses the traditional wordspace character [U+1361 ETHIOPIC WORDSPACE] to indicate word boundaries
"\u200B"
// ZERO-WIDTH-SPACE can also be considered a word boundary
);
for (const range of languages_1.UNICODE_RANGES) {
insertRangeIntoMap(range[0], range[1]);
}
function countWords(str) {
let count = 0;
let shouldCount = false;
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
const byteIndex = charCode / BYTE_SIZE | 0;
const bitIndex = charCode % BYTE_SIZE;
const byteAtIndex = BITMAP[byteIndex];
const isMatch = (byteAtIndex >> bitIndex & 1) === 1;
if (isMatch && (shouldCount || byteAtIndex === 255)) count++;
shouldCount = !isMatch;
}
if (shouldCount) count++;
return count;
}
exports.countWords = countWords;
function countLines(str) {
let count = 0;
for (let i = -1; (i = str.indexOf(NEWLINE, ++i)) !== -1 && i < str.length; count++) ;
count++;
return count;
}
exports.countLines = countLines;
}
});
export default require_index();

View file

@ -83,6 +83,7 @@
@import "elements/colorPicker";
@import "elements/guidancePanel";
@import "elements/infoBox";
@import "elements/noteBox";
@import "elements/noteEditor";
@import "elements/notesBox";
@import "elements/publicationsLicenseInfo";

View file

@ -88,6 +88,7 @@ $item-pane-sections: (
"abstract": var(--accent-azure),
"attachments": var(--accent-green),
"notes": var(--accent-yellow),
"note-info": var(--accent-yellow),
"attachment-info": var(--accent-green),
"attachment-preview": #926d70,
"attachment-annotations": var(--tag-purple),
@ -119,3 +120,4 @@ $width-sidenav: 37px;
$height-toolbar: 41px;
$min-height-items-pane: 150px;
$min-height-tab-context: 300px;

View file

@ -32,7 +32,7 @@
}
.stacked-context-placeholder {
min-height: 300px;
min-height: $min-height-tab-context - $width-sidenav;
}
&.standard .stacked-context-placeholder {

View file

@ -22,6 +22,7 @@
#zotero-pane {
min-width: 0;
min-height: 0;
flex-direction: column;
}
#zotero-pane-stack > hbox {

View file

@ -0,0 +1,41 @@
note-box {
display: flex;
flex-direction: column;
&[hidden] {
display: none;
}
.body {
display: flex;
flex-direction: column;
gap: 2px;
}
.metadata-table {
@include meta-table;
.clicky-item {
@include clicky-item;
@include focus-ring;
align-items: center;
padding: 0;
box-sizing: border-box;
min-height: 20px;
@include comfortable {
min-height: 22px;
}
.clicky-text {
width: 100%;
--editable-text-padding-inline: var(--editable-text-tight-padding-inline);
--editable-text-padding-block: var(--editable-text-tight-padding-block);
padding-inline: var(--editable-text-padding-inline);
padding-block: var(--editable-text-padding-block);
border: 1px solid rgba(0,0,0,0);
}
}
}
}

View file

@ -5,6 +5,12 @@ note-editor {
@include elements-custom-head;
}
tab-content {
note-editor.note-tab {
width: 100%;
}
}
links-box {
display: flex;
flex-direction: column;

55
test/tests/noteTabTest.js Normal file
View file

@ -0,0 +1,55 @@
describe("Note Tab", function () {
var win, doc, ZoteroPane, Zotero_Tabs, ZoteroContextPane;
before(async function () {
win = await loadZoteroPane();
doc = win.document;
ZoteroPane = win.ZoteroPane;
Zotero_Tabs = win.Zotero_Tabs;
ZoteroContextPane = win.ZoteroContextPane;
});
after(function () {
Zotero_Tabs.closeAll();
win.close();
});
describe("Note Tab Operations", function () {
beforeEach(function () {
// Reset the state before each test
Zotero_Tabs.closeAll();
});
it("should open note in tab", async function () {
let item = new Zotero.Item('note');
item.setNote('This is a test note.');
await item.saveTx();
let noteEditor = await Zotero.Notes.open(item.id);
assert.isNotNull(noteEditor, "Note editor should be opened");
assert.equal(noteEditor.item.id, item.id, "Note editor should be associated with the correct item");
let sameNoteEditor = await Zotero.Notes.open(item.id, undefined, {
tabID: Zotero_Tabs.selectedID,
});
assert.equal(noteEditor, sameNoteEditor, "Opening the same note should return the existing editor");
let duplicateNoteEditor = await Zotero.Notes.open(item.id, undefined, {
allowDuplicate: true,
});
assert.isNotNull(duplicateNoteEditor, "Duplicate note editor should be opened");
assert.notEqual(noteEditor, duplicateNoteEditor, "Duplicate note editor should be a new instance");
assert.equal(duplicateNoteEditor.item.id, item.id, "Duplicate note editor should be associated with the correct item");
Zotero_Tabs.closeAll();
await Zotero.Notes.open(item.id, undefined, {
openInBackground: true,
});
assert.equal(Zotero_Tabs.selectedType, 'library', "Tab should be opened in background");
});
});
});