CEify itemPane

This commit is contained in:
windingwind 2024-01-30 18:43:16 +08:00 committed by Dan Stillman
parent 827bcd704d
commit b2ad17d604
45 changed files with 2081 additions and 1637 deletions

View file

@ -23,6 +23,7 @@
"XRegExp": false,
"XULElement": false,
"XULElementBase": false,
"ItemPaneSectionElementBase": false,
"Cu": false,
"ChromeWorker": false,
"Localization": false,
@ -31,7 +32,6 @@
"ZoteroPane_Local": false,
"ZoteroPane": false,
"Zotero_Tabs": false,
"ZoteroItemPane": false,
"IOUtils": false,
"NetUtil": false,
"FileUtils": false,

View file

@ -1,15 +0,0 @@
#zotero-feed-item-toggleRead-button {
margin: 5px 0 3px 6px;
}
#zotero-feed-item-addTo-button {
margin: 5px 6px 3px;
padding-left: 8px;
}
/* Show duplicates date list item as selected even when not focused
(default behavior on other platforms) */
#zotero-duplicates-merge-original-date:not(:focus) > richlistitem[selected="true"] {
background-color: -moz-cellhighlight;
color: -moz-cellhighlighttext;
}

View file

@ -17,11 +17,6 @@ tree {
padding-left: 0.05em;
}
#zotero-item-pane-groupbox {
-moz-appearance: none !important;
border-width: 0;
}
.zotero-editpane-item-box > scrollbox, .zotero-view-item > tabpanel > vbox,
#zotero-editpane-tags > scrollbox, .zotero-editpane-related {
padding-top: 5px;

View file

@ -82,12 +82,12 @@ var ZoteroContextPane = new function () {
window.addEventListener('resize', _update);
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
Zotero.Reader.onToggleSidebar = _updatePaneWidth;
_contextPaneInner.addEventListener("keypress", ZoteroItemPane.handleKeypress);
_contextPaneInner.addEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress);
};
this.destroy = function () {
window.removeEventListener('resize', _update);
_contextPaneInner.removeEventListener("keypress", ZoteroItemPane.handleKeypress);
_contextPaneInner.removeEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress);
Zotero.Notifier.unregisterObserver(this._notifierID);
Zotero.Reader.onChangeSidebarWidth = () => {};
Zotero.Reader.onToggleSidebar = () => {};
@ -227,10 +227,6 @@ var ZoteroContextPane = new function () {
_selectItemContext(ids[0]);
_update();
// When a loaded tab is selected, scroll to the pinned pane, if any
if (_sidenav.pinnedPane) {
_sidenav.scrollToPane(_sidenav.pinnedPane, 'instant');
}
}
}
};
@ -265,10 +261,12 @@ var ZoteroContextPane = new function () {
}
}
}
return null;
}
function _focus() {
var splitter;
let node;
if (Zotero.Prefs.get('layout') == 'stacked') {
splitter = _contextPaneSplitterStacked;
}
@ -284,7 +282,7 @@ var ZoteroContextPane = new function () {
return true;
}
else {
var node = _notesPaneDeck.selectedPanel;
node = _notesPaneDeck.selectedPanel;
if (node.selectedIndex == 0) {
node.querySelector('search-textbox').focus();
return true;
@ -375,12 +373,13 @@ var ZoteroContextPane = new function () {
_updatePaneWidth();
_updateAddToNote();
_sidenav.showPendingPane();
_sidenav.container?.render();
}
function _togglePane() {
var splitter = Zotero.Prefs.get('layout') == 'stacked'
? _contextPaneSplitterStacked : _contextPaneSplitter;
? _contextPaneSplitterStacked
: _contextPaneSplitter;
var open = true;
if (splitter.getAttribute('state') != 'collapsed') {
@ -396,6 +395,7 @@ var ZoteroContextPane = new function () {
if (reader) {
return Zotero.Items.get(reader.itemID);
}
return null;
}
function _addNotesContext(libraryID) {
@ -522,7 +522,7 @@ var ZoteroContextPane = new function () {
if (item) {
document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-list-popup');
let handleCommand = (event) => _handleListPopupClick(id, event);
let handleCommand = event => _handleListPopupClick(id, event);
popup.addEventListener('popupshowing', () => {
popup.addEventListener('command', handleCommand, { once: true });
popup.addEventListener('popuphiding', () => {
@ -549,7 +549,8 @@ var ZoteroContextPane = new function () {
function _isVisible() {
let splitter = Zotero.Prefs.get('layout') == 'stacked'
? _contextPaneSplitterStacked : _contextPaneSplitter;
? _contextPaneSplitterStacked
: _contextPaneSplitter;
return Zotero_Tabs.selectedID != 'zotero-pane'
&& _panesDeck.selectedIndex == 1
@ -604,7 +605,7 @@ var ZoteroContextPane = new function () {
for (let cachedNote of context.cachedNotes) {
cachedNotesIndex.set(cachedNote.id, cachedNote);
}
notes = notes.map(note => {
notes = notes.map((note) => {
var parentItem = note.parentItem;
// If neither note nor parent item is affected try to return the cached note
if (!context.affectedIDs.has(note.id)
@ -760,8 +761,9 @@ var ZoteroContextPane = new function () {
var tabNotesDeck = _notesPaneDeck.selectedPanel.querySelector('.zotero-context-pane-tab-notes-deck');
var parentTitleContainer;
let vbox;
if (isChild) {
var vbox = document.createXULElement('vbox');
vbox = document.createXULElement('vbox');
vbox.setAttribute('data-tab-id', Zotero_Tabs.selectedID);
vbox.style.display = 'flex';
@ -821,24 +823,16 @@ var ZoteroContextPane = new function () {
}
function _selectItemContext(tabID) {
let previousPinnedPane = _sidenav.container?.pinnedPane || "";
let selectedPanel = Array.from(_itemPaneDeck.children).find(x => x.id == tabID + '-context');
if (selectedPanel) {
_itemPaneDeck.selectedPanel = selectedPanel;
let div = selectedPanel.querySelector('.zotero-view-item');
// _addItemContext() awaits, so the div may not have been created yet. We'll set _sidenav.container
// below even if we don't set it here.
if (div) {
_sidenav.container = div;
}
selectedPanel.sidenav = _sidenav;
if (previousPinnedPane) selectedPanel.pinnedPane = previousPinnedPane;
}
}
async function _addItemContext(tabID, itemID) {
var container = document.createXULElement('vbox');
container.id = tabID + '-context';
container.className = 'zotero-item-pane-content';
_itemPaneDeck.appendChild(container);
var { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
var library = Zotero.Libraries.get(libraryID);
await library.waitForDataLoad('item');
@ -860,85 +854,21 @@ var ZoteroContextPane = new function () {
};
_itemContexts.push(context);
let previousPinnedPane = _sidenav.container?.pinnedPane || "";
let targetItem = parentID ? Zotero.Items.get(parentID) : item;
// Dynamically create item pane tabs and panels as in zoteroPane.xhtml.
// Keep the code below in sync with zoteroPane.xhtml
let itemDetails = document.createXULElement('item-details');
itemDetails.id = tabID + '-context';
itemDetails.className = 'zotero-item-pane-content';
_itemPaneDeck.appendChild(itemDetails);
// hbox
var hbox = document.createXULElement('hbox');
hbox.setAttribute('flex', '1');
hbox.className = 'zotero-view-item-container';
container.append(hbox);
itemDetails.mode = readOnly ? "view" : null;
itemDetails.item = targetItem;
itemDetails.sidenav = _sidenav;
if (previousPinnedPane) itemDetails.pinnedPane = previousPinnedPane;
// main
var main = document.createElement('div');
main.className = 'zotero-view-item-main';
hbox.append(main);
// pane-header
var paneHeader = document.createXULElement('pane-header');
main.append(paneHeader);
// div
var div = document.createElement('div');
div.className = 'zotero-view-item';
div.setAttribute("tabindex", "0");
main.append(div);
// Info
var itemBox = new (customElements.get('item-box'));
itemBox.setAttribute('data-pane', 'info');
div.append(itemBox);
// Abstract
var abstractBox = new (customElements.get('abstract-box'));
abstractBox.className = 'zotero-editpane-abstract';
abstractBox.setAttribute('data-pane', 'abstract');
div.append(abstractBox);
// Attachment info
var attachmentBox = new (customElements.get('attachment-box'));
attachmentBox.className = 'zotero-editpane-attachment';
attachmentBox.setAttribute('data-pane', 'attachment-info');
div.append(attachmentBox);
// Tags
var tagsBox = new (customElements.get('tags-box'));
tagsBox.className = 'zotero-editpane-tags';
tagsBox.setAttribute('data-pane', 'tags');
div.append(tagsBox);
// Related
var relatedBox = new (customElements.get('related-box'));
relatedBox.className = 'zotero-editpane-related';
relatedBox.setAttribute('data-pane', 'related');
div.append(relatedBox);
paneHeader.mode = readOnly ? 'view' : 'edit';
paneHeader.item = targetItem;
itemBox.mode = readOnly ? 'view' : 'edit';
itemBox.item = targetItem;
abstractBox.mode = readOnly ? 'view' : 'edit';
abstractBox.item = targetItem;
attachmentBox.mode = readOnly ? 'view' : 'edit';
attachmentBox.item = targetItem;
tagsBox.mode = readOnly ? 'view' : 'edit';
tagsBox.item = targetItem;
relatedBox.mode = readOnly ? 'view' : 'edit';
relatedBox.item = targetItem;
if (_itemPaneDeck.selectedPanel === container) {
_sidenav.container = div;
}
// When a tab is loaded, scroll to the pinned pane, if any
if (_sidenav.pinnedPane) {
_sidenav.scrollToPane(_sidenav.pinnedPane, 'instant');
}
_selectItemContext(tabID);
await itemDetails.render();
}
};

View file

@ -31,13 +31,18 @@ Services.scriptloader.loadSubScript("resource://zotero/require.js", this);
Services.scriptloader.loadSubScript("chrome://global/content/customElements.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js", this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPaneSection.js', this);
// Load our custom elements
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentPreview.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentPreviewBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/duplicatesMergePane.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/guidancePanel.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemDetails.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPane.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemMessagePane.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/mergeGroup.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/menulistItemTypes.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteEditor.js', this);

View file

@ -1,157 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
var Zotero_Duplicates_Pane = new function () {
var _masterItem;
var _items = [];
var _otherItems = [];
var _ignoreFields = ['dateAdded', 'dateModified', 'accessDate'];
this.setItems = function (items, displayNumItemsOnTypeError) {
var itemTypeID, oldestItem, otherItems = [];
for (let item of items) {
// Find the oldest item
if (!oldestItem) {
oldestItem = item;
}
else if (item.dateAdded < oldestItem.dateAdded) {
otherItems.push(oldestItem);
oldestItem = item;
}
else {
otherItems.push(item);
}
if (!item.isRegularItem() || ['annotation', 'attachment', 'note'].includes(item.itemType)) {
var msg = Zotero.getString('pane.item.duplicates.onlyTopLevel');
ZoteroPane_Local.setItemPaneMessage(msg);
return false;
}
// Make sure all items are of the same type
if (itemTypeID) {
if (itemTypeID != item.itemTypeID) {
if (displayNumItemsOnTypeError) {
var msg = Zotero.getString('pane.item.selected.multiple', items.length);
}
else {
var msg = Zotero.getString('pane.item.duplicates.onlySameItemType');
}
ZoteroPane_Local.setItemPaneMessage(msg);
return false;
}
}
else {
itemTypeID = item.itemTypeID;
}
}
_items = items;
_items.sort(function (a, b) {
return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1;
});
//
// Update the UI
//
var button = document.getElementById('zotero-duplicates-merge-button');
var versionSelect = document.getElementById('zotero-duplicates-merge-version-select');
var itembox = document.getElementById('zotero-duplicates-merge-item-box');
var fieldSelect = document.getElementById('zotero-duplicates-merge-field-select');
var alternatives = oldestItem.multiDiff(otherItems, _ignoreFields);
if (alternatives) {
// Populate menulist with Date Added values from all items
var dateList = document.getElementById('zotero-duplicates-merge-original-date');
dateList.innerHTML = '';
var numRows = 0;
for (let item of items) {
var date = Zotero.Date.sqlToDate(item.dateAdded, true);
dateList.appendItem(date.toLocaleString());
numRows++;
}
dateList.setAttribute('rows', numRows);
// If we set this inline, the selection doesn't take on the first
// selection after unhiding versionSelect (when clicking
// from a set with no differences) -- tested in Fx5.0.1
setTimeout(function () {
dateList.selectedIndex = 0;
}, 0);
}
button.label = Zotero.getString('pane.item.duplicates.mergeItems', (otherItems.length + 1));
versionSelect.hidden = fieldSelect.hidden = !alternatives;
itembox.hiddenFields = alternatives ? [] : ['dateAdded', 'dateModified'];
this.setMaster(0);
return true;
}
this.setMaster = function (pos) {
var itembox = document.getElementById('zotero-duplicates-merge-item-box');
itembox.mode = 'fieldmerge';
_otherItems = _items.concat();
var item = _otherItems.splice(pos, 1)[0];
// Add master item's values to the beginning of each set of
// alternative values so that they're still available if the item box
// modifies the item
var alternatives = item.multiDiff(_otherItems, _ignoreFields);
if (alternatives) {
let itemValues = item.toJSON();
for (let i in alternatives) {
alternatives[i].unshift(itemValues[i] !== undefined ? itemValues[i] : '');
}
itembox.fieldAlternatives = alternatives;
}
_masterItem = item;
itembox.item = item.clone();
}
this.merge = Zotero.Promise.coroutine(function* () {
var itembox = document.getElementById('zotero-duplicates-merge-item-box');
Zotero.CollectionTreeCache.clear();
// Update master item with any field alternatives from the item box
var json = _masterItem.toJSON();
// Exclude certain properties that are empty in the cloned object, so we don't clobber them
const { relations, collections, tags, ...keep } = itembox.item.toJSON();
Object.assign(json, keep);
_masterItem.fromJSON(json);
Zotero.Items.merge(_masterItem, _otherItems);
});
}

View file

@ -26,7 +26,7 @@
"use strict";
{
class AbstractBox extends XULElementBase {
class AbstractBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
<html:div class="body">
@ -50,7 +50,6 @@
this._item = item;
if (item?.isRegularItem()) {
this.hidden = false;
this.render();
}
else {
this.hidden = true;
@ -67,13 +66,12 @@
}
this.blurOpenField();
this._mode = mode;
this.render();
}
init() {
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'abstractBox');
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._abstractField = this.querySelector('editable-text');
this._abstractField.addEventListener('change', () => this.save());
@ -88,7 +86,7 @@
notify(action, type, ids) {
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
this.render();
this.render(true);
}
}
@ -97,7 +95,7 @@
this.item.setField('abstractNote', this._abstractField.value);
await this.item.saveTx();
}
this.render();
this.render(true);
}
async blurOpenField() {
@ -107,10 +105,9 @@
}
}
render() {
if (!this.item) {
return;
}
render(force = false) {
if (!this.item) return;
if (!force && this._isAlreadyRendered()) return;
let abstract = this.item.getField('abstractNote');
this._section.summary = abstract;

View file

@ -24,7 +24,7 @@
*/
{
class AttachmentAnnotationsBox extends XULElementBase {
class AttachmentAnnotationsBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-attachments-annotations" data-pane="attachment-annotations">
<html:div class="body">
@ -32,41 +32,41 @@
</collapsible-section>
`);
get tabType() {
return this._tabType;
}
set tabType(tabType) {
this._tabType = tabType;
this._updateHidden();
}
get item() {
return this._item;
}
set item(item) {
this._item = item;
if (item?.isFileAttachment()) {
this.hidden = false;
this.render();
}
else {
this.hidden = true;
}
this._updateHidden();
}
init() {
this._section = this.querySelector('collapsible-section');
this._section.addEventListener("toggle", this._handleSectionOpen);
this.initCollapsibleSection();
this._body = this.querySelector('.body');
this.render();
}
destroy() {
this._section.removeEventListener("toggle", this._handleSectionOpen);
}
destroy() {}
notify(action, type, ids) {
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
this.render();
this.render(true);
}
}
render() {
render(force = false) {
if (!this.initialized || !this.item?.isFileAttachment()) return;
if (!force && this._isAlreadyRendered()) return;
let annotations = this.item.getAnnotations();
this._section.setCount(annotations.length);
@ -91,12 +91,9 @@
}
}
_handleSectionOpen = (event) => {
if (event.target !== this._section || !this._section.open) {
return;
}
this.render();
};
_updateHidden() {
this.hidden = !this.item?.isFileAttachment() || this.tabType == "reader";
}
}
customElements.define("attachment-annotations-box", AttachmentAnnotationsBox);
}

View file

@ -28,7 +28,7 @@
{
class AttachmentBox extends XULElementBase {
class AttachmentBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-attachment-info" data-pane="attachment-info">
<html:div class="body">
@ -181,6 +181,15 @@
this.toggleAttribute('data-use-preview', val);
}
get tabType() {
return this._tabType;
}
set tabType(tabType) {
this._tabType = tabType;
if (tabType == "reader") this.usePreview = false;
}
get item() {
return this._item;
}
@ -192,15 +201,16 @@
if (val.isAttachment()) {
this._item = val;
this.hidden = false;
this.render();
this._preview.disableResize = false;
}
else {
this.hidden = true;
this._preview.disableResize = true;
}
}
init() {
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._id('url').addEventListener('contextmenu', (event) => {
this._id('url-menu').openPopupAtScreen(event.screenX, event.screenY, true);
@ -230,12 +240,6 @@
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentbox');
this._section.addEventListener("toggle", (ev) => {
if (ev.target.open && this.usePreview) {
this._preview.render();
}
});
// Work around the reindex toolbarbutton not wanting to properly receive focus on tab.
// Make <image> focusable. On focus of the image, bounce the focus to the toolbarbutton.
// Temporarily remove tabindex from the <image> so that the focus can move past the
@ -282,15 +286,17 @@
continue;
}
this.render();
this.render(true);
break;
}
}
async render() {
if (this._isRendering) {
return;
}
async render(force = false) {
if (!this.item) return;
if (this._isRendering) return;
if (!this._section.open) return;
if (!force && this._isAlreadyRendered()) return;
Zotero.debug('Refreshing attachment box');
this._isRendering = true;
// Cancel editing filename when refreshing
@ -298,6 +304,7 @@
if (this.usePreview) {
this._preview.item = this.item;
this._preview.render();
}
let fileNameRow = this._id('fileNameRow');
@ -521,7 +528,7 @@
}
// Don't allow empty filename
if (!newFilename) {
this.render();
this.render(true);
return;
}
let newExt = getExtension(newFilename);
@ -577,7 +584,7 @@
Zotero.getString('pane.item.attachments.fileNotFound.text1')
);
}
this.render();
this.render(true);
}
initAttachmentNoteEditor() {

View file

@ -24,8 +24,7 @@
*/
{
// eslint-disable-next-line no-undef
class AttachmentPreview extends XULElementBase {
class AttachmentPreview extends ItemPaneSectionElementBase {
static fileTypeMap = {
// TODO: support video and audio
// 'video/mp4': 'video',
@ -49,7 +48,7 @@
this._isDiscarding = false;
this._failedCount = 0;
this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this));
// this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this));
this._resizeOb = new ResizeObserver(this._handleResize.bind(this));
}
@ -97,14 +96,6 @@
set item(val) {
this._item = (val instanceof Zotero.Item && val.isFileAttachment()) ? val : null;
if (this.isVisible) {
this.render();
}
}
setItemAndRender(item) {
this._item = item;
this.render();
}
get previewType() {
@ -140,7 +131,16 @@
}
get hasPreview() {
return this.getAttribute("data-preview-status") === "success";
return this.dataset.previewStatus === "success";
}
get disableResize() {
return this.dataset.disableResize !== "false";
}
set disableResize(val) {
this.dataset.disableResize = val ? "true" : "false";
this._handleResize();
}
setPreviewStatus(val) {
@ -151,32 +151,9 @@
this.setAttribute("data-preview-status", val);
}
get isVisible() {
const rect = this.getBoundingClientRect();
// Sample per 20 px
const samplePeriod = 20;
let x = rect.left + rect.width / 2;
let yStart = rect.top;
let yEnd = rect.bottom;
let elAtPos;
// Check visibility from top/bottom to center
for (let dy = 1; dy < Math.floor((yEnd - yStart) / 2); dy += samplePeriod) {
elAtPos = document.elementFromPoint(x, yStart + dy);
if (this.contains(elAtPos)) {
return true;
}
elAtPos = document.elementFromPoint(x, yEnd - dy);
if (this.contains(elAtPos)) {
return true;
}
}
return false;
}
init() {
this.setPreviewStatus("loading");
this._dragImageContainer = this.querySelector(".drag-container");
this._intersectionOb.observe(this);
this._resizeOb.observe(this);
this.addEventListener("dblclick", (event) => {
this.openAttachment(event);
@ -193,7 +170,6 @@
destroy() {
this._reader?.uninit();
this._intersectionOb.disconnect();
this._resizeOb.disconnect();
this.removeEventListener("DOMContentLoaded", this._handleReaderLoad);
this.removeEventListener("mouseenter", this.updateGoto);
@ -401,41 +377,8 @@
}
}
async _handleIntersection(entries) {
const DISCARD_TIMEOUT = 60000;
let needsRefresh = false;
let needsDiscard = false;
entries.forEach((entry) => {
if (entry.isIntersecting) {
needsRefresh = true;
}
else {
needsDiscard = true;
}
});
if (needsRefresh) {
let sidenav = this._getSidenav();
// Sidenav is in smooth scrolling mode
if (sidenav?._disableScrollHandler) {
// Wait for scroll to finish
await sidenav._waitForScroll();
// If the preview is not visible, do not render
if (!this.isVisible) {
return;
}
}
// Try to render the preview when the preview enters viewport
this.render();
}
else if (!this._isDiscardPlanned && needsDiscard) {
this._isDiscardPlanned = true;
setTimeout(() => {
this.discard();
}, DISCARD_TIMEOUT);
}
}
_handleResize() {
if (this.disableResize) return;
this.style.setProperty("--preview-width", `${this.clientWidth}px`);
}
@ -475,14 +418,6 @@
this.style.setProperty("--width-height-ratio", scaleRatio);
}
_getSidenav() {
// TODO: update this after unifying item pane & context pane
return document.querySelector(
Zotero_Tabs.selectedType === 'library'
? "#zotero-view-item-sidenav"
: "#zotero-context-pane-sidenav");
}
_id(id) {
return this.querySelector(`#${id}`);
}

View file

@ -25,7 +25,7 @@
{
class AttachmentPreviewBox extends XULElementBase {
class AttachmentPreviewBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-attachment-preview" data-pane="attachment-preview">
<html:div class="body">
@ -64,19 +64,14 @@
}
init() {
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._preview = this.querySelector("#attachment-preview");
this._section.addEventListener("toggle", (ev) => {
if (ev.target.open && this.usePreview) {
this._preview.render();
}
});
}
destroy() {}
async render() {
if (!this._section.open) return;
let bestAttachment = await this.item.getBestAttachment();
if (bestAttachment) {
this._preview.item = bestAttachment;

View file

@ -93,7 +93,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
_handleAnnotationClick = () => {
// TODO: jump to annotations pane
// ZoteroItemPane.setNextPane("attachment-annotations");
let pane = this._getSidenav()?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`);
if (pane) {
pane._section.open = true;

View file

@ -26,7 +26,7 @@
"use strict";
{
class AttachmentsBox extends XULElementBase {
class AttachmentsBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-attachments" data-pane="attachments" extra-buttons="add">
<html:div class="body">
@ -52,22 +52,14 @@
}
set item(item) {
let isRegularItem = item?.isRegularItem();
this.hidden = !isRegularItem;
if (!isRegularItem || this._item === item) {
if (this._item === item) {
return;
}
this._item = item;
this.refresh();
}
get mode() {
return this._mode;
}
set mode(mode) {
this._mode = mode;
let hidden = !item?.isRegularItem() || item?.isFeedItem;
this.hidden = hidden;
this._preview.disableResize = !!hidden;
}
get inTrash() {
@ -88,17 +80,28 @@
this.updateCount();
}
get tabType() {
return this._tabType;
}
set tabType(tabType) {
this._tabType = tabType;
this._updateHidden();
}
get usePreview() {
if (this.tabType == "reader") return false;
return this.hasAttribute('data-use-preview');
}
set usePreview(val) {
if (this.tabType == "reader") return;
this.toggleAttribute('data-use-preview', val);
this.updatePreview();
}
init() {
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAdd);
// this._section.addEventListener('togglePreview', this._handleTogglePreview);
@ -112,16 +115,11 @@
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentsBox');
this._section.addEventListener("toggle", (ev) => {
if (ev.target.open) {
this._preview.render();
}
});
this._section._contextMenu.addEventListener('popupshowing', this._handleContextMenu, { once: true });
}
destroy() {
this._section.removeEventListener('add', this._handleAdd);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
@ -180,8 +178,9 @@
return row;
}
async refresh() {
async render(force = false) {
if (!this._item) return;
if (!force && this._isAlreadyRendered()) return;
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
@ -202,15 +201,12 @@
}
async updatePreview() {
if (!this.usePreview) {
if (!this.usePreview || !this._section.open) {
return;
}
let attachment = await this._item.getBestAttachment();
if (!this._preview.hasPreview) {
this._preview.setItemAndRender(attachment);
return;
}
this._preview.item = attachment;
await this._preview.render();
}
_handleAdd = (event) => {
@ -232,6 +228,7 @@
};
_handleContextMenu = () => {
if (this.tabType == "reader") return;
let contextMenu = this._section._contextMenu;
let menu = document.createXULElement("menuitem");
menu.classList.add('menuitem-iconic', 'zotero-menuitem-toggle-preview');
@ -264,6 +261,10 @@
}
this._attachmentIDs = sortedAttachmentIDs;
}
_updateHidden() {
this.hidden = !this._item?.isRegularItem();
}
}
customElements.define("attachments-box", AttachmentsBox);
}

View file

@ -212,7 +212,7 @@
pinSection.setAttribute('data-l10n-id', 'pin-section');
pinSection.addEventListener('command', () => {
let sidenav = this._getSidenav();
sidenav.scrollToPane(this.dataset.pane, 'smooth');
sidenav.container.scrollToPane(this.dataset.pane, 'smooth');
sidenav.pinnedPane = this.dataset.pane;
});
contextMenu.append(pinSection);
@ -401,6 +401,7 @@
if (document.documentElement.getAttribute('windowtype') !== 'navigator:browser') {
return null;
}
if (typeof Zotero_Tabs == "undefined") return null;
// TODO: update this after unifying item pane & context pane
return document.querySelector(
Zotero_Tabs.selectedType === 'library'

View file

@ -0,0 +1,186 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
{
class DuplicatesMergePane extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<groupbox>
<button id="zotero-duplicates-merge-button" />
</groupbox>
<groupbox id="zotero-duplicates-merge-version-select">
<description>&zotero.duplicatesMerge.versionSelect;</description>
<hbox>
<richlistbox id="zotero-duplicates-merge-original-date" rows="0"/>
</hbox>
</groupbox>
<groupbox flex="1">
<description id="zotero-duplicates-merge-field-select">&zotero.duplicatesMerge.fieldSelect;</description>
<vbox id="zotero-duplicates-merge-item-box-container" flex="1">
<item-box id="zotero-duplicates-merge-item-box" flex="1"/>
</vbox>
</groupbox>
`, ['chrome://zotero/locale/zotero.dtd']);
init() {
this._masterItem = null;
this._items = [];
this._otherItems = [];
this._ignoreFields = ['dateAdded', 'dateModified', 'accessDate'];
this.querySelector("#zotero-duplicates-merge-button").addEventListener(
"command", () => this.merge());
this.querySelector("#zotero-duplicates-merge-original-date").addEventListener(
"select", event => this.setMaster(event.target.selectedIndex));
}
setItems(items, displayNumItemsOnTypeError) {
let itemTypeID, oldestItem, otherItems = [];
for (let item of items) {
// Find the oldest item
if (!oldestItem) {
oldestItem = item;
}
else if (item.dateAdded < oldestItem.dateAdded) {
otherItems.push(oldestItem);
oldestItem = item;
}
else {
otherItems.push(item);
}
if (!item.isRegularItem() || ['annotation', 'attachment', 'note'].includes(item.itemType)) {
let msg = Zotero.getString('pane.item.duplicates.onlyTopLevel');
ZoteroPane.itemPane.setItemPaneMessage(msg);
return false;
}
// Make sure all items are of the same type
if (itemTypeID) {
if (itemTypeID != item.itemTypeID) {
let msg;
if (displayNumItemsOnTypeError) {
msg = Zotero.getString('pane.item.selected.multiple', items.length);
}
else {
msg = Zotero.getString('pane.item.duplicates.onlySameItemType');
}
ZoteroPane.itemPane.setItemPaneMessage(msg);
return false;
}
}
else {
itemTypeID = item.itemTypeID;
}
}
this._items = items;
this._items.sort(function (a, b) {
return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1;
});
//
// Update the UI
//
let button = document.getElementById('zotero-duplicates-merge-button');
let versionSelect = document.getElementById('zotero-duplicates-merge-version-select');
let itembox = document.getElementById('zotero-duplicates-merge-item-box');
let fieldSelect = document.getElementById('zotero-duplicates-merge-field-select');
let alternatives = oldestItem.multiDiff(otherItems, this._ignoreFields);
if (alternatives) {
// Populate menulist with Date Added values from all items
let dateList = document.getElementById('zotero-duplicates-merge-original-date');
dateList.innerHTML = '';
let numRows = 0;
for (let item of items) {
let date = Zotero.Date.sqlToDate(item.dateAdded, true);
dateList.appendItem(date.toLocaleString());
numRows++;
}
dateList.setAttribute('rows', numRows);
// If we set this inline, the selection doesn't take on the first
// selection after unhiding versionSelect (when clicking
// from a set with no differences) -- tested in Fx5.0.1
setTimeout(function () {
dateList.selectedIndex = 0;
}, 0);
}
button.label = Zotero.getString('pane.item.duplicates.mergeItems', (otherItems.length + 1));
versionSelect.hidden = fieldSelect.hidden = !alternatives;
itembox.hiddenFields = alternatives ? [] : ['dateAdded', 'dateModified'];
this.setMaster(0);
return true;
}
setMaster(pos) {
let itembox = document.getElementById('zotero-duplicates-merge-item-box');
itembox.mode = 'fieldmerge';
this._otherItems = this._items.concat();
let item = this._otherItems.splice(pos, 1)[0];
// Add master item's values to the beginning of each set of
// alternative values so that they're still available if the item box
// modifies the item
let alternatives = item.multiDiff(this._otherItems, this._ignoreFields);
if (alternatives) {
let itemValues = item.toJSON();
for (let i in alternatives) {
alternatives[i].unshift(itemValues[i] !== undefined ? itemValues[i] : '');
}
itembox.fieldAlternatives = alternatives;
}
this._masterItem = item;
itembox.item = item.clone();
// The item.id is null which equals to _lastRenderItemID, so we need to force render it
itembox.render(true);
}
async merge() {
let itembox = document.getElementById('zotero-duplicates-merge-item-box');
Zotero.CollectionTreeCache.clear();
// Update master item with any field alternatives from the item box
let json = this._masterItem.toJSON();
// Exclude certain properties that are empty in the cloned object, so we don't clobber them
const { relations: _r, collections: _c, tags: _t, ...keep } = itembox.item.toJSON();
Object.assign(json, keep);
this._masterItem.fromJSON(json);
Zotero.Items.merge(this._masterItem, this._otherItems);
}
}
customElements.define("duplicates-merge-pane", DuplicatesMergePane);
}

View file

@ -26,7 +26,7 @@
"use strict";
{
class ItemBox extends XULElement {
class ItemBox extends ItemPaneSectionElementBase {
constructor() {
super();
@ -56,8 +56,10 @@
this._draggedCreator = false;
this._ztabindex = 0;
this._selectField = null;
}
this.content = MozXULElement.parseXULToFragment(`
get content() {
return MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-info" data-pane="info" style="width:100%">
<html:div class="body">
<div id="item-box" xmlns="http://www.w3.org/1999/xhtml">
@ -102,12 +104,8 @@
`, ['chrome://zotero/locale/zotero.dtd']);
}
connectedCallback() {
this._destroyed = false;
window.addEventListener("unload", this.destroy);
this.appendChild(document.importNode(this.content, true));
init() {
this.initCollapsibleSection();
this._creatorTypeMenu.addEventListener('command', async (event) => {
var typeBox = document.popupNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
@ -129,7 +127,7 @@
}
});
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (event) => {
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
var row = document.popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
@ -185,7 +183,6 @@
break;
}
this.moveCreator(index, dir);
return;
}
else if (event.explicitOriginalTarget.id == "creator-transform-switch") {
// Switch creator field mode action
@ -193,7 +190,6 @@
var lastName = creatorNameBox.firstChild;
let fieldMode = parseInt(lastName.getAttribute("fieldMode"));
this.switchCreatorMode(row, fieldMode == 1 ? 0 : 1, false, true, index);
return;
}
});
@ -235,7 +231,7 @@
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox');
Zotero.Prefs.registerObserver('fontSize', () => {
this.refresh();
this.render(true);
});
this.style.setProperty('--comma-character',
@ -243,24 +239,9 @@
}
destroy() {
if (this._destroyed) {
return;
}
window.removeEventListener("unload", this.destroy);
this._destroyed = true;
Zotero.Notifier.unregisterObserver(this._notifierID);
}
disconnectedCallback() {
// Empty the DOM. We will rebuild if reconnected.
while (this.lastChild) {
this.removeChild(this.lastChild);
}
this.destroy();
}
//
// Public properties
//
@ -340,7 +321,6 @@
this._item = val;
this._lastTabIndex = null;
this.scrollToTop();
this.refresh();
}
// .ref is an alias for .item
@ -484,18 +464,24 @@
if (document.activeElement == this.itemTypeMenu) {
this._selectField = "item-type-menu";
}
this.refresh();
this.render(true);
break;
}
}
refresh() {
render(force = false) {
Zotero.debug('Refreshing item box');
if (!this.item) {
Zotero.debug('No item to refresh', 2);
return;
}
if (!this._section.open) return;
// Always update retraction status
this.updateRetracted();
if (!force && this._isAlreadyRendered()) return;
this.updateRetracted();
@ -672,14 +658,14 @@
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
// eslint-disable-next-line no-loop-func
let triggerPopup = (e) => {
let menupopup = ZoteroItemPane.buildFieldTransformMenu({
let menupopup = ZoteroPane.buildFieldTransformMenu({
target: valueElement,
onTransform: (newValue) => {
this._setFieldTransformedValue(valueElement, newValue);
}
});
this.querySelector('popupset').append(menupopup);
menupopup.addEventListener('popuphidden', (e) => {
menupopup.addEventListener('popuphidden', () => {
menupopup.remove();
optionsButton.style.visibility = '';
});
@ -722,7 +708,7 @@
menuitem.getAttribute('fieldname'),
menuitem.getAttribute('originalValue')
);
this.refresh();
this.render(true);
});
popup.appendChild(menuitem);
}
@ -879,7 +865,7 @@
}
this._refreshed = true;
// Add tabindex=0 to all focusable element
this.querySelectorAll("[ztabindex]").forEach((node) =>{
this.querySelectorAll("[ztabindex]").forEach((node) => {
node.setAttribute("tabindex", 0);
});
// Make sure that any opened popup closes
@ -1155,7 +1141,7 @@
// If the row is still hidden, no 'drop' event happened, meaning creator rows
// were not reordered. To make sure everything is in correct order, just refresh.
if (row.classList.contains("drag-hidden-creator")) {
this.refresh();
this.render(true);
}
});
@ -1200,12 +1186,12 @@
rowData.setAttribute("ztabindex", ++this._ztabindex);
rowData.addEventListener('click', () => {
this._displayAllCreators = true;
this.refresh();
this.render(true);
});
rowData.addEventListener('keypress', (e) => {
if (["Enter", ' '].includes(e.key)) {
this._displayAllCreators = true;
this.refresh();
this.render(true);
}
});
rowData.textContent = Zotero.getString('general.numMore', num);
@ -1421,7 +1407,7 @@
await this.item.saveTx();
}
else {
this.refresh();
this.render(true);
}
functionsToRun.forEach(f => f.bind(this)());
@ -1512,7 +1498,7 @@
valueElement.setAttribute('tight', true);
valueElement.addEventListener("focus", e => this.updateLastFocused(e));
valueElement.addEventListener("keypress", (e) => this.handleKeyPress(e));
valueElement.addEventListener("keypress", e => this.handleKeyPress(e));
switch (fieldName) {
case 'itemType':
valueElement.setAttribute('itemTypeID', valueText);

View file

@ -0,0 +1,575 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
{
const AsyncFunction = (async () => {}).constructor;
const waitFrame = async () => {
return waitNoLongerThan(new Promise((resolve) => {
requestAnimationFrame(resolve);
}), 30);
};
const waitFrames = async (n) => {
for (let i = 0; i < n; i++) {
await waitFrame();
}
};
const waitNoLongerThan = async (promise, ms = 1000) => {
return Promise.race([
promise,
Zotero.Promise.delay(ms)
]);
};
class ItemDetails extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<hbox id="zotero-view-item-container" class="zotero-view-item-container" flex="1">
<html:div class="zotero-view-item-main">
<pane-header id="zotero-item-pane-header" />
<html:div id="zotero-view-item" class="zotero-view-item" tabindex="0">
<item-box id="zotero-editpane-item-box" data-pane="info"/>
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract" data-pane="abstract"/>
<attachments-box id="zotero-editpane-attachments" data-pane="attachments"/>
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" data-pane="notes"/>
<attachment-box id="zotero-attachment-box" flex="1" data-pane="attachment-info" data-use-preview="true" hidden="true"/>
<attachment-annotations-box id="zotero-editpane-attachment-annotations" flex="1" data-pane="attachment-annotations" hidden="true"/>
<libraries-collections-box id="zotero-editpane-libraries-collections" class="zotero-editpane-libraries-collections" data-pane="libraries-collections"/>
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" data-pane="tags"/>
<related-box id="zotero-editpane-related" class="zotero-editpane-related" data-pane="related"/>
</html:div>
</html:div>
</hbox>
`);
get item() {
return this._item;
}
set item(item) {
this._item = item;
}
get mode() {
return this._mode;
}
set mode(mode) {
this._mode = mode;
}
get pinnedPane() {
return this.getAttribute('pinnedPane');
}
set pinnedPane(val) {
if (!val || !this.getPane(val)) {
val = '';
}
this.setAttribute('pinnedPane', val);
if (val) {
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
}
this.sidenav.updatePaneStatus(val);
}
get _minScrollHeight() {
return parseFloat(this._paneParent.style.getPropertyValue('--min-scroll-height') || 0);
}
set _minScrollHeight(val) {
this._paneParent.style.setProperty('--min-scroll-height', val + 'px');
}
get _collapsed() {
let collapsible = this.closest('splitter:not([hidden="true"]) + *');
if (!collapsible) return false;
return collapsible.getAttribute('collapsed') === 'true';
}
set _collapsed(val) {
let collapsible = this.closest('splitter:not([hidden="true"]) + *');
if (!collapsible) return;
let splitter = collapsible.previousElementSibling;
if (val) {
collapsible.setAttribute('collapsed', 'true');
collapsible.removeAttribute("width");
collapsible.removeAttribute("height");
splitter.setAttribute('state', 'collapsed');
splitter.setAttribute('substate', 'after');
}
else {
collapsible.removeAttribute('collapsed');
splitter.setAttribute('state', '');
splitter.setAttribute('substate', 'after');
}
window.dispatchEvent(new Event('resize'));
}
get sidenav() {
return this._sidenav;
}
set sidenav(sidenav) {
this._sidenav = sidenav;
sidenav.container = this;
}
static get observedAttributes() {
return ['pinnedPane'];
}
init() {
this._container = this.querySelector('#zotero-view-item-container');
this._header = this.querySelector('#zotero-item-pane-header');
this._paneParent = this.querySelector('#zotero-view-item');
this._container.addEventListener("keypress", this._handleKeypress);
this._paneParent.addEventListener('scroll', this._handleContainerScroll);
this._paneHiddenOb = new MutationObserver(this._handlePaneStatus);
this._paneHiddenOb.observe(this._paneParent, {
attributes: true,
attributeFilter: ["hidden"],
subtree: true,
});
this._initIntersectionObserver();
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'ItemDetails');
this._disableScrollHandler = false;
this._pinnedPaneMinScrollHeight = 0;
}
destroy() {
this._container.removeEventListener("keypress", this._handleKeypress);
this._paneParent.removeEventListener('scroll', this._handleContainerScroll);
this._paneHiddenOb.disconnect();
this._intersectionOb.disconnect();
Zotero.Notifier.unregisterObserver(this._unregisterID);
}
async render() {
if (!this.initialized || !this.item) {
return;
}
let item = this.item;
Zotero.debug('Viewing item');
this._isRendering = true;
let panes = this.getPanes();
let pendingBoxes = [];
let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
let tabType = Zotero_Tabs.selectedType;
for (let box of [this._header, ...panes]) {
if (!box.showInFeeds && item.isFeedItem) {
box.style.display = 'none';
box.hidden = true;
continue;
}
else {
box.style.display = '';
box.hidden = false;
}
if (this.mode) {
box.mode = this.mode;
if (box.mode == 'view') {
box.hideEmptyFields = true;
}
}
else {
box.mode = 'edit';
}
box.item = item;
box.inTrash = inTrash;
box.tabType = tabType;
// Render sync boxes immediately
if (!box.hidden && box.render) {
if (box.render instanceof AsyncFunction) {
pendingBoxes.push(box);
}
else {
box.render();
}
}
}
let pinnedPaneElem = this.getPane(this.pinnedPane);
let pinnedIndex = panes.indexOf(pinnedPaneElem);
this._paneParent.style.paddingBottom = '';
if (pinnedPaneElem) {
let paneID = pinnedPaneElem.dataset.pane;
this.scrollToPane(paneID, 'instant');
this.pinnedPane = paneID;
}
else {
this._paneParent.scrollTo(0, 0);
}
// Only render visible panes
for (let box of pendingBoxes) {
if (pinnedIndex > -1 && panes.indexOf(box) < pinnedIndex) {
continue;
}
if (!this.isPaneVisible(box.dataset.pane)) {
continue;
}
await waitNoLongerThan(box.render(), 500);
}
// After all panes finish first rendering, try secondary rendering
for (let box of panes) {
if (!box.secondaryRender) {
continue;
}
if (pinnedIndex > -1 && panes.indexOf(box) < pinnedIndex) {
continue;
}
if (this.isPaneVisible(box.dataset.pane)) {
continue;
}
await waitNoLongerThan(box.secondaryRender(), 500);
}
if (this.item.id == item.id) {
this._isRendering = false;
}
}
renderCustomSections() {
let lastUpdate = Zotero.ItemPaneManager.getUpdateTime();
if (this._lastUpdateCustomSection == lastUpdate) return;
this._lastUpdateCustomSection = lastUpdate;
let targetPanes = Zotero.ItemPaneManager.getCustomSections();
let currentPaneElements = this.getCustomPanes();
// Remove
for (let elem of currentPaneElements) {
let elemPaneID = elem.dataset.pane;
if (targetPanes.find(pane => pane.paneID == elemPaneID)) continue;
this._intersectionOb.unobserve(elem);
elem.remove();
this.sidenav.removePane(elemPaneID);
}
// Create
let currentPaneIDs = currentPaneElements.map(elem => elem.dataset.pane);
for (let section of targetPanes) {
let { paneID, head, sidenav, fragment,
onInit, onDestroy, onDataChange, onRender, onSecondaryRender, onToggle,
sectionButtons } = section;
if (currentPaneIDs.includes(paneID)) continue;
let elem = new (customElements.get("item-pane-custom-section"));
elem.dataset.sidenavOptions = JSON.stringify(sidenav || {});
elem.paneID = paneID;
elem.fragment = fragment;
elem.registerSectionIcon({ icon: head.icon, darkIcon: head.darkIcon });
elem.registerHook({ type: "init", callback: onInit });
elem.registerHook({ type: "destroy", callback: onDestroy });
elem.registerHook({ type: "dataChange", callback: onDataChange });
elem.registerHook({ type: "render", callback: onRender });
elem.registerHook({ type: "secondaryRender", callback: onSecondaryRender });
elem.registerHook({ type: "toggle", callback: onToggle });
if (sectionButtons) {
for (let buttonOptions of sectionButtons) {
elem.registerSectionButton(buttonOptions);
}
}
this._paneParent.append(elem);
elem.setL10nID(head.l10nID);
elem.setL10nArgs(head.l10nArgs);
this._intersectionOb.observe(elem);
this.sidenav.addPane(paneID);
}
}
renderCustomHead(callback) {
this._header.renderCustomHead(callback);
}
notify = async (action, _type, _ids, _extraData) => {
if (action == 'refresh' && this.item) {
await this.render();
}
};
getPane(id) {
return this._paneParent.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
}
getPanes() {
return Array.from(this._paneParent.querySelectorAll(':scope > [data-pane]'));
}
getEnabledPanes() {
return Array.from(this._paneParent.querySelectorAll(':scope > [data-pane]:not([hidden])'));
}
getVisiblePanes() {
let panes = this.getPanes();
let visiblePanes = [];
for (let paneElem of panes) {
if (this.isPaneVisible(paneElem.dataset.pane)) {
visiblePanes.push(paneElem);
}
else if (visiblePanes.length > 0) {
// Early stop at first invisible pane after some visible panes
break;
}
}
return visiblePanes;
}
isPaneVisible(paneID) {
let paneElem = this.getPane(paneID);
if (!paneElem) return false;
let paneRect = paneElem.getBoundingClientRect();
let containerRect = this._paneParent.getBoundingClientRect();
if (paneRect.top >= containerRect.bottom || paneRect.bottom <= containerRect.top) {
return false;
}
return true;
}
async scrollToPane(paneID, behavior = 'smooth') {
let pane = this.getPane(paneID);
if (!pane) return null;
let scrollPromise;
// If the itemPane is collapsed, just remember which pane needs to be scrolled to
// when itemPane is expanded.
if (this._collapsed || this.getAttribute("no-render")) {
return null;
}
// The pane should always be at the very top
// If there isn't enough stuff below it for it to be at the top, we add padding
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
this._makeSpaceForPane(pane);
if (behavior == 'smooth') {
this._disableScrollHandler = true;
scrollPromise = this._waitForScroll();
scrollPromise.then(() => this._disableScrollHandler = false);
}
pane.scrollIntoView({ block: 'start', behavior });
pane.focus();
return scrollPromise;
}
_makeSpaceForPane(pane) {
let oldMinScrollHeight = this._minScrollHeight;
let newMinScrollHeight = this._getMinScrollHeightForPane(pane);
if (newMinScrollHeight > oldMinScrollHeight) {
this._minScrollHeight = newMinScrollHeight;
}
}
_getMinScrollHeightForPane(pane) {
let paneRect = pane.getBoundingClientRect();
let containerRect = this._paneParent.getBoundingClientRect();
// No offsetTop property for XUL elements
let offsetTop = paneRect.top - containerRect.top + this._paneParent.scrollTop;
return offsetTop + containerRect.height;
}
async _waitForScroll() {
let scrollPromise = Zotero.Promise.defer();
let lastScrollTop = this._paneParent.scrollTop;
const checkScrollStart = () => {
// If the scrollTop is not changed, wait for scroll to happen
if (lastScrollTop === this._paneParent.scrollTop) {
requestAnimationFrame(checkScrollStart);
}
// Wait for scroll to end
else {
requestAnimationFrame(checkScrollEnd);
}
};
const checkScrollEnd = async () => {
// Wait for 3 frames to make sure not further scrolls
await waitFrames(3);
if (lastScrollTop === this._paneParent.scrollTop) {
scrollPromise.resolve();
}
else {
lastScrollTop = this._paneParent.scrollTop;
requestAnimationFrame(checkScrollEnd);
}
};
checkScrollStart();
// Abort after 3 seconds, which should be enough
return Promise.race([
scrollPromise.promise,
Zotero.Promise.delay(3000)
]);
}
async blurOpenField() {
let panes = [this._header, ...this.getPanes()];
for (let pane of panes) {
if (pane.blurOpenField && pane.contains(document.activeElement)) {
await pane.blurOpenField();
break;
}
}
this._paneParent.focus();
}
_initIntersectionObserver() {
if (this._intersectionOb) {
this._intersectionOb.disconnect();
}
this._intersectionOb = new IntersectionObserver(this._handleIntersection);
this.getPanes().forEach(elem => this._intersectionOb.observe(elem));
}
_handleContainerScroll = () => {
// Don't scroll hidden pane
if (this.hidden || this._disableScrollHandler) return;
let minHeight = this._minScrollHeight;
if (minHeight) {
let newMinScrollHeight = this._paneParent.scrollTop + this._paneParent.clientHeight;
// Ignore overscroll (which generates scroll events on Windows 11, unlike on macOS)
// and don't shrink below the pinned pane's min scroll height
if (newMinScrollHeight > this._paneParent.scrollHeight
|| this.getPane(this.pinnedPane) && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
return;
}
this._minScrollHeight = newMinScrollHeight;
}
};
// Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
_handleKeypress = (event) => {
let stopEvent = () => {
event.preventDefault();
event.stopPropagation();
};
let isLibraryTab = Zotero_Tabs.selectedIndex == 0;
let sidenav = document.getElementById(
isLibraryTab ? 'zotero-view-item-sidenav' : 'zotero-context-pane-sidenav'
);
// Shift-tab from title when reader is opened focuses the last button in tabs toolbar
if (event.target.closest(".title") && event.key == "Tab"
&& event.shiftKey && Zotero_Tabs.selectedType == "reader") {
let focusable = [...document.querySelectorAll("#zotero-tabs-toolbar toolbarbutton:not([disabled]):not([hidden])")];
let btn = focusable[focusable.length - 1];
btn.focus();
stopEvent();
return;
}
// Tab from the scrollable area focuses the pinned pane if it exists
if (event.target.classList.contains("zotero-view-item") && event.key == "Tab" && !event.shiftKey && sidenav.pinnedPane) {
let pane = sidenav.getPane(sidenav.pinnedPane);
pane.firstChild._head.focus();
stopEvent();
return;
}
// Tab tavigation between entries and buttons within library, related and notes boxes
if (event.key == "Tab" && event.target.closest(".box")) {
let next = null;
if (event.key == "Tab" && !event.shiftKey) {
next = event.target.nextElementSibling;
}
if (event.key == "Tab" && event.shiftKey) {
next = event.target.parentNode.previousElementSibling?.lastChild;
}
// Force the element to be visible before focusing
if (next) {
next.style.visibility = "visible";
next.focus();
next.style.removeProperty("visibility");
stopEvent();
}
}
};
_handlePaneStatus = (muts) => {
for (let mut of muts) {
let paneID = mut.target.dataset.pane;
if (paneID) {
this.sidenav.updatePaneStatus(paneID);
}
}
};
_handleIntersection = async (entries) => {
if (this._isRendering) return;
let needsRefresh = [];
let needsDiscard = [];
entries.forEach((entry) => {
let targetPaneElem = entry.target;
if (entry.isIntersecting && targetPaneElem.render) {
needsRefresh.push(targetPaneElem);
}
else if (targetPaneElem.discard) {
needsDiscard.push(targetPaneElem);
}
});
let needsCheckVisibility = false;
// Sidenav is in smooth scrolling mode
if (this._disableScrollHandler) {
// Wait for scroll to finish
await this._waitForScroll();
needsCheckVisibility = true;
}
if (needsRefresh.length > 0) {
needsRefresh.forEach(async (paneElem) => {
if (needsCheckVisibility && !this.isPaneVisible(paneElem.dataset.pane)) {
return;
}
await paneElem.render();
if (paneElem.secondaryRender) await paneElem.secondaryRender();
});
}
if (needsDiscard.length > 0) {
needsDiscard.forEach((paneElem) => {
if (needsCheckVisibility && this.isPaneVisible(paneElem.dataset.pane)) {
return;
}
paneElem.discard();
});
}
};
}
customElements.define("item-details", ItemDetails);
}

View file

@ -0,0 +1,70 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
{
class ItemMessagePane extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="custom-head empty"></html:div>
<groupbox id="zotero-item-pane-groupbox" pack="center" align="center">
<vbox id="zotero-item-pane-message-box"/>
</groupbox>
`);
init() {
this._messageBox = this.querySelector('#zotero-item-pane-message-box');
}
render(content) {
this._messageBox.textContent = '';
if (typeof content == 'string') {
let contentParts = content.split("\n\n");
for (let part of contentParts) {
let desc = document.createXULElement('description');
desc.appendChild(document.createTextNode(part));
this._messageBox.appendChild(desc);
}
}
else {
this._messageBox.appendChild(content);
}
}
renderCustomHead(callback) {
let customHead = this.querySelector(".custom-head");
customHead.replaceChildren();
let append = (...args) => {
customHead.append(...args);
};
if (callback) callback({
doc: document,
append: (...args) => {
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
}
});
}
}
customElements.define("item-message-pane", ItemMessagePane);
}

View file

@ -0,0 +1,502 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
{
class ItemPane extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<deck id="zotero-item-pane-content" class="zotero-item-pane-content" selectedIndex="0" flex="1" zotero-persist="width height" height="300">
<item-message-pane id="zotero-item-message" />
<item-details id="zotero-item-details" />
<note-editor id="zotero-note-editor" flex="1" notitle="1"
previousfocus="zotero-items-tree" />
<duplicates-merge-pane id="zotero-duplicates-merge-pane" />
</deck>
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
`);
init() {
this._itemDetails = this.querySelector("#zotero-item-details");
this._noteEditor = this.querySelector("#zotero-note-editor");
this._duplicatesPane = this.querySelector("#zotero-duplicates-merge-pane");
this._messagePane = this.querySelector("#zotero-item-message");
this._sidenav = this.querySelector("#zotero-view-item-sidenav");
this._deck = this.querySelector("#zotero-item-pane-content");
this._itemDetails.sidenav = this._sidenav;
this._notifierID = Zotero.Notifier.registerObserver(this, ['item']);
this._translationTarget = null;
}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
}
get data() {
return this._data;
}
set data(data) {
this._data = data;
}
get viewMode() {
return this._viewMode;
}
set viewMode(mode) {
this._viewMode = mode;
}
get editable() {
return this._editable;
}
set editable(editable) {
this._editable = editable;
}
get viewType() {
return ["message", "item", "note", "duplicates"][this._deck.selectedIndex];
}
/**
* Set view type
* @param {"message" | "item" | "note" | "duplicates"} type view type
*/
set viewType(type) {
switch (type) {
case "message": {
this._deck.selectedIndex = 0;
break;
}
case "item": {
this._deck.selectedIndex = 1;
break;
}
case "note": {
this._deck.selectedIndex = 2;
break;
}
case "duplicates": {
this._deck.selectedIndex = 3;
break;
}
}
// If item pane is no selected, do not render
this._itemDetails.toggleAttribute("no-render", type == "item");
this._itemDetails.sidenav.toggleDefaultStatus(type != "item");
}
render() {
if (!this.data) return false;
let hideSidenav = false;
let renderStatus = false;
// Single item selected
if (this.data.length == 1) {
let item = this.data[0];
if (item.isNote()) {
hideSidenav = true;
renderStatus = this.renderNoteEditor(item);
}
else {
renderStatus = this.renderItemPane(item);
}
}
// Zero or multiple items selected
else {
renderStatus = this.renderMessage();
}
this._sidenav.hidden = hideSidenav;
return renderStatus;
}
notify(action, type) {
if (type == 'item' && action == 'modify') {
if (this.viewMode.isFeedsOrFeed) {
this.updateReadLabel();
}
}
}
renderNoteEditor(item) {
this.viewType = "note";
let noteEditor = document.getElementById('zotero-note-editor');
noteEditor.mode = this.editable ? 'edit' : 'view';
noteEditor.viewMode = 'library';
noteEditor.parent = null;
noteEditor.item = item;
return true;
}
renderItemPane(item) {
this.viewType = "item";
this._itemDetails.mode = this.editable ? null : "view";
this._itemDetails.item = item;
this._itemDetails.render();
if (item.isFeedItem) {
let lastTranslationTarget = Zotero.Prefs.get('feeds.lastTranslationTarget');
if (lastTranslationTarget) {
let id = parseInt(lastTranslationTarget.substr(1));
if (lastTranslationTarget[0] == "L") {
this._translationTarget = Zotero.Libraries.get(id);
}
else if (lastTranslationTarget[0] == "C") {
this._translationTarget = Zotero.Collections.get(id);
}
}
if (!this._translationTarget) {
this._translationTarget = Zotero.Libraries.userLibrary;
}
this.setTranslateButton();
// Too slow for now
// if (!item.isTranslated) {
// item.translate();
// }
ZoteroPane.startItemReadTimeout(item.id);
}
return true;
}
renderMessage() {
let msg;
let count = this.data.length;
// Display duplicates merge interface in item pane
if (this.viewMode.isDuplicates) {
if (!this.editable) {
if (count) {
msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
}
else {
msg = Zotero.getString('pane.item.selected.zero');
}
this.setItemPaneMessage(msg);
}
else if (count) {
this.viewType = "duplicates";
// On a Select All of more than a few items, display a row
// count instead of the usual item type mismatch error
let displayNumItemsOnTypeError = count > 5 && count == this.viewMode.rowCount;
// Initialize the merge pane with the selected items
this._duplicatesPane.setItems(this.data, displayNumItemsOnTypeError);
}
else {
msg = Zotero.getString('pane.item.duplicates.selectToMerge');
this.setItemPaneMessage(msg);
}
}
// Display label in the middle of the item pane
else {
if (count) {
msg = Zotero.getString('pane.item.selected.multiple', count);
}
else {
let rowCount = this.viewMode.rowCount;
let str = 'pane.item.unselected.';
switch (rowCount) {
case 0:
str += 'zero';
break;
case 1:
str += 'singular';
break;
default:
str += 'plural';
break;
}
msg = Zotero.getString(str, [rowCount]);
}
this.setItemPaneMessage(msg);
// Return false for itemTreeTest#shouldn't select a modified item
return false;
}
return true;
}
setItemPaneMessage(msg) {
this.viewType = "message";
this._messagePane.render(msg);
}
/**
* Display buttons at top of item pane depending on context
*/
updateItemPaneButtons() {
let container;
if (!this.data.length) {
return;
}
else if (this.data.length > 1) {
container = this._messagePane;
}
else if (this.data[0].isNote()) {
container = this._noteEditor;
}
else {
container = this._itemDetails;
}
// My Publications buttons
var isPublications = this.viewMode.isPublications;
// Show in My Publications view if selected items are all notes or non-linked-file attachments
var showMyPublicationsButtons = isPublications
&& this.data.every((item) => {
return item.isNote()
|| (item.isAttachment()
&& item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
});
if (showMyPublicationsButtons) {
container.renderCustomHead(this.renderPublicationsHead.bind(this));
return;
}
// Trash button
let nonDeletedItemsSelected = this.data.some(item => !item.deleted);
if (this.viewMode.isTrash && !nonDeletedItemsSelected) {
container.renderCustomHead(this.renderTrashHead.bind(this));
return;
}
// Feed buttons
if (this.viewMode.isFeedsOrFeed) {
container.renderCustomHead(this.renderFeedHead.bind(this));
this.updateReadLabel();
return;
}
container.renderCustomHead();
}
renderPublicationsHead(data) {
let { doc, append } = data;
let button = doc.createXULElement("button");
button.id = 'zotero-item-pane-my-publications-button';
let hiddenItemsSelected = this.data.some(item => !item.inPublications);
let str, onclick;
if (hiddenItemsSelected) {
str = 'showInMyPublications';
onclick = () => Zotero.Items.addToPublications(this.data);
}
else {
str = 'hideFromMyPublications';
onclick = () => Zotero.Items.removeFromPublications(this.data);
}
button.label = Zotero.getString('pane.item.' + str);
button.onclick = onclick;
append(button);
}
renderTrashHead(data) {
let { doc, append } = data;
let restoreButton = doc.createXULElement("button");
restoreButton.id = "zotero-item-restore-button";
restoreButton.dataset.l10nId = "menu-restoreToLibrary";
restoreButton.addEventListener("command", () => {
ZoteroPane.restoreSelectedItems();
});
let deleteButton = doc.createXULElement("button");
deleteButton.id = "zotero-item-delete-button";
deleteButton.dataset.l10nId = "menu-deletePermanently";
deleteButton.addEventListener("command", () => {
ZoteroPane.deleteSelectedItems();
});
append(restoreButton, deleteButton);
}
renderFeedHead(data) {
let { doc, append } = data;
let toggleReadButton = doc.createXULElement("button");
toggleReadButton.id = "zotero-feed-item-toggleRead-button";
toggleReadButton.addEventListener("command", () => {
ZoteroPane.toggleSelectedItemsRead();
});
let addToButton = new (customElements.get('split-menu-button'));
addToButton.id = "zotero-feed-item-addTo-button";
addToButton.setAttribute("popup", "zotero-item-addTo-menu");
addToButton.addEventListener("command", () => this.translateSelectedItems());
append(toggleReadButton, addToButton);
this.setTranslateButton();
}
updateReadLabel() {
var items = this.data;
var isUnread = false;
for (let item of items) {
if (!item.isRead) {
isUnread = true;
break;
}
}
this.setReadLabel(!isUnread);
}
setReadLabel(isRead) {
var elem = document.getElementById('zotero-feed-item-toggleRead-button');
var label = Zotero.getString('pane.item.' + (isRead ? 'markAsUnread' : 'markAsRead'));
elem.label = label;
var key = Zotero.Keys.getKeyForCommand('toggleRead');
var tooltip = label + (Zotero.rtl ? ' \u202B' : ' ') + '(' + key + ')';
elem.title = tooltip;
}
async translateSelectedItems() {
var collectionID = this._translationTarget.objectType == 'collection' ? this._translationTarget.id : undefined;
var items = this.data;
for (let item of items) {
await item.translate(this._translationTarget.libraryID, collectionID);
}
}
buildTranslateSelectContextMenu(event) {
var menu = document.getElementById('zotero-item-addTo-menu');
// Don't trigger rebuilding on nested popupmenu open/close
if (event.target != menu) {
return;
}
// Clear previous items
while (menu.firstChild) {
menu.removeChild(menu.firstChild);
}
let target = Zotero.Prefs.get('feeds.lastTranslationTarget');
if (!target) {
target = "L" + Zotero.Libraries.userLibraryID;
}
var libraries = Zotero.Libraries.getAll();
for (let library of libraries) {
if (!library.editable || library.libraryType == 'publications') {
continue;
}
Zotero.Utilities.Internal.createMenuForTarget(
library,
menu,
target,
async (event, libraryOrCollection) => {
if (event.target.tagName == 'menu') {
// Simulate menuitem flash on OS X
if (Zotero.isMac) {
event.target.setAttribute('_moz-menuactive', false);
await Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', true);
await Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', false);
await Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', true);
}
menu.hidePopup();
this.setTranslationTarget(libraryOrCollection);
event.stopPropagation();
}
else {
this.setTranslationTarget(libraryOrCollection);
event.stopPropagation();
}
}
);
}
}
setTranslateButton() {
if (!this._translationTarget) return;
var label = Zotero.getString('pane.item.addTo', this._translationTarget.name);
var elem = document.getElementById('zotero-feed-item-addTo-button');
elem.label = label;
var key = Zotero.Keys.getKeyForCommand('saveToZotero');
var tooltip = label
+ (Zotero.rtl ? ' \u202B' : ' ') + '('
+ (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
+ key + ')';
elem.title = tooltip;
elem.image = this._translationTarget.treeViewImage;
}
setTranslationTarget(translationTarget) {
this._translationTarget = translationTarget;
Zotero.Prefs.set('feeds.lastTranslationTarget', translationTarget.treeViewID);
this.setTranslateButton();
}
static get observedAttributes() {
return ['collapsed'];
}
attributeChangedCallback(name) {
switch (name) {
case "collapsed": {
this.handleResize();
}
}
}
handleResize() {
if (this.getAttribute("collapsed")) {
this.removeAttribute("width");
this.removeAttribute("height");
}
else {
// Must have width or height to auto-resize when changing sidenav visibility
// Keep in sync with $min-width-item-pane and min-height + sidebar size
let minWidth = 337;
let minHeight = 205;
let width = this.getAttribute("width");
let height = this.getAttribute("height");
if (!width || Number(width) < minWidth) this.setAttribute("width", String(minWidth));
if (!height || Number(height) < minHeight) this.setAttribute("height", String(minHeight));
// Render item pane after open
if ((!width || !height) && this.viewType == "item") {
this._itemDetails.render();
}
}
}
}
customElements.define("item-pane", ItemPane);
}

View file

@ -0,0 +1,78 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
class ItemPaneSectionElementBase extends XULElementBase {
connectedCallback() {
super.connectedCallback();
if (!this.render) {
Zotero.warn("Pane section must have method render().");
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._section) {
this._section.removeEventListener("toggle", this._handleSectionToggle);
this._section = null;
}
}
initCollapsibleSection() {
this._section = this.querySelector('collapsible-section');
if (this._section) {
this._section.addEventListener("toggle", this._handleSectionToggle);
}
}
/**
* @returns {boolean} if false, data change will not be saved
*/
_handleDataChange(_type, _value) {
return true;
}
_handleSectionToggle = async (event) => {
if (event.target !== this._section || !this._section.open) {
return;
}
if (this.render) await this.render(true);
if (this.secondaryRender) await this.secondaryRender(true);
};
/**
* @param {"primary" | "secondary"} [type]
* @returns {boolean}
*/
_isAlreadyRendered(type = "primary") {
let key = `_${type}RenderItemID`;
let cachedFlag = this[key];
if (cachedFlag && this.item?.id == cachedFlag) {
return true;
}
this._lastRenderItemID = this.item.id;
return false;
}
}

View file

@ -120,17 +120,13 @@
_disableScrollHandler = false;
_pendingPane = null;
get container() {
return this._container;
}
set container(val) {
if (this._container == val) return;
this._container?.removeEventListener('scroll', this._handleContainerScroll);
this._container = val;
this._container.addEventListener('scroll', this._handleContainerScroll);
this.render(true);
}
@ -145,25 +141,21 @@
}
get pinnedPane() {
return this.getAttribute('pinnedPane');
return this.container?.pinnedPane;
}
set pinnedPane(val) {
if (!val || !this.getPane(val)) {
val = '';
}
this.setAttribute('pinnedPane', val);
if (val) {
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
}
if (!this.container) return;
this.container.pinnedPane = val;
}
get _minScrollHeight() {
return parseFloat(this._container.style.getPropertyValue('--min-scroll-height') || 0);
get _collapsed() {
return this.container?._collapsed;
}
set _minScrollHeight(val) {
this._container.style.setProperty('--min-scroll-height', val + 'px');
set _collapsed(val) {
if (!this.container) return;
this.container._collapsed = val;
}
get _contextNotesPaneVisible() {
@ -192,212 +184,17 @@
return false;
}
get _collapsed() {
let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
if (!collapsible) return false;
return collapsible.getAttribute('collapsed') === 'true';
}
set _collapsed(val) {
let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
if (!collapsible) return;
let splitter = collapsible.previousElementSibling;
if (val) {
collapsible.setAttribute('collapsed', 'true');
splitter.setAttribute('state', 'collapsed');
splitter.setAttribute('substate', 'after');
}
else {
collapsible.removeAttribute('collapsed');
splitter.setAttribute('state', '');
splitter.setAttribute('substate', 'after');
}
window.dispatchEvent(new Event('resize'));
this.render();
}
static get observedAttributes() {
return ['pinnedPane'];
}
attributeChangedCallback() {
this.render();
}
scrollToPane(id, behavior = 'smooth') {
// If the itemPane is collapsed, just remember which pane needs to be scrolled to
// when itemPane is expanded.
if (this._collapsed) {
this._pendingPane = id;
return;
}
if (this._contextNotesPane && this._contextNotesPaneVisible) {
this._contextNotesPaneVisible = false;
behavior = 'instant';
}
let pane = this.getPane(id);
if (!pane) return;
// The pane should always be at the very top
// If there isn't enough stuff below it for it to be at the top, we add padding
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
this._makeSpaceForPane(pane);
if (behavior == 'smooth') {
this._disableScrollHandler = true;
this._waitForScroll().then(() => this._disableScrollHandler = false);
}
pane.scrollIntoView({ block: 'start', behavior });
pane.focus();
}
_makeSpaceForPane(pane) {
let oldMinScrollHeight = this._minScrollHeight;
let newMinScrollHeight = this._getMinScrollHeightForPane(pane);
if (newMinScrollHeight > oldMinScrollHeight) {
this._minScrollHeight = newMinScrollHeight;
}
}
_getMinScrollHeightForPane(pane) {
let paneRect = pane.getBoundingClientRect();
let containerRect = this._container.getBoundingClientRect();
// No offsetTop property for XUL elements
let offsetTop = paneRect.top - containerRect.top + this._container.scrollTop;
return offsetTop + containerRect.height;
}
_handleContainerScroll = () => {
// Don't scroll hidden pane
if (this.hidden || this._disableScrollHandler) return;
let minHeight = this._minScrollHeight;
if (minHeight) {
let newMinScrollHeight = this._container.scrollTop + this._container.clientHeight;
// Ignore overscroll (which generates scroll events on Windows 11, unlike on macOS)
// and don't shrink below the pinned pane's min scroll height
if (newMinScrollHeight > this._container.scrollHeight
|| this.pinnedPane && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
return;
}
this._minScrollHeight = newMinScrollHeight;
}
};
async _waitForScroll() {
let scrollPromise = Zotero.Promise.defer();
let lastScrollTop = this._container.scrollTop;
const waitFrame = async () => {
return new Promise((resolve) => {
requestAnimationFrame(resolve);
});
};
const waitFrames = async (n) => {
for (let i = 0; i < n; i++) {
await waitFrame();
}
};
const checkScrollStart = () => {
// If the scrollTop is not changed, wait for scroll to happen
if (lastScrollTop === this._container.scrollTop) {
requestAnimationFrame(checkScrollStart);
}
// Wait for scroll to end
else {
requestAnimationFrame(checkScrollEnd);
}
};
const checkScrollEnd = async () => {
// Wait for 3 frames to make sure not further scrolls
await waitFrames(3);
if (lastScrollTop === this._container.scrollTop) {
scrollPromise.resolve();
}
else {
lastScrollTop = this._container.scrollTop;
requestAnimationFrame(checkScrollEnd);
}
};
checkScrollStart();
// Abort after 3 seconds, which should be enough
return Promise.race([
scrollPromise.promise,
Zotero.Promise.delay(3000)
]);
}
getPanes() {
return Array.from(this.container.querySelectorAll(':scope > [data-pane]:not([hidden])'));
}
getPane(id) {
return this.container.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
}
isPanePinnable(id) {
return id !== 'info' && id !== 'context-all-notes' && id !== 'context-item-notes';
}
showPendingPane() {
if (!this._pendingPane || this._collapsed) return;
this.scrollToPane(this._pendingPane, 'instant');
this._pendingPane = null;
}
init() {
if (!this.container) {
this.container = document.getElementById('zotero-view-item');
}
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
let pane = toolbarbutton.dataset.pane;
if (pane === 'context-notes') {
toolbarbutton.addEventListener('click', (event) => {
if (event.button !== 0) {
return;
}
if (event.detail == 2) {
this.pinnedPane = null;
}
this._contextNotesPaneVisible = true;
});
continue;
}
else if (pane === 'toggle-collapse') {
toolbarbutton.addEventListener('click', (event) => {
if (event.button !== 0) {
return;
}
this._collapsed = !this._collapsed;
});
continue;
}
let pinnable = this.isPanePinnable(pane);
toolbarbutton.parentElement.classList.toggle('pinnable', pinnable);
toolbarbutton.addEventListener('click', (event) => {
if (event.button !== 0) {
return;
}
let scrollType = this._collapsed ? 'instant' : 'smooth';
this._collapsed = false;
switch (event.detail) {
case 1:
this.scrollToPane(pane, scrollType);
break;
case 2:
if (this.pinnedPane == pane || !pinnable) {
this.pinnedPane = null;
}
else {
this.pinnedPane = pane;
}
break;
}
});
if (pinnable) {
toolbarbutton.addEventListener('contextmenu', (event) => {
this._contextMenuTarget = pane;
@ -409,24 +206,23 @@
}
}
this.addEventListener('click', this.handleButtonClick);
this.querySelector('.zotero-menuitem-pin').addEventListener('command', () => {
this.scrollToPane(this._contextMenuTarget, 'smooth');
this.container.scrollToPane(this._contextMenuTarget, 'smooth');
this.pinnedPane = this._contextMenuTarget;
});
this.querySelector('.zotero-menuitem-unpin').addEventListener('command', () => {
this.pinnedPane = null;
});
this.render();
}
render(force = false) {
// TEMP: only render sidenav when pane is visible
if (!force && this.container.id === "zotero-view-item"
&& document.querySelector("#zotero-item-pane-content").selectedIndex !== "1"
) {
return;
}
destroy() {
this.removeEventListener('click', this.handleButtonClick);
}
render() {
if (!this.container) return;
let contextNotesPaneVisible = this._contextNotesPaneVisible;
let pinnedPane = this.pinnedPane;
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
@ -460,7 +256,7 @@
}
toolbarbutton.setAttribute('aria-selected', !contextNotesPaneVisible && pane == pinnedPane);
toolbarbutton.parentElement.hidden = !this.getPane(pane);
toolbarbutton.parentElement.hidden = !this.container.getPane(pane);
// Set .pinned on the container, for pin styling
toolbarbutton.parentElement.classList.toggle('pinned', pane == pinnedPane);
@ -470,6 +266,96 @@
this.querySelector('.highlight-notes-inactive').classList.toggle('highlight',
this._contextNotesPane && !contextNotesPaneVisible);
}
updatePaneStatus(paneID) {
if (!paneID) {
this.render();
return;
}
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
if (!toolbarbutton) return;
toolbarbutton.parentElement.hidden = !this.container.getPane(paneID);
if (this.pinnedPane) {
if (paneID == this.pinnedPane && !toolbarbutton.parentElement.classList.contains("pinned")) {
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
toolbarbutton.parentElement.classList.add('pinned');
}
}
else {
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
}
}
toggleDefaultStatus(isDefault) {
this._defaultStatus = isDefault;
this.renderDefaultStatus();
}
renderDefaultStatus() {
if (this._defaultStatus) {
this.querySelectorAll('toolbarbutton').forEach((elem) => {
elem.disabled = true;
elem.parentElement.hidden = !(
["info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"]
.includes(elem.dataset.pane));
});
}
else {
this.querySelectorAll('toolbarbutton').forEach((elem) => {
elem.disabled = false;
});
this.render(true);
}
}
handleButtonClick = (event) => {
let toolbarbutton = event.target;
let pane = toolbarbutton.dataset.pane;
if (!pane) return;
switch (pane) {
case "context-notes":
if (event.button !== 0) {
return;
}
if (event.detail == 2) {
this.pinnedPane = null;
}
this._contextNotesPaneVisible = true;
break;
case "toggle-collapse":
if (event.button !== 0) {
return;
}
this._collapsed = !this._collapsed;
break;
default: {
if (event.button !== 0) {
return;
}
let pinnable = this.isPanePinnable(pane);
let scrollType = this._collapsed ? 'instant' : 'smooth';
if (this._collapsed) this._collapsed = false;
switch (event.detail) {
case 1:
if (this._contextNotesPane && this._contextNotesPaneVisible) {
this._contextNotesPaneVisible = false;
scrollType = 'instant';
}
this.container.scrollToPane(pane, scrollType);
break;
case 2:
if (this.pinnedPane == pane || !pinnable) {
this.pinnedPane = null;
}
else {
this.pinnedPane = pane;
}
break;
}
}
}
this.render();
};
}
customElements.define("item-pane-sidenav", ItemPaneSidenav);
}

View file

@ -28,7 +28,7 @@
import { getCSSIcon } from 'components/icons';
{
class LibrariesCollectionsBox extends XULElementBase {
class LibrariesCollectionsBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-libraries-collections" data-pane="libraries-collections" extra-buttons="add">
<html:div class="body"/>
@ -61,11 +61,7 @@ import { getCSSIcon } from 'components/icons';
return;
}
this._item = item;
// Getting linked items is an async process, so start by rendering without them
this._linkedItems = [];
this.render();
this._updateLinkedItems();
}
get mode() {
@ -75,33 +71,25 @@ import { getCSSIcon } from 'components/icons';
set mode(mode) {
this._mode = mode;
this.setAttribute('mode', mode);
this.render();
}
init() {
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'librariesCollectionsBox');
this._body = this.querySelector('.body');
this._section = this.querySelector('collapsible-section');
this._section.addEventListener('add', (event) => {
this.querySelector('.add-popup').openPopupAtScreen(
event.detail.button.screenX,
event.detail.button.screenY,
true
);
this._section.open = true;
});
this.render();
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAdd);
}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
this._section.removeEventListener('add', this._handleAdd);
}
notify(action, type, ids) {
if (action == 'modify'
&& this._item
&& (ids.includes(this._item.id) || this._linkedItems.some(item => ids.includes(item.id)))) {
this.render();
this.render(true);
}
}
@ -238,27 +226,53 @@ import { getCSSIcon } from 'components/icons';
return row;
}
async _updateLinkedItems() {
render(force = false) {
if (!this._item) return;
if (!this._section.open) return;
if (!force && this._isAlreadyRendered()) return;
this._body.replaceChildren();
for (let item of [this._item]) {
this._addObject(Zotero.Libraries.get(item.libraryID), item);
for (let collection of Zotero.Collections.get(item.getCollections())) {
this._addObject(collection, item);
}
}
if (force) {
this.secondaryRender();
}
}
async secondaryRender() {
if (!this._item) {
return;
}
// Skip if already rendered
if (this._linkedItems.length > 0) {
return;
}
this._linkedItems = (await Promise.all(Zotero.Libraries.getAll()
.filter(lib => lib.libraryID !== this._item.libraryID)
.map(lib => this._item.getLinkedItem(lib.libraryID, true))))
.filter(Boolean);
this.render();
}
render() {
if (!this._item) {
return;
}
this._body.replaceChildren();
for (let item of [this._item, ...this._linkedItems]) {
for (let item of this._linkedItems) {
this._addObject(Zotero.Libraries.get(item.libraryID), item);
for (let collection of Zotero.Collections.get(item.getCollections())) {
this._addObject(collection, item);
}
}
}
_handleAdd = (event) => {
this.querySelector('.add-popup').openPopupAtScreen(
event.detail.button.screenX,
event.detail.button.screenY,
true
);
this._section.open = true;
};
}
customElements.define("libraries-collections-box", LibrariesCollectionsBox);
}

View file

@ -40,6 +40,7 @@
this._destroyed = false;
this.content = MozXULElement.parseXULToFragment(`
<html:div class="custom-head empty"></html:div>
<box flex="1" tooltip="html-tooltip" style="display: flex; flex-grow: 1" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<div id="note-editor" style="display: flex;flex-direction: column;flex-grow: 1;" xmlns="http://www.w3.org/1999/xhtml">
<iframe id="editor-view" style="border: 0;width: 100%;flex-grow: 1;" src="resource://zotero/note-editor/editor.html" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" type="content"/>
@ -321,6 +322,20 @@
}
}
renderCustomHead(callback) {
let customHead = this.querySelector(".custom-head");
customHead.replaceChildren();
let append = (...args) => {
customHead.append(...args);
};
if (callback) callback({
doc: document,
append: (...args) => {
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
}
});
}
_id(id) {
return this.querySelector(`#${id}`);
}
@ -394,6 +409,8 @@
}
refresh() {
this._id('related').render();
this._id('tags').render();
}
_id(id) {

View file

@ -28,29 +28,24 @@
import { getCSSItemTypeIcon } from 'components/icons';
{
class NotesBox extends XULElementBase {
class NotesBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-notes" data-pane="notes" extra-buttons="add">
<html:div class="body"/>
</collapsible-section>
`);
constructor() {
super();
init() {
this._mode = 'view';
this._item = null;
this._noteIDs = [];
}
init() {
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAdd);
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'notesBox');
}
destroy() {
this._section = null;
this._section.removeEventListener('add', this._handleAdd);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
@ -86,19 +81,19 @@ import { getCSSItemTypeIcon } from 'components/icons';
return;
}
this._item = val;
this._refresh();
}
notify(event, type, ids, extraData) {
notify(event, type, ids, _extraData) {
if (['modify', 'delete'].includes(event) && ids.some(id => this._noteIDs.includes(id))) {
this._refresh();
this.render(true);
}
}
_refresh() {
render(force = false) {
if (!this._item) {
return;
}
if (!force && this._isAlreadyRendered()) return;
this._noteIDs = this._item.getNotes();

View file

@ -26,7 +26,7 @@
"use strict";
{
class PaneHeader extends XULElementBase {
class PaneHeader extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="head">
<html:div class="title">
@ -44,6 +44,8 @@
</toolbarbutton>
</html:div>
</html:div>
<html:div class="custom-head">
</html:div>
`, ['chrome://zotero/locale/zotero.dtd']);
showInFeeds = true;
@ -61,7 +63,6 @@
set item(item) {
this.blurOpenField();
this._item = item;
this.render();
}
get mode() {
@ -70,7 +71,6 @@
set mode(mode) {
this._mode = mode;
this.render();
}
init() {
@ -85,7 +85,7 @@
if (!this._item) return;
event.preventDefault();
let menupopup = ZoteroItemPane.buildFieldTransformMenu({
let menupopup = ZoteroPane.buildFieldTransformMenu({
target: this.titleField,
onTransform: (newValue) => {
this._setTransformedValue(newValue);
@ -95,8 +95,6 @@
menupopup.addEventListener('popuphidden', () => menupopup.remove());
menupopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true);
});
this.render();
}
destroy() {
@ -105,7 +103,7 @@
notify(action, type, ids) {
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
this.render();
this.render(true);
}
}
@ -124,7 +122,7 @@
this.item.setField(this._titleFieldID, this.titleField.value);
await this.item.saveTx();
}
this.render();
this.render(true);
}
async blurOpenField() {
@ -134,10 +132,11 @@
}
}
render() {
render(force = false) {
if (!this.item) {
return;
}
if (!force && this._isAlreadyRendered()) return;
this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
@ -156,6 +155,20 @@
}
this.menuButton.hidden = !this.item.isRegularItem() && !this.item.isAttachment();
}
renderCustomHead(callback) {
let customHead = this.querySelector(".custom-head");
customHead.replaceChildren();
let append = (...args) => {
customHead.append(...args);
};
if (callback) callback({
doc: document,
append: (...args) => {
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
}
});
}
}
customElements.define("pane-header", PaneHeader);
}

View file

@ -28,29 +28,24 @@
import { getCSSItemTypeIcon } from 'components/icons';
{
class RelatedBox extends XULElementBase {
class RelatedBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-related" data-pane="related" extra-buttons="add">
<html:div class="body"/>
</collapsible-section>
`);
constructor() {
super();
init() {
this._mode = 'view';
this._item = null;
}
init() {
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._section.addEventListener('add', this.add);
}
destroy() {
this._section.removeEventListener('add', this.add);
Zotero.Notifier.unregisterObserver(this._notifierID);
this._section = null;
}
get mode() {
@ -78,7 +73,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
set item(val) {
this._item = val;
this.refresh();
}
notify(event, type, ids, _extraData) {
@ -86,7 +80,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
// Refresh if this item has been modified
if (event == 'modify' && ids.includes(this._item.id)) {
this.refresh();
this.render(true);
return;
}
@ -96,14 +90,17 @@ import { getCSSItemTypeIcon } from 'components/icons';
let relatedItemIDs = new Set(this._item.relatedItems.map(key => Zotero.Items.getIDFromLibraryAndKey(libraryID, key)));
for (let id of ids) {
if (relatedItemIDs.has(id)) {
this.refresh();
this.render(true);
return;
}
}
}
}
refresh() {
render(force = false) {
if (!this.item) return;
if (!force && this._isAlreadyRendered()) return;
let body = this.querySelector('.body');
body.replaceChildren();

View file

@ -25,14 +25,17 @@
"use strict";
/**
* A split menubutton with a clickable left side and a dropmarker that opens a menu.
*/
{
/**
* A split menubutton with a clickable left side and a dropmarker that opens a menu.
*/
class SplitMenuButton extends HTMLButtonElement {
_image = null;
_label = null;
_commandListenerCache = [];
constructor() {
super();
@ -54,23 +57,45 @@
}
connectedCallback() {
this.append(this.constructor.contentFragment);
this.append(this.contentFragment);
}
// Prevent DOM-attached mouse handlers from running in the dropmarker area
for (const eventType of ['mousedown', 'mouseup', 'click']) {
const handler = this.getAttribute('on' + eventType);
if (!handler) {
continue;
}
this['on' + eventType] = null;
this.addEventListener(eventType, (event) => {
if (!this._isEventInDropmarkerBox(event)) {
eval(handler).bind(this);
}
});
disconnectedCallback() {
while (this._commandListenerCache.length) {
let cache = this._commandListenerCache.pop();
super.removeEventListener("click", cache.actual);
}
}
addEventListener(type, listener, options) {
if (type == "command") {
let newListener = (event) => {
if (!this._isEventInDropmarkerBox(event)) {
listener(event);
}
};
this._commandListenerCache.push({
original: listener,
actual: newListener
});
super.addEventListener("click", newListener, options);
return;
}
super.addEventListener(type, listener, options);
}
removeEventListener(type, listener, options) {
if (type == "command") {
let cacheIndex = this._commandListenerCache.findIndex(cache => cache.original == listener);
if (cacheIndex != -1) {
let cache = this._commandListenerCache.splice(cacheIndex, 1)[0];
super.removeEventListener("click", cache.actual, options);
return;
}
}
super.removeEventListener(type, listener, options);
}
get image() {
return this.querySelector('[anonid="button-image"]').src;
}
@ -87,7 +112,7 @@
this.querySelector('[anonid="button-text"]').textContent = value;
}
static get contentFragment() {
get contentFragment() {
// Zotero.hiDPI[Suffix] may not have been initialized yet, so calculate it ourselves
let hiDPISuffix = window.devicePixelRatio > 1 ? '@2x' : '';
let frag = document.importNode(
@ -109,7 +134,7 @@
_isEventInDropmarkerBox(event) {
let rect = this.querySelector('[anonid="dropmarker-box"]').getBoundingClientRect();
return !Zotero.rtl && event.clientX >= rect.left || Zotero.rtl && event.clientX <= rect.right
return !Zotero.rtl && event.clientX >= rect.left || Zotero.rtl && event.clientX <= rect.right;
}
}

View file

@ -26,20 +26,8 @@
"use strict";
{
class TagsBox extends XULElement {
constructor() {
super();
this.count = 0;
this.clickHandler = null;
this._tabDirection = null;
this._tagColors = [];
this._notifierID = null;
this._mode = 'view';
this._item = null;
this.content = MozXULElement.parseXULToFragment(`
class TagsBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-tags" data-pane="tags" extra-buttons="add">
<html:div class="body">
<html:div id="rows" class="tags-box-list"/>
@ -52,16 +40,18 @@
</html:div>
</collapsible-section>
`, ['chrome://zotero/locale/zotero.dtd']);
}
connectedCallback() {
this._destroyed = false;
window.addEventListener("unload", this.destroy);
init() {
this.count = 0;
this.clickHandler = null;
let content = document.importNode(this.content, true);
this.append(content);
this._tabDirection = null;
this._tagColors = [];
this._notifierID = null;
this._mode = 'view';
this._item = null;
this._section = this.querySelector('collapsible-section');
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAddButtonClick);
this.addEventListener('click', (event) => {
if (event.target === this) {
@ -100,21 +90,10 @@
}
destroy() {
if (this._destroyed) {
return;
}
window.removeEventListener("unload", this.destroy);
this._destroyed = true;
this._section = null;
this._section.removeEventListener('add', this._handleAddButtonClick);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
disconnectedCallback() {
this.replaceChildren();
this.destroy();
}
get mode() {
return this._mode;
}
@ -151,12 +130,11 @@
return;
}
this._item = val;
this.reload();
}
notify(event, type, ids, extraData) {
if (type == 'setting' && ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
this.reload();
this.render(true);
}
else if (type == 'item-tag') {
let itemID, _tagID;
@ -186,11 +164,14 @@
this.updateCount();
}
else if (type == 'tag' && event == 'modify') {
this.reload();
this.render(true);
}
}
reload() {
render(force = false) {
if (!this.item) return;
if (!force && this._isAlreadyRendered()) return;
Zotero.debug('Reloading tags box');
// Cancel field focusing while we're updating
@ -316,7 +297,7 @@
await item.saveTx();
}
catch (e) {
this.reload();
this.render(true);
throw e;
}
}
@ -490,7 +471,7 @@
await this.item.saveTx();
}
catch (e) {
this.reload();
this.render(true);
throw e;
}
}
@ -507,7 +488,7 @@
await this.item.saveTx();
}
catch (e) {
this.reload();
this.render(true);
throw e;
}
}
@ -530,7 +511,7 @@
tags.forEach(tag => this.item.addTag(tag));
await this.item.saveTx();
this.reload();
this.render(true);
}
// Single tag at end
else {
@ -548,7 +529,7 @@
await this.item.saveTx();
}
catch (e) {
this.reload();
this.render(true);
throw e;
}
}

View file

@ -1,352 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
var ZoteroItemPane = new function() {
var _container;
var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _tagsBox, _notesBox, _librariesCollectionsBox, _relatedBox, _boxes;
var _deck;
var _lastItem;
var _selectedNoteID;
var _translationTarget;
this.onLoad = function () {
if (!Zotero) {
return;
}
_container = document.getElementById('zotero-view-item-container');
_header = document.getElementById('zotero-item-pane-header');
_sidenav = document.getElementById('zotero-view-item-sidenav');
_scrollParent = document.getElementById('zotero-view-item');
_itemBox = document.getElementById('zotero-editpane-item-box');
_abstractBox = document.getElementById('zotero-editpane-abstract');
_notesBox = document.getElementById('zotero-editpane-notes');
_attachmentsBox = document.getElementById('zotero-editpane-attachments');
_attachmentInfoBox = document.getElementById('zotero-attachment-box');
_attachmentAnnotationsBox = document.getElementById('zotero-editpane-attachment-annotations');
_tagsBox = document.getElementById('zotero-editpane-tags');
_librariesCollectionsBox = document.getElementById('zotero-editpane-libraries-collections');
_relatedBox = document.getElementById('zotero-editpane-related');
_boxes = [_itemBox, _abstractBox, _notesBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _librariesCollectionsBox, _tagsBox, _relatedBox];
_deck = document.getElementById('zotero-item-pane-content');
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
_container.addEventListener("keypress", this.handleKeypress);
};
this.onUnload = function () {
Zotero.Notifier.unregisterObserver(this._unregisterID);
};
/*
* Load a top-level item
*/
this.viewItem = Zotero.Promise.coroutine(function* (item, mode, pinnedPane) {
Zotero.debug('Viewing item');
_notesBox.parentItem = item;
let isSameItem = _lastItem?.id === item.id;
_lastItem = item;
_container.classList.toggle('feed-item', !!item.isFeedItem);
if (item.isFeedItem) {
let lastTranslationTarget = Zotero.Prefs.get('feeds.lastTranslationTarget');
if (lastTranslationTarget) {
let id = parseInt(lastTranslationTarget.substr(1));
if (lastTranslationTarget[0] == "L") {
_translationTarget = Zotero.Libraries.get(id);
}
else if (lastTranslationTarget[0] == "C") {
_translationTarget = Zotero.Collections.get(id);
}
}
if (!_translationTarget) {
_translationTarget = Zotero.Libraries.userLibrary;
}
this.setTranslateButton();
}
let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
for (let box of [_header, ..._boxes]) {
if (!box.showInFeeds && item.isFeedItem) {
box.style.display = 'none';
box.hidden = true;
continue;
}
else {
box.style.display = '';
box.hidden = false;
}
if (mode) {
box.mode = mode;
if (box.mode == 'view') {
box.hideEmptyFields = true;
}
}
else {
box.mode = 'edit';
}
box.item = item;
box.inTrash = inTrash;
}
if (!isSameItem) {
if (pinnedPane && !_sidenav.getPane(pinnedPane)) {
pinnedPane = "";
}
_scrollParent.style.paddingBottom = '';
if (pinnedPane) {
_sidenav.scrollToPane(pinnedPane, 'instant');
_sidenav.pinnedPane = pinnedPane;
}
else if (pinnedPane !== false) {
_sidenav.scrollToPane(_sidenav.getPanes()[0]?.getAttribute('data-pane'), 'instant');
}
}
_sidenav.render();
});
this.notify = Zotero.Promise.coroutine(function* (action, _type, _ids, _extraData) {
if (action == 'refresh' && _lastItem) {
yield this.viewItem(_lastItem, null, false);
}
});
this.blurOpenField = async function () {
if (_itemBox.contains(document.activeElement)) {
await _itemBox.blurOpenField();
}
else if (_header.contains(document.activeElement)) {
await _header.blurOpenField();
}
_scrollParent.focus();
};
this.onNoteSelected = function (item, editable) {
_selectedNoteID = item.id;
var noteEditor = document.getElementById('zotero-note-editor');
noteEditor.mode = editable ? 'edit' : 'view';
noteEditor.viewMode = 'library';
noteEditor.parent = null;
noteEditor.item = item;
document.getElementById('zotero-item-pane-content').selectedIndex = 2;
};
// Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
this.handleKeypress = function (event) {
let stopEvent = () => {
event.preventDefault();
event.stopPropagation();
};
let isLibraryTab = Zotero_Tabs.selectedIndex == 0;
let sidenav = document.getElementById(
isLibraryTab ? 'zotero-view-item-sidenav' : 'zotero-context-pane-sidenav'
);
// Tab from the scrollable area focuses the pinned pane if it exists
if (event.target.classList.contains("zotero-view-item") && event.key == "Tab" && !event.shiftKey && sidenav.pinnedPane) {
let pane = sidenav.getPane(sidenav.pinnedPane);
pane.firstChild._head.focus();
stopEvent();
return;
}
// Tab tavigation between entries and buttons within library, related and notes boxes
if (event.key == "Tab" && event.target.closest(".box")) {
let next = null;
if (event.key == "Tab" && !event.shiftKey) {
next = event.target.nextElementSibling;
}
if (event.key == "Tab" && event.shiftKey) {
next = event.target.parentNode.previousElementSibling?.lastChild;
}
// Force the element to be visible before focusing
if (next) {
next.style.visibility = "visible";
next.focus();
next.style.removeProperty("visibility");
stopEvent();
}
}
};
/**
* Select the parent item and open the note editor
*/
this.openNoteWindow = async function () {
var selectedNote = Zotero.Items.get(_selectedNoteID);
ZoteroPane.openNoteWindow(selectedNote.id);
};
this.translateSelectedItems = Zotero.Promise.coroutine(function* () {
var collectionID = _translationTarget.objectType == 'collection' ? _translationTarget.id : undefined;
var items = ZoteroPane_Local.itemsView.getSelectedItems();
for (let item of items) {
yield item.translate(_translationTarget.libraryID, collectionID);
}
});
this.buildTranslateSelectContextMenu = function (event) {
var menu = document.getElementById('zotero-item-addTo-menu');
// Don't trigger rebuilding on nested popupmenu open/close
if (event.target != menu) {
return;
}
// Clear previous items
while (menu.firstChild) {
menu.removeChild(menu.firstChild);
}
let target = Zotero.Prefs.get('feeds.lastTranslationTarget');
if (!target) {
target = "L" + Zotero.Libraries.userLibraryID;
}
var libraries = Zotero.Libraries.getAll();
for (let library of libraries) {
if (!library.editable || library.libraryType == 'publications') {
continue;
}
Zotero.Utilities.Internal.createMenuForTarget(
library,
menu,
target,
function(event, libraryOrCollection) {
if (event.target.tagName == 'menu') {
Zotero.Promise.coroutine(function* () {
// Simulate menuitem flash on OS X
if (Zotero.isMac) {
event.target.setAttribute('_moz-menuactive', false);
yield Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', true);
yield Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', false);
yield Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', true);
}
menu.hidePopup();
ZoteroItemPane.setTranslationTarget(libraryOrCollection);
event.stopPropagation();
})();
}
else {
ZoteroItemPane.setTranslationTarget(libraryOrCollection);
event.stopPropagation();
}
}
);
}
};
this.setTranslateButton = function() {
var label = Zotero.getString('pane.item.addTo', _translationTarget.name);
var elem = document.getElementById('zotero-feed-item-addTo-button');
elem.label = label;
var key = Zotero.Keys.getKeyForCommand('saveToZotero');
var tooltip = label
+ (Zotero.rtl ? ' \u202B' : ' ') + '('
+ (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
+ key + ')';
elem.title = tooltip;
elem.image = _translationTarget.treeViewImage;
};
this.setTranslationTarget = function(translationTarget) {
_translationTarget = translationTarget;
Zotero.Prefs.set('feeds.lastTranslationTarget', translationTarget.treeViewID);
ZoteroItemPane.setTranslateButton();
};
this.setReadLabel = function (isRead) {
var elem = document.getElementById('zotero-feed-item-toggleRead-button');
var label = Zotero.getString('pane.item.' + (isRead ? 'markAsUnread' : 'markAsRead'));
elem.textContent = label;
var key = Zotero.Keys.getKeyForCommand('toggleRead');
var tooltip = label + (Zotero.rtl ? ' \u202B' : ' ') + '(' + key + ')';
elem.title = tooltip;
};
this.getPinnedPane = function () {
return _sidenav.pinnedPane;
};
this.buildFieldTransformMenu = function ({ target, onTransform }) {
let value = target.value;
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
let menupopup = document.createXULElement('menupopup');
let titleCase = document.createXULElement('menuitem');
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
titleCase.addEventListener('command', () => {
onTransform(valueTitleCased);
});
titleCase.disabled = valueTitleCased == value;
menupopup.append(titleCase);
let sentenceCase = document.createXULElement('menuitem');
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
sentenceCase.addEventListener('command', () => {
onTransform(valueSentenceCased);
});
sentenceCase.disabled = valueSentenceCased == value;
menupopup.append(sentenceCase);
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, target);
return menupopup;
};
};
addEventListener("load", function(e) { ZoteroItemPane.onLoad(e); }, false);
addEventListener("unload", function(e) { ZoteroItemPane.onUnload(e); }, false);

View file

@ -581,7 +581,7 @@ var ItemTree = class ItemTree extends LibraryTree {
}
}
else if (collectionTreeRow.isFeedsOrFeed()) {
window.ZoteroPane.updateReadLabel();
// Moved to itemPane CE
}
// If not a search, process modifications manually
else {
@ -1173,7 +1173,7 @@ var ItemTree = class ItemTree extends LibraryTree {
// Single item
if (rowsToSelect.length == 1) {
// this.selection.select() triggers the tree onSelect handler attribute, which calls
// ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(), which refreshes the
// ZoteroPane.itemSelected(), which calls ZoteroPane.itemPane.render(), which refreshes the
// itembox. But since the 'onselect' doesn't handle promises, itemSelected() isn't waited for
// here, which means that 'yield selectItem(itemID)' continues before the itembox has been
// refreshed. To get around this, we wait for a select event that's triggered by

View file

@ -753,8 +753,7 @@ var Zotero_Tabs = new function () {
// Used to move focus back to itemTree or contextPane from the tabs.
this.focusWrapAround = function () {
// If no item is selected, focus items list.
const pane = document.getElementById("zotero-item-pane-content");
if (pane.selectedIndex === "0") {
if (ZoteroPane.itemPane.viewType == "message") {
document.getElementById("item-tree-main-default").focus();
}
else {

View file

@ -33,6 +33,7 @@ var ZoteroPane = new function()
var _unserialized = false;
this.collectionsView = false;
this.itemsView = false;
this.itemPane = false;
this.progressWindow = false;
this._listeners = {};
this.__defineGetter__('loaded', function () { return _loaded; });
@ -113,6 +114,7 @@ var ZoteroPane = new function()
this.itemsView?.updateFontSize();
});
Zotero.UIProperties.registerRoot(document.getElementById('zotero-context-pane'));
this.itemPane = document.querySelector("#zotero-item-pane");
ZoteroPane_Local.updateLayout();
ZoteroPane_Local.updateToolbarPosition();
this.updateWindow();
@ -1219,8 +1221,7 @@ var ZoteroPane = new function()
if (this.itemsView && from == this.itemsView.id) {
// Focus TinyMCE explicitly on tab key, since the normal focusing doesn't work right
if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) {
var deck = document.getElementById('zotero-item-pane-content');
if (deck.selectedPanel.id == 'zotero-view-note') {
if (ZoteroPane.itemPane.viewType == "note") {
document.getElementById('zotero-note-editor').focus();
event.preventDefault();
return;
@ -1310,7 +1311,7 @@ var ZoteroPane = new function()
case 'saveToZotero':
var collectionTreeRow = this.getCollectionTreeRow();
if (collectionTreeRow.isFeedsOrFeed()) {
ZoteroItemPane.translateSelectedItems();
this.itemPane.translateSelectedItems();
} else {
Zotero.debug(command + ' does not do anything in non-feed views')
}
@ -1369,7 +1370,7 @@ var ZoteroPane = new function()
}
}
yield ZoteroItemPane.blurOpenField();
yield this.itemPane._itemDetails.blurOpenField();
if (row !== undefined && row !== null) {
var collectionTreeRow = this.collectionsView.getRow(row);
@ -1395,9 +1396,8 @@ var ZoteroPane = new function()
});
// Expand the item pane if it's closed
var itemPane = document.getElementById("zotero-item-pane");
if (itemPane.getAttribute("collapsed") == "true") {
itemPane.setAttribute("collapsed", false)
if (this.itemPane.getAttribute("collapsed") == "true") {
this.itemPane.setAttribute("collapsed", false);
}
//set to Info tab
@ -1857,12 +1857,27 @@ var ZoteroPane = new function()
Zotero.debug("Items view not available in itemSelected", 2);
return false;
}
let collectionTreeRow = this.getCollectionTreeRow();
// I don't think this happens in normal usage, but it can happen during tests
if (!collectionTreeRow) {
return false;
}
var selectedItems = this.itemsView.getSelectedItems();
// Display buttons at top of item pane depending on context. This needs to run even if the
// selection hasn't changed, because the selected items might have been modified.
this.updateItemPaneButtons(selectedItems);
this.itemPane.data = selectedItems;
let viewMode = {
isFeedsOrFeed: collectionTreeRow.isFeedsOrFeed(),
isDuplicates: collectionTreeRow.isDuplicates(),
isPublications: collectionTreeRow.isPublications(),
isTrash: collectionTreeRow.isTrash(),
rowCount: this.itemsView.rowCount,
};
this.itemPane.viewMode = viewMode;
this.itemPane.editable = this.collectionsView.editable;
this.itemPane.updateItemPaneButtons(selectedItems);
// Tab selection observer in standalone.js makes sure that
// updateQuickCopyCommands is called
@ -1880,163 +1895,7 @@ var ZoteroPane = new function()
}
_lastSelectedItems = ids;
var collectionTreeRow = this.getCollectionTreeRow();
// I don't think this happens in normal usage, but it can happen during tests
if (!collectionTreeRow) {
return false;
}
let pane = document.getElementById('zotero-item-pane');
let deck = document.getElementById('zotero-item-pane-content');
let sidenav = document.getElementById('zotero-view-item-sidenav');
let hideSidenav = false;
// Single item selected
if (selectedItems.length == 1) {
var item = selectedItems[0];
sidenav.querySelectorAll('toolbarbutton').forEach(button => button.disabled = false);
if (item.isNote()) {
hideSidenav = true;
ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
}
// Regular item
else {
var isCommons = collectionTreeRow.isBucket();
deck.selectedIndex = 1;
let pane = ZoteroItemPane.getPinnedPane();
var button = document.getElementById('zotero-item-show-original');
if (isCommons) {
button.hidden = false;
button.disabled = !this.getOriginalItem();
}
else {
button.hidden = true;
}
if (this.collectionsView.editable) {
yield ZoteroItemPane.viewItem(item, null, pane);
}
else {
yield ZoteroItemPane.viewItem(item, 'view', pane);
}
if (item.isFeedItem) {
// Too slow for now
// if (!item.isTranslated) {
// item.translate();
// }
this.updateReadLabel();
this.startItemReadTimeout(item.id);
}
}
}
// Zero or multiple items selected
else {
let defaultSidenavButtons = [
"info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"
];
sidenav.querySelectorAll('toolbarbutton').forEach((button) => {
button.disabled = true;
button.parentElement.hidden = !defaultSidenavButtons.includes(button.dataset.pane);
});
if (collectionTreeRow.isFeedsOrFeed()) {
this.updateReadLabel();
}
let count = selectedItems.length;
// Display duplicates merge interface in item pane
if (collectionTreeRow.isDuplicates()) {
if (!collectionTreeRow.editable) {
if (count) {
var msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
}
else {
var msg = Zotero.getString('pane.item.selected.zero');
}
this.setItemPaneMessage(msg);
}
else if (count) {
deck.selectedIndex = 3;
// Load duplicates UI code
if (typeof Zotero_Duplicates_Pane == 'undefined') {
Zotero.debug("Loading duplicatesMerge.js");
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader)
.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
}
// On a Select All of more than a few items, display a row
// count instead of the usual item type mismatch error
var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
// Initialize the merge pane with the selected items
Zotero_Duplicates_Pane.setItems(selectedItems, displayNumItemsOnTypeError);
}
else {
var msg = Zotero.getString('pane.item.duplicates.selectToMerge');
this.setItemPaneMessage(msg);
}
}
// Display label in the middle of the item pane
else {
if (count) {
var msg = Zotero.getString('pane.item.selected.multiple', count);
}
else {
var rowCount = this.itemsView.rowCount;
var str = 'pane.item.unselected.';
switch (rowCount){
case 0:
str += 'zero';
break;
case 1:
str += 'singular';
break;
default:
str += 'plural';
break;
}
var msg = Zotero.getString(str, [rowCount]);
}
this.setItemPaneMessage(msg);
return false;
}
}
if (!document.querySelector("#zotero-items-splitter").collapsed) {
let isStackedMode = Zotero.Prefs.get("layout") === "stacked";
const sidenavSize = 37;
if (hideSidenav && !sidenav.hidden) {
sidenav.hidden = true;
if (isStackedMode) {
pane.height = `${(pane.clientHeight) + sidenavSize}`;
}
else {
pane.width = `${(pane.clientWidth) + sidenavSize}`;
}
}
else if (!hideSidenav && sidenav.hidden) {
sidenav.hidden = false;
if (isStackedMode) {
pane.height = `${(pane.clientHeight) - sidenavSize}`;
}
else {
pane.width = `${(pane.clientWidth) - sidenavSize}`;
}
}
}
return true;
return this.itemPane.render();
}.bind(this))()
.catch(function (e) {
Zotero.logError(e);
@ -2048,56 +1907,6 @@ var ZoteroPane = new function()
}.bind(this));
};
/**
* Display buttons at top of item pane depending on context
*
* @param {Zotero.Item[]}
*/
this.updateItemPaneButtons = function (selectedItems) {
if (!selectedItems.length) {
document.querySelectorAll('.zotero-item-pane-top-buttons').forEach(x => x.hidden = true);
return;
}
// My Publications buttons
var isPublications = this.getCollectionTreeRow().isPublications();
// Show in My Publications view if selected items are all notes or non-linked-file attachments
var showMyPublicationsButtons = isPublications
&& selectedItems.every((item) => {
return item.isNote()
|| (item.isAttachment()
&& item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
});
var myPublicationsButtons = document.getElementById('zotero-item-pane-top-buttons-my-publications');
myPublicationsButtons.hidden = !showMyPublicationsButtons;
if (showMyPublicationsButtons) {
let button = myPublicationsButtons.firstChild;
let hiddenItemsSelected = selectedItems.some(item => !item.inPublications);
let str, onclick;
if (hiddenItemsSelected) {
str = 'showInMyPublications';
onclick = () => Zotero.Items.addToPublications(selectedItems);
}
else {
str = 'hideFromMyPublications';
onclick = () => Zotero.Items.removeFromPublications(selectedItems);
}
button.label = Zotero.getString('pane.item.' + str);
button.onclick = onclick;
}
// Trash button
let nonDeletedItemsSelected = selectedItems.some(item => !item.deleted);
document.getElementById('zotero-item-pane-top-buttons-trash').hidden
= !this.getCollectionTreeRow().isTrash() || nonDeletedItemsSelected;
// Feed buttons
document.getElementById('zotero-item-pane-top-buttons-feed').hidden
= !this.getCollectionTreeRow().isFeedsOrFeed()
};
this.updateAddAttachmentMenu = function (popup) {
if (!this.canEdit()) {
for (let node of popup.childNodes) {
@ -2436,17 +2245,10 @@ var ZoteroPane = new function()
return;
}
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
if (typeof Zotero_Duplicates_Pane == 'undefined') {
Zotero.debug("Loading duplicatesMerge.js");
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader)
.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
}
this.itemPane.viewType = "duplicates";
// Initialize the merge pane with the selected items
Zotero_Duplicates_Pane.setItems(this.getSelectedItems());
this.itemPane._duplicatesPane.setItems(this.getSelectedItems());
}
@ -2523,26 +2325,6 @@ var ZoteroPane = new function()
}
}
// Currently used only for Commons to find original linked item
this.getOriginalItem = function () {
var item = this.getSelectedItems()[0];
var collectionTreeRow = this.getCollectionTreeRow();
// TEMP: Commons buckets only
return collectionTreeRow.ref.getLocalItem(item);
}
this.showOriginalItem = function () {
var item = this.getOriginalItem();
if (!item) {
Zotero.debug("Original item not found");
return;
}
this.selectItem(item.id).done();
}
/**
* Check whether every selected item can be restored from trash
*
@ -4257,25 +4039,6 @@ var ZoteroPane = new function()
}
this.setItemPaneMessage = function (content) {
document.getElementById('zotero-item-pane-content').selectedIndex = 0;
var elem = document.getElementById('zotero-item-pane-message-box');
elem.textContent = '';
if (typeof content == 'string') {
let contentParts = content.split("\n\n");
for (let part of contentParts) {
let desc = document.createXULElement('description');
desc.appendChild(document.createTextNode(part));
elem.appendChild(desc);
}
}
else {
elem.appendChild(content);
}
}
/**
* @return {Promise<Integer|null|false>} - The id of the new note in non-popup mode, null in
* popup mode (where a note isn't created immediately), or false if library isn't editable
@ -4862,11 +4625,10 @@ var ZoteroPane = new function()
}
}
else if (item.isNote()) {
var type = Zotero.Libraries.get(item.libraryID).libraryType;
if (!this.collectionsView.editable) {
continue;
}
ZoteroItemPane.openNoteWindow();
ZoteroPane.openNoteWindow(item.id);
}
else if (item.isAttachment()) {
yield this.viewAttachment(item.id, event);
@ -6074,20 +5836,6 @@ var ZoteroPane = new function()
}
};
this.updateReadLabel = function () {
var items = this.getSelectedItems();
var isUnread = false;
for (let item of items) {
if (!item.isRead) {
isUnread = true;
break;
}
}
ZoteroItemPane.setReadLabel(!isUnread);
};
var itemReadPromise;
this.startItemReadTimeout = function (feedItemID) {
if (itemReadPromise) {
@ -6114,7 +5862,7 @@ var ZoteroPane = new function()
}
await feedItem.toggleRead(true);
ZoteroItemPane.setReadLabel(true);
this.itemPane.setReadLabel(true);
}.bind(this))
.catch(function (e) {
if (e instanceof Zotero.Promise.CancellationError) {
@ -6266,14 +6014,17 @@ var ZoteroPane = new function()
var itemsSplitter = document.getElementById("zotero-items-splitter");
var sidenav = document.getElementById("zotero-view-item-sidenav");
if(Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane
if (Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane
layoutSwitcher.setAttribute("orient", "vertical");
itemsSplitter.setAttribute("orient", "vertical");
sidenav.classList.add("stacked");
} else { // three-vertical-pane
this.itemPane.classList.add("stacked");
}
else { // three-vertical-pane
layoutSwitcher.setAttribute("orient", "horizontal");
itemsSplitter.setAttribute("orient", "horizontal");
sidenav.classList.remove("stacked");
this.itemPane.classList.remove("stacked");
}
this.updateToolbarPosition();
@ -6381,7 +6132,6 @@ var ZoteroPane = new function()
var collectionsPane = document.getElementById("zotero-collections-pane");
var tagSelector = document.getElementById("zotero-tag-selector");
var sidenav = document.getElementById("zotero-view-item-sidenav");
var collectionsPaneWidth = collectionsPane.getBoundingClientRect().width;
tagSelector.style.maxWidth = collectionsPaneWidth + 'px';
@ -6404,9 +6154,7 @@ var ZoteroPane = new function()
this.handleTagSelectorResize();
sidenav.render();
// If the itemPane has just been expanded, scroll to the correct pane
sidenav.showPendingPane();
this.itemPane.handleResize();
}
/**
@ -6436,20 +6184,50 @@ var ZoteroPane = new function()
* Implements nsIObserver for Zotero reload
*/
var _reloadObserver = {
/**
* Called when Zotero is reloaded (i.e., if it is switched into or out of connector mode)
*/
"observe":function(aSubject, aTopic, aData) {
if(aTopic == "zotero-reloaded") {
observe: function (aSubject, aTopic, aData) {
if (aTopic == "zotero-reloaded") {
Zotero.debug("Reloading Zotero pane");
for (let func of _reloadFunctions) func(aData);
} else if(aTopic == "zotero-before-reload") {
}
else if (aTopic == "zotero-before-reload") {
Zotero.debug("Zotero pane caught before-reload event");
for (let func of _beforeReloadFunctions) func(aData);
}
}
};
}
this.buildFieldTransformMenu = function ({ target, onTransform }) {
let value = target.value;
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
let menupopup = document.createXULElement('menupopup');
let titleCase = document.createXULElement('menuitem');
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
titleCase.addEventListener('command', () => {
onTransform(valueTitleCased);
});
titleCase.disabled = valueTitleCased == value;
menupopup.append(titleCase);
let sentenceCase = document.createXULElement('menuitem');
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
sentenceCase.addEventListener('command', () => {
onTransform(valueSentenceCased);
});
sentenceCase.disabled = valueSentenceCased == value;
menupopup.append(sentenceCase);
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, target);
return menupopup;
};
};
/**
* Keep track of which ZoteroPane was local (since ZoteroPane object might get swapped out for a

View file

@ -30,8 +30,6 @@
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform-version/content/style.css"?>
<?xml-stylesheet href="chrome://zotero/skin/itemPane.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/itemPane.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
<!DOCTYPE window [
@ -79,7 +77,6 @@
Services.scriptloader.loadSubScript("chrome://zotero/content/tabs.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/zoteroPane.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/itemPane.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/contextPane.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/fileInterface.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/reportInterface.js", this);
@ -1182,105 +1179,7 @@
</splitter>
<!-- itemPane.xul -->
<vbox id="zotero-item-pane" flex="0" zotero-persist="width height" height="300">
<!-- My Publications -->
<hbox id="zotero-item-pane-top-buttons-my-publications" class="zotero-item-pane-top-buttons" hidden="true">
<button id="zotero-item-collection-show-hide"/>
</hbox>
<!-- Trash -->
<hbox id="zotero-item-pane-top-buttons-trash" class="zotero-item-pane-top-buttons" hidden="true">
<button id="zotero-item-restore-button" label="&zotero.items.menu.restoreToLibrary;"
oncommand="ZoteroPane_Local.restoreSelectedItems()"/>
<button id="zotero-item-delete-button" label="&zotero.item.deletePermanently;"
oncommand="ZoteroPane_Local.deleteSelectedItems()"/>
</hbox>
<!-- Feed -->
<hbox id="zotero-item-pane-top-buttons-feed" class="zotero-item-pane-top-buttons" hidden="true">
<html:button id="zotero-feed-item-toggleRead-button"
onclick="ZoteroPane_Local.toggleSelectedItemsRead();"/>
<html:button is="split-menu-button" id="zotero-feed-item-addTo-button"
onclick="ZoteroItemPane.translateSelectedItems()"
popup="zotero-item-addTo-menu"/>
<menupopup id="zotero-item-addTo-menu" onpopupshowing="ZoteroItemPane.buildTranslateSelectContextMenu(event);"/>
</hbox>
<!-- Commons -->
<button id="zotero-item-show-original" label="Show Original"
oncommand="ZoteroPane_Local.showOriginalItem()" hidden="true"/>
<deck id="zotero-item-pane-content" class="zotero-item-pane-content" selectedIndex="0" flex="1">
<!-- Center label (for zero or multiple item selection) -->
<groupbox id="zotero-item-pane-groupbox" pack="center" align="center">
<vbox id="zotero-item-pane-message-box"/>
</groupbox>
<!-- Regular item -->
<!--
Keep in sync with contextPane.js (_addItemContext function) which
dynamically creates this itemPane part for each tab
-->
<hbox id="zotero-view-item-container" class="zotero-view-item-container">
<html:div class="zotero-view-item-main">
<pane-header id="zotero-item-pane-header" />
<html:div id="zotero-view-item" class="zotero-view-item" tabindex="0">
<item-box id="zotero-editpane-item-box" data-pane="info"/>
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract" data-pane="abstract"/>
<attachments-box id="zotero-editpane-attachments" data-pane="attachments"/>
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" data-pane="notes"/>
<attachment-box id="zotero-attachment-box" flex="1" data-pane="attachment-info" data-use-preview="true" hidden="true"/>
<attachment-annotations-box id="zotero-editpane-attachment-annotations" flex="1" data-pane="attachment-annotations" hidden="true"/>
<libraries-collections-box id="zotero-editpane-libraries-collections" class="zotero-editpane-libraries-collections" data-pane="libraries-collections"/>
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" data-pane="tags"/>
<related-box id="zotero-editpane-related" class="zotero-editpane-related" data-pane="related"/>
</html:div>
</html:div>
</hbox>
<!-- Note item -->
<groupbox id="zotero-view-note" flex="1">
<!--
'onerror' handler crashes the app on a save error to prevent typing in notes
while they're not being saved
-->
<note-editor id="zotero-note-editor" flex="1" notitle="1"
previousfocus="zotero-items-tree"/>
</groupbox>
<!-- Duplicate merging -->
<vbox id="zotero-duplicates-merge-pane">
<groupbox>
<button id="zotero-duplicates-merge-button" oncommand="Zotero_Duplicates_Pane.merge()"/>
</groupbox>
<groupbox id="zotero-duplicates-merge-version-select">
<description>&zotero.duplicatesMerge.versionSelect;</description>
<hbox>
<richlistbox id="zotero-duplicates-merge-original-date" onselect="Zotero_Duplicates_Pane.setMaster(this.selectedIndex)" rows="0"/>
</hbox>
</groupbox>
<groupbox flex="1">
<description id="zotero-duplicates-merge-field-select">&zotero.duplicatesMerge.fieldSelect;</description>
<vbox id="zotero-duplicates-merge-item-box-container" flex="1">
<item-box id="zotero-duplicates-merge-item-box" flex="1"/>
</vbox>
</groupbox>
</vbox>
</deck>
</vbox>
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
<item-pane id="zotero-item-pane" zotero-persist="width height" flex="0"/>
</box>
</hbox>
</vbox>
@ -1383,6 +1282,8 @@
oncommand="ZoteroPane.tagSelector.deleteAutomatic();
this.setAttribute('checked', false);"/>
</menupopup>
<!-- itemPane translateItem -->
<menupopup id="zotero-item-addTo-menu" onpopupshowing="ZoteroPane.itemPane.buildTranslateSelectContextMenu(event);"></menupopup>
</popupset>
</hbox>

View file

@ -37,6 +37,11 @@ menu-new-standalone-note =
menu-new-item-note =
.label = New Item Note
menu-restoreToLibrary =
.label = Restore to Library
menu-deletePermanently =
.label = Delete Permanently…
zotero-toolbar-tabs-menu =
.tooltiptext = List all tabs
filter-collections = Filter Collections

View file

@ -60,7 +60,6 @@
@import "components/tabsMenu";
@import "components/newCollectionDialog";
@import "components/reader";
@import "components/itemPane";
// Elements
// --------------------------------------------------
@ -90,3 +89,8 @@
@import "elements/annotationRow";
@import "elements/noteRow";
@import "elements/librariesCollectionsBox";
@import "elements/duplicatesMergePane";
@import "elements/itemMessagePane";
@import "elements/itemDetails";
@import "elements/itemPane";
@import "elements/contextPane";

View file

@ -1,7 +0,0 @@
#zotero-item-pane {
width: $min-width-item-pane;
min-width: $min-width-item-pane;
/* Need a min height to prevent layout issues in stacked mode */
min-height: 168px;
background: var(--material-sidepane);
}

View file

@ -0,0 +1,2 @@
context-pane {
}

View file

@ -0,0 +1,24 @@
duplicates-merge-pane {
-moz-box-orient: vertical;
groupbox {
margin: 8px 0 0 0;
}
#zotero-duplicates-merge-button
{
font-size: 13px;
}
#zotero-duplicates-merge-item-box-container {
overflow-y: auto;
padding: 0 8px;
}
/* Show duplicates date list item as selected even when not focused
(default behavior on other platforms) */
#zotero-duplicates-merge-original-date:not(:focus) > richlistitem[selected="true"] {
background-color: -moz-cellhighlight;
color: -moz-cellhighlighttext;
}
}

View file

@ -11,6 +11,11 @@
.zotero-item-pane-content {
min-height: 0;
flex: 1;
width: $min-width-item-pane;
min-width: $min-width-item-pane;
/* Need a min height to prevent layout issues in stacked mode */
min-height: 168px;
background: var(--material-sidepane);
}
.zotero-view-item-container {
@ -33,6 +38,9 @@
padding: 0 8px;
overflow-anchor: none; /* Work around tags box causing scroll to jump - figure this out */
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
display: flex;
flex-direction: column;
gap: 1px; /* Need gap for intersection computing */
}
.zotero-view-item::before {
@ -54,33 +62,3 @@
{
margin-left: 5px;
}
/* Buttons in trash and feed views */
.zotero-item-pane-top-buttons > button {
-moz-box-flex: 1
}
/* Merge pane in duplicates view */
#zotero-duplicates-merge-button
{
font-size: 13px;
}
#zotero-duplicates-merge-pane > groupbox {
margin: 8px 0 0 0;
}
#zotero-duplicates-merge-item-box-container {
overflow-y: scroll;
}
#zotero-feed-item-toggleRead-button {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 150px;
}
#zotero-feed-item-addTo-button {
max-width: 250px;
}

View file

@ -0,0 +1,32 @@
item-message-pane {
-moz-box-orient: vertical;
.custom-head {
display: flex;
flex-direction: row;
align-self: stretch;
gap: 6px;
padding: 6px 8px;
background: var(--material-toolbar);
border-bottom: var(--material-panedivider);
height: 28px;
&:empty {
display: none;
}
button {
height: 26px;
margin: 0;
flex-grow: 1;
}
}
#zotero-item-pane-groupbox {
-moz-box-flex: 1;
-moz-box-pack: center;
-moz-box-align: center;
-moz-appearance: none !important;
border-width: 0;
}
}

View file

@ -0,0 +1,13 @@
item-pane {
&[collapsed="true"] {
visibility: inherit;
#zotero-item-pane-content {
visibility: collapse;
}
}
&.stacked {
-moz-box-orient: vertical;
}
}

View file

@ -1,3 +1,28 @@
note-editor {
-moz-box-orient: vertical;
.custom-head {
display: flex;
flex-direction: row;
align-self: stretch;
gap: 6px;
padding: 6px 8px;
background: var(--material-toolbar);
border-bottom: var(--material-panedivider);
height: 28px;
&:empty {
display: none;
}
button {
height: 26px;
margin: 0;
flex-grow: 1;
}
}
}
links-box {
display: flex;
flex-direction: column;

View file

@ -2,27 +2,34 @@ pane-header {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 6px 8px 0 8px;
padding: 6px 8px;
gap: 6px;
border-bottom: 1px solid var(--fill-quinary);
.head {
display: flex;
align-self: stretch;
gap: 4px;
padding-bottom: 6px;
border-bottom: 1px solid var(--fill-quinary);
}
.title {
align-self: center;
margin-top: calc(0px - var(--editable-text-padding-block));
padding: 2px 0px 1px 0px;
flex: 1 1 0;
font-weight: 600;
line-height: 1.333;
.title {
align-self: center;
margin-top: calc(0px - var(--editable-text-padding-block));
padding: 2px 0px 1px 0px;
flex: 1 1 0;
font-weight: 600;
line-height: 1.333;
editable-text {
flex: 1;
}
}
editable-text {
flex: 1;
.menu-button {
align-self: start;
}
.menu-button toolbarbutton {
@include svgicon-menu("go-to", "universal", "20");
}
}
@ -35,4 +42,21 @@ pane-header {
--width-focus-border: 2px;
@include focus-ring;
}
.custom-head {
display: flex;
flex-direction: row;
align-self: stretch;
gap: 6px;
&:empty {
display: none;
}
button {
height: 26px;
margin: 0;
flex-grow: 1;
}
}
}

View file

@ -403,9 +403,11 @@ describe("Item pane", function () {
let button = doc.getElementById('zotero-feed-item-toggleRead-button');
assert.equal(button.textContent, Zotero.getString('pane.item.markAsUnread'));
assert.equal(button.label, Zotero.getString('pane.item.markAsUnread'));
yield item.toggleRead(false);
assert.equal(button.textContent, Zotero.getString('pane.item.markAsRead'));
// Button is re-created
button = doc.getElementById('zotero-feed-item-toggleRead-button');
assert.equal(button.label, Zotero.getString('pane.item.markAsRead'));
});
});
});

View file

@ -856,7 +856,7 @@ describe("Zotero.ItemTree", function() {
yield itemsView.selectItem(attachment.id);
yield Zotero.Promise.delay();
var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
var box = win.document.getElementById('zotero-item-pane-my-publications-button');
assert.isFalse(box.hidden);
});
@ -872,8 +872,9 @@ describe("Zotero.ItemTree", function() {
yield itemsView.selectItem(attachment.id);
var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
assert.isTrue(box.hidden);
var box = win.document.getElementById('zotero-item-pane-my-publications-button');
// box is not created if it shouldn't show
assert.isNull(box);
});
});
})