mirror of
https://github.com/zotero/zotero.git
synced 2026-09-27 01:21:27 +00:00
Draggable item pane sections (#5094)
This commit is contained in:
parent
fcfb6e16d0
commit
0b8b4c0ff6
26 changed files with 1352 additions and 391 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
`);
|
||||
|
||||
init() {
|
||||
this._item = null;
|
||||
this._noteIDs = [];
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this._handleAdd);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@
|
|||
this._tabDirection = null;
|
||||
this._tagColors = [];
|
||||
this._notifierID = null;
|
||||
this._item = null;
|
||||
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this._handleAddButtonClick);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
chrome/skin/default/zotero/16/universal/down.svg
Normal file
3
chrome/skin/default/zotero/16/universal/down.svg
Normal 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 |
3
chrome/skin/default/zotero/16/universal/reset.svg
Normal file
3
chrome/skin/default/zotero/16/universal/reset.svg
Normal 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 |
3
chrome/skin/default/zotero/16/universal/up.svg
Normal file
3
chrome/skin/default/zotero/16/universal/up.svg
Normal 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 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue