diff --git a/chrome/content/zotero-platform/mac/integration.css b/chrome/content/zotero-platform/mac/integration.css
index 2bce2b8afd..0a45fd42c9 100644
--- a/chrome/content/zotero-platform/mac/integration.css
+++ b/chrome/content/zotero-platform/mac/integration.css
@@ -26,9 +26,6 @@ window.citation-dialog {
border-radius: 10px;
}
-.citation-dialog.search:not([multiline="true"]) {
- height: 32px !important;
-}
.citation-dialog.entry {
background: -moz-linear-gradient(-90deg, rgb(243,123,119) 0, rgb(180,47,38) 50%, rgb(156,36,27) 50%);
@@ -95,8 +92,4 @@ panel button:hover:active {
panel button:-moz-focusring {
box-shadow: 0 0 1px -moz-mac-focusring inset, 0 0 4px 1px -moz-mac-focusring, 0 0 2px 1px -moz-mac-focusring;
-}
-
-.citation-dialog.bubble {
- padding: 1px 6px 1px 6px;
}
\ No newline at end of file
diff --git a/chrome/content/zotero-platform/unix/integration.css b/chrome/content/zotero-platform/unix/integration.css
index cfed6efacf..6213774b4b 100644
--- a/chrome/content/zotero-platform/unix/integration.css
+++ b/chrome/content/zotero-platform/unix/integration.css
@@ -18,6 +18,7 @@ window.citation-dialog {
}
#zotero-icon {
- margin: 0 0 0 2px;
+ padding: 4px 4px 6px 4px !important;
+ margin: 2px 0;
-moz-appearance: none;
}
diff --git a/chrome/content/zotero/integration/citationDialogIframe.html b/chrome/content/zotero/integration/citationDialogIframe.html
deleted file mode 100644
index 656c6958f3..0000000000
--- a/chrome/content/zotero/integration/citationDialogIframe.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/chrome/content/zotero/integration/insertNoteDialog.xhtml b/chrome/content/zotero/integration/insertNoteDialog.xhtml
index 58c3285963..a48cb2cb47 100644
--- a/chrome/content/zotero/integration/insertNoteDialog.xhtml
+++ b/chrome/content/zotero/integration/insertNoteDialog.xhtml
@@ -26,6 +26,7 @@
+
@@ -39,7 +40,7 @@
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
persist="screenX screenY"
- onkeypress="Zotero_QuickFormat.onKeyPress(event)"
+ onkeypress="Zotero_QuickFormat.onWindowKeyPress(event)"
onunload="Zotero_QuickFormat.onUnload()">
@@ -49,18 +50,12 @@
-
+
-
-
+
+
+
+
diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js
index e82882841b..57ad67cdbe 100644
--- a/chrome/content/zotero/integration/quickFormat.js
+++ b/chrome/content/zotero/integration/quickFormat.js
@@ -34,21 +34,25 @@ var Zotero_QuickFormat = new function () {
const charRe = /[\w\u007F-\uFFFF]/;
const numRe = /^[0-9\-–]+$/;
- var initialized, io, qfs, qfi, qfiWindow, qfiDocument, qfe, qfb, qfbHeight, qfGuidance,
- keepSorted, showEditor, referencePanel, referenceBox, referenceHeight = 0,
- separatorHeight = 0, currentLocator, currentLocatorLabel, currentSearchTime, dragging,
- panel, panelPrefix, panelSuffix, panelSuppressAuthor, panelLocatorLabel, panelLocator,
+ var initialized, io, editor, dialog, qfGuidance,
+ keepSorted, showEditor, referencePanel, referenceBox,
+ currentLocator, currentLocatorLabel, currentSearchTime, bubbleDraggedIndex = null,
+ itemPopover, itemPopoverPrefix, itemPopoverSuffix, itemPopoverSuppressAuthor, itemPopoverLocatorLabel, itemPopoverLocator,
panelLibraryLink, panelInfo, panelRefersToBubble, panelFrameHeight = 0, accepted = false,
- isPaste = false;
- var locatorLocked = true;
+ isPaste = false, _itemPopoverClosed, skipInputRefocus;
var locatorNode = null;
var _searchPromise;
var inputIsPristine = true;
+ var _lastFocusedInput = null;
+ var _bubbleMouseDown = false;
+
+ const ITEM_LIST_MAX_ITEMS = 50;
const SEARCH_TIMEOUT = 250;
const SHOWN_REFERENCES = 7;
- const ITEM_LIST_MAX_ITEMS = 50;
-
+ const WINDOW_WIDTH = 800;
+ const ESC_ENTER_THROTTLE = 1000;
+
/**
* Pre-initialization, when the dialog has loaded but has not yet appeared
*/
@@ -70,7 +74,7 @@ var Zotero_QuickFormat = new function () {
} else if(Zotero.isWin) {
document.documentElement.setAttribute("hidechrome", true);
}
-
+
// Include a different key combo in message on Mac
if(Zotero.isMac) {
var qf = document.querySelector('.citation-dialog.guidance');
@@ -79,23 +83,63 @@ var Zotero_QuickFormat = new function () {
new WindowDraggingElement(document.querySelector("window.citation-dialog"), window);
- qfs = document.querySelector(".citation-dialog.search");
- qfi = document.querySelector(".citation-dialog.iframe");
- qfb = document.querySelector(".citation-dialog.entry");
- qfbHeight = qfb.scrollHeight;
+ dialog = document.querySelector(".citation-dialog.entry");
+ editor = document.querySelector(".citation-dialog.editor");
+ dialog.addEventListener("click", _onQuickSearchClick, false);
+ dialog.addEventListener("keypress", _onQuickSearchKeyPress, false);
+ editor.addEventListener("dragover", _onEditorDragOver);
+ editor.addEventListener("drop", _onEditorDrop, false);
referencePanel = document.querySelector(".citation-dialog.reference-panel");
referenceBox = document.querySelector(".citation-dialog.reference-list");
-
+ // Navigation within the reference panel
+ referenceBox.addEventListener("keypress", (event) => {
+ // Enter or ; selects the reference
+ if (event.key == "Enter" || event.charCode == 59) {
+ event.preventDefault();
+ event.stopPropagation();
+ event.target.closest("richlistitem").click();
+ }
+ // Tab will move focus back to the input field
+ else if (event.key === "Tab") {
+ event.preventDefault();
+ event.stopPropagation();
+ _lastFocusedInput.focus();
+ }
+ // Can keep typing from the reference box
+ else if (isKeypressPrintable(event) || event.key === 'Backspace') {
+ event.preventDefault();
+ if (event.key === 'Backspace') {
+ _lastFocusedInput.value = _lastFocusedInput.value.slice(0, -1);
+ }
+ else {
+ _lastFocusedInput.value += event.key;
+ }
+ _lastFocusedInput.dispatchEvent(new Event('input', { bubbles: true }));
+ _lastFocusedInput.focus();
+ }
+ else if (["ArrowDown", "ArrowUp"].includes(event.key)) {
+ handleItemSelection(event);
+ }
+ // Right/Left arrow will hide ref panel and move focus to the previour/next element
+ else if ("ArrowLeft" == event.key) {
+ _lastFocusedInput.focus();
+ moveFocusBack(_lastFocusedInput);
+ }
+ else if ("ArrowRight" == event.key) {
+ _lastFocusedInput.focus();
+ moveFocusForward(_lastFocusedInput);
+ }
+ });
if (Zotero.isWin) {
- referencePanel.style.marginTop = "-29px";
+ dialog.style.marginBottom = "-36px";
if (Zotero.Prefs.get('integration.keepAddCitationDialogRaised')) {
- qfb.setAttribute("square", "true");
+ dialog.setAttribute("square", "true");
}
}
// With fx60 and drawintitlebar=true Firefox calculates the minHeight
// as titlebar+maincontent, so we have hack around that here.
else if (Zotero.isMac) {
- qfb.style.marginBottom = "-28px";
+ dialog.style.marginBottom = "-28px";
}
keepSorted = document.getElementById("keep-sorted");
@@ -106,15 +150,21 @@ var Zotero_QuickFormat = new function () {
keepSorted.setAttribute("checked", "true");
}
}
-
+ // Make icon focusable only if there's a visible menuitem
+ for (let menuitemID of ["keep-sorted", "show-editor", "classic-view"]) {
+ let menuitem = document.getElementById(menuitemID);
+ if (menuitem && menuitem.getAttribute("hidden") != "true") {
+ document.getElementById("zotero-icon").removeAttribute("disabled");
+ }
+ }
// Nodes for citation properties panel
- panel = document.getElementById("citation-properties");
- if (panel) {
- panelPrefix = document.getElementById("prefix");
- panelSuffix = document.getElementById("suffix");
- panelSuppressAuthor = document.getElementById("suppress-author");
- panelLocatorLabel = document.getElementById("locator-label");
- panelLocator = document.getElementById("locator");
+ itemPopover = document.getElementById("citation-properties");
+ if (itemPopover) {
+ itemPopoverPrefix = document.getElementById("prefix");
+ itemPopoverSuffix = document.getElementById("suffix");
+ itemPopoverSuppressAuthor = document.getElementById("suppress-author");
+ itemPopoverLocatorLabel = document.getElementById("locator-label");
+ itemPopoverLocator = document.getElementById("locator");
panelInfo = document.getElementById("citation-properties-info");
panelLibraryLink = document.getElementById("citation-properties-library-link");
@@ -130,26 +180,40 @@ var Zotero_QuickFormat = new function () {
child.setAttribute("label", locatorLabel);
labelList.appendChild(child);
}
-
+ // If the focus leaves the citation properties panel, close it
+ itemPopover.addEventListener("focusout", (_) => {
+ setTimeout(() => {
+ if (!itemPopover.contains(document.activeElement)) {
+ itemPopover.hidePopup();
+ }
+ });
+ });
+ // Focus locator when popover appears
+ itemPopover.addEventListener("popupshown", (e) => {
+ if (e.target.id == "citation-properties") {
+ document.getElementById('locator').focus();
+ }
+ });
}
// Don't need to set noautohide dynamically on these platforms, so do it now
if(Zotero.isMac || Zotero.isWin) {
referencePanel.setAttribute("noautohide", true);
}
- } else if (event.target === qfi.contentDocument) {
- qfiWindow = qfi.contentWindow;
- qfiDocument = qfi.contentDocument;
- qfb.addEventListener("click", _onQuickSearchClick, false);
- qfb.addEventListener("keypress", _onQuickSearchKeyPress, false);
- qfe = qfiDocument.querySelector(".citation-dialog.editor");
- qfe.addEventListener("drop", _onBubbleDrop, false);
- qfe.addEventListener("paste", _onPaste, false);
- if (Zotero_QuickFormat.citingNotes) {
- _quickFormat();
- }
+
+ // Resize whenever a bubble/input is added or removed of their attributes change
+ let resizeObserver = new MutationObserver(function (_) {
+ // Due to very odd mozilla behavior, calls to resize the window when
+ // dragging is happening make the next drag events after resize
+ // act strange (dragging doesn't happen, though dragstart fires but dragend doesn't)
+ // Resizing after delay in _onEditorDrop seems to not cause this issue
+ if (bubbleDraggedIndex === null) {
+ _resizeWindow();
+ }
+ });
+ resizeObserver.observe(editor, { childList: true, subtree: true, attributes: true });
}
- }
+ };
/**
* Initialize add citation dialog
@@ -165,7 +229,7 @@ var Zotero_QuickFormat = new function () {
screenX = window.screenX;
screenY = window.screenY;
}
- window.resizeTo(window.outerWidth, qfb.clientHeight);
+ window.resizeTo(window.outerWidth, dialog.clientHeight);
var xRange = [window.screen.availLeft, window.screen.left + window.screen.width - window.outerWidth];
var yRange = [window.screen.availTop, window.screen.top + window.screen.height - window.outerHeight];
if (screenX < xRange[0] || screenX > xRange[1] || screenY < yRange[0] || screenY > yRange[1]) {
@@ -176,76 +240,144 @@ var Zotero_QuickFormat = new function () {
}
qfGuidance = document.querySelector('.citation-dialog.guidance');
qfGuidance && qfGuidance.show();
- _refocusQfe();
})();
- window.focus();
- qfe.focus();
-
// load citation data
if (io.citation.citationItems.length) {
- // hack to get spacing right
- let event = new KeyboardEvent(
- "keypress",
- {
- key: " ",
- code: "Space",
- bubbles: true,
- cancelable: true,
- }
- );
- qfe.dispatchEvent(event);
await resizePromise;
- var node = qfe.firstChild;
- node.nodeValue = "";
- _showCitation(node);
- _resize();
+ _showCitation(null);
}
requestAnimationFrame(() => {
_updateItemList({ citedItems: [] });
});
+ refocusInput();
}
catch (e) {
Zotero.logError(e);
}
};
-
- function _refocusQfe() {
- referencePanel.blur();
- window.focus();
- qfe.focus();
- }
-
- /**
- * Gets the content of the text node that the cursor is currently within
- */
- function _getCurrentEditorTextNode() {
- var selection = qfiWindow.getSelection();
- if (!selection) return false;
- var range = selection.getRangeAt(0);
-
- var node = range.startContainer;
- if(node !== range.endContainer) return false;
- if(node.nodeType === Node.TEXT_NODE) return node;
- // Range could be referenced to the body element
- if(node === qfe) {
- for (let i = qfe.childNodes.length - 1; i >= 0; i--) {
- node = qfe.childNodes[i];
- if(node.nodeType === Node.TEXT_NODE) return node;
+ // If this input field was counted as previously focused,
+ // it will be cleared. Call before removing the field
+ function clearLastFocused(input) {
+ if (input == _lastFocusedInput) {
+ _lastFocusedInput = null;
+ }
+ }
+
+ // Create input in the end of the editor and focus it
+ function addInputToTheEnd() {
+ let newInput = _createInputField();
+ editor.appendChild(newInput);
+ newInput.focus();
+ return newInput;
+ }
+
+ // Return the focus to the input.
+ // If tryLastFocused=true, try to focus on the last active input first.
+ // Then, try to focus the last input from the editor.
+ // If there are no inputs, append one to the end and focus that.
+ function refocusInput(tryLastFocused = true) {
+ let input = tryLastFocused ? _lastFocusedInput : null;
+ if (!input) {
+ let allInputs = editor.querySelectorAll(".zotero-bubble-input");
+ if (allInputs.length > 0) {
+ input = allInputs[allInputs.length - 1];
}
}
- return false;
+ if (!input) {
+ input = addInputToTheEnd();
+ }
+ input.focus();
+ return input;
+ }
+
+ function _createInputField() {
+ let newInput = document.createElement("input");
+ newInput.className = "zotero-bubble-input";
+ newInput.setAttribute("aria-describedby", "input-description");
+ newInput.addEventListener("input", (_) => {
+ _resetSearchTimer();
+ // Expand/shrink the input field to match the width of content
+ let width = getContentWidth(newInput);
+ newInput.style.width = width + 'px';
+ });
+ newInput.addEventListener("keypress", onInputPress);
+ newInput.addEventListener("paste", _onPaste, false);
+ newInput.addEventListener("focus", (_) => {
+ // If the input used for the last search run is refocused,
+ // just make sure the reference panel is opened if it has items.
+ if (_lastFocusedInput == newInput && referenceBox.childElementCount > 0) {
+ _openReferencePanel();
+ return;
+ }
+ // Otherwise, run the search if the input is non-empty.
+ if (!isInputEmpty(newInput)) {
+ _resetSearchTimer();
+ }
+ else {
+ _updateItemList({ citedItems: [] });
+ }
+ });
+ // Delete empty input on blur unless focus is inside of the reference panel
+ newInput.addEventListener("blur", (_) => {
+ // Timeout to know where the focus landed after
+ setTimeout(() => {
+ let refPanelFocused = referencePanel.contains(document.activeElement);
+ if (isInputEmpty(newInput) && !refPanelFocused) {
+ // Resizing window right before drag-drop reordering starts, will interrupt the
+ // drag event. To avoid it, hide the input immediately and delete it after delay.
+ if (_bubbleMouseDown) {
+ newInput.style.display = "none";
+ setTimeout(() => {
+ newInput.remove();
+ }, 500);
+ clearLastFocused(newInput);
+ }
+ else if (document.activeElement !== newInput && !isEditorCleared()) {
+ // If no dragging, delete it if focus has moved elsewhere.
+ // If focus remained, the entire dialog lost focus, so do nothing
+ // If this is the last, non-removable, input - do not remove it as well.
+ newInput.remove();
+ clearLastFocused(newInput);
+ }
+ }
+ // If focus leaves input, hide the reference panel.
+ if (refPanelFocused) {
+ return;
+ }
+ let focusedInput = _getCurrentInput();
+ if (!focusedInput) {
+ referencePanel.hidePopup();
+ }
+ });
+ // If there was a br added before input so that it doesn't appear on the previous line,
+ // remove it
+ if (newInput.previousElementSibling?.tagName == "br") {
+ newInput.previousElementSibling.remove();
+ }
+ locatorNode = null;
+ });
+ return newInput;
+ }
+
+ // Theoritically, we can have two inputs next to each other. For example,
+ // if a bubble between two inputs is deleted. In this case,
+ // combine two inputs into one.
+ function _combineNeighboringInputs() {
+ let node = editor.firstChild;
+ while (node && node.nextElementSibling) {
+ if (node.classList == "zotero-bubble-input"
+ && node.nextElementSibling.classList == "zotero-bubble-input") {
+ let secondInputValue = node.nextElementSibling.value;
+ node.value += ` ${secondInputValue}`;
+ node.dispatchEvent(new Event('input', { bubbles: true }));
+ node.nextElementSibling.remove();
+ }
+ node = node.nextElementSibling;
+ }
}
- /**
- * Gets text within the currently selected node
- * @param {Boolean} [clear] If true, also remove these nodes
- */
- function _getEditorContent(clear) {
- var node = _getCurrentEditorTextNode();
- return node ? node.wholeText.trim() : false;
- }
/**
* Updates currentLocator based on a string
@@ -266,9 +398,6 @@ var Zotero_QuickFormat = new function () {
*/
var _quickFormat = Zotero.Promise.coroutine(function* () {
var str = _getEditorContent();
- if (str && str.match(/\s$/)) {
- locatorLocked = true;
- }
var haveConditions = false;
const etAl = " et al.";
@@ -282,9 +411,8 @@ var Zotero_QuickFormat = new function () {
currentLocatorLabel = false;
// check for adding a number onto a previous page number
- if(!locatorLocked && numRe.test(str)) {
+ if (locatorNode && numRe.test(str)) {
// add to previous cite
- var node = _getCurrentEditorTextNode();
let citationItem = JSON.parse(locatorNode && locatorNode.dataset.citationItem || "null");
if (citationItem) {
if (!("locator" in citationItem)) {
@@ -293,8 +421,10 @@ var Zotero_QuickFormat = new function () {
citationItem.locator += str;
locatorNode.dataset.citationItem = JSON.stringify(citationItem);
locatorNode.textContent = _buildBubbleString(citationItem);
- node.nodeValue = "";
_clearEntryList();
+ let input = _getCurrentInput();
+ input.value = "";
+ input.dispatchEvent(new Event('input', { bubbles: true }));
return;
}
}
@@ -305,17 +435,18 @@ var Zotero_QuickFormat = new function () {
if(m) {
if(m.index === 0) {
// add to previous cite
- var node = _getCurrentEditorTextNode();
- var prevNode = locatorLocked ? node.previousSibling : locatorNode;
+ let node = _getCurrentInput();
+ var prevNode = locatorNode || node.previousElementSibling;
let citationItem = JSON.parse(prevNode && prevNode.dataset.citationItem || "null");
if (citationItem) {
citationItem.locator = m[2];
+ citationItem.label = "page";
prevNode.dataset.citationItem = JSON.stringify(citationItem);
prevNode.textContent = _buildBubbleString(citationItem);
- node.nodeValue = "";
_clearEntryList();
- locatorLocked = false;
- locatorNode = prevNode;
+ let input = _getCurrentInput();
+ input.value = "";
+ input.dispatchEvent(new Event('input', { bubbles: true }));
return;
}
}
@@ -636,9 +767,6 @@ var Zotero_QuickFormat = new function () {
previousItemID = parseInt(referenceBox.selectedItem.getAttribute("zotero-item"));
}
- // Clear item list
- _clearEntryList();
-
// Take into account items cited in this citation. This means that the sorting isn't
// exactly by # of items cited from each library, but maybe it's better this way.
_updateCitationObject();
@@ -655,10 +783,14 @@ var Zotero_QuickFormat = new function () {
}
}
}
-
+
+ // To avoid blinking, fetch all necessary data first and then clear the
+ // reference box without resizing the panel (it happens later)
let openItems = await _getMatchingReaderOpenItems(options);
let citedItems = _getMatchingCitedItems(options);
let [selectedItems, libraryItems] = await _getMatchingLibraryItems(options);
+
+ _clearEntryList(true);
// Selected items are only returned if the currently selected tab is a library tab and
// in that case displayed at the top
@@ -708,7 +840,7 @@ var Zotero_QuickFormat = new function () {
previousLibrary = libraryID;
}
- _resize();
+ _resizeReferencePanel();
if (!referenceBox.children.length) {
return;
}
@@ -725,6 +857,11 @@ var Zotero_QuickFormat = new function () {
referenceBox.selectedIndex = selectedIndex;
referenceBox.ensureIndexIsVisible(selectedIndex);
+ // Record the last input used for a search
+ let currentInput = _getCurrentInput();
+ if (currentInput) {
+ _lastFocusedInput = currentInput;
+ }
}
/**
@@ -830,6 +967,7 @@ var Zotero_QuickFormat = new function () {
rll.setAttribute("orient", "vertical");
rll.setAttribute("class", "citation-dialog item");
rll.setAttribute("zotero-item", item.cslItemID ? item.cslItemID : item.id);
+ rll.setAttribute("aria-describedby", "item-description");
rll.appendChild(titleNode);
rll.appendChild(infoNode);
rll.addEventListener("click", Zotero_QuickFormat._bubbleizeSelected, false);
@@ -885,14 +1023,9 @@ var Zotero_QuickFormat = new function () {
// Locator
if(citationItem.locator) {
- if(citationItem.label) {
- // TODO localize and use short forms
- var label = citationItem.label;
- } else if(/[\-–,]/.test(citationItem.locator)) {
- var label = "pp.";
- } else {
- var label = "p."
- }
+ // Try to fetch the short form of the locator label. E.g. "p." for "page"
+ // If there is no locator label, default to "page" for now
+ let label = (Zotero.Cite.getLocatorString(citationItem.label || 'page', 'short') || '').toLocaleLowerCase();
str += ", "+label+" "+citationItem.locator;
}
@@ -920,34 +1053,44 @@ var Zotero_QuickFormat = new function () {
*/
function _insertBubble(citationItem, nextNode) {
var str = _buildBubbleString(citationItem);
-
- var bubble = qfiDocument.createElement("span");
- bubble.setAttribute("class", "citation-dialog bubble");
+ let bubble = document.createElement("div");
bubble.setAttribute("draggable", "true");
+ bubble.setAttribute("role", "button");
+ bubble.setAttribute("tabindex", "0");
+ bubble.setAttribute("aria-describedby", "bubble-description");
+ bubble.className = "citation-dialog bubble";
+ // VoiceOver works better without it
+ if (!Zotero.isMac) {
+ bubble.setAttribute("aria-label", str);
+ }
bubble.textContent = str;
- bubble.addEventListener("click", _onBubbleClick, false);
- bubble.addEventListener("dragstart", _onBubbleDrag, false);
+ bubble.addEventListener("click", _onBubbleClick);
+ bubble.addEventListener("dragstart", _onBubbleDrag);
+ bubble.addEventListener("dragend", onBubbleDragEnd);
+ bubble.addEventListener("keypress", onBubblePress);
+ bubble.addEventListener("mousedown", (_) => {
+ _bubbleMouseDown = true;
+ });
+ bubble.addEventListener("mouseup", (_) => {
+ _bubbleMouseDown = false;
+ });
bubble.dataset.citationItem = JSON.stringify(citationItem);
- if(nextNode && nextNode instanceof Range) {
- nextNode.insertNode(bubble);
- } else {
- qfe.insertBefore(bubble, (nextNode ? nextNode : null));
- }
-
- // make sure that there are no rogue
s
- var elements = qfe.getElementsByTagName("br");
- while(elements.length) {
- elements[0].parentNode.removeChild(elements[0]);
- }
+ editor.insertBefore(bubble, (nextNode ? nextNode : null));
return bubble;
}
+
+ function getAllBubbles() {
+ return [...editor.querySelectorAll(".bubble")];
+ }
/**
- * Clear list of bubbles
+ * Clear reference box
*/
- function _clearEntryList() {
+ function _clearEntryList(skipResize = false) {
while(referenceBox.hasChildNodes()) referenceBox.removeChild(referenceBox.firstChild);
- _resize();
+ if (!skipResize) {
+ _resizeReferencePanel();
+ }
}
/**
@@ -955,8 +1098,10 @@ var Zotero_QuickFormat = new function () {
*/
this._bubbleizeSelected = Zotero.Promise.coroutine(function* () {
const panelShowing = referencePanel.state === "open" || referencePanel.state === "showing";
+ let inputExists = _lastFocusedInput || _getCurrentInput()
if(!panelShowing || !referenceBox.hasChildNodes() || !referenceBox.selectedItem) 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);
@@ -1002,21 +1147,19 @@ var Zotero_QuickFormat = new function () {
citationItem["label"] = currentLocatorLabel;
}
}
- locatorLocked = "locator" in citationItem;
-
- // get next node and clear this one
- var node = _getCurrentEditorTextNode();
- node.nodeValue = "";
- // We are setting a locator node here, but below 2 calls reset
- // the bubble list for sorting, so we do some additional
- // handling to maintain the correct locator node in
- // _showCitation()
- var bubble = locatorNode = _insertBubble(citationItem, node);
+ let input = _getCurrentInput() || _lastFocusedInput;
+ let newBubble = _insertBubble(citationItem, input);
isPaste = false;
_clearEntryList();
+ // After the first bubble was made, the next input should not display the panel
+ // even if there were no searches yet
+ inputIsPristine = false;
+ clearLastFocused(input);
+ input.remove();
+
yield _previewAndSort();
- _refocusQfe();
-
+ refocusInput(false);
+ locatorNode = getAllBubbles().filter(bubble => bubble.textContent == newBubble.textContent)[0];
return true;
});
@@ -1027,93 +1170,90 @@ var Zotero_QuickFormat = new function () {
e.stopPropagation();
e.preventDefault();
}
-
- /**
- * Resizes window to fit content
- */
- function _resize() {
- var childNodes = referenceBox.childNodes, numReferences = 0, numSeparators = 0,
- firstReference, firstSeparator, numCitationItems, editorContent;
- for(var i=0, n=childNodes.length; i 30) {
- qfe.setAttribute("multiline", true);
- qfs.setAttribute("multiline", true);
- qfs.style.height = ((Zotero.isMac ? 6 : 4)+qfe.scrollHeight)+"px";
- window.sizeToContent();
- // the above line causes drawing artifacts to appear due to a bug with drawintitle property
- // in fx60. this fixes the artifacting
- if (Zotero.isMac && Zotero.platformMajorVersion >= 60) {
- document.children[0].setAttribute('drawintitlebar', 'false');
- document.children[0].setAttribute('drawintitlebar', 'true');
- }
- } else {
- delete qfs.style.height;
- qfe.removeAttribute("multiline");
- qfs.removeAttribute("multiline");
- window.sizeToContent();
- if (Zotero.isMac && Zotero.platformMajorVersion >= 60) {
- document.children[0].setAttribute('drawintitlebar', 'false');
- document.children[0].setAttribute('drawintitlebar', 'true');
- }
+ if (Math.abs(contentHeight - window.innerHeight) < 5) {
+ // Do not do anything if the difference is just a few pixels
+ return;
}
- var panelShowing = referencePanel.state === "open" || referencePanel.state === "showing";
-
- // Open the reference panel if there are references to show, except unless the user types something, and then
- // backspaces to remove that text (otherwise if we don't close the reference panel in that instance it is impossible
- // to accept the dialog without adding the selected reference by backspacing the search query).
- if((numReferences || numSeparators) && (!numCitationItems || editorContent.length || inputIsPristine)) {
- if(((!referenceHeight && firstReference) || (!separatorHeight && firstSeparator)
- || !panelFrameHeight) && !panelShowing) {
- _openReferencePanel();
- panelShowing = true;
- }
-
- if(!referenceHeight && firstReference) {
- referenceHeight = firstReference.scrollHeight + 1;
- }
-
- if(!separatorHeight && firstSeparator) {
- separatorHeight = firstSeparator.scrollHeight + 1;
- }
-
- if(!panelFrameHeight) {
- panelFrameHeight = referencePanel.getBoundingClientRect().height - referencePanel.clientHeight;
- var computedStyle = window.getComputedStyle(referenceBox, null);
- for(var attr of ["border-top-width", "border-bottom-width"]) {
- var val = computedStyle.getPropertyValue(attr);
- if(val) {
- var m = pixelRe.exec(val);
- if(m) panelFrameHeight += parseInt(m[1], 10);
- }
- }
- }
- referencePanel.sizeTo(window.outerWidth-30,
- numReferences*referenceHeight+numSeparators*separatorHeight+panelFrameHeight);
- if(!panelShowing) _openReferencePanel();
- } else if(panelShowing) {
- referencePanel.hidePopup();
- referencePanel.sizeTo(window.outerWidth-30, 0);
- _refocusQfe();
+ // Resized so that outerHeight=contentHeight
+ let outerHeightAdjustment = Math.max(window.outerHeight - window.innerHeight, 0);
+ window.resizeTo(WINDOW_WIDTH, contentHeight + outerHeightAdjustment);
+ if (Zotero.isMac && Zotero.platformMajorVersion >= 60) {
+ document.children[0].setAttribute('drawintitlebar', 'false');
+ document.children[0].setAttribute('drawintitlebar', 'true');
}
}
+ function _resizeReferencePanel() {
+ let visibleNodes = [];
+ let countedItems = 0;
+ // Fetch enough list items to include SHOWN_REFERENCES items
+ for (let n of [...referenceBox.childNodes]) {
+ visibleNodes.push(n);
+ if (n.className === "citation-dialog item") {
+ countedItems++;
+ }
+ if (countedItems >= SHOWN_REFERENCES) {
+ break;
+ }
+ }
+ let inputNode = _getCurrentInput() || _lastFocusedInput;
+ // References should be shown if:
+ // - there are matching items and the input is non-empty
+ // - the dialog just opened
+ // - everything but the last, non-removable, input has been cleared.
+ // Otherwise, the panel is hidden.
+ let showReferencePanel = visibleNodes.length > 0 && (!isInputEmpty(inputNode) || inputIsPristine || isEditorCleared());
+ if (!showReferencePanel) {
+ referencePanel.hidePopup();
+ return;
+ }
+ _openReferencePanel();
+ if (!panelFrameHeight) {
+ panelFrameHeight = referencePanel.getBoundingClientRect().height - referencePanel.clientHeight;
+ var computedStyle = window.getComputedStyle(referenceBox, null);
+ for (var attr of ["border-top-width", "border-bottom-width"]) {
+ var val = computedStyle.getPropertyValue(attr);
+ if (val) {
+ var m = pixelRe.exec(val);
+ if (m) panelFrameHeight += parseInt(m[1], 10);
+ }
+ }
+ }
+ // Find the height needed to display SHOWN_REFERENCES items
+ let height = visibleNodes.reduce((prev, cur) => {
+ return prev + cur.scrollHeight + 1;
+ }, 0);
+ referencePanel.sizeTo(window.outerWidth - 30, height + panelFrameHeight);
+ }
+
/**
- * Opens the reference panel and potentially refocuses the main text box
+ * When bubbles are reshufled during drag-drop they may required X or X+1 lines of the editor.
+ * Then the height of the window can get out-of-sync with the height of the editor.
+ * To avoid the editor being cutoff by the window or having blank space below the editor,
+ * this will lock the height of the editor until dragging is over.
+ */
+ function lockEditorHeight(isLocked) {
+ if (isLocked) {
+ let editorHeight = editor.getBoundingClientRect().height;
+ editor.style.height = `${editorHeight}px`;
+ editor.style['overflow-y'] = 'scroll';
+ editor.style['scrollbar-width'] = 'none'; // avoid scrollbar/arrows
+ return;
+ }
+ editor.style.removeProperty("height");
+ editor.style.removeProperty("overflow-y");
+ editor.style.removeProperty("scrollbar-width");
+ }
+
+ /**
+ * Opens the reference panel
*/
function _openReferencePanel() {
var panelShowing = referencePanel.state === "open" || referencePanel.state === "showing";
@@ -1131,14 +1271,14 @@ var Zotero_QuickFormat = new function () {
}
referencePanel.openPopup(document.documentElement, "after_start", 15,
- qfb.clientHeight-window.clientHeight, false, false, null);
+ dialog.clientHeight-window.clientHeight, false, false, null);
}
/**
* Clears all citations
*/
function _clearCitation() {
- var citations = qfe.getElementsByClassName("citation-dialog bubble");
+ var citations = document.getElementsByClassName("citation-dialog bubble");
while(citations.length) {
citations[0].parentNode.removeChild(citations[0]);
}
@@ -1153,17 +1293,11 @@ var Zotero_QuickFormat = new function () {
&& io.citation.sortedItems
&& io.citation.sortedItems.length) {
for(var i=0, n=io.citation.sortedItems.length; i label.value).join(".");
+ panelInfo.setAttribute('aria-label', description);
panelLibraryLink.hidden = !item.id;
if(item.id) {
var libraryName = item.libraryID ? Zotero.Libraries.getName(item.libraryID)
@@ -1260,9 +1395,9 @@ var Zotero_QuickFormat = new function () {
}
target.setAttribute("selected", "true");
- panel.openPopup(target, "after_start",
+ itemPopover.openPopup(target, "after_start",
target.clientWidth/2, 0, false, false, null);
- panelLocator.focus();
+ referencePanel.hidePopup();
}
/**
@@ -1305,51 +1440,112 @@ var Zotero_QuickFormat = new function () {
/**
* Handle escape for entire window
*/
- this.onKeyPress = function (event) {
+ this.onWindowKeyPress = function (event) {
+ if (accepted || new Date() - _itemPopoverClosed < ESC_ENTER_THROTTLE) return;
var keyCode = event.keyCode;
- if (keyCode === event.DOM_VK_ESCAPE && !accepted) {
+ if (keyCode === event.DOM_VK_ESCAPE) {
accepted = true;
io.citation.citationItems = [];
io.accept();
window.close();
}
+ else if (event.key == "Enter") {
+ event.preventDefault();
+ Zotero_QuickFormat._accept();
+ }
+ // In rare circumstances, the focus can get lost (e.g. if the focus is on an item
+ // in reference panel when it is refreshed and all nodes are deleted).
+ // To be able to recover from this, a tab from the window will focus the last input
+ else if (event.key == "Tab" && event.target.tagName == "window") {
+ refocusInput();
+ event.preventDefault();
+ }
};
- /**
- * Get bubbles within the current selection
- */
- function _getSelectedBubble(right) {
- var selection = qfiWindow.getSelection(),
- range = selection.getRangeAt(0);
- qfe.normalize();
-
- // Check whether the bubble is selected
- // Not sure whether this ever happens anymore
- var container = range.startContainer;
- if (container !== qfe) {
- if (container.dataset && container.dataset.citationItem) {
- return container;
- } else if (container.nodeType === Node.TEXT_NODE && container.wholeText == "") {
- if (container.parentNode === qfe) {
- var node = container;
- while (node = container.previousSibling) {
- if (node.dataset.citationItem) {
- return node;
- }
- }
- } else if (container.parentNode.dataset && container.parentNode.dataset.citationItem) {
- return container.parentNode;
- }
- }
- return null;
+ function moveFocusForward(node) {
+ if (node.nextElementSibling?.focus) {
+ node.nextElementSibling.focus();
+ return true;
}
+ return false;
+ }
- // Check whether there is a bubble anywhere to the left of this one
- var offset = range.startOffset,
- childNodes = qfe.childNodes,
- node = childNodes[offset-(right ? 0 : 1)];
- if (node && node.dataset && node.dataset.citationItem) return node;
- return null;
+ function moveFocusBack(node) {
+ // Skip line break if it's before the node
+ if (node.previousElementSibling?.tagName == "br") {
+ node = node.previousElementSibling;
+ }
+ if (node.previousElementSibling?.focus) {
+ node.previousElementSibling.focus();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets text within the currently selected node
+ */
+ function _getEditorContent() {
+ let node = _getCurrentInput();
+ return node ? node.value.trim() : false;
+ }
+
+ function _getCurrentInput() {
+ if (isInput(document.activeElement)) {
+ return document.activeElement;
+ }
+ return false;
+ }
+
+ // Get width of the text inside of the input
+ function getContentWidth(input) {
+ let span = document.createElement("span");
+ span.classList = "zotero-bubble-input";
+ span.innerText = input.value;
+ editor.appendChild(span);
+ let spanWidth = span.getBoundingClientRect().width;
+ span.remove();
+ return spanWidth;
+ }
+
+ // Determine if keypress event is on a printable character.
+ /* eslint-disable array-element-newline */
+ function isKeypressPrintable(event) {
+ if (event.ctrlKey || event.metaKey || event.altKey) return false;
+ // If it's a single character, for latin locales it has to be printable
+ if (event.key.length === 1) {
+ return true;
+ }
+ // Otherwise, check against a list of common control keys
+ let nonPrintableKeys = [
+ 'Enter', 'Escape', 'Backspace', 'Tab',
+ 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
+ 'Home', 'End', 'PageUp', 'PageDown',
+ 'Delete', 'Insert',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
+ 'Control', 'Meta', 'Alt', 'Shift', 'CapsLock'
+ ];
+ /* eslint-enable array-element-newline */
+
+ return !nonPrintableKeys.includes(event.key);
+ }
+
+ // Determine if the input is empty
+ function isInputEmpty(input) {
+ if (!input) {
+ return true;
+ }
+ return input.value.length == 0;
+ }
+
+ function isInput(node) {
+ if (!node) return false;
+ return node.classList.contains("zotero-bubble-input");
+ }
+
+ // Check if the editor has only one child node: the non-removable input
+ function isEditorCleared() {
+ return editor.childElementCount == 1 && editor.firstChild.classList.contains("zotero-bubble-input");
}
/**
@@ -1358,7 +1554,7 @@ var Zotero_QuickFormat = new function () {
*/
function _resetSearchTimer() {
// Show spinner
- var spinner = document.querySelector('.citation-dialog.spinner');
+ var spinner = document.querySelector('.citation-dialog.spinner image');
spinner.setAttribute("status", "animate");
// Cancel current search if active
if (_searchPromise && _searchPromise.isPending()) {
@@ -1373,25 +1569,223 @@ var Zotero_QuickFormat = new function () {
spinner.removeAttribute("status");
});
}
-
- async function _onQuickSearchClick(event) {
- if (qfGuidance) qfGuidance.hide();
- let bubble = _getSelectedBubble(false);
- if (bubble) {
- event.preventDefault();
- var nodeRange = qfiDocument.createRange();
- nodeRange.selectNode(bubble);
- nodeRange.collapse(false);
-
- var selection = qfiWindow.getSelection();
- selection.removeAllRanges();
- selection.addRange(nodeRange);
+ /**
+ * Find the last bubble (lastBubble) before a given coordinate and indicate if there are no bubbles
+ * to the left of the x-coordinate (startOfTheLine). If there is no last bubble, null is returned.
+ * startOfTheLine indicates if a
should be added so that a new input placed after lastBubble
+ * does not land on the previous line.
+ * Outputs for a sample of coordiantes (with #3 having startOfTheLine=true):
+ * NULL #1 #2 #3
+ * ↓ ↓ ↓ ↓
+ * [ bubble_1 bubble_2 bubble_3
+ * bubble_4, bubble_5 ]
+ * ↑ ↑ ↑ ↑
+ * #3 #4 #5 #5
+ * @param {Int} x - X coordinate
+ * @param {Int} y - Y coordinate
+ * @returns {lastBubble: Node, startOfTheLine: Bool}
+ */
+ function getLastBubbleBeforePoint(x, y) {
+ let bubbles = getAllBubbles();
+ let lastBubble = null;
+ let startOfTheLine = false;
+ for (let i = 0; i < bubbles.length; i++) {
+ let rect = bubbles[i].getBoundingClientRect();
+ // If within the vertical range of a bubble
+ if (y >= rect.top && y <= rect.bottom) {
+ // If the click is to the right of a bubble, it becomes a candidate
+ if (x > rect.right) {
+ lastBubble = i;
+ }
+ // Otherwise, stop and return the last bubble we saw if any
+ else {
+ if (i == 0) {
+ lastBubble = null;
+ }
+ else {
+ // Indicate there is no bubble before this one
+ startOfTheLine = lastBubble === null;
+ lastBubble = Math.max(i - 1, 0);
+ }
+ break;
+ }
+ }
}
+ if (lastBubble !== null) {
+ lastBubble = bubbles[lastBubble];
+ }
+ return { lastBubble: lastBubble, startOfTheLine: startOfTheLine };
}
+ 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);
+ // If click happened right before another input, focus that input
+ // instead of adding another one
+ if (isInput(lastBubble?.nextElementSibling)) {
+ lastBubble.nextElementSibling.focus();
+ return;
+ }
+ let newInput = _createInputField();
+ if (lastBubble !== null) {
+ lastBubble.after(newInput);
+ if (startOfTheLine) {
+ let lineBreak = document.createElement("br");
+ lastBubble.after(lineBreak);
+ }
+ }
+ else {
+ editor.prepend(newInput);
+ }
+ 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 selectNext = (node) => {
+ return event.key == "ArrowDown" ? node.nextElementSibling : node.previousElementSibling;
+ };
+ do {
+ selected = selectNext(selected);
+ referenceBox.ensureElementIsVisible(selected);
+ }
+ while (selected && selected.disabled);
+ if (selected) {
+ referenceBox.selectedItem = selected;
+ selected.focus();
+ }
+ };
+
+
+ var onInputPress = function (event) {
+ if (accepted) return;
+ if ((event.charCode === 59 /* ; */ || event.key === "Enter") && referencePanel.state === "open") {
+ event.preventDefault();
+ event.stopPropagation();
+ Zotero_QuickFormat._bubbleizeSelected();
+ }
+ else if (["ArrowLeft", "ArrowRight"].includes(event.key) && !event.shiftKey) {
+ // On arrow left from the beginning of the input, move to previous bubble
+ if (event.key === "ArrowLeft" && this.selectionStart === 0) {
+ moveFocusBack(this);
+ event.preventDefault();
+ }
+ // On arrow right from the end of the input, move to next bubble
+ else if (event.key === "ArrowRight" && this.selectionStart === this.value.length) {
+ moveFocusForward(this);
+ event.preventDefault();
+ }
+ }
+ else if (["Backspace", "Delete"].includes(event.key) && !this.value) {
+ event.preventDefault();
+ moveFocusBack(this);
+ // Cannot delete the input if there is nothing else left in the editor
+ if (getAllBubbles().length > 0) {
+ this.remove();
+ }
+ }
+ else if (["ArrowDown", "ArrowUp"].includes(event.key) && referencePanel.state === "open") {
+ // Arrow up/down from wherever will navigate the references panel if that's opened
+ handleItemSelection(event);
+ }
+ else if (event.key == "Tab" && !event.shiftKey && referencePanel.state === "open") {
+ // Tab from the input will focus the selected item in the references list
+ event.preventDefault();
+ event.stopPropagation();
+ referenceBox.selectedItem.focus();
+ }
+ };
+
+ var onBubblePress = function(event) {
+ if (accepted) return;
+ if (event.key == "ArrowDown" || event.key == " ") {
+ // On arrow down or whitespace, open new citation properties panel
+ _showItemPopover(this);
+ event.preventDefault();
+ }
+ else if (["ArrowLeft", "ArrowRight"].includes(event.key) && !event.shiftKey) {
+ event.preventDefault();
+ let newInput = _createInputField();
+
+ if (["ArrowLeft"].includes(event.key)) {
+ if (isInput(this.previousElementSibling)) {
+ moveFocusBack(this);
+ }
+ else {
+ this.before(newInput);
+ newInput.focus();
+ }
+ }
+ else if (["ArrowRight"].includes(event.key)) {
+ if (isInput(this.nextElementSibling)) {
+ moveFocusForward(this);
+ }
+ else {
+ this.after(newInput);
+ newInput.focus();
+ }
+ }
+ }
+ else if (["ArrowLeft", "ArrowRight"].includes(event.key) && event.shiftKey) {
+ // On Shift-Left/Right swap focused bubble with it's neighbor
+ event.preventDefault();
+ let findNextBubble = () => {
+ let node = event.target;
+ do {
+ node = event.key == "ArrowLeft" ? node.previousElementSibling : node.nextElementSibling;
+ } while (node && !(node.classList.contains("bubble") || node.classList.contains("zotero-bubble-input")));
+ return node;
+ };
+ let nextBubble = findNextBubble();
+ if (nextBubble) {
+ if (event.key == "ArrowLeft") {
+ nextBubble.before(this);
+ }
+ else {
+ nextBubble.after(this);
+ }
+ // Do not "Keep Sources Sorted"
+ if (io.sortable && keepSorted?.hasAttribute("checked")) {
+ keepSorted.removeAttribute("checked");
+ }
+ _previewAndSort();
+ }
+
+ this.focus();
+ }
+ else if (["Backspace", "Delete"].includes(event.key)) {
+ event.preventDefault();
+ if (!moveFocusBack(this)) {
+ moveFocusForward(this);
+ }
+ this.remove();
+ _combineNeighboringInputs();
+ // If all bubbles are removed, add and focus an input
+ if (getAllBubbles().length == 0) {
+ refocusInput();
+ }
+ }
+ else if (isKeypressPrintable(event) && event.key !== " ") {
+ event.preventDefault();
+ let input = refocusInput();
+ // Typing when you are focused on the bubble will re-focus the last input
+ input.value += event.key;
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+ };
+
/**
- * Handle return or escape
+ * Handle keyboard navigation not covered by other components
*/
var _onQuickSearchKeyPress = Zotero.Promise.coroutine(function* (event) {
// Prevent hang if another key is pressed after Enter
@@ -1403,127 +1797,136 @@ var Zotero_QuickFormat = new function () {
if(qfGuidance) qfGuidance.hide();
var keyCode = event.keyCode;
- if (keyCode === event.DOM_VK_RETURN) {
- event.preventDefault();
- if(!(yield Zotero_QuickFormat._bubbleizeSelected()) && !_getEditorContent()) {
- Zotero_QuickFormat._accept();
+ let focusedInput = _getCurrentInput();
+ if (event.key == ' ') {
+ // Space on toolbarbutton opens the popup
+ if (event.target.tagName == "toolbarbutton") {
+ event.target.firstChild.openPopup();
}
- } else if (keyCode === event.DOM_VK_ESCAPE) {
- // Handled in the event handler up, but we have to cancel it here
- // so that we do not issue another _quickFormat call
- return;
- } else if(keyCode === event.DOM_VK_TAB || event.charCode === 59 /* ; */) {
- event.preventDefault();
- Zotero_QuickFormat._bubbleizeSelected();
- } else if(keyCode === event.DOM_VK_BACK_SPACE || keyCode === event.DOM_VK_DELETE) {
- var bubble = _getSelectedBubble(keyCode === event.DOM_VK_DELETE);
-
- if(bubble) {
- event.preventDefault();
- bubble.parentNode.removeChild(bubble);
+ }
+ // On Home from the beginning of the input, create and focus input in the beginning
+ // If there is an input in the beginning already, just focus it
+ else if (event.key === "Home"
+ && (!focusedInput || (focusedInput.selectionStart === 0 && focusedInput.previousElementSibling))) {
+ let input;
+ if (isInput(editor.firstChild)) {
+ input = editor.firstChild;
}
-
- _resize();
- _resetSearchTimer();
- } else if(keyCode === event.DOM_VK_LEFT || keyCode === event.DOM_VK_RIGHT) {
- locatorLocked = true;
- var right = keyCode === event.DOM_VK_RIGHT,
- bubble = _getSelectedBubble(right);
- if(bubble) {
- event.preventDefault();
-
- var nodeRange = qfiDocument.createRange();
- nodeRange.selectNode(bubble);
- nodeRange.collapse(!right);
-
- var selection = qfiWindow.getSelection();
- selection.removeAllRanges();
- selection.addRange(nodeRange);
+ else {
+ input = _createInputField();
+ editor.prepend(input);
}
- } else if (["Home", "End"].includes(event.key)) {
- locatorLocked = true;
- setTimeout(() => {
- right = event.key == "End";
- bubble = _getSelectedBubble(right);
- if (bubble) {
+ input.focus();
+ }
+ // On End from the beginning of the input, create and focus input in the end.
+ // If there is an input in the end already, just focus it
+ else if (event.key === "End"
+ && (!focusedInput || (focusedInput.selectionStart === focusedInput.value.length && focusedInput.nextElementSibling))) {
+ let input;
+ if (isInput(editor.childNodes[editor.childNodes.length - 1])) {
+ input = editor.childNodes[editor.childNodes.length - 1];
+ input.focus();
+ }
+ else {
+ addInputToTheEnd();
+ }
+ }
+ else if (keyCode == event.DOM_VK_TAB) {
+ // Shift-Tab from the input field tries to focus on zotero icon dropdown
+ if (event.shiftKey) {
+ let icon = document.getElementById("zotero-icon");
+ if (icon && icon.getAttribute("disabled") != "true") {
+ icon.focus();
event.preventDefault();
-
- var nodeRange = qfiDocument.createRange();
- nodeRange.selectNode(bubble);
- nodeRange.collapse(!right);
-
- var selection = qfiWindow.getSelection();
- selection.removeAllRanges();
- selection.addRange(nodeRange);
+ return;
}
- })
- } else if(keyCode === event.DOM_VK_UP && referencePanel.state === "open") {
- locatorLocked = true;
- var selectedItem = referenceBox.selectedItem;
-
- var previousSibling;
+ }
+ // Tab places focus on the existing input or creates a new one in the end
+ refocusInput();
- // Seek the closet previous sibling that is not disabled
- while((previousSibling = selectedItem.previousSibling) && previousSibling.hasAttribute("disabled")) {
- selectedItem = previousSibling;
- }
- // If found, change to that
- if(previousSibling) {
- referenceBox.selectedItem = previousSibling;
-
- // If there are separators before this item, ensure that they are visible
- var visibleItem = previousSibling;
-
- while(visibleItem.previousSibling && visibleItem.previousSibling.hasAttribute("disabled")) {
- visibleItem = visibleItem.previousSibling;
- }
- referenceBox.ensureElementIsVisible(visibleItem);
- };
event.preventDefault();
- } else if(keyCode === event.DOM_VK_DOWN) {
- locatorLocked = true;
- if((Zotero.isMac ? event.metaKey : event.ctrlKey)) {
- // If meta key is held down, show the citation properties panel
- var bubble = _getSelectedBubble();
-
- if(bubble) _showCitationProperties(bubble);
- event.preventDefault();
- } else if (referencePanel.state === "open") {
- var selectedItem = referenceBox.selectedItem;
- var nextSibling;
-
- // Seek the closet next sibling that is not disabled
- while((nextSibling = selectedItem.nextSibling) && nextSibling.hasAttribute("disabled")) {
- selectedItem = nextSibling;
- }
-
- // If found, change to that
- if(nextSibling){
- referenceBox.selectedItem = nextSibling;
- referenceBox.ensureElementIsVisible(nextSibling);
- };
- event.preventDefault();
- }
- } else {
+ }
+ else {
isPaste = false;
- _resetSearchTimer();
}
});
- /**
- * Adds a dummy element to make dragging work
- */
+ // Dragging the bubble for drag-drop reordering
function _onBubbleDrag(event) {
- dragging = event.currentTarget;
- event.dataTransfer.setData("text/plain", '');
+ this.style.cursor = "grabbing";
+ // Sometimes due to odd mozilla drag-drop behavior, the dragend event may not fire
+ // so the element will get stuck with the id. Clean it up on next dragstart if it happens.
+ let lastDragged = document.querySelector("#dragged-bubble");
+ if (lastDragged) {
+ lastDragged.removeAttribute("id");
+ }
+ setTimeout(() => {
+ this.setAttribute("id", "dragged-bubble");
+ });
+ bubbleDraggedIndex = _getBubbleIndex(this);
+ event.dataTransfer.setData("zotero/citation_bubble", bubbleDraggedIndex);
+ event.dataTransfer.effectAllowed = "move";
event.stopPropagation();
+
+ // Lock editors height till the drag is over
+ lockEditorHeight(true);
+ }
+
+ // Bubble being dragged over the editor
+ function _onEditorDragOver(event) {
+ event.preventDefault();
+ let draggedBubble = document.querySelector("#dragged-bubble");
+ let bubble = event.target;
+ // If the target is an editor, find the last bubble before
+ // mouse location to insert the placeholder after it.
+ // This handles an edge case if a bubble is dragged to an empty spot in the end of the editor.
+ if (bubble.classList?.contains("editor")) {
+ let { lastBubble, _ } = getLastBubbleBeforePoint(event.clientX, event.clientY);
+ if (!lastBubble) {
+ return false;
+ }
+ bubble = lastBubble;
+ }
+ if (!draggedBubble || bubbleDraggedIndex === null || !bubble.classList?.contains("bubble")) {
+ return false;
+ }
+ if (bubble.getAttribute("id") == "dragged-bubble") {
+ return true;
+ }
+
+ let bubbleRect = bubble.getBoundingClientRect();
+ let midpoint = (bubbleRect.right + bubbleRect.left) / 2;
+ // If dragged-bubble is placed over the right half of another bubble, insert placeholder after it.
+ if (event.clientX > midpoint && bubble.nextElementSibling?.id !== "dragged-bubble") {
+ bubble.after(draggedBubble);
+ }
+ // If dragged-bubble is placed over the left half of another bubble, insert placeholder before it.
+ else if (event.clientX < midpoint && bubble.previousElementSibling?.id !== "dragged-bubble") {
+ bubble.before(draggedBubble);
+ }
+ return false;
+ }
+
+ function onBubbleDragEnd(_) {
+ bubbleDraggedIndex = null;
+ let bubble = document.getElementById("dragged-bubble");
+ if (bubble) {
+ bubble.style = "";
+ bubble.removeAttribute("id");
+ }
+ // Manual call to resize after delay to avoid strange mozilla behaviors that affect
+ // subsequent drag events when resizing happens around the same time as drag events
+ setTimeout(() => {
+ _resizeWindow();
+ }, 50);
+ lockEditorHeight(false);
}
/**
* Get index of bubble in citations
*/
function _getBubbleIndex(bubble) {
- var nodes = qfe.childNodes, index = 0;
+ var nodes = editor.childNodes, index = 0;
for (let node of nodes) {
if (node.dataset && node.dataset.citationItem) {
if (node == bubble) return index;
@@ -1534,45 +1937,35 @@ var Zotero_QuickFormat = new function () {
}
/**
- * Replaces the dummy element with a node to make dropping work
+ * Drop dragged bubble into the editor
*/
- var _onBubbleDrop = Zotero.Promise.coroutine(function* (event) {
+ var _onEditorDrop = Zotero.Promise.coroutine(function* (event) {
event.preventDefault();
event.stopPropagation();
- if (!dragging) return;
-
- // Find old position in list
- var oldPosition = _getBubbleIndex(dragging);
-
- // Move bubble
- var range = document.createRange();
- // Prevent dragging out of qfe
- if (event.target === qfe) {
- range.setStartAfter(qfe.childNodes[qfe.childNodes.length-1]);
- }
- else {
- range.setStartAfter(event.target);
- }
- dragging.parentNode.removeChild(dragging);
- var bubble = _insertBubble(JSON.parse(dragging.dataset.citationItem), range);
- dragging = null;
+ let bubble = document.getElementById("dragged-bubble");
+ if (bubbleDraggedIndex === null || !bubble) return;
// If moved out of order, turn off "Keep Sources Sorted"
- if(io.sortable && keepSorted && keepSorted.hasAttribute("checked") && oldPosition !== -1 &&
- oldPosition != _getBubbleIndex(bubble)) {
+ if(io.sortable && keepSorted && keepSorted.hasAttribute("checked")
+ && bubbleDraggedIndex != _getBubbleIndex(bubble)) {
keepSorted.removeAttribute("checked");
}
+ onBubbleDragEnd();
+
yield _previewAndSort();
- _moveCursorToEnd();
});
/**
* Handle a click on a bubble
*/
- function _onBubbleClick(event) {
- _moveCursorToEnd();
- _showCitationProperties(event.currentTarget);
+ function _onBubbleClick(_) {
+ // If citation properties dialog is opened for another bubble, just close it.
+ if (panelRefersToBubble) {
+ itemPopover.hidePopup();
+ return;
+ }
+ _showItemPopover(this);
}
/**
@@ -1585,11 +1978,11 @@ var Zotero_QuickFormat = new function () {
var str = Zotero.Utilities.Internal.getClipboard("text/unicode");
if (str) {
isPaste = true;
- var selection = qfiWindow.getSelection();
- var range = selection.getRangeAt(0);
- range.deleteContents();
- range.insertNode(qfiDocument.createTextNode(str.replace(/[\r\n]/g, " ").trim()));
- range.collapse(false);
+ this.value += str.replace(/[\r\n]/g, " ").trim();
+ let width = getContentWidth(this);
+ this.style.width = width + 'px';
+ // Move curor to the end
+ this.setSelectionRange(str.length, str.length);
_resetSearchTimer();
}
}
@@ -1599,52 +1992,102 @@ var Zotero_QuickFormat = new function () {
*/
this.onCitationPropertiesChanged = function(event) {
let citationItem = JSON.parse(panelRefersToBubble.dataset.citationItem || "{}");
- if(panelPrefix.value) {
- citationItem["prefix"] = panelPrefix.value;
+ if(itemPopoverPrefix.value) {
+ citationItem["prefix"] = itemPopoverPrefix.value;
} else {
delete citationItem["prefix"];
}
- if(panelSuffix.value) {
- citationItem["suffix"] = panelSuffix.value;
+ if(itemPopoverSuffix.value) {
+ citationItem["suffix"] = itemPopoverSuffix.value;
} else {
delete citationItem["suffix"];
}
- if(panelLocatorLabel.selectedIndex !== 0) {
- citationItem["label"] = panelLocatorLabel.selectedItem.value;
- } else {
- delete citationItem["label"];
- }
- if(panelLocator.value) {
- citationItem["locator"] = panelLocator.value;
+ if(itemPopoverLocator.value) {
+ citationItem["locator"] = itemPopoverLocator.value;
+ citationItem["label"] = itemPopoverLocatorLabel.selectedItem.value;
} else {
delete citationItem["locator"];
+ delete citationItem["label"];
}
- if(panelSuppressAuthor.checked) {
+ if(itemPopoverSuppressAuthor.checked) {
citationItem["suppress-author"] = true;
} else {
delete citationItem["suppress-author"];
}
- locatorLocked = "locator" in citationItem;
- locatorNode = panelRefersToBubble;
panelRefersToBubble.dataset.citationItem = JSON.stringify(citationItem);
panelRefersToBubble.textContent = _buildBubbleString(citationItem);
};
+
+ this.ignoreEvent = function (event) {
+ event.stopPropagation();
+ };
/**
* Handle closing citation properties panel
*/
- this.onCitationPropertiesClosed = function(event) {
+ this.onItemPopoverClosed = function(event) {
+ if (!panelRefersToBubble) {
+ return;
+ }
panelRefersToBubble.removeAttribute("selected");
Zotero_QuickFormat.onCitationPropertiesChanged();
+ // On ArrowUp from the citation popup, keep focus on the bubble
+ if (skipInputRefocus) {
+ panelRefersToBubble.focus();
+ skipInputRefocus = false;
+ }
+ else {
+ refocusInput();
+ }
+ panelRefersToBubble = null;
}
/**
- * Makes "Enter" work in the panel
+ * Keyboard navigation within citation properties dialog
*/
this.onPanelKeyPress = function(event) {
- var keyCode = event.keyCode;
- if (keyCode === event.DOM_VK_RETURN) {
- document.getElementById("citation-properties").hidePopup();
+ let stopEvent = () => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+ // Tabbing through the fields. This should not be necessary but for some reason
+ // without manually handling focus, the buttons are skipped during tabbing
+ if (event.key === "Tab") {
+ event.preventDefault();
+ let tabIndex = parseInt(event.target.closest("[tabindex]").getAttribute("tabindex"));
+ tabIndex += event.shiftKey ? -1 : 1;
+ if (tabIndex == 9) {
+ tabIndex = 1;
+ }
+ if (tabIndex == 0) {
+ tabIndex = 8;
+ }
+
+ itemPopover.querySelector(`[tabindex='${tabIndex}']`).focus();
+ }
+ // Space or enter triggers a click on buttons, checkboxes or menulist
+ if (event.key == ' ' || event.key === "Enter") {
+ if (event.target.tagName == "button"
+ || event.target.getAttribute("type") == "checkbox") {
+ event.target.click();
+ return stopEvent();
+ }
+ else if (event.target.tagName == "menulist") {
+ event.target.firstChild.openPopup();
+ return stopEvent();
+ }
+ }
+ // Arrow up closes the popup
+ if (event.key === "ArrowUp") {
+ skipInputRefocus = true;
+ itemPopover.hidePopup();
+ return stopEvent();
+ }
+ // Close on Escape or Enter and record when it happened to throttle the next
+ // window close attempt
+ if (["Enter", "Escape"].includes(event.key)) {
+ _itemPopoverClosed = new Date();
+ itemPopover.hidePopup();
}
};
diff --git a/chrome/content/zotero/integration/quickFormat.xhtml b/chrome/content/zotero/integration/quickFormat.xhtml
index 0f4a75635e..f21cab179a 100644
--- a/chrome/content/zotero/integration/quickFormat.xhtml
+++ b/chrome/content/zotero/integration/quickFormat.xhtml
@@ -41,72 +41,72 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
persist="screenX screenY"
width="800"
- onkeypress="Zotero_QuickFormat.onKeyPress(event)"
+ onkeypress="Zotero_QuickFormat.onWindowKeyPress(event)"
onunload="Zotero_QuickFormat.onUnload()">
-
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onkeydown="Zotero_QuickFormat.onPanelKeyPress(event)"
+ onpopuphidden="Zotero_QuickFormat.onItemPopoverClosed(event)">
-
-
+
+
-
-
+
-
-
-
+
-
-
+
-
&zotero.citation.suppressAuthor.label;
@@ -114,7 +114,7 @@
-
+
single`);
- if (!elem) {
+ // Fetch all s - including the short form
+ let terms = [...doc.querySelectorAll(`term[name="${locator}"]`)];
+ if (!terms.length) {
// If locator not found, get from the U.S. English locale
if (cslLocale != 'en-US') {
Zotero.logError(`Locator '${locator}' not found in ${cslLocale} locale -- trying en-US`);
if (!englishDoc) {
englishDoc = parser.parseFromString(Zotero.Cite.Locale.get('en-US'), 'text/xml');
}
- elem = englishDoc.querySelector(`term[name="${locator}"]:not([form="short"]) > single`);
- if (!elem) {
+ terms = englishDoc.querySelectorAll(`term[name="${locator}"]}`);
+ if (!terms.length) {
Zotero.logError(`Locator '${locator}' not found in en-US locale -- using name`);
}
}
@@ -95,22 +102,31 @@ Zotero.Cite = {
Zotero.logError(`Locator '${locator}' not found in en-US locale -- using name`);
}
// If still not found, use the locator name directly
- if (!elem) {
+ if (!terms.length) {
map.set(locator, Zotero.Utilities.capitalize(locator));
continue;
}
}
- // If is empty, use the locator name directly
- let str = elem.textContent;
- if (!str) {
- Zotero.logError(`Locator '${locator}' is empty in ${cslLocale} locale -- using name`);
- map.set(locator, Zotero.Utilities.capitalize(locator));
- continue;
+ // Add all terms to the cache. If a term has a form, add "_