QuickFormat: adding multiple items with shift-arrow (#3851)
- multiple reference items can be selected with shift-arrowUp/Down or Cmd/Ctrl-click and added as bubbles via Enter or click on one of selected items - Shift-click can be used to select the range of items - if there is a retracted item among multiselected that is not OK'd (cancel or view in library clicked), no bubbles are added and the item is removed from the multiselection - added a red note to item's description indicating if an item is retracted
This commit is contained in:
parent
4d7c641f7b
commit
56980080ad
2 changed files with 157 additions and 64 deletions
|
@ -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)
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
<html:div id="item-description" class="aria-hidden" role="tooltip" data-l10n-id="quickformat-aria-item"></html:div>
|
||||
<panel class="citation-dialog reference-panel" noautofocus="true" norestorefocus="true"
|
||||
height="0" width="0" flip="none">
|
||||
<richlistbox class="citation-dialog reference-list" flex="1"/>
|
||||
<richlistbox class="citation-dialog reference-list" flex="1" seltype="multiple"/>
|
||||
</panel>
|
||||
<panel id="citation-properties" type="arrow" orient="vertical"
|
||||
onkeydown="Zotero_QuickFormat.onPanelKeyPress(event)"
|
||||
|
|
Loading…
Reference in a new issue