Draggable item pane sections (#5094)

This commit is contained in:
windingwind 2025-04-04 03:32:37 +02:00 • committed by GitHub
parent fcfb6e16d0
commit 0b8b4c0ff6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1352 additions and 391 deletions

View file

@ -72,12 +72,13 @@
this.initCollapsibleSection();
this._abstractField = this.querySelector('editable-text');
this._abstractField.addEventListener('blur', () => this.save());
this._abstractField.addEventListener('blur', this._handleFieldBlur);
this._abstractField.ariaLabel = Zotero.getString('itemFields.abstractNote');
this.render();
}
destroy() {
this._abstractField?.removeEventListener('blur', this._handleFieldBlur);
Zotero.Notifier.unregisterObserver(this._notifierID);
}
@ -192,6 +193,10 @@
this._feedAbstractBrowser = null;
}
}
_handleFieldBlur = () => {
this.save();
};
}
customElements.define("abstract-box", AbstractBox);
}

View file

@ -243,34 +243,20 @@
this._body = this.querySelector('.body');
this._id('url').addEventListener('contextmenu', (event) => {
this._id('url-menu').openPopupAtScreen(event.screenX, event.screenY, true);
});
this._id('url').addEventListener('contextmenu', this._handleURLContextMenu);
this._id("title").addEventListener('blur', () => {
this.item.setField('title', this._id('title').value);
this.item.saveTx();
});
this._id("title").addEventListener('blur', this._handleTitleBlur);
let fileName = this._id("fileName");
fileName.addEventListener('focus', () => {
this._isEditingFilename = true;
});
fileName.addEventListener('blur', () => {
this.editFileName(fileName.value);
this._isEditingFilename = false;
});
fileName.addEventListener('focus', this._handleFileNameFocus);
fileName.addEventListener('blur', this._handleFileNameBlur);
let noteButton = this._id('note-button');
noteButton.addEventListener("command", () => {
this.convertAttachmentNote();
});
noteButton.addEventListener("command", this._handleNoteButtonCommand);
let copyMenuitem = this._id('url-menuitem-copy');
copyMenuitem.label = Zotero.getString('general.copy');
copyMenuitem.addEventListener('command', () => {
Zotero.Utilities.Internal.copyTextToClipboard(this.item.getField('url'));
});
copyMenuitem.addEventListener('command', this._handleCopyURL);
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentbox');
@ -280,37 +266,44 @@
// reindex button
let reindexButton = this._id("indexStatusRow").querySelector(".meta-data toolbarbutton");
if (reindexButton) {
reindexButton.addEventListener("focusin", function (e) {
if (e.target.tagName == "image") {
reindexButton.focus();
reindexButton.querySelector("image").removeAttribute("tabindex");
}
});
reindexButton.addEventListener("blur", function (_) {
setTimeout(() => {
if (document.activeElement !== reindexButton) {
reindexButton.querySelector("image").setAttribute("tabindex", "0");
}
});
});
reindexButton.addEventListener("focusin", this._handleReindexButtonFocus);
reindexButton.addEventListener("blur", this._handleReindexButtonBlur);
// Prevents the button from getting stuck in active state
reindexButton.addEventListener("keydown", this._handleReindexButtonKeydown);
}
// Prevents the button from getting stuck in active state
reindexButton.addEventListener("keydown", (e) => {
if (e.key == " ") {
e.preventDefault();
reindexButton.click();
}
});
for (let label of this.querySelectorAll(".meta-label")) {
// Prevent default focus/blur behavior - we implement our own below
label.addEventListener("mousedown", event => event.preventDefault());
label.addEventListener("mousedown", this._handleMetaLabelMousedown);
label.addEventListener("click", this._handleMetaLabelClick);
}
}
destroy() {
this.discard();
this._preview?.remove();
delete this._preview;
Zotero.Notifier.unregisterObserver(this._notifierID);
this._id('url')?.removeEventListener('contextmenu', this._handleURLContextMenu);
this._id("title")?.removeEventListener('blur', this._handleTitleBlur);
this._id("fileName")?.removeEventListener('focus', this._handleFileNameFocus);
this._id("fileName")?.removeEventListener('blur', this._handleFileNameBlur);
this._id('note-button')?.removeEventListener("command", this._handleNoteButtonCommand);
this._id('url-menuitem-copy')?.removeEventListener('command', this._handleCopyURL);
let reindexButton = this._id("indexStatusRow")?.querySelector(".meta-data toolbarbutton");
if (reindexButton) {
reindexButton.removeEventListener("focusin", this._handleReindexButtonFocus);
reindexButton.removeEventListener("blur", this._handleReindexButtonBlur);
reindexButton.removeEventListener("keydown", this._handleReindexButtonKeydown);
}
for (let label of this.querySelectorAll(".meta-label")) {
label.removeEventListener("mousedown", this._handleMetaLabelMousedown);
label.removeEventListener("click", this._handleMetaLabelClick);
}
}
notify(event, _type, ids, _extraData) {
@ -739,6 +732,58 @@
this._body.prepend(this._preview);
this._preview.disableResize = !!this.hidden;
}
_handleURLContextMenu = (event) => {
this._id('url-menu').openPopupAtScreen(event.screenX, event.screenY, true);
};
_handleTitleBlur = () => {
this.item.setField('title', this._id('title').value);
this.item.saveTx();
};
_handleFileNameFocus = () => {
this._isEditingFilename = true;
};
_handleFileNameBlur = () => {
this.editFileName(this._id("fileName").value);
this._isEditingFilename = false;
};
_handleNoteButtonCommand = () => {
this.convertAttachmentNote();
};
_handleCopyURL = () => {
Zotero.Utilities.Internal.copyTextToClipboard(this.item.getField('url'));
};
_handleReindexButtonFocus = (event) => {
if (event.target.tagName == "image") {
reindexButton.focus();
reindexButton.querySelector("image").removeAttribute("tabindex");
}
};
_handleReindexButtonBlur = () => {
setTimeout(() => {
if (document.activeElement !== reindexButton) {
reindexButton.querySelector("image").setAttribute("tabindex", "0");
}
});
};
_handleReindexButtonKeydown = (event) => {
if (event.key == " ") {
event.preventDefault();
reindexButton.click();
}
};
_handleMetaLabelMousedown = (event) => {
event.preventDefault();
};
}
customElements.define("attachment-box", AttachmentBox);

View file

@ -220,7 +220,12 @@
}
destroy() {
this._reader?.uninit();
try {
this._reader?.uninit();
}
catch (e) {
this._debug("Error uninitializing reader", e);
}
this._resizeOb.disconnect();
this.removeEventListener("DOMContentLoaded", this._handleReaderLoad);
this.removeEventListener("mouseenter", this.updateGoto);

View file

@ -116,20 +116,10 @@
this._addPopup = this.querySelector('.add-popup');
let [addFile, addLink, addWebLink] = this._addPopup.children;
this._addPopup.addEventListener('popupshowing', () => {
let canAddAny = this.item?.isRegularItem() && this.item.library.editable;
addFile.disabled = addLink.disabled = !(canAddAny && this.item.library.filesEditable);
addWebLink.disabled = !canAddAny;
});
addFile.addEventListener('command', () => {
ZoteroPane.addAttachmentFromDialog(false, this.item.id);
});
addLink.addEventListener('command', () => {
ZoteroPane.addAttachmentFromDialog(true, this.item.id);
});
addWebLink.addEventListener('command', () => {
ZoteroPane.addAttachmentFromURI(true, this.item.id);
});
this._addPopup.addEventListener('popupshowing', this._handleAddPopupShowing);
addFile.addEventListener('command', this._handleAddFile);
addLink.addEventListener('command', this._handleAddLink);
addWebLink.addEventListener('command', this._handleAddWebLink);
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
@ -144,8 +134,22 @@
}
destroy() {
this._section?.removeEventListener('add', this._handleAdd);
this.discard();
this._preview?.remove();
delete this._preview;
Zotero.Notifier.unregisterObserver(this._notifierID);
this._section?.removeEventListener('add', this._handleAdd);
if (this._addPopup) {
this._addPopup.removeEventListener('popupshowing', this._handleAddPopupShowing);
let [addFile, addLink, addWebLink] = this._addPopup.children;
addFile?.removeEventListener('command', this._handleAddFile);
addLink?.removeEventListener('command', this._handleAddLink);
addWebLink?.removeEventListener('command', this._handleAddWebLink);
}
this._section?._contextMenu?.removeEventListener('popupshowing', this._handleContextMenu);
}
notify(action, type, ids) {
@ -293,6 +297,24 @@
this._addPopup.openPopup(event.detail.button, 'after_end');
};
_handleAddPopupShowing = () => {
let canAddAny = this.item?.isRegularItem() && this.item.library.editable;
addFile.disabled = addLink.disabled = !(canAddAny && this.item.library.filesEditable);
addWebLink.disabled = !canAddAny;
};
_handleAddFile = () => {
ZoteroPane.addAttachmentFromDialog(false, this.item.id);
};
_handleAddLink = () => {
ZoteroPane.addAttachmentFromDialog(true, this.item.id);
};
_handleAddWebLink = () => {
ZoteroPane.addAttachmentFromURI(true, this.item.id);
};
_handleTogglePreview = () => {
let toOpen = !Zotero.Prefs.get('showAttachmentPreview');
Zotero.Prefs.set('showAttachmentPreview', toOpen);

View file

@ -199,6 +199,29 @@
let containerRoot = this.closest('.zotero-view-item-container, context-notes-list');
let contextMenu = document.createXULElement('menupopup');
let pinSection, unpinSection;
pinSection = document.createXULElement('menuitem');
pinSection.classList.add('menuitem-iconic', 'zotero-menuitem-pin');
pinSection.setAttribute('data-l10n-id', 'pin-section');
pinSection.addEventListener('command', () => {
let sidenav = this._getSidenav();
sidenav.container.scrollToPane(this.dataset.pane, 'smooth');
sidenav.pinnedPane = this.dataset.pane;
});
contextMenu.append(pinSection);
unpinSection = document.createXULElement('menuitem');
unpinSection.classList.add('menuitem-iconic', 'zotero-menuitem-unpin');
unpinSection.setAttribute('data-l10n-id', 'unpin-section');
unpinSection.addEventListener('command', () => {
this._getSidenav().pinnedPane = null;
});
contextMenu.append(unpinSection);
let pinSeparator = document.createXULElement('menuseparator');
contextMenu.append(pinSeparator);
let collapseOtherSections = document.createXULElement('menuitem');
collapseOtherSections.classList.add('menuitem-iconic', 'zotero-menuitem-collapse-others');
collapseOtherSections.setAttribute('data-l10n-id', 'collapse-other-sections');
@ -223,27 +246,35 @@
});
contextMenu.append(expandAllSections);
let pinSection, unpinSection;
let pinUnpinSeparator = document.createXULElement('menuseparator');
contextMenu.append(pinUnpinSeparator);
let reorderSeparator = document.createXULElement('menuseparator');
contextMenu.append(reorderSeparator);
pinSection = document.createXULElement('menuitem');
pinSection.classList.add('menuitem-iconic', 'zotero-menuitem-pin');
pinSection.setAttribute('data-l10n-id', 'pin-section');
pinSection.addEventListener('command', () => {
let moveSectionUp = document.createXULElement('menuitem');
moveSectionUp.classList.add('menuitem-iconic', 'zotero-menuitem-reorder-up');
moveSectionUp.setAttribute('data-l10n-id', 'sidenav-reorder-up');
moveSectionUp.addEventListener('command', () => {
let sidenav = this._getSidenav();
sidenav.container.scrollToPane(this.dataset.pane, 'smooth');
sidenav.pinnedPane = this.dataset.pane;
sidenav.handlePaneMove(this.dataset.pane, 'up');
});
contextMenu.append(pinSection);
contextMenu.append(moveSectionUp);
unpinSection = document.createXULElement('menuitem');
unpinSection.classList.add('menuitem-iconic', 'zotero-menuitem-unpin');
unpinSection.setAttribute('data-l10n-id', 'unpin-section');
unpinSection.addEventListener('command', () => {
this._getSidenav().pinnedPane = null;
let moveSectionDown = document.createXULElement('menuitem');
moveSectionDown.classList.add('menuitem-iconic', 'zotero-menuitem-reorder-down');
moveSectionDown.setAttribute('data-l10n-id', 'sidenav-reorder-down');
moveSectionDown.addEventListener('command', () => {
let sidenav = this._getSidenav();
sidenav.handlePaneMove(this.dataset.pane, 'down');
});
contextMenu.append(unpinSection);
contextMenu.append(moveSectionDown);
let resetSectionOrder = document.createXULElement('menuitem');
resetSectionOrder.classList.add('menuitem-iconic', 'zotero-menuitem-reorder-reset');
resetSectionOrder.setAttribute('data-l10n-id', 'sidenav-reorder-reset');
resetSectionOrder.addEventListener('command', () => {
let sidenav = this._getSidenav();
sidenav.resetPaneOrder();
});
contextMenu.append(resetSectionOrder);
contextMenu.addEventListener('popupshowing', () => {
let sections = Array.from(containerRoot.querySelectorAll('collapsible-section'));
@ -252,15 +283,25 @@
let sidenav = this._getSidenav();
if (sidenav?.isPanePinnable(this.dataset.pane)) {
pinUnpinSeparator.hidden = false;
pinSection.hidden = sidenav.pinnedPane == this.dataset.pane;
unpinSection.hidden = sidenav.pinnedPane != this.dataset.pane;
pinSeparator.hidden = false;
}
else {
pinUnpinSeparator.hidden = true;
pinSection.hidden = true;
unpinSection.hidden = true;
pinSeparator.hidden = true;
}
let canMoveUp = sidenav?.isPaneMovable(this.dataset.pane, 'up');
let canMoveDown = sidenav?.isPaneMovable(this.dataset.pane, 'down');
let canReset = sidenav?.isOrderChanged();
moveSectionUp.hidden = !canMoveUp;
moveSectionDown.hidden = !canMoveDown;
resetSectionOrder.hidden = !canReset;
reorderSeparator.hidden = !canMoveUp && !canMoveDown && !canReset;
});
return contextMenu;
@ -297,6 +338,8 @@
this._head.removeEventListener('mousedown', this._handleMouseDown);
this._head.removeEventListener('keydown', this._handleKeyDown);
this._head.removeEventListener('contextmenu', this._handleContextMenu);
this._contextMenu?.remove();
Zotero.Prefs.unregisterObserver(this._prefsObserverID);
}

View file

@ -125,142 +125,34 @@
init() {
this.initCollapsibleSection();
this._creatorTypeMenu.addEventListener('command', async (event) => {
var typeBox = this._popupNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var typeID = event.explicitOriginalTarget.getAttribute('typeid');
var row = typeBox.parentNode;
var fields = this.getCreatorFields(row);
fields.creatorTypeID = typeID;
typeBox.querySelector("#creator-type-label-inner").textContent = Zotero.getString(
'creatorTypes.' + Zotero.CreatorTypes.getName(typeID)
);
typeBox.setAttribute('typeid', typeID);
this.modifyCreator(index, fields);
if (this.saveOnEdit) {
await this.item.saveTx();
}
});
this._creatorTypeMenu.addEventListener('command', this._handleCreatorTypeChange);
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var item = this.item;
var exists = item.hasCreatorAt(index);
var fieldMode = row.querySelector("[fieldMode]").getAttribute("fieldMode");
var moreCreators = item.numCreators() > index + 1;
var hideMoveToTop = !exists || index < 2;
var hideMoveUp = !exists || index == 0;
var hideMoveDown = !exists || !moreCreators;
var hideMoveSep = hideMoveUp && hideMoveDown;
var hideNameSwap = fieldMode == '1' || !exists;
this._id('creator-transform-swap-names').hidden = hideNameSwap;
this._id('creator-transform-capitalize').disabled = !this.canCapitalizeCreatorName(row);
this._id('zotero-creator-move-sep').setAttribute('hidden', hideMoveSep);
this._id('zotero-creator-move-to-top').setAttribute('hidden', hideMoveToTop);
this._id('zotero-creator-move-up').setAttribute('hidden', hideMoveUp);
this._id('zotero-creator-move-down').setAttribute('hidden', hideMoveDown);
});
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', this._handleCreatorTransformMenuShowing);
// Ensure no button is forced to stay visible once the menu is closed
this.addEventListener('popuphidden', (event) => {
for (let node of this.querySelectorAll('.show-without-hover')) {
node.classList.remove('show-without-hover');
node.classList.add("show-on-hover");
}
// Some toolbarbuttons get stuck with open=true if popup is
// opened via keyboard (e.g. select version btn in merge mode)
let popupParent = event.target.parentElement;
if (popupParent?.getAttribute("open") == "true") {
popupParent.removeAttribute("open");
}
});
this._id('zotero-creator-transform-menu').addEventListener('command', this._handleCreatorTransformMenuCommand);
this._id('zotero-creator-transform-menu').addEventListener('command', async (event) => {
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
if (event.explicitOriginalTarget.className == 'zotero-creator-move') {
let dir;
switch (event.explicitOriginalTarget.id) {
case 'zotero-creator-move-to-top':
dir = 'top';
break;
case 'zotero-creator-move-up':
dir = 'up';
break;
case 'zotero-creator-move-down':
dir = 'down';
break;
}
this.moveCreator(index, dir);
}
});
this._id('creator-transform-swap-names').addEventListener('command', this._handleCreatorTransformSwapNames);
this._id('creator-transform-swap-names').addEventListener('command',
event => this.swapNames(event));
this._id('creator-transform-capitalize').addEventListener('command',
event => this.capitalizeCreatorName(event));
this._id('creator-transform-capitalize').addEventListener('command', this._handleCreatorTransformCapitalize);
this._linkMenu.addEventListener('popupshowing', () => {
let menu = this._linkMenu;
let link = menu.dataset.link;
let val = menu.dataset.val;
let viewOnline = this._id('zotero-link-menu-view-online');
let copy = this._id('zotero-link-menu-copy');
viewOnline.disabled = !link;
copy.disabled = !link;
copy.hidden = link === val;
let existingCopyMenuitem = menu.querySelector('menuitem[data-action="copy"]');
if (existingCopyMenuitem) {
existingCopyMenuitem.after(copy);
}
else {
menu.append(copy);
}
});
this._linkMenu.addEventListener('popupshowing', this._handleLinkMenuShowing);
this._id('zotero-link-menu-view-online').addEventListener(
'command',
event => ZoteroPane.loadURI(this._linkMenu.dataset.link, event)
this._handleLinkMenuViewOnline
);
this._id('zotero-link-menu-copy').addEventListener(
'command',
() => Zotero.Utilities.Internal.copyTextToClipboard(this._linkMenu.dataset.link)
this._handleLinkMenuCopy
);
this._infoTable.addEventListener("focusout", async (_) => {
await Zotero.Promise.delay();
// If the focus leaves the itemBox, clear the last focused element
let focused = document.activeElement;
if (!this._infoTable.contains(focused)) {
this._clearSavedFieldFocus();
}
// If user moves focus outside of empty unsaved creator row, remove it.
let unsavedCreatorRow = this.querySelector(".creator-type-value[unsaved=true]")?.closest(".meta-row");
// But not if these parent components receive focus which happens when menus are opened
if (["zotero-view-item", "main-window"].includes(focused.id) || !unsavedCreatorRow) return;
let focusLeftUnsavedCreatorRow = !unsavedCreatorRow.contains(focused);
if (focusLeftUnsavedCreatorRow) {
this.removeUnsavedCreatorRow(true);
}
});
this._infoTable.addEventListener("focusout", this._handleFocusout);
// Ensure no button is forced to stay visible once the menu is closed
this.addEventListener('popuphidden', this._handlePopupHidden);
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'infobox'], 'itemBox');
Zotero.Prefs.registerObserver('fontSize', () => {
this._prefsObserverID = Zotero.Prefs.registerObserver('fontSize', () => {
this._forceRenderAll();
});
@ -272,6 +164,18 @@
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
Zotero.Prefs.unregisterObserver(this._prefsObserverID);
this._id('zotero-creator-transform-menu')?.removeEventListener('popupshowing', this._handleCreatorTransformMenuShowing);
this._id('zotero-creator-transform-menu')?.removeEventListener('command', this._handleCreatorTransformMenuCommand);
this._id('creator-transform-swap-names')?.removeEventListener('command', this._handleCreatorTransformSwapNames);
this._id('creator-transform-capitalize')?.removeEventListener('command', this._handleCreatorTransformCapitalize);
this._linkMenu?.removeEventListener('popupshowing', this._handleLinkMenuShowing);
this._id('zotero-link-menu-view-online')?.removeEventListener('command', this._handleLinkMenuViewOnline);
this._id('zotero-link-menu-copy')?.removeEventListener('command', this._handleLinkMenuCopy);
this._infoTable?.removeEventListener("focusout", this._handleFocusout);
this.removeEventListener('popuphidden', this._handlePopupHidden);
}
//
@ -887,7 +791,7 @@
// If rowIDs are provided, always update them
if (rowIDs?.length > 0) {
for (let rowID of rowIDs) {
let rowElem = this._infoTable.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = this._infoTable.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
if (!rowElem) continue;
this.updateCustomRowData(rowElem);
}
@ -905,7 +809,7 @@
// Add rows that are in the target rows but not in the current rows
for (let row of targetRows) {
let rowElem = this._infoTable.querySelector(`[data-custom-row-id="${row.rowID}"]`);
let rowElem = this._infoTable.querySelector(`[data-custom-row-id="${CSS.escape(row.rowID)}"]`);
if (rowElem) {
// If the row is already in the table, and not already updated, update it
if (!rowIDs?.includes(row.rowID)) {
@ -2854,6 +2758,138 @@
_id(id) {
return this.querySelector(`#${id}`);
}
_handleCreatorTypeChange = async (event) => {
var typeBox = this._popupNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var typeID = event.explicitOriginalTarget.getAttribute('typeid');
var row = typeBox.parentNode;
var fields = this.getCreatorFields(row);
fields.creatorTypeID = typeID;
typeBox.querySelector("#creator-type-label-inner").textContent = Zotero.getString(
'creatorTypes.' + Zotero.CreatorTypes.getName(typeID)
);
typeBox.setAttribute('typeid', typeID);
this.modifyCreator(index, fields);
if (this.saveOnEdit) {
await this.item.saveTx();
}
};
_handleCreatorTransformMenuShowing = (_event) => {
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var item = this.item;
var exists = item.hasCreatorAt(index);
var fieldMode = row.querySelector("[fieldMode]").getAttribute("fieldMode");
var moreCreators = item.numCreators() > index + 1;
var hideMoveToTop = !exists || index < 2;
var hideMoveUp = !exists || index == 0;
var hideMoveDown = !exists || !moreCreators;
var hideMoveSep = hideMoveUp && hideMoveDown;
var hideNameSwap = fieldMode == '1' || !exists;
this._id('creator-transform-swap-names').hidden = hideNameSwap;
this._id('creator-transform-capitalize').disabled = !this.canCapitalizeCreatorName(row);
this._id('zotero-creator-move-sep').setAttribute('hidden', hideMoveSep);
this._id('zotero-creator-move-to-top').setAttribute('hidden', hideMoveToTop);
this._id('zotero-creator-move-up').setAttribute('hidden', hideMoveUp);
this._id('zotero-creator-move-down').setAttribute('hidden', hideMoveDown);
};
_handleCreatorTransformMenuCommand = async (event) => {
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
if (event.explicitOriginalTarget.className == 'zotero-creator-move') {
let dir;
switch (event.explicitOriginalTarget.id) {
case 'zotero-creator-move-to-top':
dir = 'top';
break;
case 'zotero-creator-move-up':
dir = 'up';
break;
case 'zotero-creator-move-down':
dir = 'down';
break;
}
this.moveCreator(index, dir);
}
};
_handleCreatorTransformSwapNames = (event) => {
this.swapNames(event);
};
_handleCreatorTransformCapitalize = (event) => {
this.capitalizeCreatorName(event);
}
_handleLinkMenuShowing = () => {
let menu = this._linkMenu;
let link = menu.dataset.link;
let val = menu.dataset.val;
let viewOnline = this._id('zotero-link-menu-view-online');
let copy = this._id('zotero-link-menu-copy');
viewOnline.disabled = !link;
copy.disabled = !link;
copy.hidden = link === val;
let existingCopyMenuitem = menu.querySelector('menuitem[data-action="copy"]');
if (existingCopyMenuitem) {
existingCopyMenuitem.after(copy);
}
else {
menu.append(copy);
}
};
_handleLinkMenuViewOnline = event => ZoteroPane.loadURI(this._linkMenu.dataset.link, event);
_handleLinkMenuCopy = () => {
Zotero.Utilities.Internal.copyTextToClipboard(this._linkMenu.dataset.link);
};
_handlePopupHidden = (event) => {
for (let node of this.querySelectorAll('.show-without-hover')) {
node.classList.remove('show-without-hover');
node.classList.add("show-on-hover");
}
// Some toolbarbuttons get stuck with open=true if popup is
// opened via keyboard (e.g. select version btn in merge mode)
let popupParent = event.target.parentElement;
if (popupParent?.getAttribute("open") == "true") {
popupParent.removeAttribute("open");
}
};
_handleFocusout = async (_) => {
await Zotero.Promise.delay();
// If the focus leaves the itemBox, clear the last focused element
let focused = document.activeElement;
if (!this._infoTable.contains(focused)) {
this._clearSavedFieldFocus();
}
// If user moves focus outside of empty unsaved creator row, remove it.
let unsavedCreatorRow = this.querySelector(".creator-type-value[unsaved=true]")?.closest(".meta-row");
// But not if these parent components receive focus which happens when menus are opened
if (["zotero-view-item", "main-window"].includes(focused.id) || !unsavedCreatorRow) return;
let focusLeftUnsavedCreatorRow = !unsavedCreatorRow.contains(focused);
if (focusLeftUnsavedCreatorRow) {
this.removeUnsavedCreatorRow(true);
}
};
}
customElements.define("info-box", InfoBox);
}

View file

@ -234,6 +234,8 @@
this._lastUpdateCustomSection = "";
this._lastScrollTop = 0;
// If true, will render on tab select
this._pendingRender = false;
// If true, will skip render
@ -306,7 +308,9 @@
this.pinnedPane = paneID;
}
else {
this._paneParent.scrollTo(0, 0);
// Keep the scroll position after reordering
this._paneParent.scrollTo(0, this._lastScrollTop);
this._lastScrollTop = 0;
}
// Only execute async render for visible panes
@ -382,11 +386,16 @@
elem.setL10nArgs(header.l10nArgs);
this._intersectionOb.observe(elem);
this.sidenav.addPane(paneID);
this.sidenav.updatePaneStatus(paneID);
}
// Update pending pinned pane
if (this._pendingPinnedPane && this.getEnabledPane(this._pendingPinnedPane)) {
this.pinnedPane = this._pendingPinnedPane;
}
if (this.sidenav) {
this.initPaneOrder(this.sidenav.getPersistedOrder());
}
}
renderCustomHead(callback) {
@ -456,6 +465,69 @@
this.getPanes().forEach(elem => this._sidenav.updatePaneStatus(elem.dataset.pane));
}
initPaneOrder(order) {
let panes = this.getPanes();
let paneIDs = panes.map(elem => elem.dataset.pane);
// Compare the order of paneIDs with the given order
let isOrderDifferent = false;
let lastOrderIdx = -1;
for (let paneID of paneIDs) {
let idx = order.indexOf(paneID);
if (idx == -1) {
continue;
}
if (idx < lastOrderIdx) {
isOrderDifferent = true;
break;
}
lastOrderIdx = idx;
}
if (!isOrderDifferent) {
return;
}
// Rearrange panes according to the given order. Unordered panes will stay at the end
for (let i = order.length - 1; i >= 0; i--) {
let paneID = order[i];
let idx = paneIDs.indexOf(paneID);
if (idx == -1) {
continue;
}
this._paneParent.prepend(panes[idx]);
}
}
/**
* Change the order of panes
* @param {string} paneID
* @param {number} newIdx
* @param {Object} options
* @param {boolean} options.render - Whether to rerender panes after reordering
*/
async changePaneOrder(paneID, newIdx, options = {}) {
let panes = this.getPanes();
let paneIDs = panes.map(elem => elem.dataset.pane);
let currentIndex = paneIDs.indexOf(paneID);
if (currentIndex == -1) return;
if (currentIndex == newIdx || currentIndex == newIdx - 1) return;
let currentPane = panes[currentIndex];
if (newIdx < panes.length) {
this._paneParent.insertBefore(currentPane, panes[newIdx]);
}
else {
this._paneParent.appendChild(currentPane);
}
// Rerender panes after reordering
if (options.render !== false) {
this._lastScrollTop = this._paneParent.scrollTop;
await this.render();
}
}
async scrollToPane(paneID, behavior = 'smooth') {
let panes = this.getEnabledPanes();
let paneIndex = panes.findIndex(elem => elem.dataset.pane == paneID);

View file

@ -106,6 +106,7 @@ class ItemPaneSectionElementBase extends XULElementBase {
this._section.removeEventListener("toggle", this._handleSectionToggle);
this._section = null;
}
this._resetRenderedFlags()
}
initCollapsibleSection() {
@ -185,6 +186,11 @@ class ItemPaneSectionElementBase extends XULElementBase {
_refreshDisabled = true;
// Cache section l10n ID and args for reconnecting
_sectionL10nId = null;
_sectionL10nArgs = null;
get content() {
let extraButtons = Object.keys(this._sectionButtons).join(",");
let content = `
@ -235,6 +241,9 @@ class ItemPaneSectionElementBase extends XULElementBase {
if (this._label) this._section.label = this._label;
this.updateSectionIcon();
if (this._sectionL10nId !== null) this.setL10nID(this._sectionL10nId);
if (this._sectionL10nArgs !== null) this.setL10nArgs(this._sectionL10nArgs);
this._sectionListeners = [];
let styles = [];
@ -270,17 +279,19 @@ class ItemPaneSectionElementBase extends XULElementBase {
destroy() {
this._sectionListeners.forEach(data => this._section?.removeEventListener(data.type, data.listener));
this._sectionListeners = [];
this._handleDestroy();
this._hooks = null;
}
setL10nID(l10nId) {
this._section.dataset.l10nId = l10nId;
this._sectionL10nId = l10nId;
}
setL10nArgs(l10nArgs) {
this._section.dataset.l10nArgs = l10nArgs;
this._sectionL10nArgs = l10nArgs;
}
registerSectionIcon(options) {

View file

@ -28,85 +28,25 @@
{
class ItemPaneSidenav extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="inherit-flex highlight-notes-inactive"
tabindex="0" role="tab" data-l10n-id="sidenav-main-btn-grouping">
<html:div class="pin-wrapper">
<toolbarbutton
id="sidenav-info-btn"
disabled="true"
data-l10n-id="sidenav-info"
data-pane="info"/>
</html:div>
<html:div class="pin-wrapper">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-abstract"
data-pane="abstract"/>
</html:div>
<html:div class="pin-wrapper" hidden="true">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-attachment-preview"
data-pane="attachment-preview"/>
</html:div>
<html:div class="pin-wrapper">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-attachments"
data-pane="attachments"/>
</html:div>
<html:div class="pin-wrapper">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-notes"
data-pane="notes"/>
</html:div>
<html:div class="pin-wrapper" hidden="true">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-attachment-info"
data-pane="attachment-info"/>
</html:div>
<html:div class="pin-wrapper" hidden="true">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-attachment-annotations"
data-pane="attachment-annotations"/>
</html:div>
<html:div class="pin-wrapper">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-libraries-collections"
data-pane="libraries-collections"/>
</html:div>
<html:div class="pin-wrapper">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-tags"
data-pane="tags"/>
</html:div>
<html:div class="pin-wrapper">
<toolbarbutton
disabled="true"
data-l10n-id="sidenav-related"
data-pane="related"/>
</html:div>
<html:div class="inherit-flex highlight-notes-inactive" tabindex="0" role="tab" data-l10n-id="sidenav-main-btn-grouping">
<!-- Buttons will be added dynamically -->
</html:div>
<html:div class="divider"/>
<html:div class="pin-wrapper highlight-notes-active">
<toolbarbutton
<html:div class="btn"
data-l10n-id="sidenav-notes"
data-pane="context-notes"
tabindex="0"
role="tab"/>
role="tab">
</html:div>
</html:div>
<html:div class="divider"/>
<html:div class="pin-wrapper">
<toolbarbutton
<toolbarbutton class="btn"
tooltiptext="&zotero.toolbar.openURL.label;"
type="menu"
data-action="locate"
@ -119,9 +59,15 @@
<menupopup class="context-menu">
<menuitem class="menuitem-iconic zotero-menuitem-pin" data-l10n-id="pin-section"/>
<menuitem class="menuitem-iconic zotero-menuitem-unpin" data-l10n-id="unpin-section"/>
<menuseparator class="zotero-menuitem-pin-separator"/>
<menuitem class="menuitem-iconic zotero-menuitem-reorder zotero-menuitem-reorder-up" data-l10n-id="sidenav-reorder-up"/>
<menuitem class="menuitem-iconic zotero-menuitem-reorder zotero-menuitem-reorder-down" data-l10n-id="sidenav-reorder-down"/>
<menuitem class="menuitem-iconic zotero-menuitem-reorder zotero-menuitem-reorder-reset" data-l10n-id="sidenav-reorder-reset"/>
</menupopup>
</popupset>
`, ['chrome://zotero/locale/zotero.dtd']);
_initialized = false;
_container = null;
@ -129,6 +75,20 @@
_contextMenuTarget = null;
_draggedWrapper = null;
_dropIndicator = null;
_prefObserverID = null;
get _defaultPanes() {
return ["info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"];
}
get _builtInPanes() {
return ["info", "abstract", "attachments", "notes", "attachment-info", "attachment-annotations", "libraries-collections", "tags", "related"];
}
get container() {
return this._container;
}
@ -188,50 +148,92 @@
}
this.render();
}
isPanePinnable(id) {
return id !== 'info' && id !== 'context-notes' && id !== 'context-all-notes' && id !== 'context-item-notes';
get _wrappers() {
return Array.from(this._buttonContainer.querySelectorAll('.pin-wrapper'));
}
init() {
this._buttonContainer = this.querySelector('.inherit-flex');
for (let toolbarbutton of this.querySelectorAll('toolbarbutton[data-pane]')) {
let pane = toolbarbutton.dataset.pane;
let pinnable = this.isPanePinnable(pane);
toolbarbutton.parentElement.classList.toggle('pinnable', pinnable);
if (pinnable) {
toolbarbutton.addEventListener('contextmenu', (event) => {
this._contextMenuTarget = pane;
this.querySelector('.zotero-menuitem-pin').hidden = this.pinnedPane == pane;
this.querySelector('.zotero-menuitem-unpin').hidden = this.pinnedPane != pane;
this.querySelector('.context-menu')
.openPopupAtScreen(event.screenX, event.screenY, true);
});
get _enabledWrappers() {
return Array.from(this._buttonContainer.querySelectorAll('.pin-wrapper:not([hidden])'));
}
isPanePinnable(id) {
if (['context-notes', 'context-all-notes', 'context-item-notes'].includes(id)) {
return false;
}
// The first button in the group is not pinnable
if (this._buttonContainer.querySelector(".pin-wrapper:not([hidden])").querySelector(".btn").dataset.pane == id) {
return false;
}
return true;
}
isPaneOrderable(paneID) {
let orderable =
// Built-in or orderable custom sections
this._builtInPanes.includes(paneID) || Zotero.ItemPaneManager.isSectionOrderable(paneID);
return orderable;
}
isPaneMovable(paneID, direction) {
let wrappers = this._enabledWrappers;
let currentWrapper = this.querySelector(`.btn[data-pane=${CSS.escape(paneID)}]`).parentElement;
let currentIndex = wrappers.indexOf(currentWrapper);
let isOrderable = this.isPaneOrderable(paneID);
let isLast = currentIndex === wrappers.length - 1;
let isNextOrderable = !isLast && this.isPaneOrderable(
wrappers[currentIndex + 1]?.querySelector(".btn")?.dataset.pane);
if (direction === 'up') {
return isOrderable && currentIndex !== 0;
}
else if (direction === 'down') {
return isOrderable && isNextOrderable && !isLast;
}
}
isOrderChanged() {
let order = this.getPersistedOrder();
for (let i = 0; i < this._builtInPanes.length; i++) {
if (this._builtInPanes[i] != order[i]) {
return true;
}
}
return false;
}
init() {
this._buttonContainer = this.querySelector('.inherit-flex');
this.loadBuiltInButtons();
this.toggleDefaultStatus(true);
this.addEventListener('click', this.handleButtonClick);
this.addEventListener('keydown', this.handleKeyDown);
this.addEventListener('focusin', this.handleFocusIn);
// Set up action toolbarbuttons
for (let toolbarbutton of this.querySelectorAll('toolbarbutton[data-action]')) {
let action = toolbarbutton.dataset.action;
// Set up action buttons
for (let button of this.querySelectorAll('.btn[data-action]')) {
let action = button.dataset.action;
if (action === 'locate') {
toolbarbutton.addEventListener('mousedown', async (event) => {
if (event.button !== 0 || toolbarbutton.open) {
button.addEventListener('mousedown', async (event) => {
if (event.button !== 0 || button.open) {
return;
}
event.preventDefault();
let menu = toolbarbutton.querySelector('menupopup');
let menu = button.querySelector('menupopup');
await Zotero_LocateMenu.buildLocateMenu(menu);
await document.l10n.translateFragment(menu);
toolbarbutton.open = true;
button.open = true;
});
}
}
this._buttonContainer.addEventListener('dragstart', this.handleButtonDragStart, true);
this._buttonContainer.addEventListener('dragover', this.handleButtonDragOver);
this._buttonContainer.addEventListener('drop', this.handleButtonDrop);
this._buttonContainer.addEventListener('dragend', this.handleButtonDragEnd);
this._buttonContainer.addEventListener('dragleave', this.handleButtonDragLeave);
this.querySelector('.zotero-menuitem-pin').addEventListener('command', () => {
this.container.scrollToPane(this._contextMenuTarget, 'smooth');
@ -240,99 +242,237 @@
this.querySelector('.zotero-menuitem-unpin').addEventListener('command', () => {
this.pinnedPane = null;
});
this.querySelector('.zotero-menuitem-reorder-up').addEventListener('command', () => {
this.handlePaneMove(this._contextMenuTarget, 'up');
});
this.querySelector('.zotero-menuitem-reorder-down').addEventListener('command', () => {
this.handlePaneMove(this._contextMenuTarget, 'down');
});
this.querySelector('.zotero-menuitem-reorder-reset').addEventListener('command', () => {
this.resetPaneOrder();
});
this.setAttribute("role", "tablist");
this._prefObserverID = Zotero.Prefs.registerObserver("sidenav.order", this.handlePaneOrderChange);
this._initialized = true;
}
destroy() {
this.removeEventListener('click', this.handleButtonClick);
this.removeEventListener('keydown', this.handleKeyDown);
this.removeEventListener('focusin', this.handleFocusIn);
this._buttonContainer.removeEventListener('dragstart', this.handleButtonDragStart, true);
this._buttonContainer.removeEventListener('dragover', this.handleButtonDragOver);
this._buttonContainer.removeEventListener('drop', this.handleButtonDrop);
this._buttonContainer.removeEventListener('dragend', this.handleButtonDragEnd);
this._buttonContainer.removeEventListener('dragleave', this.handleButtonDragLeave);
Zotero.Prefs.unregisterObserver(this._prefObserverID);
this._initialized = false;
}
render() {
if (!this.container) return;
for (let paneElem of this.container.getPanes()) {
let paneID = paneElem.dataset.pane;
this.addPane(paneID);
this.updatePaneStatus(paneID);
}
let contextNotesPaneVisible = this._contextNotesPaneVisible;
let pinnedPane = this.pinnedPane;
for (let toolbarbutton of this.querySelectorAll('toolbarbutton[data-pane]')) {
let pane = toolbarbutton.dataset.pane;
for (let button of this.querySelectorAll('.btn[data-pane]')) {
let pane = button.dataset.pane;
// TEMP: never disable context notes button
if (this._contextNotesPane) {
toolbarbutton.disabled = false;
button.removeAttribute('disabled');
}
if (pane == 'context-notes') {
let hidden = !this._contextNotesPane;
let selected = contextNotesPaneVisible;
toolbarbutton.parentElement.hidden = hidden;
toolbarbutton.parentElement.previousElementSibling.hidden = hidden; // Divider
button.parentElement.hidden = hidden;
button.parentElement.previousElementSibling.hidden = hidden; // Divider
toolbarbutton.setAttribute('aria-selected', selected);
button.setAttribute('aria-selected', selected);
continue;
}
toolbarbutton.closest("[role='tab']").setAttribute('aria-selected', !contextNotesPaneVisible);
button.closest("[role='tab']").setAttribute('aria-selected', !contextNotesPaneVisible);
// No need to set `hidden` here, since it's updated by ItemDetails#_handlePaneStatus
// Set .pinned on the container, for pin styling
toolbarbutton.parentElement.classList.toggle('pinned', pane == pinnedPane);
button.parentElement.classList.toggle('pinned', pane == pinnedPane);
}
for (let toolbarbutton of this.querySelectorAll('toolbarbutton[data-action]')) {
let action = toolbarbutton.dataset.action;
for (let button of this.querySelectorAll('.btn[data-action]')) {
let action = button.dataset.action;
if (action == 'locate') {
toolbarbutton.parentElement.hidden = false;
button.parentElement.hidden = false;
}
}
this.querySelector('.highlight-notes-active').classList.toggle('highlight', contextNotesPaneVisible);
this.querySelector('.highlight-notes-inactive').classList.toggle('highlight',
this._contextNotesPane && !contextNotesPaneVisible);
// Update the pane order
this.container.initPaneOrder(this.getPersistedOrder());
}
addPane(paneID) {
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
if (toolbarbutton) {
toolbarbutton.parentElement.hidden = false;
async persistOrder(currentOrder = undefined) {
let panes = Array.from(this._buttonContainer.querySelectorAll('.btn[data-pane]'));
if (currentOrder === undefined) {
currentOrder = [];
for (let pane of panes) {
let paneID = pane.dataset.pane;
if (this.isPaneOrderable(paneID)) {
currentOrder.push(paneID);
}
}
}
currentOrder = [...currentOrder];
// Restore the order from installed plugins but not registered in the current order
let prevOrder = this.getPersistedOrder();
let installedPluginIDs = undefined;
for (let paneID of prevOrder) {
if (currentOrder.includes(paneID)) {
continue;
}
// If the pane ID is not in the current order, check if it's a plugin ID
if (!installedPluginIDs) {
installedPluginIDs = (await Zotero.Plugins.getAllPluginIDs()).map(
// Escape the plugin ID to match the pane ID generation
id => CSS.escape(id)
);
}
if (!installedPluginIDs.find(id => paneID.startsWith(id))) {
continue;
}
// If the pane ID is not in the current order, add it to the end
currentOrder.push(paneID);
}
Zotero.Prefs.set("sidenav.order", currentOrder.join(","));
}
getPersistedOrder(value = null) {
if (value === null) {
value = Zotero.Prefs.get("sidenav.order");
}
if (!value) return this._builtInPanes;
try {
return value.split(",");
}
catch(e) {
return this._builtInPanes;
}
}
addPane(paneID, order = null) {
let button = this.querySelector(`.btn[data-pane=${CSS.escape(paneID)}]`);
if (button) {
button.parentElement.hidden = false;
return;
}
let pane = this.container.getPane(paneID);
if (!pane) return;
let sidenavOptions = {};
try {
sidenavOptions = JSON.parse(pane.dataset.sidenavOptions);
}
catch (e) {}
let { icon, darkIcon, l10nID, l10nArgs } = sidenavOptions;
if (!darkIcon) darkIcon = icon;
toolbarbutton = document.createXULElement("toolbarbutton");
toolbarbutton.setAttribute("custom", "true");
toolbarbutton.dataset.pane = paneID;
toolbarbutton.dataset.l10nId = l10nID;
toolbarbutton.dataset.l10nArgs = l10nArgs;
toolbarbutton.style = `--custom-sidenav-icon-light: url('${icon}'); --custom-sidenav-icon-dark: url('${darkIcon}');`;
toolbarbutton.addEventListener('contextmenu', (event) => {
this._contextMenuTarget = paneID;
this.querySelector('.zotero-menuitem-pin').hidden = this.pinnedPane == paneID;
this.querySelector('.zotero-menuitem-unpin').hidden = this.pinnedPane != paneID;
this.querySelector('.context-menu')
.openPopupAtScreen(event.screenX, event.screenY, true);
});
button = document.createXULElement("div");
button.classList.add("btn");
button.dataset.pane = paneID;
button.addEventListener('contextmenu', this.handleButtonContextMenu);
let container = document.createElement("div");
container.classList.add("pin-wrapper");
container.classList.add("pinnable");
container.append(toolbarbutton);
if (this._defaultStatus) toolbarbutton.disabled = true;
toolbarbutton.parentElement.hidden = this._defaultStatus || !this.container.getEnabledPane(paneID);
this._buttonContainer.append(container);
container.append(button);
if (this._defaultStatus) button.setAttribute("disabled", "true");
let isBuiltin = this._builtInPanes.includes(paneID);
if (isBuiltin) {
button.dataset.l10nId = `sidenav-${paneID}`;
}
else {
let pane = this.container?.getPane(paneID);
if (!pane) return;
let sidenavOptions = {};
try {
sidenavOptions = JSON.parse(pane.dataset.sidenavOptions);
}
catch (e) {}
let { icon, darkIcon, l10nID, l10nArgs } = sidenavOptions;
if (!darkIcon) darkIcon = icon;
button.setAttribute("custom", "true");
button.dataset.l10nId = l10nID;
button.dataset.l10nArgs = l10nArgs;
button.style = `--custom-sidenav-icon-light: url('${icon}'); --custom-sidenav-icon-dark: url('${darkIcon}');`;
container.hidden = this._defaultStatus || !this.container.getEnabledPane(paneID);
}
if (this.isPaneOrderable(paneID)) {
container.draggable = true;
} else {
// If the pane is not orderable, always insert it at the end
this._buttonContainer.appendChild(container);
return;
}
// Insert the new button according to the persisted order.
if (order === null) {
order = this.getPersistedOrder();
}
let index = order.indexOf(paneID);
if (index >= 0) {
let children = Array.from(this._buttonContainer.children);
let inserted = false;
for (let child of children) {
let comparedPaneID = child.querySelector('.btn')?.dataset.pane;
// If the compared pane is not orderable, insert before it
if (!this.isPaneOrderable(comparedPaneID)) {
this._buttonContainer.insertBefore(container, child);
inserted = true;
break;
}
let comparedIndex = order.indexOf(comparedPaneID);
// If the compared pane should go after the new pane, insert before it
if (comparedIndex > index) {
this._buttonContainer.insertBefore(container, child);
inserted = true;
break;
}
}
if (!inserted) {
this._buttonContainer.appendChild(container);
}
} else {
this._buttonContainer.appendChild(container);
}
}
removePane(paneID) {
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
if (!toolbarbutton) return;
toolbarbutton.parentElement.remove();
let button = this.querySelector(`.btn[data-pane=${CSS.escape(paneID)}]`);
if (!button) return;
button.parentElement.remove();
}
loadBuiltInButtons() {
// Clear existing buttons in the first group.
this._buttonContainer.innerHTML = "";
let order = this.getPersistedOrder();
for (let paneID of this._builtInPanes) {
this.addPane(paneID, order);
}
}
updatePaneStatus(paneID) {
@ -341,18 +481,77 @@
return;
}
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
if (!toolbarbutton) return;
toolbarbutton.parentElement.hidden = !this.container.getEnabledPane(paneID);
let button = this.querySelector(`.btn[data-pane=${CSS.escape(paneID)}]`);
if (!button) return;
button.parentElement.hidden = !this.container.getEnabledPane(paneID);
if (this.pinnedPane) {
if (paneID == this.pinnedPane && !toolbarbutton.parentElement.classList.contains("pinned")) {
if (paneID == this.pinnedPane && !button.parentElement.classList.contains("pinned")) {
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
toolbarbutton.parentElement.classList.add('pinned');
button.parentElement.classList.add('pinned');
}
}
else {
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
}
if (this._defaultStatus && this._defaultPanes.includes(paneID)) {
button.setAttribute("disabled", "true");
}
else {
button.removeAttribute("disabled");
}
}
/**
* Change the order of the panes in the sidenav.
* @param {string} paneID
* @param {number} newIndex
* @param {Object} options
* @param {boolean} options.render Whether to re-render the panes
* @param {boolean} options.scroll Whether to scroll to the new position
* @param {boolean} options.persist Whether to persist the new order
* @returns {Promise<boolean>} Whether the order was changed
*/
async changePaneOrder(paneID, newIndex, options = {}) {
let button = this.querySelector(`.btn[data-pane=${CSS.escape(paneID)}]`);
if (!button) return false;
let wrappers = this._wrappers;
let currentWrapper = button.parentElement;
let currentIndex = wrappers.indexOf(currentWrapper);
if (currentIndex == -1) return false;
// Inserting to the same position or the position before it does nothing
if (currentIndex == newIndex || currentIndex == newIndex - 1) return false;
if (newIndex < wrappers.length) {
this._buttonContainer.insertBefore(currentWrapper, wrappers[newIndex]);
}
else {
this._buttonContainer.appendChild(currentWrapper);
}
if (this.container) {
// Notify the container to update the pane order
await this.container.changePaneOrder(paneID, newIndex, {
render: options.render
});
}
// Update the pinned pane if it's no longer pinnable
if (!this.isPanePinnable(this.pinnedPane)) {
this.pinnedPane = null;
}
// If no pinned pane, scroll to the new position
// if (options.scroll !== false) {
// this.container.scrollToPane(paneID);
// }
if (options.persist !== false) {
await this.persistOrder();
}
return true;
}
async resetPaneOrder() {
await this.persistOrder([...this._builtInPanes]);
}
toggleDefaultStatus(isDefault) {
@ -362,25 +561,49 @@
renderDefaultStatus() {
if (this._defaultStatus) {
this.querySelectorAll('toolbarbutton[data-pane]').forEach((elem) => {
elem.disabled = true;
elem.parentElement.hidden = !(
["info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"]
.includes(elem.dataset.pane));
this.querySelectorAll('.btn[data-pane]').forEach((elem) => {
elem.setAttribute("disabled", "true");
elem.parentElement.hidden = !this._defaultPanes.includes(elem.dataset.pane);
});
this.querySelectorAll('toolbarbutton[data-action]').forEach((elem) => {
elem.disabled = false;
this.querySelectorAll('.btn[data-action]').forEach((elem) => {
elem.removeAttribute("disabled");
});
}
else {
this.querySelectorAll('toolbarbutton').forEach((elem) => {
elem.disabled = false;
this.querySelectorAll('.btn').forEach((elem) => {
elem.removeAttribute("disabled");
});
this.render();
}
}
/**
* Compute the drop position based on the offset from the button container.
* @param {number} offset Offset from the left or top of the button container
* @returns {Object} Object containing the index and position of the drop position
* @property {number} index Index of the drop position after the drop in the enabled wrappers
* @property {number} position Position of the drop indicator
*/
computeDropPosition(offset) {
// Keep in sync with _itemPaneSidenav.scss
let btnSize = 28;
let btnGap = 6;
// Before the center of the button, insert before it; otherwise, insert after it.
let index = 0;
if (offset < btnSize / 2) {
index = 0;
}
else {
offset -= btnSize / 2;
index = Math.floor(offset / (btnSize + btnGap)) + 1;
}
return {
index,
position: index === 0 ? 0 : index * (btnSize + btnGap) + btnGap / 2
}
};
handleKeyDown = (event) => {
if (event.key == "Tab" && !event.shiftKey) {
// Wrap focus around to the tab bar
@ -415,7 +638,7 @@
if (!(event.target == this._buttonContainer || event.target.closest(".highlight-notes-active"))) return;
// Click the first itemPane button in a group to switch from notes to item details pane
if (event.target === this._buttonContainer && this._contextNotesPaneVisible) {
let firstBtn = event.target.querySelector("toolbarbutton");
let firstBtn = event.target.querySelector(".btn");
let clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
@ -467,8 +690,8 @@
};
handleButtonClick = (event) => {
let toolbarbutton = event.target;
let pane = toolbarbutton.dataset.pane;
let button = event.target;
let pane = button.dataset.pane;
if (!pane) return;
switch (pane) {
case "context-notes":
@ -508,6 +731,174 @@
}
this.render();
};
handleButtonContextMenu = (event) => {
event.preventDefault();
let button = event.target;
let paneID = button.dataset.pane;
if (!paneID) return;
this._contextMenuTarget = paneID;
let isPinnable = this.isPanePinnable(paneID);
this.querySelector('.zotero-menuitem-pin').hidden = !isPinnable || this.pinnedPane == paneID;
this.querySelector('.zotero-menuitem-unpin').hidden = !isPinnable || this.pinnedPane != paneID;
this.querySelector('.zotero-menuitem-pin-separator').hidden = !isPinnable;
this.querySelector('.zotero-menuitem-reorder-up').hidden = !this.isPaneMovable(paneID, 'up');
this.querySelector('.zotero-menuitem-reorder-down').hidden = !this.isPaneMovable(paneID, 'down');
this.querySelector('.zotero-menuitem-reorder-reset').hidden = !this.isOrderChanged();
this.querySelector('.context-menu')
.openPopupAtScreen(event.screenX, event.screenY, true);
};
handlePaneMove = (paneID, direction) => {
let enabledWrappers = this._enabledWrappers;
let currentWrapper = this.querySelector(`.btn[data-pane=${CSS.escape(paneID)}]`).parentElement;
let currentIndex = enabledWrappers.indexOf(currentWrapper);
let targetIndex;
if (direction === 'up') {
targetIndex = currentIndex - 1;
}
else if (direction === 'down') {
targetIndex = currentIndex + 2;
}
else {
return;
}
let targetWrapper = enabledWrappers[targetIndex];
if (targetWrapper) {
// Insert at the index of the previous wrapper
this.changePaneOrder(paneID, this._wrappers.indexOf(targetWrapper));
}
}
handleButtonDragStart = (event) => {
let wrapper = event.target.closest('.pin-wrapper');
if (!wrapper) return;
let button = wrapper.querySelector(".btn");
if (button.hasAttribute("disabled")) return;
let paneID = button.dataset.pane;
if (!this.isPaneOrderable(paneID)) return;
this._draggedWrapper = wrapper;
event.dataTransfer.dropEffect = "move";
// Create a clone for a custom drag image.
let clone = button.cloneNode(true);
clone.style.position = "absolute";
clone.style.top = "-1000px";
clone.style.left = "-1000px";
this.appendChild(clone);
event.dataTransfer.setDragImage(clone, 0, 0);
// Remove the clone after the drag has started.
setTimeout(() => {
clone.remove();
}, 0);
// Set the data to the pane ID.
event.dataTransfer.setData("zotero/sidenav", paneID);
};
handleButtonDragOver = (event) => {
let paneID = event.dataTransfer.getData("zotero/sidenav");
if (!paneID) return;
event.preventDefault();
let rect = this._buttonContainer.getBoundingClientRect();
let isStacked = this.classList.contains("stacked");
let offset = isStacked ? event.clientX - rect.left : event.clientY - rect.top;
let { index, position } = this.computeDropPosition(offset);
let currentIndex = this._enabledWrappers.indexOf(this._draggedWrapper);
if (
// Dragging the button to the same position
currentIndex === index || currentIndex === index - 1
// Dragging the button after the non-orderable button
|| (
index > 0
&& !this.isPaneOrderable(this._enabledWrappers[index - 1].querySelector(".btn").dataset.pane)
)
) {
this._dropIndicator?.setAttribute("hidden", "true");
// Make pointer forbidden
event.dataTransfer.dropEffect = "none";
return;
}
event.dataTransfer.dropEffect = "move";
// Create a drop indicator if one doesn't exist.
if (!this._dropIndicator) {
this._dropIndicator = document.createElement('div');
this._dropIndicator.classList.add('drop-indicator');
this._buttonContainer.appendChild(this._dropIndicator);
}
this.style.setProperty("--drop-indicator-offset", `${position}px`);
this._dropIndicator.removeAttribute("hidden");
};
handleButtonDrop = async (event) => {
let paneID = event.dataTransfer.getData("zotero/sidenav");
if (!paneID) return;
event.preventDefault();
let rect = this._buttonContainer.getBoundingClientRect();
let isStacked = this.classList.contains("stacked");
let offset = isStacked ? event.clientX - rect.left : event.clientY - rect.top;
let { index } = this.computeDropPosition(offset);
// Drop the button after the non-orderable button is not allowed
if (
index > 0
&& !this.isPaneOrderable(this._enabledWrappers[index - 1].querySelector(".btn").dataset.pane)) {
return;
}
let actualIndex = this._wrappers.indexOf(this._enabledWrappers[index]);
await this.changePaneOrder(paneID, actualIndex);
};
handleButtonDragEnd = (event) => {
if (!event.dataTransfer.types.includes("zotero/sidenav")) {
return;
}
this._draggedWrapper = null;
// Clean up the drop indicator if it still exists.
if (this._dropIndicator) {
this._dropIndicator.remove();
this._dropIndicator = null;
}
};
handleButtonDragLeave = (event) => {
if (this._dropIndicator) {
this._dropIndicator.setAttribute("hidden", "true");
}
};
handlePaneOrderChange = async (value) => {
// If no container, wait until it's set
if (!this.container) return;
let order = this.getPersistedOrder(value);
let hasChange = false;
for (let i = 0; i < order.length; i++) {
let paneID = order[i];
let changed = await this.changePaneOrder(paneID, i, {
scroll: false,
persist: false,
render: false
});
if (changed && !hasChange) {
hasChange = true;
}
}
if (hasChange) {
if (this.pinnedPane) {
this.container?.scrollToPane(this.pinnedPane, 'instant');
}
this.container?.render();
}
}
}
customElements.define("item-pane-sidenav", ItemPaneSidenav);
}

View file

@ -66,15 +66,14 @@ import { getCSSIcon } from 'components/icons';
this._body = this.querySelector('.body');
this.initCollapsibleSection();
this._addPopup = this.querySelector('.add-popup');
this._addPopup.addEventListener('popupshowing', (event) => {
ZoteroPane.buildAddItemToCollectionMenu(event, [this._item]);
});
this._addPopup.addEventListener('popupshowing', this._handleAddPopupShowing);
this._section.addEventListener('add', this._handleAdd);
}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
this._section?.removeEventListener('add', this._handleAdd);
this._addPopup?.removeEventListener('popupshowing', this._handleAddPopupShowing);
}
notify(action, type, ids) {
@ -279,6 +278,10 @@ import { getCSSIcon } from 'components/icons';
);
this._section.open = true;
};
_handleAddPopupShowing = (event) => {
ZoteroPane.buildAddItemToCollectionMenu(event, [this._item]);
};
}
customElements.define("libraries-collections-box", LibrariesCollectionsBox);
}

View file

@ -192,13 +192,16 @@
}
if (ids.includes(id) || this._parentItem && ids.includes(this._parentItem.id)) {
this._id('links-box').refresh();
this._id('links-box')?.refresh();
}
};
set notitle(val) {
this._notitle = !!val;
this._id('links-box').notitle = val;
let linksBox = this._id('links-box');
if (linksBox) {
linksBox.notitle = val;
}
}
set navigateHandler(val) {
@ -407,8 +410,8 @@
}
refresh() {
this._id('related').render();
this._id('tags').render();
this._id('related')?.render();
this._id('tags')?.render();
}
_id(id) {

View file

@ -36,7 +36,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
`);
init() {
this._item = null;
this._noteIDs = [];
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAdd);

View file

@ -36,7 +36,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
`);
init() {
this._item = null;
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
this.initCollapsibleSection();
this._section.addEventListener('add', this.add);

View file

@ -47,7 +47,6 @@
this._tabDirection = null;
this._tagColors = [];
this._notifierID = null;
this._item = null;
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAddButtonClick);

View file

@ -198,6 +198,10 @@
type: "string",
optional: true,
},
orderable: {
type: "boolean",
optional: true,
},
}
},
sectionButtons: {
@ -328,6 +332,14 @@
return this._sectionManager.data;
}
isSectionOrderable(paneID) {
let option = this._sectionManager._optionsCache[paneID];
if (!option) {
return false;
}
return option.sidenav.orderable ?? true;
}
registerInfoRow(options) {
return this._infoRowManager.register(options);
}

View file

@ -314,7 +314,7 @@ class PluginAPIBase {
let pluginID = this._getOptionPluginID(option);
if (pluginID && mainKey) {
// Make sure the return value is valid as class name or element id
return CSS.escape(`${pluginID}-${mainKey}`.replace(/[^a-zA-Z0-9-_]/g, "-"));
return CSS.escape(`${pluginID}-${mainKey}`);
}
return mainKey;
}

View file

@ -309,6 +309,12 @@ Zotero.Plugins = new function () {
};
this.getAllPluginIDs = async function () {
let addons = await AddonManager.getAddonsByTypes(["extension"]);
return addons.map(addon => addon.id);
}
/**
* @param {String} id
* @param {Number} idealSize In logical pixels (scaled automatically on hiDPI displays)

View file

@ -523,6 +523,12 @@ sidenav-related =
.tooltiptext = { pane-related }
sidenav-main-btn-grouping =
.aria-label = { pane-item-details }
sidenav-reorder-up =
.label = Move Section Up
sidenav-reorder-down =
.label = Move Section Down
sidenav-reorder-reset =
.label = Reset Section Order
pin-section =
.label = Pin Section

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 13.293V0.99997H9V13.293L13.293 8.99997L14 9.70697L8.5 15.207L3 9.70697L3.707 8.99997L8 13.293Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.79297 3.50001L8.29298 0L9 0.70719L6.70719 3L8.5 3V2.99998C12.0899 2.99998 15 5.91013 15 9.49998C15 13.087 12.0945 15.9953 8.50856 16L8.5 16L8.49145 16C4.90553 15.9954 2 13.087 2 9.49998C2 9.33175 2.00639 9.16501 2.01894 8.99999L3.02242 9C3.00758 9.16467 3 9.33144 3 9.49998C3 12.5375 5.46243 15 8.5 15C11.5376 15 14 12.5375 14 9.49998C14 6.46241 11.5376 3.99998 8.5 3.99998V4L6.70717 4L9 6.29282L8.29296 7L4.79297 3.50001Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2.70697V15H9V2.70697L13.293 6.99997L14 6.29297L8.5 0.792969L3 6.29297L3.707 6.99997L8 2.70697Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View file

@ -51,6 +51,9 @@ $menu-icons: (
retrieve-metadata: "retrieve-metadata",
unrecognize: "restore",
reindex: "reindex",
reorder-up: "up",
reorder-down: "down",
reorder-reset: "reset",
create-parent: "page",
rename-from-parent: "rename",
create-note-from-annotations: "light-dark:note-annotation",

View file

@ -65,7 +65,7 @@ item-pane-sidenav {
}
}
toolbarbutton {
.btn {
// TODO: Extract button styles?
width: 28px;
@ -73,16 +73,29 @@ item-pane-sidenav {
margin: 0;
padding: 4px;
pointer-events: all;
background-repeat: no-repeat;
background-position: center;
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
border-radius: 5px;
box-sizing: border-box;
&:disabled,
&[disabled="true"] {
opacity: 60%;
pointer-events: none;
}
&:hover {
background-color: var(--fill-quinary);
}
&:active {
background-color: var(--fill-quarternary);
}
@each $pane, $color in $item-pane-sections {
&[data-pane="#{$pane}"] {
list-style-image: url("chrome://zotero/skin/itempane/20/#{$pane}.svg");
background-image: url("chrome://zotero/skin/itempane/20/#{$pane}.svg");
fill: $color;
stroke: $color;
}
@ -90,13 +103,14 @@ item-pane-sidenav {
// Notes context pane button
&[data-pane="context-notes"] {
list-style-image: url("chrome://zotero/skin/itempane/20/notes.svg");
background-image: url("chrome://zotero/skin/itempane/20/notes.svg");
fill: map.get($item-pane-sections, "notes");
stroke: map.get($item-pane-sections, "notes");
}
// Locate button
&[data-action="locate"] {
color: var(--fill-secondary);
@include svgicon-menu("go-to", "universal", "20");
&:-moz-locale-dir(rtl) {
@ -106,10 +120,10 @@ item-pane-sidenav {
&[custom] {
@media (prefers-color-scheme: light) {
list-style-image: var(--custom-sidenav-icon-light);
background-image: var(--custom-sidenav-icon-light);
}
@media (prefers-color-scheme: dark) {
list-style-image: var(--custom-sidenav-icon-dark);
background-image: var(--custom-sidenav-icon-dark);
}
fill: var(--fill-secondary);
stroke: var(--fill-secondary);
@ -132,4 +146,32 @@ item-pane-sidenav {
.context-menu {
@include macOS-hide-menu-icons;
}
.drop-indicator {
position: absolute;
display: block;
background-color: var(--color-accent);
z-index: 1000;
--drop-indicator-size: 2px;
&[hidden] {
display: none;
}
}
&.stacked .drop-indicator {
left: var(--drop-indicator-offset);
top: 0;
width: var(--drop-indicator-size);
height: 28px;
margin-block: 4px;
}
&:not(.stacked) .drop-indicator {
top: var(--drop-indicator-offset);
left: 0;
width: 28px;
height: var(--drop-indicator-size);
margin-inline: 4px;
}
}

View file

@ -353,6 +353,17 @@ function waitForNotifierEvent(event, type) {
return deferred.promise;
}
async function waitForPrefsChange(key, global) {
var deferred = Zotero.Promise.defer();
let observerID;
var observer = function() {
Zotero.Prefs.unregisterObserver(observerID);
deferred.resolve();
};
observerID = Zotero.Prefs.registerObserver(key, observer, global);
return deferred.promise;
}
/**
* Hang tests for manual inspection
*/

View file

@ -1838,4 +1838,243 @@ describe("Item pane", function () {
itemDetails.pinnedPane = "";
});
});
describe("Sidenav", function () {
async function waitForSidenav() {
await waitForCallback(() => {
return !!ZoteroPane.itemPane._itemDetails.sidenav?._initialized;
});
}
async function waitForSidenavActive() {
await waitForCallback(() => {
return !ZoteroPane.itemPane._itemDetails.sidenav._defaultStatus;
});
}
function compareOrder(order, targetOrder) {
let lastIndex = -1;
for (let paneID of order) {
let index = targetOrder.indexOf(paneID);
if (index === -1) {
continue;
}
if (index < lastIndex) {
return false;
}
lastIndex = index;
}
return true;
}
function compareButtonOrder(order) {
let wrappers = ZoteroPane.itemPane._itemDetails.sidenav._enabledWrappers;
let buttonOrder = wrappers.map(wrapper => wrapper.querySelector('.btn').dataset.pane);
return compareOrder(buttonOrder, order)
}
function compareSectionOrder(order) {
let sections = ZoteroPane.itemPane._itemDetails.getEnabledPanes();
let sectionOrder = sections.map(section => section.dataset.pane);
return compareOrder(sectionOrder, order)
}
function getSidenavOrder() {
return Zotero.Prefs.get('sidenav.order') || ZoteroPane.itemPane._itemDetails.sidenav._builtInPanes.join(',');
}
async function clickSidenavMenu(paneIdx, menuSelector) {
let sidenav = ZoteroPane.itemPane._itemDetails.sidenav;
if (paneIdx < 0) {
paneIdx = sidenav._enabledWrappers.length + paneIdx;
}
let btn = sidenav._enabledWrappers[paneIdx].querySelector('.btn');
let btnRect = btn.getBoundingClientRect();
let popup = sidenav.querySelector('.context-menu');
let promise = waitForDOMEvent(popup, 'popupshown');
sidenav.handleButtonContextMenu({
target: btn,
preventDefault: function () {},
screenX: btnRect.left,
screenY: btnRect.top,
});
await promise;
let menu = popup.querySelector(menuSelector);
if (!menu || menu.hidden) {
popup.hidePopup();
return false;
}
menu.click();
popup.hidePopup();
return true;
}
it("should reorder section when prefs change", async function () {
await waitForSidenav();
let orderRaw = getSidenavOrder();
let order = orderRaw.split(',');
let newOrder = order.reverse()
let newOrderRaw = newOrder.join(',');
Zotero.Prefs.set('sidenav.order', newOrderRaw);
// If the order is not updated, a timeout exception will be thrown to fail the test
await waitForCallback(() => {
return compareSectionOrder(newOrder) && compareButtonOrder(newOrder);
}
, 100, 3);
});
it("should move section up", async function () {
await waitForSidenav();
// Create an item so that the sidenav is active
let item = await createDataObject('item');
await ZoteroPane.selectItem(item.id);
await waitForSidenavActive();
let orderRaw = getSidenavOrder();
let order = orderRaw.split(',');
let promise = waitForPrefsChange('sidenav.order');
let menuEnabled = await clickSidenavMenu(1, '.zotero-menuitem-reorder-up');
assert.isTrue(menuEnabled);
await promise;
let newOrderRaw = getSidenavOrder();
let newOrder = newOrderRaw.split(',');
let expectedOrder = [...order];
// Exchange 0 and 1
let temp = expectedOrder[0];
expectedOrder[0] = expectedOrder[1];
expectedOrder[1] = temp;
assert.deepEqual(newOrder, expectedOrder);
// Remove the temp item
await Zotero.Items.erase(item.id);
});
it("should move section down", async function () {
await waitForSidenav();
// Create an item so that the sidenav is active
let item = await createDataObject('item');
await ZoteroPane.selectItem(item.id);
await waitForSidenavActive();
let orderRaw = getSidenavOrder();
let order = orderRaw.split(',');
let promise = waitForPrefsChange('sidenav.order');
let menuEnabled = await clickSidenavMenu(0, '.zotero-menuitem-reorder-down');
assert.isTrue(menuEnabled);
await promise;
let newOrderRaw = getSidenavOrder();
let newOrder = newOrderRaw.split(',');
let expectedOrder = [...order];
// Exchange 0 and 1
let temp = expectedOrder[0];
expectedOrder[0] = expectedOrder[1];
expectedOrder[1] = temp;
assert.deepEqual(newOrder, expectedOrder);
// Remove the temp item
await Zotero.Items.erase(item.id);
});
it("should not show move up menu for first section", async function () {
await waitForSidenav();
// Create an item so that the sidenav is active
let item = await createDataObject('item');
await ZoteroPane.selectItem(item.id);
await waitForSidenavActive();
let menuEnabled = await clickSidenavMenu(0, '.zotero-menuitem-reorder-up');
assert.isFalse(menuEnabled);
await Zotero.Items.erase(item.id);
});
it("should not show move down menu for last section", async function () {
await waitForSidenav();
// Create an item so that the sidenav is active
let item = await createDataObject('item');
await ZoteroPane.selectItem(item.id);
await waitForSidenavActive();
let menuEnabled = await clickSidenavMenu(-1, '.zotero-menuitem-reorder-down');
assert.isFalse(menuEnabled);
await Zotero.Items.erase(item.id);
});
it("should unpin section if it moves to the top", async function () {
await waitForSidenav();
// Create an item so that the sidenav is active
let item = await createDataObject('item');
await ZoteroPane.selectItem(item.id);
await waitForSidenavActive();
let orderRaw = getSidenavOrder();
let order = orderRaw.split(',');
// Pin the second section
ZoteroPane.itemPane._itemDetails.pinnedPane = order[1];
let promise = waitForPrefsChange('sidenav.order');
let menuEnabled = await clickSidenavMenu(1, '.zotero-menuitem-reorder-up');
assert.isTrue(menuEnabled);
await promise;
assert.isEmpty(ZoteroPane.itemPane._itemDetails.pinnedPane);
await Zotero.Items.erase(item.id);
});
it("should not show reorder menu for custom section with orderable disabled", async function () {
await waitForSidenav();
const registeredID = Zotero.ItemPaneManager.registerSection({
paneID: "custom-section-example",
pluginID: "example@example.com",
header: {
l10nID: "example-item-pane-header",
icon: "chrome://zotero/skin/16/universal/note.svg",
},
sidenav: {
l10nID: "example-item-pane-header",
icon: "chrome://zotero/skin/20/universal/note.svg",
// Disable orderable
orderable: false,
},
onRender: ({ body }) => {
body.textContent = "Custom section";
},
});
// Create an item so that the sidenav is active
let item = await createDataObject('item');
await ZoteroPane.selectItem(item.id);
await waitForSidenavActive();
let menuUpEnabled = await clickSidenavMenu(-1, '.zotero-menuitem-reorder-up');
assert.isFalse(menuUpEnabled);
let menuDownEnabled = await clickSidenavMenu(-1, '.zotero-menuitem-reorder-down');
assert.isFalse(menuDownEnabled);
await Zotero.Items.erase(item.id);
Zotero.ItemPaneManager.unregisterSection(registeredID);
});
});
});

View file

@ -89,7 +89,7 @@ describe("Plugin API", function () {
let result = await getDataPromise;
// Should render custom row
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.exists(rowElem);
// Should call onGetData and render
@ -113,7 +113,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(option);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
// Should call onSetData on value change
@ -152,7 +152,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(option);
let result = await itemChangePromise;
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
// Should be enabled and editable
@ -192,7 +192,7 @@ describe("Plugin API", function () {
result = await itemChangePromise;
let itemDetails = ZoteroContextPane.context._getItemContext(tabID);
rowElem = itemDetails.getPane("info").querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = itemDetails.getPane("info").querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
// Should not be enabled in non-library tab
@ -219,14 +219,14 @@ describe("Plugin API", function () {
// Row at start
let rowID = await waitForRegister(startOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.notExists(rowElem.previousElementSibling);
await waitForUnregister(rowID);
// Row after creator rows
rowID = await waitForRegister(afterCreatorsOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.exists(rowElem.previousElementSibling.querySelector(".creator-type-value"));
assert.notExists(rowElem.nextElementSibling.querySelector(".creator-type-value"));
@ -234,7 +234,7 @@ describe("Plugin API", function () {
// Row at end
rowID = rowID = await waitForRegister(endOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
assert.exists(rowElem.nextElementSibling.querySelector("*[fieldname=dateAdded]"));
await waitForUnregister(rowID);
@ -254,7 +254,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.readOnly);
@ -263,7 +263,7 @@ describe("Plugin API", function () {
rowID = await waitForRegister(editableOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.readOnly);
@ -272,7 +272,7 @@ describe("Plugin API", function () {
rowID = await waitForRegister(notEditableOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.readOnly);
@ -294,7 +294,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.multiline);
@ -303,7 +303,7 @@ describe("Plugin API", function () {
rowID = await waitForRegister(multilineOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.multiline);
@ -312,7 +312,7 @@ describe("Plugin API", function () {
rowID = await waitForRegister(notMultilineOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.multiline);
@ -334,7 +334,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.noWrap);
@ -343,7 +343,7 @@ describe("Plugin API", function () {
rowID = await waitForRegister(noWrapOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isTrue(valueElem.noWrap);
@ -352,7 +352,7 @@ describe("Plugin API", function () {
rowID = await waitForRegister(wrapOption);
rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
valueElem = rowElem.querySelector(".value");
assert.isFalse(valueElem.noWrap);
@ -367,7 +367,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
let oldValue = valueElem.value;
@ -397,7 +397,7 @@ describe("Plugin API", function () {
let rowID = await waitForRegister(defaultOption);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${rowID}"]`);
let rowElem = infoSection.querySelector(`[data-custom-row-id="${CSS.escape(rowID)}"]`);
let valueElem = rowElem.querySelector(".value");
let value = valueElem.value;
@ -458,7 +458,7 @@ describe("Plugin API", function () {
// Wait for column header to render
await waitForCallback(
() => !!doc.querySelector(`#zotero-items-tree .virtualized-table-header .cell.${dataKey}`),
() => !!doc.querySelector(`#zotero-items-tree .virtualized-table-header .cell.${CSS.escape(dataKey)}`),
100, 3);
};
@ -470,7 +470,7 @@ describe("Plugin API", function () {
};
let getSelectedRowCell = (dataKey) => {
let cell = doc.querySelector(`#zotero-items-tree .row.selected .${dataKey}`);
let cell = doc.querySelector(`#zotero-items-tree .row.selected .${CSS.escape(dataKey)}`);
return cell;
};