From fdd73d4ada5de721db145b84b5cd608d24b17d5b Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Tue, 24 May 2022 15:42:54 -0600 Subject: [PATCH] fx-compat: Item box: Fix multiline fields & autocomplete --- chrome/content/zotero/elements/itemBox.js | 122 +++++++++--------- .../elements/shadowAutocompleteInput.js | 60 +++++++++ scss/components/_itemBox.scss | 8 ++ 3 files changed, 131 insertions(+), 59 deletions(-) create mode 100644 chrome/content/zotero/elements/shadowAutocompleteInput.js diff --git a/chrome/content/zotero/elements/itemBox.js b/chrome/content/zotero/elements/itemBox.js index bbe493e926..ff7af25d28 100644 --- a/chrome/content/zotero/elements/itemBox.js +++ b/chrome/content/zotero/elements/itemBox.js @@ -26,6 +26,10 @@ "use strict"; { + var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + Services.scriptloader.loadSubScript("chrome://zotero/content/elements/shadowAutocompleteInput.js", this); + class ItemBox extends XULElement { constructor() { super(); @@ -1146,13 +1150,7 @@ } ensureElementIsVisible(elem) { - try { - var sbo = this.boxObject; - sbo.ensureElementIsVisible(elem); - } - catch (e) { - Zotero.logError(e); - } + elem.scrollIntoView(); } changeTypeTo(itemTypeID, menu) { @@ -1441,7 +1439,7 @@ } showEditor(elem) { - return (async function () { + return (async () => { Zotero.debug(`Showing editor for ${elem.getAttribute('fieldname')}`); var label = elem.closest('tr').querySelector('th'); @@ -1508,61 +1506,67 @@ } } - var t = document.createElement("input"); - t.setAttribute('id', `itembox-field-textbox-${fieldName}`); - t.setAttribute('value', value); - t.setAttribute('fieldname', fieldName); - t.setAttribute('ztabindex', tabindex); - t.setAttribute('flex', '1'); - - if (creatorField=='lastName') { - t.setAttribute('fieldMode', elem.getAttribute('fieldMode')); - t.setAttribute('newlines','pasteintact'); - } - + var t; if (Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName)) { - t.setAttribute('multiline', true); + t = document.createElement("textarea"); t.setAttribute('rows', 8); } - else { - // Add auto-complete for certain fields - if (Zotero.ItemFields.isAutocompleteField(fieldName) - || fieldName == 'creator') { - t.setAttribute('type', 'autocomplete'); - t.setAttribute('autocompletesearch', 'zotero'); + // Add auto-complete for certain fields + else if (Zotero.ItemFields.isAutocompleteField(fieldName) + || fieldName == 'creator') { + t = document.createElement("input", { is: 'shadow-autocomplete-input' }); + t.setAttribute('autocompletesearch', 'zotero'); + + let params = { + fieldName: fieldName, + libraryID: this.item.libraryID + }; + if (field == 'creator') { + params.fieldMode = parseInt(elem.getAttribute('fieldMode')); - let params = { - fieldName: fieldName, - libraryID: this.item.libraryID - }; - if (field == 'creator') { - params.fieldMode = parseInt(elem.getAttribute('fieldMode')); - - // Include itemID and creatorTypeID so the autocomplete can - // avoid showing results for creators already set on the item - let row = elem.closest('tr'); - let creatorTypeID = parseInt( - row.getElementsByClassName('creator-type-label')[0] - .getAttribute('typeid') - ); - if (itemID) { - params.itemID = itemID; - params.creatorTypeID = creatorTypeID; - } - - // Return - t.setAttribute('ontextentered', - 'this.handleCreatorAutoCompleteSelect(this, true)'); - // Tab/Shift-Tab - t.setAttribute('onchange', - 'this.handleCreatorAutoCompleteSelect(this)'); - }; - t.setAttribute( - 'autocompletesearchparam', JSON.stringify(params) + // Include itemID and creatorTypeID so the autocomplete can + // avoid showing results for creators already set on the item + let row = elem.closest('tr'); + let creatorTypeID = parseInt( + row.getElementsByClassName('creator-type-label')[0] + .getAttribute('typeid') ); - t.setAttribute('completeselectedindex', true); + if (itemID) { + params.itemID = itemID; + params.creatorTypeID = creatorTypeID; + } + + // Return + t.addEventListener('keydown', (event) => { + if (event.key == 'Enter') { + this.handleCreatorAutoCompleteSelect(t, true); + } + }); + // Tab/Shift-Tab + t.setAttribute('onchange', + 'this.handleCreatorAutoCompleteSelect(this)'); + + if (creatorField == 'lastName') { + t.setAttribute('fieldMode', elem.getAttribute('fieldMode')); + } } + t.setAttribute( + 'autocompletesearchparam', JSON.stringify(params) + ); + t.setAttribute('completeselectedindex', true); } + + if (!t) { + t = document.createElement("input"); + } + + t.id = `itembox-field-textbox-${fieldName}`; + t.value = value; + t.dataset.originalValue = value; + t.style.mozBoxFlex = 1; + t.setAttribute('fieldname', fieldName); + t.setAttribute('ztabindex', tabindex); + var box = elem.parentNode; box.replaceChild(t, elem); @@ -1597,7 +1601,7 @@ t.onkeypress = (event) => this.handleKeyPress(event); return t; - }.bind(this))(); + })(); } @@ -1701,7 +1705,7 @@ // Shift-enter adds new creator row if (fieldname.indexOf('creator-') == 0 && event.shiftKey) { // Value hasn't changed - if (target.getAttribute('value') == target.value) { + if (target.dataset.originalValue == target.value) { Zotero.debug("Value hasn't changed"); // If + button is disabled, just focus next creator row if (target.closest('tr').lastChild.lastChild.disabled) { @@ -1732,7 +1736,7 @@ case event.DOM_VK_ESCAPE: // Reset field to original value - target.value = target.getAttribute('value'); + target.value = target.dataset.originalValue; focused.blur(); diff --git a/chrome/content/zotero/elements/shadowAutocompleteInput.js b/chrome/content/zotero/elements/shadowAutocompleteInput.js new file mode 100644 index 0000000000..5d5a5aaa71 --- /dev/null +++ b/chrome/content/zotero/elements/shadowAutocompleteInput.js @@ -0,0 +1,60 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2022 Corporation for Digital Scholarship + Vienna, Virginia, USA + https://www.zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +"use strict"; + +{ + var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + // Load specific element because customElements.js loads on element creation only + Services.scriptloader.loadSubScript("chrome://global/content/elements/autocomplete-input.js", this); + + /** + * Extends AutocompleteInput to fix document.activeElement checks that + * don't work in a shadow DOM context. + */ + class ShadowAutocompleteInput extends customElements.get('autocomplete-input') { + get focused() { + // document.activeElement by itself doesn't traverse shadow DOMs; see + // https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/ + function activeElement(root) { + let activeHere = root.activeElement; + + if (activeHere?.shadowRoot) { + return activeElement(activeHere.shadowRoot); + } + else { + return activeHere; + } + } + + return this === activeElement(document); + } + } + + customElements.define("shadow-autocomplete-input", ShadowAutocompleteInput, { + extends: "input", + }); +} diff --git a/scss/components/_itemBox.scss b/scss/components/_itemBox.scss index fcbc935ae0..ef929c304e 100644 --- a/scss/components/_itemBox.scss +++ b/scss/components/_itemBox.scss @@ -25,6 +25,10 @@ td > [fieldname] { width: 100%; } +.value.multiline { + white-space: pre-line; +} + /*td > vbox > description { margin: 0 !important; @@ -66,6 +70,10 @@ label[singleField=false]:after margin: 0; }*/ +textarea { + font: inherit; +} + /* metadata field names */ th, .creator-type-label { font-weight: normal;