diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js
index f51b09f01d..bc74cf4fbd 100644
--- a/chrome/content/zotero/integration/quickFormat.js
+++ b/chrome/content/zotero/integration/quickFormat.js
@@ -106,7 +106,7 @@ var Zotero_QuickFormat = new function () {
if ((event.key == "Enter" && !event.shiftKey) || event.charCode == 59) {
event.preventDefault();
event.stopPropagation();
- event.target.closest("richlistitem").click();
+ Zotero_QuickFormat._bubbleizeSelected();
}
// Tab will move focus back to the input field
else if (event.key === "Tab") {
@@ -127,8 +127,10 @@ var Zotero_QuickFormat = new function () {
_lastFocusedInput.focus();
}
else if (["ArrowDown", "ArrowUp"].includes(event.key)) {
+ event.preventDefault();
+ event.stopPropagation();
// ArrowUp from first item focuses the input
- if (referenceBox.selectedIndex == 1 && event.key == "ArrowUp") {
+ if (event.key == "ArrowUp" && referenceBox.selectedIndex == 1 && referenceBox.selectedItem == document.activeElement) {
_lastFocusedInput.focus();
referenceBox.selectedIndex = -1;
}
@@ -146,6 +148,34 @@ var Zotero_QuickFormat = new function () {
moveFocusForward(_lastFocusedInput);
}
});
+ referenceBox.addEventListener("click", (e) => {
+ let item = e.target.closest("richlistitem");
+ if (!item || e.button !== 0 || item.disabled) return;
+
+ let mouseMultiSelect = e.shiftKey || (Zotero.isMac && e.metaKey) || (!Zotero.isMac && e.ctrlKey);
+ let multipleSelected = referenceBox.selectedCount > 1;
+ let isItemSelected = [...referenceBox.selectedItems].findIndex(node => node == item) !== -1;
+ // Click without multiselect modifier will confirm the selection
+ if (!mouseMultiSelect) {
+ // If item is multi-selected, do not discard the selection
+ if (multipleSelected && isItemSelected) {
+ e.preventDefault();
+ }
+ Zotero_QuickFormat._bubbleizeSelected();
+ return;
+ }
+ // Make sure there is a selected item when shift-click is handled
+ if (e.shiftKey && referenceBox.selectedCount < 1) {
+ _selectFirstReference();
+ }
+ // Shift-click can end up selecting disabled separator, so make sure it's removed
+ setTimeout(() => {
+ let selectedSeparators = [...document.querySelectorAll("richlistitem[disabled='true'][selected='true']")];
+ for (let node of selectedSeparators) {
+ referenceBox.removeItemFromSelection(node);
+ }
+ });
+ }, true);
if (Zotero.isWin) {
if (Zotero.Prefs.get('integration.keepAddCitationDialogRaised')) {
dialog.setAttribute("square", "true");
@@ -890,6 +920,15 @@ var Zotero_QuickFormat = new function () {
var nodes = [];
var str = "";
+ // Add a red label to retracted items
+ if (Zotero.Retractions.isRetracted(item)) {
+ var label = document.createXULElement("label");
+ label.setAttribute("value", Zotero.getString("retraction.banner"));
+ label.setAttribute("crop", "end");
+ label.style.color = 'red';
+ label.style['margin-inline-end'] = '5px';
+ infoHbox.appendChild(label);
+ }
if (item.isNote()) {
var date = Zotero.Date.sqlToDate(item.dateModified, true);
date = Zotero.Date.toFriendlyDate(date);
@@ -989,7 +1028,6 @@ var Zotero_QuickFormat = new function () {
rll.setAttribute("aria-describedby", "item-description");
rll.appendChild(titleNode);
rll.appendChild(infoNode);
- rll.addEventListener("click", Zotero_QuickFormat._bubbleizeSelected, false);
return rll;
}
@@ -1104,6 +1142,14 @@ var Zotero_QuickFormat = new function () {
function getAllBubbles() {
return [...editor.querySelectorAll(".bubble")];
}
+
+ // Delete the bubble and clear locator node if it pointed at this bubble
+ function _deleteBubble(bubble) {
+ if (bubble == locatorNode) {
+ locatorNode = null;
+ }
+ bubble.remove();
+ }
/**
* Clear reference box
@@ -1113,64 +1159,85 @@ var Zotero_QuickFormat = new function () {
if (!skipResize) {
_resizeReferencePanel();
}
+ referenceBox.selectedIndex = -1;
+ }
+
+ // Select the first appropriate reference from the items list.
+ // If there are multi-selected items, select the first one of them.
+ // Otherwise, select the first non-header row.
+ function _selectFirstReference() {
+ if (referenceBox.selectedIndex > 0) return;
+ let firstItem = [...referenceBox.selectedItems].find(node => referenceBox.contains(node));
+ if (firstItem) {
+ referenceBox.selectedItem = firstItem;
+ }
+ else {
+ referenceBox.selectedIndex = 1;
+ }
}
/**
* Converts the selected item to a bubble
*/
this._bubbleizeSelected = Zotero.Promise.coroutine(function* () {
- const panelShowing = referencePanel.state === "open" || referencePanel.state === "showing";
- let inputExists = _lastFocusedInput || _getCurrentInput()
- if(!panelShowing || !referenceBox.hasChildNodes() || !referenceBox.selectedItem || _searchPromise?.isPending()) return false;
-
- if(!referenceBox.hasChildNodes() || !referenceBox.selectedItem || !inputExists) return false;
- var citationItem = {"id":referenceBox.selectedItem.getAttribute("zotero-item")};
- if (typeof citationItem.id === "string" && citationItem.id.indexOf("/") !== -1) {
- var item = Zotero.Cite.getItem(citationItem.id);
- citationItem.uris = item.cslURIs;
- citationItem.itemData = item.cslItemData;
- }
- else if (Zotero.Retractions.isRetracted({ id: parseInt(citationItem.id) })) {
- citationItem.id = parseInt(citationItem.id);
- if (Zotero.Retractions.shouldShowCitationWarning(citationItem)) {
- referencePanel.hidden = true;
- var ps = Services.prompt;
- var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
- + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL
- + ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
- var checkbox = { value: false };
- var result = ps.confirmEx(null,
- Zotero.getString('general.warning'),
- Zotero.getString('retraction.citeWarning.text1') + '\n\n'
- + Zotero.getString('retraction.citeWarning.text2'),
- buttonFlags,
- Zotero.getString('general.continue'),
- null,
- Zotero.getString('pane.items.showItemInLibrary'),
- Zotero.getString('retraction.citationWarning.dontWarn'), checkbox);
- referencePanel.hidden = false;
- if (result > 0) {
- if (result == 2) {
- Zotero_QuickFormat.showInLibrary(parseInt(citationItem.id));
+ let input = _lastFocusedInput || _getCurrentInput();
+ if (referenceBox.selectedCount == 0 || referencePanel.state !== 'open' || !input) return false;
+ let lastAddedBubble;
+ let multipleSelected = referenceBox.selectedCount > 1;
+ // It is technically possible for referenceBox.selectedItems to include nodes that were removed
+ // (e.g. during panel refreshing). This should never happen but this is a sanity check to make sure
+ let selectedItems = [...referenceBox.selectedItems].filter(node => referenceBox.contains(node));
+ for (let selectedItem of selectedItems) {
+ let itemID = parseInt(selectedItem.getAttribute("zotero-item"));
+ let item = { id: itemID };
+ if (Zotero.Retractions.isRetracted(item)) {
+ if (Zotero.Retractions.shouldShowCitationWarning(item)) {
+ var ps = Services.prompt;
+ var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
+ + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL
+ + ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
+ var checkbox = { value: false };
+ var result = ps.confirmEx(null,
+ Zotero.getString('general.warning'),
+ Zotero.getString('retraction.citeWarning.text1') + '\n\n'
+ + Zotero.getString('retraction.citeWarning.text2'),
+ buttonFlags,
+ Zotero.getString('general.continue'),
+ null,
+ Zotero.getString('pane.items.showItemInLibrary'),
+ Zotero.getString('retraction.citationWarning.dontWarn'), checkbox);
+ if (result > 0) {
+ if (result == 2) {
+ Zotero_QuickFormat.showInLibrary(itemID);
+ }
+ // Retraction should not be added
+ referenceBox.removeItemFromSelection(selectedItem);
+ return false;
+ }
+ if (checkbox.value) {
+ Zotero.Retractions.disableCitationWarningsForItem(item);
}
- return false;
- }
- if (checkbox.value) {
- Zotero.Retractions.disableCitationWarningsForItem(citationItem);
}
}
- citationItem.ignoreRetraction = true;
}
-
- _updateLocator(_getEditorContent());
- if(currentLocator) {
- citationItem["locator"] = currentLocator;
- if(currentLocatorLabel) {
- citationItem["label"] = currentLocatorLabel;
+ for (let selectedItem of selectedItems) {
+ var citationItem = { id: selectedItem.getAttribute("zotero-item") };
+ if (typeof citationItem.id === "string" && citationItem.id.indexOf("/") !== -1) {
+ var item = Zotero.Cite.getItem(citationItem.id);
+ citationItem.uris = item.cslURIs;
+ citationItem.itemData = item.cslItemData;
}
+ if (currentLocator) {
+ citationItem.locator = currentLocator;
+ if (currentLocatorLabel) {
+ citationItem.label = currentLocatorLabel;
+ }
+ }
+ lastAddedBubble = _insertBubble(citationItem, input);
+ }
+ if (!lastAddedBubble) {
+ return false;
}
- let input = _getCurrentInput() || _lastFocusedInput;
- let newBubble = _insertBubble(citationItem, input);
isPaste = false;
_clearEntryList();
clearLastFocused(input);
@@ -1178,7 +1245,10 @@ var Zotero_QuickFormat = new function () {
yield _previewAndSort();
refocusInput(false);
- locatorNode = getAllBubbles().filter(bubble => bubble.textContent == newBubble.textContent)[0];
+ // Do not record locator node if multiple bubbles are added
+ if (!multipleSelected) {
+ locatorNode = getAllBubbles().filter(bubble => bubble.textContent == lastAddedBubble.textContent)[0];
+ }
return true;
});
@@ -1270,7 +1340,7 @@ var Zotero_QuickFormat = new function () {
// to position references panel properly
let dialogBottom = dialog.getBoundingClientRect().bottom;
let panelTop = referencePanel.getBoundingClientRect().top;
- if (Math.abs(dialogBottom - panelTop) > 5) {
+ if (Math.abs(dialogBottom - panelTop) > 5 && referencePanel.state == "open") {
referencePanel.hidePopup();
// Skip a tick, otherwise the panel may just remain open where it was
setTimeout(_openReferencePanel);
@@ -1709,9 +1779,6 @@ var Zotero_QuickFormat = new function () {
function _onQuickSearchClick(event) {
if (qfGuidance) qfGuidance.hide();
- if (!event.target.classList.contains("editor")) {
- return;
- }
let clickX = event.clientX;
let clickY = event.clientY;
let { lastBubble, startOfTheLine } = getLastBubbleBeforePoint(clickX, clickY);
@@ -1746,15 +1813,20 @@ var Zotero_QuickFormat = new function () {
else {
editor.prepend(newInput);
}
+ locatorNode = null;
newInput.focus();
}
// Essentially a rewrite of default richlistbox arrow navigation
// so that it works with voiceover on CMD-ArrowUp/Down
var handleItemSelection = (event) => {
- event.preventDefault();
- event.stopPropagation();
- let selected = referenceBox.selectedItem;
+ let selected = referenceBox.contains(document.activeElement) ? document.activeElement : referenceBox.selectedItem;
+ // Multiselect happens during arrowUp/Down navigation when Shift/Cmd is being held
+ let selectMultiple = event.shiftKey || event.metaKey;
+ let initiallySelected = null;
+ if (referenceBox.contains(document.activeElement) && selectMultiple) {
+ initiallySelected = selected;
+ }
let selectNext = (node) => {
return event.key == "ArrowDown" ? node.nextElementSibling : node.previousElementSibling;
};
@@ -1764,8 +1836,27 @@ var Zotero_QuickFormat = new function () {
}
while (selected && selected.disabled);
if (selected) {
- referenceBox.selectedItem = selected;
selected.focus();
+ let multiSelected = [...referenceBox.selectedItems];
+ if (selectMultiple) {
+ // If the selected item is already selected, the previous one
+ // should be un-selected
+ if (multiSelected.includes(selected)) {
+ referenceBox.removeItemFromSelection(initiallySelected);
+ }
+ else {
+ referenceBox.addItemToSelection(selected);
+ // If there are multiple selected items, focus the last one
+ let following = selectNext(selected);
+ while (following && following.selected) {
+ following.focus();
+ following = selectNext(following);
+ }
+ }
+ }
+ else {
+ referenceBox.selectedItem = selected;
+ }
}
};
@@ -1795,20 +1886,22 @@ var Zotero_QuickFormat = new function () {
// Backspace/Delete from the beginning of an input will delete the previous bubble.
// If there are two inputs next to each other as a result, they are merged
if (this.previousElementSibling) {
- this.previousElementSibling.remove();
+ _deleteBubble(this.previousElementSibling);
_combineNeighboringInputs();
}
// Rerun search to update opened documents section if needed
_resetSearchTimer();
}
else if (["ArrowDown", "ArrowUp"].includes(event.key) && referencePanel.state === "open") {
+ event.preventDefault();
+ event.stopPropagation();
// ArrowUp when item is selected does nothing
if (referenceBox.selectedIndex < 1 && event.key == "ArrowUp") {
return;
}
// Arrow up/down will navigate the references panel if that's opened
if (referenceBox.selectedIndex < 1) {
- referenceBox.selectedIndex = 1;
+ _selectFirstReference();
referenceBox.selectedItem.focus();
}
else {
@@ -1820,7 +1913,7 @@ var Zotero_QuickFormat = new function () {
event.preventDefault();
event.stopPropagation();
if (referenceBox.selectedIndex < 1) {
- referenceBox.selectedIndex = 1;
+ _selectFirstReference();
}
referenceBox.selectedItem.focus();
}
@@ -1888,7 +1981,7 @@ var Zotero_QuickFormat = new function () {
if (!moveFocusBack(this)) {
moveFocusForward(this);
}
- this.remove();
+ _deleteBubble(this);
// Removed item bubble may belong to opened documents section. Reference panel
// needs to be reset so that it appears among other items.
_clearEntryList();
@@ -2247,7 +2340,7 @@ var Zotero_QuickFormat = new function () {
* Show an item in the library it came from
*/
this.showInLibrary = async function (itemID) {
- let citationItem = JSON.parse(panelRefersToBubble.dataset.citationItem || "{}");
+ let citationItem = JSON.parse(panelRefersToBubble?.dataset.citationItem || "{}");
var id = itemID || citationItem.id;
var pane = Zotero.getActiveZoteroPane();
// Open main window if it's not open (Mac)
diff --git a/chrome/content/zotero/integration/quickFormat.xhtml b/chrome/content/zotero/integration/quickFormat.xhtml
index 598d0459c2..a140f921ac 100644
--- a/chrome/content/zotero/integration/quickFormat.xhtml
+++ b/chrome/content/zotero/integration/quickFormat.xhtml
@@ -85,7 +85,7 @@
-
+