diff --git a/chrome/content/zotero/elements/editableText.js b/chrome/content/zotero/elements/editableText.js index 7ff37d9a43..a134bcd42a 100644 --- a/chrome/content/zotero/elements/editableText.js +++ b/chrome/content/zotero/elements/editableText.js @@ -139,9 +139,7 @@ span.innerText = this.value; this.append(span); let size = span.getBoundingClientRect(); - let inlinePadding = getComputedStyle(this).getPropertyValue('--editable-text-padding-inline'); - let blockPadding = getComputedStyle(this).getPropertyValue('--editable-text-padding-block'); - this.style['max-width'] = `calc(${size.width}px + 2*${inlinePadding} + 2*${blockPadding})`; + this.style['max-width'] = `calc(${size.width}px)`; this.querySelector("span").remove(); }; diff --git a/chrome/content/zotero/elements/itemBox.js b/chrome/content/zotero/elements/itemBox.js index 5ac5607bfd..6d5731700c 100644 --- a/chrome/content/zotero/elements/itemBox.js +++ b/chrome/content/zotero/elements/itemBox.js @@ -36,12 +36,10 @@ this.showTypeMenu = false; this.hideEmptyFields = false; this.clickByRow = false; - this.clickByItem = false; this.preventFocus = false; - this.clickHandler = null; - this.blurHandler = null; this.eventHandlers = []; + this.itemTypeMenu = null; this.showInFeeds = true; @@ -53,13 +51,14 @@ this._fieldAlternatives = {}; this._fieldOrder = []; this._tabIndexMinCreators = 10; - this._tabIndexMaxCreators = 0; - this._tabIndexMinFields = 1000; this._tabIndexMaxFields = 0; this._initialVisibleCreators = 5; + this._draggedCreator = false; + this._ztabindex = 0; + this._selectField = null; this.content = MozXULElement.parseXULToFragment(` - +
@@ -67,10 +66,15 @@ + + + + + - - - + + + @@ -91,11 +95,7 @@
- - - - -
+
@@ -108,12 +108,41 @@ this.appendChild(document.importNode(this.content, true)); - this._creatorTypeMenu.addEventListener('popupshowing', () => { - var typeBox = document.popupNode.localName == 'th' ? document.popupNode : document.popupNode.parentNode; + this._creatorTypeMenu.addEventListener('command', async (event) => { + var typeBox = document.popupNode; var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); + var typeID = event.explicitOriginalTarget.getAttribute('typeid'); + var row = typeBox.parentNode; + var fields = this.getCreatorFields(row); + fields.creatorTypeID = typeID; + typeBox.querySelector("#creator-type-label-inner").textContent = Zotero.getString( + 'creatorTypes.' + Zotero.CreatorTypes.getName(typeID) + ); + typeBox.setAttribute('typeid', typeID); + + this._lastTabIndex = -1; + this.modifyCreator(index, fields); + if (this.saveOnEdit) { + await this.blurOpenField(); + await this.item.saveTx(); + } + }); + + this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (event) => { + var row = document.popupNode.closest('.meta-row'); + var typeBox = row.querySelector('.creator-type-label').parentNode; + var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var item = this.item; var exists = item.hasCreatorAt(index); + if (exists) { + var fieldMode = item.getCreator(index).name !== undefined ? 1 : 0; + } + var hideTransforms = !exists || !!fieldMode; + if (hideTransforms) { + event.preventDefault(); + } + var moreCreators = item.numCreators() > index + 1; var hideMoveToTop = !exists || index < 2; @@ -125,10 +154,21 @@ this._id('zotero-creator-move-to-top').setAttribute('hidden', hideMoveToTop); this._id('zotero-creator-move-up').setAttribute('hidden', hideMoveUp); this._id('zotero-creator-move-down').setAttribute('hidden', hideMoveDown); + var creatorNameBox = row.getElementsByClassName("creator-name-box")[0]; + this._id('creator-transform-switch').setAttribute("label", creatorNameBox.getAttribute("switch-mode-label")); }); - this._creatorTypeMenu.addEventListener('command', async (event) => { - var typeBox = document.popupNode.localName == 'th' ? document.popupNode : document.popupNode.parentNode; + // Ensure no button is forced to stay visible once the menu is cloed + this.querySelector('#zotero-creator-transform-menu').addEventListener('popuphidden', (_) => { + let row = document.popupNode.closest('.meta-row'); + for (let node of row.querySelectorAll('toolbarbutton.show-on-hover')) { + node.style.visibility = ''; + } + }); + + this._id('zotero-creator-transform-menu').addEventListener('command', async (event) => { + var row = document.popupNode.closest('.meta-row'); + var typeBox = row.querySelector('.creator-type-label').parentNode; var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); if (event.explicitOriginalTarget.className == 'zotero-creator-move') { @@ -149,43 +189,13 @@ this.moveCreator(index, dir); return; } - - var typeID = event.explicitOriginalTarget.getAttribute('typeid'); - var row = typeBox.parentNode; - var fields = this.getCreatorFields(row); - fields.creatorTypeID = typeID; - typeBox.getElementsByTagName('label')[0].textContent = Zotero.getString( - 'creatorTypes.' + Zotero.CreatorTypes.getName(typeID) - ); - typeBox.setAttribute('typeid', typeID); - - /* If a creator textbox is already open, we need to - change its autocomplete parameters so that it - completes on a creator with a different creator type */ - var changedParams = { - creatorTypeID: typeID - }; - this._updateAutoCompleteParams(row, changedParams); - - this.modifyCreator(index, fields); - if (this.saveOnEdit) { - await this.blurOpenField(); - await this.item.saveTx(); - } - }); - - this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (event) => { - var row = document.popupNode.closest('tr'); - var typeBox = row.querySelector('.creator-type-label'); - var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); - var item = this.item; - var exists = item.hasCreatorAt(index); - if (exists) { - var fieldMode = item.getCreator(index).name !== undefined ? 1 : 0; - } - var hideTransforms = !exists || !!fieldMode; - if (hideTransforms) { - event.preventDefault(); + else if (event.explicitOriginalTarget.id == "creator-transform-switch") { + // Switch creator field mode action + var creatorNameBox = row.getElementsByClassName("creator-name-box")[0]; + var lastName = creatorNameBox.firstChild; + let fieldMode = parseInt(lastName.getAttribute("fieldMode")); + this.switchCreatorMode(row, fieldMode == 1 ? 0 : 1, false, true, index); + return; } }); @@ -195,22 +205,25 @@ this._id('creator-transform-capitalize').addEventListener('command', event => this.capitalizeCreatorName(event)); - this._doiMenu.addEventListener('popupshowing', () => { - let disabled = !this._doiMenu.dataset.doi; - this._id('zotero-doi-menu-view-online').disabled = disabled; - this._id('zotero-doi-menu-copy').disabled = disabled; + this._linkMenu.addEventListener('popupshowing', () => { + let disabled = !this._linkMenu.dataset.link; + this._id('zotero-link-menu-view-online').disabled = disabled; + this._id('zotero-link-menu-copy').disabled = disabled; }); - this._id('zotero-doi-menu-view-online').addEventListener( + this._id('zotero-link-menu-view-online').addEventListener( 'command', - event => ZoteroPane.loadURI(this._doiMenu.dataset.doi, event) + event => ZoteroPane.loadURI(this._linkMenu.dataset.link, event) ); - this._id('zotero-doi-menu-copy').addEventListener( + this._id('zotero-link-menu-copy').addEventListener( 'command', - () => Zotero.Utilities.Internal.copyTextToClipboard(this._doiMenu.dataset.doi) + () => Zotero.Utilities.Internal.copyTextToClipboard(this._linkMenu.dataset.link) ); this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox'); + Zotero.Prefs.registerObserver('fontSize', () => { + this.refresh(); + }); } destroy() { @@ -248,7 +261,6 @@ this.showTypeMenu = false; this.hideEmptyFields = false; this.clickByRow = false; - this.clickByItem = false; switch (val) { case 'view': @@ -260,8 +272,6 @@ this.editable = true; this.saveOnEdit = true; this.showTypeMenu = true; - this.clickHandler = this.showEditor; - this.blurHandler = this.hideEditor; break; case 'fieldmerge': @@ -393,10 +403,6 @@ this._fieldOrder = val; } - get itemTypeMenu() { - return this._id('item-type-menu'); - } - // // Private properties // @@ -420,8 +426,8 @@ return '(' + Zotero.getString('pane.item.defaultFullName') + ')'; } - get _doiMenu() { - return this._id('zotero-doi-menu'); + get _linkMenu() { + return this._id('zotero-link-menu'); } @@ -435,6 +441,11 @@ if (id != this.item.id) { continue; } + let activeArea = this.getFocusedTextArea(); + // Re-select currently active area after refresh. + if (activeArea) { + this._selectField = activeArea.getAttribute("fieldname"); + } this.refresh(); break; } @@ -442,40 +453,29 @@ refresh() { Zotero.debug('Refreshing item box'); - + if (!this.item) { Zotero.debug('No item to refresh', 2); return; } this.updateRetracted(); - - if (this.clickByItem) { - this.onclick = () => this.clickHandler(this); - } - - // Item type menu - if (!this.itemTypeMenu) { - this.addItemTypeMenu(); - } - if (this.showTypeMenu) { - this.updateItemTypeMenuSelection(); - this.itemTypeMenu.parentNode.parentNode.style.display = 'contents'; - this.itemTypeMenu.setAttribute('ztabindex', '0'); - } - else { - this.itemTypeMenu.parentNode.parentNode.style.display = 'none'; - } - delete this._doiMenu.dataset.doi; + // Init tab index to begin after all creator rows + this._ztabindex = this._tabIndexMinCreators * (this.item.numCreators() || 1); + delete this._linkMenu.dataset.link; // // Clear and rebuild metadata fields // - while (this._infoTable.childNodes.length > 1) { + while (this._infoTable.childNodes.length > 0) { this._infoTable.removeChild(this._infoTable.lastChild); } - + + // Item type menu + this.addItemTypeMenu(); + this.updateItemTypeMenuSelection(); + this.itemTypeMenu.disabled = !this.showTypeMenu; var fieldNames = []; // Manual field order @@ -486,10 +486,6 @@ } // Get field order from database else { - if (!this.showTypeMenu) { - fieldNames.push("itemType"); - } - var fields = Zotero.ItemFields.getItemTypeFields(this.item.getField("itemTypeID")); for (let i = 0; i < fields.length; i++) { @@ -509,6 +505,9 @@ for (let i = 0; i < fieldNames.length; i++) { var fieldName = fieldNames[i]; + if (["abstractNote", "title", "caseName", "subject"].includes(fieldName)) { + continue; + } var val = ''; if (fieldName) { @@ -545,52 +544,40 @@ var fieldIsClickable = this._fieldIsClickable(fieldName); - // Start tabindex at 1001 after creators - var tabindex = fieldIsClickable - ? (i > 0 ? this._tabIndexMinFields + i : 1) - : 0; - this._tabIndexMaxFields = Math.max(this._tabIndexMaxFields, tabindex); - if (fieldIsClickable && !Zotero.Items.isPrimaryField(fieldName) && Zotero.ItemFields.isDate(fieldName) // TEMP - NSF && fieldName != 'dateSent') { - this.addDateRow(fieldName, this.item.getField(fieldName, true), tabindex); + this.addDateRow(fieldName, this.item.getField(fieldName, true)); continue; } } - let th = document.createElement("th"); - th.setAttribute('fieldname', fieldName); + let rowLabel = document.createElement("div"); + rowLabel.className = "meta-label"; + rowLabel.setAttribute('fieldname', fieldName); let valueElement = this.createValueElement( - val, fieldName, tabindex + val, fieldName ); - var prefix = ''; - // Add '(...)' before 'Abstract' for collapsed abstracts - if (fieldName == 'abstractNote') { - if (val && !Zotero.Prefs.get('lastAbstractExpand')) { - prefix = '(\u2026) '; - } - } - if (fieldName) { let label = document.createElement('label'); label.className = 'key'; - label.textContent = prefix + Zotero.ItemFields.getLocalizedString(fieldName); - th.appendChild(label); + label.textContent = Zotero.ItemFields.getLocalizedString(fieldName); + label.setAttribute("id", `itembox-field-${fieldName}-label`); + rowLabel.appendChild(label); + valueElement.setAttribute('aria-labelledby', label.id); } - + let openLinkButton; + let addContextMenu = false; // TEMP - NSF (homepage) if ((fieldName == 'url' || fieldName == 'homepage') // Only make plausible HTTP URLs clickable && Zotero.Utilities.isHTTPURL(val, true)) { - th.classList.add("pointer"); - // TODO: make getFieldValue non-private and use below instead - th.addEventListener('click', () => Zotero.launchURL(th.nextSibling.firstChild.value || th.nextSibling.firstChild.textContent)); - th.setAttribute('title', Zotero.getString('pane.item.viewOnline.tooltip')); + openLinkButton = this.createOpenLinkIcon(rowLabel, val); + addContextMenu = true; } else if (fieldName == 'DOI' && val && typeof val == 'string') { // Pull out DOI, in case there's a prefix @@ -603,49 +590,67 @@ .replace(/\?/g, '%3f') .replace(/%/g, '%25') .replace(/"/g, '%22'); - th.classList.add("pointer"); - th.addEventListener('click', event => ZoteroPane.loadURI(doi, event)); - th.setAttribute('title', Zotero.getString('pane.item.viewOnline.tooltip')); - this._doiMenu.dataset.doi = doi; + openLinkButton = this.createOpenLinkIcon(rowLabel, doi); + addContextMenu = true; } } - else if (fieldName == 'abstractNote') { - if (val.length) { - th.classList.add("pointer"); - } - th.addEventListener('click', function () { - if (this.nextSibling.querySelector('input, textarea')) { - this.nextSibling.querySelector('input, textarea').blur(); - } - else { - this.closest('item-box').toggleAbstractExpand( - this.firstElementChild, this.closest('tr').querySelector('.value') - ); - } - }); + let rowData = document.createElement('div'); + rowData.className = "meta-data"; + rowData.appendChild(valueElement); + if (openLinkButton) { + rowData.appendChild(openLinkButton); } - else { - th.addEventListener('click', function () { - if (this.nextSibling.querySelector('input, textarea')) { - this.nextSibling.querySelector('input, textarea').blur(); - } - }); + if (addContextMenu) { + let link = val; + rowData.oncontextmenu = (event) => { + this._linkMenu.dataset.link = link; + document.popupNode = rowLabel.parentElement; + this.handlePopupOpening(event, this._id('zotero-link-menu')); + }; } - let td = document.createElement('td'); - td.appendChild(valueElement); - - this.addDynamicRow(th, td); + // Add options button for title fields + if (this.editable && fieldID && val && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' + || Zotero.ItemFields.isFieldOfBase(fieldID, 'title') + || Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) { + let optionsButton = document.createXULElement("toolbarbutton"); + optionsButton.className = "zotero-clicky zotero-clicky-options show-on-hover"; + optionsButton.setAttribute("ztabindex", ++this._ztabindex); + optionsButton.setAttribute('data-l10n-id', "itembox-button-options"); + // eslint-disable-next-line no-loop-func + let triggerPopup = (e) => { + let oldValue = valueElement.value; + let menupopup = ZoteroItemPane.buildFieldTransformMenu({ + value: oldValue, + onTransform: (newValue) => { + this._setFieldTransformedValue(valueElement, newValue); + } + }); + this.querySelector('popupset').append(menupopup); + menupopup.addEventListener('popuphidden', (e) => { + menupopup.remove(); + optionsButton.style.visibility = ''; + }); + this.handlePopupOpening(e, menupopup); + }; + // Same popup triggered for right-click and options button click + optionsButton.addEventListener("click", triggerPopup); + optionsButton.addEventListener('keypress', event => this.handleKeyPress(event)); + rowData.appendChild(optionsButton); + rowData.oncontextmenu = triggerPopup; + } + + this.addDynamicRow(rowLabel, rowData); if (fieldName && this._selectField == fieldName) { - this.showEditor(valueElement); + valueElement.focus(); + this._selectField = null; } // In field merge mode, add a button to switch field versions else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') { var button = document.createXULElement("toolbarbutton"); - button.className = 'zotero-field-version-button'; - button.setAttribute('image', 'chrome://zotero/skin/treesource-duplicates.png'); + button.className = 'zotero-field-version-button zotero-clicky-merge'; button.setAttribute('type', 'menu'); button.setAttribute('wantdropmarker', true); @@ -658,11 +663,11 @@ if (v != sv) { menuitem.setAttribute('tooltiptext', v); } - menuitem.setAttribute('fieldName', fieldName); + menuitem.setAttribute('fieldname', fieldName); menuitem.setAttribute('originalValue', v); menuitem.addEventListener('command', () => { this.item.setField( - menuitem.getAttribute('fieldName'), + menuitem.getAttribute('fieldname'), menuitem.getAttribute('originalValue') ); this.refresh(); @@ -670,15 +675,16 @@ popup.appendChild(menuitem); } - td.appendChild(button); + rowData.appendChild(button); } } - this._selectField = false; + this._tabIndexMaxFields = this._ztabindex; // Save the last tab index // // Creators // + this._ztabindex = 1; // Reset tab index to 1, since creators go before other fields // Creator type menu if (this.editable) { while (this._creatorTypeMenu.hasChildNodes()) { @@ -699,36 +705,19 @@ menuitem.setAttribute("typeid", Zotero.CreatorTypes.getID(i)); this._creatorTypeMenu.appendChild(menuitem); } - - var moveSep = document.createXULElement("menuseparator"); - var moveToTop = document.createXULElement("menuitem"); - var moveUp = document.createXULElement("menuitem"); - var moveDown = document.createXULElement("menuitem"); - moveSep.id = "zotero-creator-move-sep"; - moveToTop.id = "zotero-creator-move-to-top"; - moveUp.id = "zotero-creator-move-up"; - moveDown.id = "zotero-creator-move-down"; - moveToTop.className = "zotero-creator-move"; - moveUp.className = "zotero-creator-move"; - moveDown.className = "zotero-creator-move"; - moveToTop.setAttribute("label", Zotero.getString('pane.item.creator.moveToTop')); - moveUp.setAttribute("label", Zotero.getString('pane.item.creator.moveUp')); - moveDown.setAttribute("label", Zotero.getString('pane.item.creator.moveDown')); - this._creatorTypeMenu.appendChild(moveSep); - this._creatorTypeMenu.appendChild(moveToTop); - this._creatorTypeMenu.appendChild(moveUp); - this._creatorTypeMenu.appendChild(moveDown); + this._creatorTypeMenu.addEventListener('popuphidden', () => { + // If the popup was opened with a mouse click, blur the field to hide icons + if (this._creatorTypeMenu.getAttribute("blur-on-hidden")) { + document.activeElement.blur(); + this._creatorTypeMenu.removeAttribute("blur-on-hidden"); + } + }); } // Creator rows - // Place, in order of preference, after title, after type, - // or at beginning - var titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title'); - var field = this._infoTable.querySelector(`[fieldname="${Zotero.ItemFields.getName(titleFieldID)}"]`); - if (!field) { - field = this._infoTable.querySelector('[fieldName="itemType"]'); - } + // Place, in order of preference, after type or at beginning + let field = this._infoTable.querySelector('[fieldname="itemType"]'); if (field) { this._beforeRow = field.parentNode.nextSibling; } @@ -754,6 +743,20 @@ this.disableCreatorAddButtons(); } } + if (this._draggedCreator) { + this._draggedCreator = false; + // Block hover effects on creators, enable them back on first mouse movement. + // See comment in creatorDragPlaceholder() for explanation + for (let label of document.querySelectorAll(".meta-label[fieldname^='creator-']")) { + label.closest(".meta-row").classList.add("noHover"); + } + let removeHoverBlock = () => { + let noHoverRows = document.querySelectorAll('.noHover'); + noHoverRows.forEach(el => el.classList.remove('noHover')); + document.removeEventListener('mousemove', removeHoverBlock); + }; + document.addEventListener('mousemove', removeHoverBlock); + } // Additional creators not displayed if (num > max) { @@ -794,27 +797,80 @@ this._showCreatorTypeGuidance = false; } + // On click of the label, toggle the focus of the value field + for (let label of this.querySelectorAll(".meta-label > label")) { + if (!this.editable) { + break; + } + label.addEventListener('click', (_) => { + let labelWrapper = label.closest(".meta-label"); + if (labelWrapper.nextSibling.contains(document.activeElement)) { + document.activeElement.blur(); + } + else { + let valueField = labelWrapper.nextSibling.firstChild; + if (valueField.id === "item-type-menu") { + valueField.querySelector("menupopup").openPopup(); + return; + } + labelWrapper.nextSibling.firstChild.focus(); + } + }); + } this._refreshed = true; + // Add tabindex=0 to all focusable element + this.querySelectorAll("[ztabindex]").forEach((node) =>{ + node.setAttribute("tabindex", 0); + }); + for (let elem of [...this.querySelectorAll("editable-text")]) { + if (!elem.getAttribute("autocomplete")) { + this.addAutocompleteToElement(elem); + } + } } addItemTypeMenu() { - var td = document.createElement('td'); - var menulist = document.createXULElement("menulist", { is: "menulist-item-types" }); - menulist.id = "item-type-menu"; - menulist.className = "zotero-clicky"; - menulist.addEventListener('command', (event) => { - this.changeTypeTo(event.target.value, menulist); - }); - menulist.addEventListener('focus', () => { - this.ensureElementIsVisible(menulist); - }); - menulist.addEventListener('keypress', (event) => { - if (event.keyCode == event.DOM_VK_TAB) { - this.itemTypeMenuTab(event); - } - }); - td.appendChild(menulist); - this._infoTable.firstChild.appendChild(td); + var row = document.createElement('div'); + row.className = "meta-row"; + var labelWrapper = document.createElement('div'); + labelWrapper.className = "meta-label"; + labelWrapper.setAttribute("fieldname", "itemType"); + var label = document.createElement("label"); + label.className = "key"; + label.id = "itembox-field-itemType-label"; + label.innerText = Zotero.getString("zotero.items.itemType"); + labelWrapper.appendChild(label); + var rowData = document.createElement('div'); + rowData.className = "meta-data"; + if (this.itemTypeMenu) { + rowData.appendChild(this.itemTypeMenu); + } + else { + var menulist = document.createXULElement("menulist", { is: "menulist-item-types" }); + menulist.id = "item-type-menu"; + menulist.className = "zotero-clicky"; + menulist.addEventListener('command', (event) => { + this.changeTypeTo(event.target.value, menulist); + }); + menulist.addEventListener('focus', () => { + this.ensureElementIsVisible(menulist); + }); + menulist.addEventListener('keypress', (event) => { + if (event.keyCode == event.DOM_VK_TAB) { + this.handleKeyPress(event); + } + }); + menulist.setAttribute("aria-labelledby", "itembox-field-itemType-label"); + this.itemTypeMenu = menulist; + rowData.appendChild(menulist); + } + this.itemTypeMenu.setAttribute('ztabindex', '1'); + this.itemTypeMenu.disabled = !this.showTypeMenu; + row.appendChild(labelWrapper); + row.appendChild(rowData); + this._infoTable.appendChild(row); + // Remove the default bold font from the label in shadow dom + this.querySelector("#item-type-menu").shadowRoot.querySelector("#label-box").style = "font-weight: normal"; } updateItemTypeMenuSelection() { @@ -822,15 +878,8 @@ } addDynamicRow(label, value, beforeElement) { - var row = document.createElement("tr"); - - // Add click event to row - if (this._rowIsClickable(value.getAttribute('fieldname'))) { - row.className = 'zotero-clicky'; - row.addEventListener('click', (event) => { - this.clickHandler(event.target); - }, false); - } + var row = document.createElement("div"); + row.className = "meta-row"; row.appendChild(label); row.appendChild(value); @@ -847,7 +896,7 @@ addCreatorRow(creatorData, creatorTypeIDOrName, unsaved, defaultRow) { // getCreatorFields(), switchCreatorMode() and handleCreatorAutoCompleteSelect() // may need need to be adjusted if this DOM structure changes - + var fieldMode = Zotero.Prefs.get('lastCreatorFieldMode'); var firstName = ''; var lastName = ''; @@ -878,36 +927,54 @@ : this._creatorTypeMenu.childNodes[0].getAttribute('typeid'); var rowIndex = this._creatorCount; - var tabindex = this._tabIndexMinCreators + ((rowIndex - 1) * 6); - var th = document.createElement("th"); - th.setAttribute("typeid", typeID); - th.setAttribute("fieldname", 'creator-' + rowIndex + '-typeID'); + // Creator label with draggable grippy icon for creator reordering + var rowLabel = document.createElement("div"); + rowLabel.className = "meta-label"; + rowLabel.setAttribute("typeid", typeID); + rowLabel.setAttribute("fieldname", 'creator-' + rowIndex + '-typeID'); + let labelWrapper = document.createElement('div'); + let grippy = document.createXULElement('toolbarbutton'); + + labelWrapper.className = 'creator-type-label'; + grippy.className = "zotero-clicky zotero-clicky-grippy show-on-hover"; + rowLabel.appendChild(grippy); + if (this.editable) { - th.className = 'creator-type-label zotero-clicky zotero-focusable'; + labelWrapper.classList.add('zotero-clicky'); let span = document.createElement('span'); span.className = 'creator-type-dropmarker'; - th.appendChild(span); - th.setAttribute('ztabindex', tabindex); - th.setAttribute('role', 'button'); - th.setAttribute('aria-describedby', 'creator-type-label-inner'); - th.addEventListener('click', () => { - document.popupNode = th; - this._creatorTypeMenu.openPopup(th); + labelWrapper.appendChild(span); + labelWrapper.addEventListener('click', (e) => { + document.popupNode = rowLabel; + this._creatorTypeMenu.openPopup(rowLabel); + // If the creator menu is opened via mouse-click, add a special attribute to + // blur the focused field so that icons do not show up after the menu is closed. + if (e.x !== 0 && e.y !== 0) { + this._creatorTypeMenu.setAttribute("blur-on-hidden", "true"); + } }); } - else { - th.className = 'creator-type-label'; + + labelWrapper.setAttribute('role', 'button'); + labelWrapper.setAttribute('aria-describedby', 'creator-type-label-inner'); + labelWrapper.setAttribute('ztabindex', ++this._ztabindex); + + // If not editable or only 1 creator row, hide grippy + if (!this.editable || this.item.numCreators() < 2) { + grippy.style.visibility = 'hidden'; + grippy.setAttribute('disabled', true); } - + + rowLabel.appendChild(labelWrapper); var label = document.createElement("label"); label.setAttribute('id', 'creator-type-label-inner'); label.className = 'key'; label.textContent = Zotero.getString('creatorTypes.' + Zotero.CreatorTypes.getName(typeID)); - th.appendChild(label); + labelWrapper.appendChild(label); - var td = document.createElement("td"); - td.className = 'creator-type-value'; + var rowData = document.createElement("div"); + rowData.className = 'creator-type-value'; // Name var firstlast = document.createElement("span"); @@ -918,84 +985,80 @@ this.createValueElement( lastName, fieldName, - tabindex + 1 + ++this._ztabindex ) ); - // Comma - var comma = document.createElement("span"); - comma.textContent = Zotero.getString('punctuation.comma'); - comma.className = 'comma'; - firstlast.appendChild(comma); - fieldName = 'creator-' + rowIndex + '-firstName'; - firstlast.appendChild( + var firstNameElem = firstlast.appendChild( this.createValueElement( firstName, fieldName, - tabindex + 2 + ++this._ztabindex ) ); if (fieldMode > 0) { firstlast.lastChild.hidden = true; } - if (this.editable) { - firstlast.oncontextmenu = (event) => { - document.popupNode = firstlast; - this._id('creator-transform-swap-names').hidden = fieldMode > 0; - this._id('creator-transform-capitalize').disabled = !this.canCapitalizeCreatorName(td.parentNode); - this._id('zotero-creator-transform-menu').openPopupAtScreen( - event.screenX + 1, - event.screenY + 1, - true - ); - }; - } - - this._tabIndexMaxCreators = Math.max(this._tabIndexMaxCreators, tabindex); - - td.appendChild(firstlast); - - // Single/double field toggle - var toggleButton = document.createElement('button'); - toggleButton.setAttribute('fieldname', - 'creator-' + rowIndex + '-fieldMode'); - toggleButton.className = 'zotero-field-toggle zotero-clicky zotero-focusable'; - toggleButton.setAttribute('ztabindex', tabindex + 3); - td.appendChild(toggleButton); + rowData.appendChild(firstlast); // Minus (-) button var removeButton = document.createXULElement('toolbarbutton'); - removeButton.setAttribute("class", "zotero-clicky zotero-clicky-minus zotero-focusable"); - removeButton.setAttribute('ztabindex', tabindex + 4); + removeButton.setAttribute("class", "zotero-clicky zotero-clicky-minus show-on-hover"); + removeButton.setAttribute('ztabindex', ++this._ztabindex); removeButton.setAttribute('aria-label', Zotero.getString('general.delete')); // If default first row, don't let user remove it - if (defaultRow) { + if (defaultRow || !this.editable) { this.disableButton(removeButton); } else { removeButton.addEventListener("click", () => { - this.removeCreator(rowIndex, td.parentNode); + this.removeCreator(rowIndex, rowData.parentNode); }); } - td.appendChild(removeButton); + rowData.appendChild(removeButton); // Plus (+) button var addButton = document.createXULElement('toolbarbutton'); - addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus zotero-focusable"); - addButton.setAttribute('ztabindex', tabindex + 5); + addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus show-on-hover"); + addButton.setAttribute('ztabindex', ++this._ztabindex); // If row isn't saved, don't let user add more - if (unsaved) { + if (unsaved || !this.editable) { this.disableButton(addButton); } else { this._enablePlusButton(addButton, typeID, fieldMode); } - td.appendChild(addButton); + rowData.appendChild(addButton); + + let realName = (firstName && firstName != this._defaultFirstName) + || (lastName && lastName != this._defaultLastName); + + // Options button that opens creator transform menu + let optionsButton = document.createXULElement("toolbarbutton"); + if (!realName || !this.editable) { + optionsButton.style.visibility = "hidden"; + this.disableButton(optionsButton); + } + optionsButton.className = "zotero-clicky zotero-clicky-options show-on-hover"; + optionsButton.setAttribute('ztabindex', ++this._ztabindex); + optionsButton.setAttribute('data-l10n-id', "itembox-button-options"); + let triggerPopup = (e) => { + document.popupNode = firstlast; + this._id('creator-transform-swap-names').hidden = fieldMode > 0; + this._id('creator-transform-capitalize').disabled = !this.canCapitalizeCreatorName(rowData.parentNode); + this.handlePopupOpening(e, this._id('zotero-creator-transform-menu')); + }; + rowData.appendChild(optionsButton); + + if (this.editable) { + optionsButton.addEventListener("click", triggerPopup); + rowData.oncontextmenu = triggerPopup; + } if (!this.preventFocus) { - for (const domEl of [th, toggleButton, removeButton, addButton]) { + for (const domEl of [labelWrapper, removeButton, addButton, optionsButton]) { domEl.setAttribute('tabindex', '0'); domEl.addEventListener('keypress', this.handleKeyPress.bind(this)); domEl.addEventListener('focusin', this.updateLastFocused.bind(this)); @@ -1005,115 +1068,175 @@ this._creatorCount++; if (!this.editable) { - toggleButton.hidden = true; removeButton.hidden = true; addButton.hidden = true; + optionsButton.hidden = true; } - this.addDynamicRow(th, td, true); + let row = this.addDynamicRow(rowLabel, rowData, true); + + /** + * Events handling creator drag-drop reordering + */ + + // Creator becomes draggable and can be moved to another creator's spot + grippy.addEventListener('mousedown', (_) => { + row.setAttribute('draggable', 'true'); + }); + grippy.addEventListener('mouseup', (_) => { + row.setAttribute('draggable', 'false'); + }); + row.addEventListener('dragstart', (e) => { + if (row.getAttribute("draggable") !== "true") { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.dataTransfer.setDragImage(row, 15, 15); + e.dataTransfer.setData('zotero/creator', rowIndex); + // Hide the row after the drag image is generated to make it look like + // a placeholder where the creator will be dropped. + setTimeout(() => { + row.classList.add("drag-hidden-creator"); + row.classList.add("noHover"); + }); + }); + + row.addEventListener('dragover', this.handleCreatorDragOver(rowIndex, row)); + + // The creator row is dropped into the placeholder - this will reorder creators + row.addEventListener('drop', this.handleCreatorRowDrop()); + + row.addEventListener('dragend', (_) => { + // If the row is still hidden, no 'drop' event happened, meaning creator rows + // were not reordered. To make sure everything is in correct order, just refresh. + if (row.classList.contains("drag-hidden-creator")) { + this.refresh(); + } + }); // Set single/double field toggle mode if (fieldMode) { - this.switchCreatorMode(td.parentNode, 1, true); + this.switchCreatorMode(rowData.parentNode, 1, true, rowIndex); } else { - this.switchCreatorMode(td.parentNode, 0, true); + this.switchCreatorMode(rowData.parentNode, 0, true, rowIndex); } + lastNameElem.sizeToContent(); + firstNameElem.sizeToContent(); + + if (!this.editable) { + return; + } + + lastNameElem.addEventListener("input", lastNameElem.sizeToContent); + firstNameElem.addEventListener("input", firstNameElem.sizeToContent); + + // On blur that did not trigger refresh (e.g. on escape), return to original size + lastNameElem.addEventListener("blur", lastNameElem.sizeToContent); + firstNameElem.addEventListener("blur", firstNameElem.sizeToContent); + + this.addAutocompleteToElement(lastNameElem); + this.addAutocompleteToElement(firstNameElem); + // Focus new rows if (unsaved && !defaultRow) { - lastNameElem.click(); + lastNameElem.focus(); } } addMoreCreatorsRow(num) { - var th = document.createElement('th'); + var rowLabel = document.createElement('div'); + rowLabel.className = "meta-label"; - var td = document.createElement('td'); - td.id = 'more-creators-label'; - td.setAttribute('onclick', - "var binding = this.closest('item-box'); " - + "binding._displayAllCreators = true; " - + "binding.refresh()" - ); - td.textContent = Zotero.getString('general.numMore', num); + var rowData = document.createElement('div'); + rowData.className = "meta-data"; + rowData.id = 'more-creators-label'; + rowData.setAttribute("ztabindex", ++this._ztabindex); + rowData.addEventListener('click', () => { + this._displayAllCreators = true; + this.refresh(); + }); + rowData.addEventListener('keypress', (e) => { + if (["Enter", ' '].includes(e.key)) { + this._displayAllCreators = true; + this.refresh(); + } + }); + rowData.textContent = Zotero.getString('general.numMore', num); - this.addDynamicRow(th, td, true); + this.addDynamicRow(rowLabel, rowData, true); } - addDateRow(field, value, tabindex) { - var th = document.createElement("th"); - th.setAttribute("fieldname", field); - th.setAttribute("onclick", "this.nextSibling.firstChild.blur()"); + addDateRow(field, value) { + var rowLabel = document.createElement("div"); + rowLabel.className = "meta-label"; + rowLabel.setAttribute("fieldname", field); var label = document.createElement('label'); label.className = 'key'; label.textContent = Zotero.ItemFields.getLocalizedString(field); - th.appendChild(label); + label.setAttribute("id", `itembox-field-${field}-label`); + rowLabel.appendChild(label); - var td = document.createElement('td'); - td.className = "date-box"; + var rowData = document.createElement('div'); + rowData.className = "meta-data date-box"; var elem = this.createValueElement( Zotero.Date.multipartToStr(value), - field, - tabindex + field ); - + + elem.setAttribute('aria-labelledby', label.id); // y-m-d status indicator var ymd = document.createElement('span'); ymd.id = 'zotero-date-field-status'; ymd.textContent = Zotero.Date.strToDate(Zotero.Date.multipartToStr(value)) .order.split('').join(' '); + ymd.className = "show-on-hover"; + rowData.appendChild(elem); + rowData.appendChild(ymd); - td.appendChild(elem); - td.appendChild(ymd); - - this.addDynamicRow(th, td); + this.addDynamicRow(rowLabel, rowData); } - switchCreatorMode(row, fieldMode, initial, updatePref) { + switchCreatorMode(row, fieldMode, initial, updatePref, rowIndex) { // Change if button position changes - var button = row.lastChild.lastChild.previousSibling.previousSibling; - var creatorNameBox = button.previousSibling; + var creatorNameBox = row.querySelector(".creator-name-box"); var lastName = creatorNameBox.firstChild; var comma = creatorNameBox.firstChild.nextSibling; var firstName = creatorNameBox.lastChild; - + let tab; // Switch to single-field mode if (fieldMode == 1) { - button.style.background = `url("chrome://zotero/skin/textfield-dual${Zotero.hiDPISuffix}.png") center/21px auto no-repeat`; - button.setAttribute('title', Zotero.getString('pane.item.switchFieldMode.two')); + creatorNameBox.setAttribute('switch-mode-label', Zotero.getString('pane.item.switchFieldMode.two')); lastName.setAttribute('fieldMode', '1'); - button.setAttribute('onclick', "this.closest('item-box').switchCreatorMode(this.closest('tr'), 0, false, true)"); delete lastName.style.width; delete lastName.style.maxWidth; // Remove firstname field from tabindex tab = parseInt(firstName.getAttribute('ztabindex')); firstName.setAttribute('ztabindex', -1); - if (this._tabIndexMaxCreators == tab) { - this._tabIndexMaxCreators--; - } // Hide first name field and prepend to last name field firstName.hidden = true; comma.hidden = true; if (!initial) { - var first = this._getFieldValue(firstName); + var first = firstName.value; if (first && first != this._defaultFirstName) { - let last = this._getFieldValue(lastName); - this._setFieldValue(lastName, first + ' ' + last); + let last = lastName.value; + lastName.value = first + ' ' + last; } } - if (this._getFieldValue(lastName) == this._defaultLastName) { - this._setFieldValue(lastName, this._defaultFullName); + if (lastName.value == this._defaultLastName) { + lastName.value = this._defaultFullName; } // If one of the creator fields is open, leave it open after swap - let activeField = this._infoTable.querySelector('input'); + let activeField = this.getFocusedTextArea(); if (activeField == firstName || activeField == lastName) { this._lastTabIndex = parseInt(lastName.getAttribute('ztabindex')); this._tabDirection = false; @@ -1121,66 +1244,32 @@ } // Switch to two-field mode else { - button.style.background = `url("chrome://zotero/skin/textfield-single${Zotero.hiDPISuffix}.png") center/21px auto no-repeat`; - button.setAttribute('title', Zotero.getString('pane.item.switchFieldMode.one')); + creatorNameBox.setAttribute('switch-mode-label', Zotero.getString('pane.item.switchFieldMode.one')); lastName.setAttribute('fieldMode', '0'); - button.setAttribute('onclick', "this.closest('item-box').switchCreatorMode(this.closest('tr'), 1, false, true)"); - - // appropriately truncate lastName - - // get item box width - var computedStyle = window.getComputedStyle(this, null); - var boxWidth = computedStyle.getPropertyValue('width'); - // get field label width - computedStyle = window.getComputedStyle(row.firstChild, null); - var leftHboxWidth = computedStyle.getPropertyValue('width'); - // get last name width - computedStyle = window.getComputedStyle(lastName, null); - var lastNameWidth = computedStyle.getPropertyValue('width'); - if (boxWidth.substr(-2) === 'px' - && leftHboxWidth.substr(-2) === 'px' - && lastNameWidth.substr(-2) === "px") { - // compute a maximum width - boxWidth = parseInt(boxWidth); - leftHboxWidth = parseInt(leftHboxWidth); - lastNameWidth = parseInt(lastNameWidth); - var maxWidth = boxWidth - leftHboxWidth - 140; - if (lastNameWidth > maxWidth) { - //lastName.style.width = maxWidth+"px"; - //lastName.style.maxWidth = maxWidth+"px"; - } - else { - delete lastName.style.width; - delete lastName.style.maxWidth; - } - } - + // Add firstname field to tabindex tab = parseInt(lastName.getAttribute('ztabindex')); firstName.setAttribute('ztabindex', tab + 1); - if (this._tabIndexMaxCreators == tab) { - this._tabIndexMaxCreators++; - } if (!initial) { // Move all but last word to first name field and show it - let last = this._getFieldValue(lastName); + let last = lastName.value; if (last && last != this._defaultFullName) { var lastNameRE = /(.*?)[ ]*([^ ]+[ ]*)$/; var parts = lastNameRE.exec(last); if (parts[2] && parts[2] != last) { - this._setFieldValue(lastName, parts[2]); - this._setFieldValue(firstName, parts[1]); + lastName.value = parts[2]; + firstName.value = parts[1]; } } } - if (!this._getFieldValue(firstName)) { - this._setFieldValue(firstName, this._defaultFirstName); + if (!firstName.value) { + firstName.value = this._defaultFirstName; } - if (this._getFieldValue(lastName) == this._defaultFullName) { - this._setFieldValue(lastName, this._defaultLastName); + if (lastName.value == this._defaultFullName) { + lastName.value = this._defaultLastName; } firstName.hidden = false; @@ -1194,12 +1283,11 @@ } if (!initial) { - var index = button.getAttribute('fieldname').split('-')[1]; var fields = this.getCreatorFields(row); fields.fieldMode = fieldMode; - this.modifyCreator(index, fields); + this.modifyCreator(rowIndex, fields); if (this.saveOnEdit) { - let activeField = this._infoTable.querySelector('input, textarea'); + let activeField = this.getFocusedTextArea(); if (activeField !== null && activeField !== firstName && activeField !== lastName) { this.blurOpenField(); } @@ -1207,6 +1295,12 @@ this.item.saveTx(); } } + else { + // If the creator is saved, autocomplete will be set in refresh() + // Otherwise, we'll reset it here. + this.addAutocompleteToElement(firstName); + this.addAutocompleteToElement(lastName); + } } } @@ -1309,27 +1403,6 @@ return false; } - toggleAbstractExpand(label, valueElement) { - var cur = Zotero.Prefs.get('lastAbstractExpand'); - Zotero.Prefs.set('lastAbstractExpand', !cur); - - var valueText = this.item.getField('abstractNote'); - var tabindex = valueElement.getAttribute('ztabindex'); - var newValueElement = this.createValueElement( - valueText, - 'abstractNote', - tabindex - ); - valueElement.replaceWith(newValueElement); - - var text = Zotero.ItemFields.getLocalizedString('abstractNote'); - // Add '(...)' before "Abstract" for collapsed abstracts - if (valueText && cur) { - text = '(\u2026) ' + text; - } - label.textContent = text; - } - disableButton(button) { button.setAttribute('disabled', true); button.setAttribute('onclick', false); @@ -1351,38 +1424,60 @@ } } - createValueElement(valueText, fieldName, tabindex) { + createOpenLinkIcon(rowLabel, value) { + // In duplicates/trash mode return nothing + if (!this.editable) { + return null; + } + let openLink = document.createXULElement("toolbarbutton"); + openLink.className = "zotero-clicky zotero-clicky-open-link show-on-hover"; + openLink.addEventListener("click", event => ZoteroPane.loadURI(value, event)); + openLink.addEventListener('keypress', event => this.handleKeyPress(event)); + rowLabel.setAttribute('title', Zotero.getString('pane.item.viewOnline.tooltip')); + openLink.setAttribute("ztabindex", ++this._ztabindex); + openLink.setAttribute('data-l10n-id', "itembox-button-openLink"); + return openLink; + } + + createValueElement(valueText, fieldName) { valueText += ''; if (fieldName) { var fieldID = Zotero.ItemFields.getID(fieldName); } - // Allow multiline/long fields to wrap var isMultiline = Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName); - // But treat Abstract as a multiline field only when expanded - if (fieldName == 'abstractNote') { - isMultiline &&= Zotero.Prefs.get('lastAbstractExpand'); + var valueElement = document.createXULElement("editable-text"); + valueElement.className = 'value'; + if (isMultiline) { + valueElement.setAttribute('multiline', true); + } + else { + // Usual fields occupy all available space and keep info on one line + valueElement.setAttribute("nowrap", true); } - var valueElement = document.createElement("div"); - - valueElement.setAttribute('id', `itembox-field-value-${fieldName}`); - valueElement.className = 'value'; - valueElement.setAttribute('fieldname', fieldName); if (this._fieldIsClickable(fieldName)) { - valueElement.setAttribute('ztabindex', tabindex); - valueElement.addEventListener('click', (event) => { - // Skip right-click on Windows - if (event.button) { - return; - } - this.clickHandler(event.target); - }, false); - valueElement.classList.add('zotero-clicky'); + valueElement.addEventListener("focus", e => this.showEditor(e.target)); + valueElement.addEventListener("blur", e => this.hideEditor(e.target)); + valueElement.addEventListener("escape_enter", (_) => { + setTimeout(() => { + document.getElementById('item-tree-main-default').focus(); + }); + }); } - + else { + valueElement.setAttribute('readonly', true); + } + + valueElement.setAttribute('ztabindex', ++this._ztabindex); + valueElement.setAttribute('id', `itembox-field-value-${fieldName}`); + valueElement.setAttribute('fieldname', fieldName); + valueElement.setAttribute('tight', true); + + valueElement.addEventListener("focus", e => this.updateLastFocused(e)); + valueElement.addEventListener("keypress", (e) => this.handleKeyPress(e)); switch (fieldName) { case 'itemType': valueElement.setAttribute('itemTypeID', valueText); @@ -1400,25 +1495,7 @@ if (fieldName == 'date' && this.item._objectType != 'feedItem') { break; } - if (valueText) { - var date = Zotero.Date.sqlToDate(valueText, true); - if (date) { - // If no time, interpret as local, not UTC - if (Zotero.Date.isSQLDate(valueText)) { - // Add time to avoid showing previous day if date is in - // DST (including the current date at 00:00:00) and we're - // in standard time - date = Zotero.Date.sqlToDate(valueText + ' 12:00:00'); - valueText = date.toLocaleDateString(); - } - else { - valueText = date.toLocaleString(); - } - } - else { - valueText = ''; - } - } + valueText = this.dateTimeFromUTC(valueText); break; } @@ -1429,38 +1506,9 @@ valueElement.setAttribute('title', Zotero.Date.multipartToSQL(this.item.getField(fieldName, true))); } - - // Display a context menu for certain fields - if (this.editable && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' - || Zotero.ItemFields.isFieldOfBase(fieldID, 'title') - || Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) { - valueElement.oncontextmenu = (event) => { - let oldValue = this._getFieldValue(valueElement); - let menupopup = ZoteroItemPane.buildFieldTransformMenu({ - value: oldValue, - onTransform: (newValue) => { - this._setFieldTransformedValue(valueElement, newValue); - } - }); - this.querySelector('popupset').append(menupopup); - menupopup.addEventListener('popuphidden', () => menupopup.remove()); - menupopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true); - }; - } } - // Add popup menu on DOI field with value - if (fieldName == 'DOI' && valueText) { - valueElement.oncontextmenu = (event) => { - this._id('zotero-doi-menu').openPopupAtScreen( - event.screenX + 1, - event.screenY + 1, - true - ); - }; - } - - valueElement.textContent = valueText; + valueElement.value = valueText; // Attempt to make bidi things work automatically: // If we have text to work off of, let the layout engine try to guess the text direction @@ -1479,20 +1527,6 @@ else { valueElement.style.textAlign = 'left'; } - - if (isMultiline) { - valueElement.classList.add('multiline'); - } - - // Allow toggling non-editable Abstract open and closed with click - if (fieldName == 'abstractNote' && !this.editable) { - valueElement.classList.add("pointer"); - valueElement.addEventListener('click', () => { - let label = valueElement.parentElement.previousElementSibling.firstElementChild; - this.toggleAbstractExpand(label, valueElement); - }); - } - return valueElement; } @@ -1504,7 +1538,7 @@ // Enable the "+" button on the previous row var elems = this._infoTable.getElementsByClassName('zotero-clicky-plus'); var button = elems[elems.length - 1]; - var creatorFields = this.getCreatorFields(button.closest('tr')); + var creatorFields = this.getCreatorFields(button.closest('.meta-row')); this._enablePlusButton(button, creatorFields.creatorTypeID, creatorFields.fieldMode); this._creatorCount--; @@ -1515,43 +1549,43 @@ await this.item.saveTx(); } - async showEditor(elem) { - Zotero.debug(`Showing editor for ${elem.getAttribute('fieldname')}`); - - var label = elem.closest('tr').querySelector('th > label'); - var lastTabIndex = this._lastTabIndex = parseInt(elem.getAttribute('ztabindex')); - - // If a field is open, hide it before selecting the new field, which might - // trigger a refresh - var activeField = this._infoTable.querySelector('input, textarea'); - if (activeField) { - this._refreshed = false; - await this.blurOpenField(); - this._lastTabIndex = lastTabIndex; - // If the box was refreshed, the clicked element is no longer valid, - // so just focus by tab index - if (this._refreshed) { - this._focusNextField(this._lastTabIndex); - return; + dateTimeFromUTC(valueText) { + if (valueText) { + var date = Zotero.Date.sqlToDate(valueText, true); + if (date) { + // If no time, interpret as local, not UTC + if (Zotero.Date.isSQLDate(valueText)) { + // Add time to avoid showing previous day if date is in + // DST (including the current date at 00:00:00) and we're + // in standard time + date = Zotero.Date.sqlToDate(valueText + ' 12:00:00'); + valueText = date.toLocaleDateString(); + } + else { + valueText = date.toLocaleString(); + } + } + else { + valueText = ''; } } - + return valueText; + } + + async showEditor(elem) { + Zotero.debug(`Showing editor for ${elem.getAttribute('fieldname')}`); var fieldName = elem.getAttribute('fieldname'); - var tabindex = elem.getAttribute('ztabindex'); - var [field, creatorIndex, creatorField] = fieldName.split('-'); - let value, itemID; + let value; if (field == 'creator') { value = this.item.getCreator(creatorIndex)[creatorField]; if (value === undefined) { value = ""; } - itemID = this.item.id; + elem.value = value; } else { value = this.item.getField(fieldName); - itemID = this.item.id; - // Access date needs to be converted from UTC if (value != '') { let localDate; @@ -1571,193 +1605,134 @@ // Don't show time in editor value = value.replace(' 00:00:00', ''); + elem.value = value; break; } } } - - var t; - if (Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName)) { - t = document.createElement("textarea"); - } - // Add auto-complete for certain fields - else if (field == 'creator' || Zotero.ItemFields.isAutocompleteField(fieldName)) { - 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')); - - // 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/click - // Monkey-patching onTextEntered is apparently the current official way to detect completion -- - // there's also a custom event called textEntered, but it won't be fired unless the input has its - // 'notifylegacyevents' attribute set to true - // https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/toolkit/content/widgets/autocomplete-input.js#372 - // https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/browser/components/search/content/searchbar.js#791 - t.onTextEntered = () => { - this.handleCreatorAutoCompleteSelect(t, true); - }; - // Tab/Shift-Tab - t.addEventListener('change', () => { - this.handleCreatorAutoCompleteSelect(t, true); - }); - - if (creatorField == 'lastName') { - t.setAttribute('fieldMode', elem.getAttribute('fieldMode')); - t.addEventListener('paste', (event) => { - let lastName = event.clipboardData.getData('text').trim(); - // Handle \n\r and \n delimited entries and a single line containing a tab - var rawNameArray = lastName.split(/\r\n?|\n/); - if (rawNameArray.length > 1 || rawNameArray[0].includes('\t')) { - // Pasting multiple authors; first make sure we prevent normal paste behavior - event.preventDefault(); - - // Save tab direction and add creator flags since they are reset in the - // process of adding multiple authors - var tabDirectionBuffer = this._tabDirection; - var addCreatorRowBuffer = this._addCreatorRow; - var tabIndexBuffer = this._lastTabIndex; - this._tabDirection = false; - this._addCreatorRow = false; + } - // Filter out bad names - var nameArray = rawNameArray.filter(name => name); - - // If not adding names at the end of the creator list, make new creator - // entries and then shift down existing creators. - var initNumCreators = this.item.numCreators(); - var creatorsToShift = initNumCreators - creatorIndex; - if (creatorsToShift > 0) { - // Add extra creators with dummy values - for (let i = 0; i < nameArray.length; i++) { - this.modifyCreator(i + initNumCreators, { - firstName: '', - lastName: '', - fieldMode: 0, - creatorTypeID - }); - } - - // Shift existing creators - for (let i = initNumCreators - 1; i >= creatorIndex; i--) { - let shiftedCreatorData = this.item.getCreator(i); - this.item.setCreator(nameArray.length + i, shiftedCreatorData); - } - } - - let currentIndex = creatorIndex; - let newCreator = { creatorTypeID }; - // Add the creators in lastNameArray one at a time - for (let tempName of nameArray) { - // Check for tab to determine creator name format - newCreator.fieldMode = (tempName.indexOf('\t') == -1) ? 1 : 0; - if (newCreator.fieldMode == 0) { - newCreator.lastName = tempName.split('\t')[0]; - newCreator.firstName = tempName.split('\t')[1]; - } - else { - newCreator.lastName = tempName; - newCreator.firstName = ''; - } - this.modifyCreator(currentIndex, newCreator); - currentIndex++; - } - this._tabDirection = tabDirectionBuffer; - this._addCreatorRow = (creatorsToShift == 0) ? addCreatorRowBuffer : false; - if (this._tabDirection == 1) { - this._lastTabIndex = tabIndexBuffer + 2 * (nameArray.length - 1); - if (newCreator.fieldMode == 0) { - this._lastTabIndex++; - } - } - - if (this.saveOnEdit) { - this.item.saveTx(); - } - } - }); - } - } - 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); - // We set dir in createValueElement(), so figure out what it was computed as - // and then propagate to the new text field - t.dir = getComputedStyle(elem).direction; - - var box = elem.parentNode; - box.replaceChild(t, elem); - - // Associate textbox with label - label.setAttribute('control', t.getAttribute('id')); - - // Prevent error when clicking between a changed field - // and another -- there's probably a better way - if (!t.select) { + addAutocompleteToElement(elem) { + var fieldName = elem.getAttribute('fieldname'); + var [field, creatorIndex, creatorField] = fieldName.split('-'); + if (elem.getAttribute("readonly") || !(field == 'creator' || Zotero.ItemFields.isAutocompleteField(fieldName))) { return; } - - t.select(); - - // Leave text field open when window loses focus - var ignoreBlur = () => { - this.ignoreBlur = true; + let itemID = this.item.id; + let params = { + fieldName: fieldName, + libraryID: this.item.libraryID, + search: 'zotero' }; - var unignoreBlur = () => { - this.ignoreBlur = false; - }; - addEventListener("deactivate", ignoreBlur); - addEventListener("activate", unignoreBlur); - - t.addEventListener('blur', () => { - if (this.ignoreBlur) return; + if (field == 'creator') { + params.fieldMode = parseInt(elem.getAttribute('fieldMode')); - removeEventListener("deactivate", ignoreBlur); - removeEventListener("activate", unignoreBlur); - this.blurHandler(t); - }); - t.addEventListener('keypress', event => this.handleKeyPress(event)); - - if (t instanceof HTMLTextAreaElement) { - let updateHeight = () => { - // Reset height before getting scrollHeight - // Prevents field from growing slightly each time - // https://stackoverflow.com/a/58073583 - t.style.height = 'auto'; - t.style.height = `calc(max(6em, ${t.scrollHeight}px))`; + // Include itemID and creatorTypeID so the autocomplete can + // avoid showing results for creators already set on the item + let row = elem.closest('.meta-row'); + let creatorTypeID = parseInt( + row.querySelector('.meta-label').getAttribute('typeid') + ); + if (itemID) { + params.itemID = itemID; + params.creatorTypeID = creatorTypeID; + } + + // Return/click + // Monkey-patching onTextEntered is apparently the current official way to detect completion -- + // there's also a custom event called textEntered, but it won't be fired unless the input has its + // 'notifylegacyevents' attribute set to true + // https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/toolkit/content/widgets/autocomplete-input.js#372 + // https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/browser/components/search/content/searchbar.js#791 + elem.onTextEntered = () => { + this.handleCreatorAutoCompleteSelect(elem, true); }; - t.addEventListener('input', updateHeight); - updateHeight(); + // Tab/Shift-Tab + elem.addEventListener('change', () => { + this.handleCreatorAutoCompleteSelect(elem, true); + }); + + if (creatorField == 'lastName') { + elem.addEventListener('paste', (event) => { + let lastName = event.clipboardData.getData('text').trim(); + // Handle \n\r and \n delimited entries and a single line containing a tab + var rawNameArray = lastName.split(/\r\n?|\n/); + if (rawNameArray.length > 1 || rawNameArray[0].includes('\t')) { + // Pasting multiple authors; first make sure we prevent normal paste behavior + event.preventDefault(); + + // Save tab direction and add creator flags since they are reset in the + // process of adding multiple authors + var tabDirectionBuffer = this._tabDirection; + var addCreatorRowBuffer = this._addCreatorRow; + var tabIndexBuffer = this._lastTabIndex; + this._tabDirection = false; + this._addCreatorRow = false; + + // Filter out bad names + var nameArray = rawNameArray.filter(name => name); + + // If not adding names at the end of the creator list, make new creator + // entries and then shift down existing creators. + var initNumCreators = this.item.numCreators(); + var creatorsToShift = initNumCreators - creatorIndex; + if (creatorsToShift > 0) { + // Add extra creators with dummy values + for (let i = 0; i < nameArray.length; i++) { + this.modifyCreator(i + initNumCreators, { + firstName: '', + lastName: '', + fieldMode: 0, + creatorTypeID + }); + } + + // Shift existing creators + for (let i = initNumCreators - 1; i >= creatorIndex; i--) { + let shiftedCreatorData = this.item.getCreator(i); + this.item.setCreator(nameArray.length + i, shiftedCreatorData); + } + } + + let currentIndex = creatorIndex; + let newCreator = { creatorTypeID }; + // Add the creators in lastNameArray one at a time + for (let tempName of nameArray) { + // Check for tab to determine creator name format + newCreator.fieldMode = (tempName.indexOf('\t') == -1) ? 1 : 0; + if (newCreator.fieldMode == 0) { + newCreator.lastName = tempName.split('\t')[0]; + newCreator.firstName = tempName.split('\t')[1]; + } + else { + newCreator.lastName = tempName; + newCreator.firstName = ''; + } + this.modifyCreator(currentIndex, newCreator); + currentIndex++; + } + this._tabDirection = tabDirectionBuffer; + this._addCreatorRow = (creatorsToShift == 0) ? addCreatorRowBuffer : false; + if (this._tabDirection == 1) { + this._lastTabIndex = tabIndexBuffer + 2 * (nameArray.length - 1); + if (newCreator.fieldMode == 0) { + this._lastTabIndex++; + } + } + + if (this.saveOnEdit) { + this.item.saveTx(); + } + } + }); + } } + elem.autocomplete = { + completeSelectedIndex: true, + ignoreBlurWhileSearching: false, + search: 'zotero', + searchParam: JSON.stringify(params) + }; } @@ -1766,8 +1741,12 @@ * (e.g. "Shakespeare, William") */ handleCreatorAutoCompleteSelect(textbox, stayFocused) { - var controller = textbox.controller; - if (!controller.matchCount) return; + let inputField = textbox.querySelector("input"); + if (!inputField) { + return; + } + var controller = inputField.controller; + if (!controller?.matchCount) return; var id = false; for (let i = 0; i < controller.matchCount; i++) { @@ -1788,7 +1767,7 @@ if (numFields == 2) { // Manually clear autocomplete controller's reference to // textbox to prevent error next time around - textbox.mController.input = null; + inputField.mController.input = null; var [_field, creatorIndex, creatorField] = textbox.getAttribute('fieldname').split('-'); @@ -1803,7 +1782,6 @@ var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName'; // Update this textbox - textbox.setAttribute('value', creator[creatorField]); textbox.value = creator[creatorField]; // Update the other label @@ -1815,20 +1793,14 @@ label = textbox.previousSibling.previousSibling; } - //this._setFieldValue(label, creator[otherField]); - if (label.firstChild) { - label.firstChild.nodeValue = creator[otherField]; - } - else { - label.value = creator[otherField]; - } + label.value = creator[otherField]; - var row = textbox.closest('tr'); + var row = textbox.closest('.meta-row'); var fields = this.getCreatorFields(row); fields[creatorField] = creator[creatorField]; fields[otherField] = creator[otherField]; - + this.modifyCreator(creatorIndex, fields); if (this.saveOnEdit) { this.ignoreBlur = true; @@ -1843,10 +1815,8 @@ handleKeyPress(event) { var target = event.target; - var focused = document.commandDispatcher.focusedElement; - if ((event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === ' ') - && target.classList.contains('creator-type-label')) { + && target.classList.contains('zotero-clicky')) { event.preventDefault(); target.click(); @@ -1859,34 +1829,36 @@ } let tree; - switch (event.keyCode) { - case event.DOM_VK_RETURN: - var fieldname = target.getAttribute('fieldname'); + switch (event.key) { + case "Enter": + var valueField = target.closest("editable-text"); + if (!valueField) { + return; + } + var fieldname = valueField.getAttribute('fieldname'); // Use shift-enter as the save action for the larger fields if (Zotero.ItemFields.isMultiline(fieldname) && !event.shiftKey) { return; } - - if (target.classList.contains('zotero-focusable')) { - event.preventDefault(); - return; - } - - // Prevent blur on containing textbox - // DEBUG: what happens if this isn't present? - event.preventDefault(); // Shift-enter adds new creator row if (fieldname.indexOf('creator-') == 0 && event.shiftKey) { // Value hasn't changed - if (target.dataset.originalValue == target.value) { + if (valueField.initialValue == valueField.value) { Zotero.debug("Value hasn't changed"); + let row = target.closest('.meta-row'); // If + button is disabled, just focus next creator row - if (target.closest('tr').lastChild.lastChild.disabled) { - this._focusNextField(this._lastTabIndex); + if (row.querySelector(".zotero-clicky-plus").disabled) { + let moreCreators = row.nextSibling.querySelector("#more-creators-label"); + if (moreCreators) { + moreCreators.click(); + } + else { + row.nextSibling.querySelector("editable-text").focus(); + } } else { - var creatorFields = this.getCreatorFields(target.closest('tr')); + var creatorFields = this.getCreatorFields(row); this.addCreatorRow(false, creatorFields.creatorTypeID, true); } } @@ -1894,42 +1866,37 @@ else { this._tabDirection = 1; this._addCreatorRow = true; - focused.blur(); + this.blurOpenField(); } - return; } - focused.blur(); + + return; + + case "Escape": // Return focus to items pane - tree = document.getElementById('zotero-items-tree'); + tree = document.getElementById('item-tree-main-default'); if (tree) { tree.focus(); } return; - case event.DOM_VK_ESCAPE: - // Reset field to original value - target.value = target.dataset.originalValue; - - focused.blur(); - - // Return focus to items pane - tree = document.getElementById('zotero-items-tree'); - if (tree) { - tree.focus(); - } - - return; - - case event.DOM_VK_TAB: - event.preventDefault(); + case "Tab": + this.updateLastFocused(event); if (event.shiftKey) { + // Shift-tab from the item type + if (this._lastTabIndex === 1) { + return; + } + event.preventDefault(); this._focusNextField(this._lastTabIndex, true); } else { - // If on the last field, allow default tab action + event.preventDefault(); + // If on the last field, return focus to item tree if (this._lastTabIndex == this._tabIndexMaxFields) { + document.getElementById('item-tree-main-default')?.focus(); return; } this._focusNextField(++this._lastTabIndex); @@ -1937,18 +1904,10 @@ } } - itemTypeMenuTab(event) { - if (!event.shiftKey) { - this.focusFirstField(); - event.preventDefault(); - } - // Shift-tab - else { - this._tabDirection = false; - } - } - async hideEditor(textbox) { + if (this.ignoreBlur) { + return; + } // Handle cases where creator autocomplete doesn't trigger // the textentered and change events handled in showEditor if (textbox.getAttribute('fieldname').startsWith('creator-')) { @@ -1957,7 +1916,6 @@ Zotero.debug(`Hiding editor for ${textbox.getAttribute('fieldname')}`); - var label = textbox.closest('tr').querySelector('th > label'); this._lastTabIndex = -1; // Prevent autocomplete breakage in Firefox 3 @@ -1966,18 +1924,14 @@ } var fieldName = textbox.getAttribute('fieldname'); - var tabindex = textbox.getAttribute('ztabindex'); - //var value = t.value; var value = textbox.value.trim(); - var elem; var [field, creatorIndex, creatorField] = fieldName.split('-'); - var newVal; // Creator fields if (field == 'creator') { - var row = textbox.closest('tr'); + var row = textbox.closest('.meta-row'); var otherFields = this.getCreatorFields(row); otherFields[creatorField] = value; @@ -1996,9 +1950,9 @@ else if (creatorField == 'firstName') { val = this._defaultFirstName; } + textbox.value = val; } - newVal = val; if (Zotero.ItemTypes.getName(this.item.itemTypeID) === "bookSection") { this._showCreatorTypeGuidance = true; @@ -2013,7 +1967,7 @@ case 'accessDate': // Parse 'yesterday'/'today'/'tomorrow' value = Zotero.Date.parseDescriptiveString(value); - + // Allow "now" to use current time if (value == 'now') { value = Zotero.Date.dateToSQL(new Date(), true); @@ -2035,6 +1989,7 @@ value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', ''); } } + textbox.value = this.dateTimeFromUTC(value); break; // TEMP - NSF @@ -2053,6 +2008,7 @@ value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', ''); } } + textbox.value = this.dateTimeFromUTC(value); break; default: @@ -2065,23 +2021,6 @@ } this._modifyField(fieldName, value); - newVal = this.item.getField(fieldName); - } - - // Close box - elem = this.createValueElement( - newVal, - fieldName, - tabindex - ); - textbox.replaceWith(elem); - - // Disassociate textbox from label - label.setAttribute('control', elem.getAttribute('id')); - - if (field == 'creator') { - // Set correct flex settings and fieldMode attribute - this.switchCreatorMode(row, (otherFields.fieldMode ? 1 : 0), true); } if (this.saveOnEdit) { @@ -2105,26 +2044,8 @@ this.item.setField(field, value); } - _getFieldValue(label) { - return label.firstChild?.nodeValue - || label.value - || label.textContent; - } - - _setFieldValue(label, value) { - if (label.firstChild) { - label.firstChild.nodeValue = value; - } - else if (label instanceof HTMLInputElement || label instanceof HTMLTextAreaElement) { - label.value = value; - } - else { - label.textContent = value; - } - } - async _setFieldTransformedValue(label, newValue) { - this._setFieldValue(label, newValue); + label.value = newValue; var fieldName = label.getAttribute('fieldname'); this._modifyField(fieldName, newValue); @@ -2137,19 +2058,16 @@ } getCreatorFields(row) { - var typeID = row.getElementsByClassName('creator-type-label')[0].getAttribute('typeid'); - var label1 = row.getElementsByClassName('creator-name-box')[0].firstChild; - var label2 = label1.parentNode.lastChild; - + var typeID = row.querySelector('[typeid]').getAttribute('typeid'); + var [label1, label2] = row.querySelectorAll('editable-text'); + var fieldMode = row.querySelector('[fieldMode]')?.getAttribute('fieldMode'); var fields = { - lastName: label1.firstChild ? label1.firstChild.nodeValue : label1.value, - firstName: label2.firstChild ? label2.firstChild.nodeValue : label2.value, - fieldMode: label1.getAttribute('fieldMode') - ? parseInt(label1.getAttribute('fieldMode')) - : 0, + lastName: label1.value, + firstName: label2.value, + fieldMode: fieldMode ? parseInt(fieldMode) : 0, creatorTypeID: parseInt(typeID), }; - + // Ignore '(first)' if (fields.fieldMode == 1 || fields.firstName == this._defaultFirstName) { fields.firstName = ''; @@ -2176,7 +2094,7 @@ } return this.item.removeCreator(index); } - + return this.item.setCreator(index, fields); } @@ -2184,8 +2102,8 @@ * @return {Promise} */ async swapNames(_event) { - var row = document.popupNode.closest('tr'); - var typeBox = row.querySelector('.creator-type-label'); + var row = document.popupNode.closest('.meta-row'); + var typeBox = row.querySelector('[fieldname]'); var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var fields = this.getCreatorFields(row); var lastName = fields.lastName; @@ -2211,8 +2129,8 @@ * @return {Promise} */ async capitalizeCreatorName(_event) { - var row = document.popupNode.closest('tr'); - var typeBox = row.querySelector('.creator-type-label'); + var row = document.popupNode.closest('.meta-row'); + var typeBox = row.querySelector('.creator-type-label').parentNode; var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var fields = this.getCreatorFields(row); fields.firstName = fields.firstName && Zotero.Utilities.capitalizeName(fields.firstName); @@ -2225,11 +2143,77 @@ await this.item.saveTx(); } } - + + // Returns a function that handles draggable creator row being dropped to a different location + handleCreatorRowDrop() { + return (e) => { + e.preventDefault(); + let row = e.target.closest(".meta-row"); + let index = parseInt(e.dataTransfer.getData("zotero/creator")); + let nextSibling = row.nextSibling; + let beforeCreatorField = nextSibling.querySelector('.meta-label').getAttribute('fieldname') || ""; + let beforeCreatorIndex; + // The creator row is dropped before "X more..." creators label + if (nextSibling.querySelector("#more-creators-label")) { + beforeCreatorIndex = this._initialVisibleCreators; + } + // The creator row is dropped before a non-creator row, meaning it's moved below + // all other creators + else if (!beforeCreatorField.includes("creator-")) { + beforeCreatorIndex = this.item.numCreators(); + } + // Creator row is placed before another creator + else { + beforeCreatorIndex = parseInt(beforeCreatorField.split('-')[1]); + } + // No change in order - do nothing + if (beforeCreatorIndex == index + 1) { + return; + } + this._draggedCreator = true; + this._lastTabIndex = null; + // Due to some kind of drag-drop API issue, + // after creator is dropped, the hover effect often stays at + // the row's old location. To workaround that, set noHover class to block all + // hover effects on creator rows and then remove it on the first mouse movement in refresh(). + for (let label of document.querySelectorAll(".meta-label[fieldname^='creator-']")) { + label.closest(".meta-row").classList.add("noHover"); + } + // Un-hide the moved creator row + this.querySelector(".drag-hidden-creator").classList.remove("drag-hidden-creator"); + // Update the item after small delay to avoid blinking + setTimeout(() => { + this.moveCreator(index, null, beforeCreatorIndex); + }, 250); + }; + } + + // Given the index of creator row and the row itself, returns the drag over handler. + // When a creator row is dragged over, an empty placeholder is added in its place. + handleCreatorDragOver(rowIndex, row) { + return (e) => { + e.preventDefault(); + let index = e.dataTransfer.getData("zotero/creator"); + if (!index) { + return false; + } + let placeholder = this.querySelector(".drag-hidden-creator"); + if (row.previousSibling == placeholder) { + // If the placeholder exists before the row, swap the placeholder and the row + row.parentNode.insertBefore(row, placeholder); + } + else if (index != rowIndex) { + // Insert placeholder before the row + row.parentNode.insertBefore(placeholder, row); + } + return true; + }; + } + /** * @return {Promise} */ - moveCreator(index, dir) { + moveCreator(index, dir, newIndex) { return Zotero.spawn(function* () { yield this.blurOpenField(); if (index == 0 && dir == 'up') { @@ -2240,35 +2224,40 @@ Zotero.debug("Can't move down last creator"); return; } - - var newIndex; - switch (dir) { - case 'top': - newIndex = 0; - break; - - case 'up': - newIndex = index - 1; - break; - - case 'down': - newIndex = index + 1; - break; + else if (newIndex && index == newIndex) { + return; } - let creator = this.item.getCreator(index); - // When moving to top, increment index of all other creators - if (dir == 'top') { - let otherCreators = this.item.getCreators(); - this.item.setCreator(newIndex, creator); - for (let i = 0; i < index; i++) { - this.item.setCreator(i + 1, otherCreators[i]); + + if (!newIndex) { + switch (dir) { + case 'top': + newIndex = 0; + break; + + case 'up': + newIndex = index - 1; + break; + + case 'down': + newIndex = index + 2; // Insert after the desired element + break; } } - // When moving up or down, swap places with next creator - else { - let otherCreator = this.item.getCreator(newIndex); - this.item.setCreator(newIndex, creator); - this.item.setCreator(index, otherCreator); + let creator = this.item.getCreator(index); + let creators = this.item.getCreators(); + // Insert creator + creators.splice(newIndex, 0, creator); + // Remove creator from old location + creators.splice(newIndex < index ? index + 1 : index, 1); + // Determine range where indices need to be updated + let startUpdateIndex = Math.min(index, newIndex); + let endUpdateIndex = Math.max(index, newIndex); + // Shift indices of affected creators + for (let i = startUpdateIndex; i <= endUpdateIndex; i++) { + if (!creators[i]) { + break; + } + this.item.setCreator(i, creators[i]); } if (this.saveOnEdit) { this.item.saveTx(); @@ -2276,40 +2265,24 @@ }, this); } - _updateAutoCompleteParams(row, changedParams) { - var textboxes = row.querySelectorAll('input'); - if (textboxes.length) { - var t = textboxes[0]; - var params = JSON.parse(t.getAttribute('autocompletesearchparam')); - for (var param in changedParams) { - params[param] = changedParams[param]; - } - t.setAttribute('autocompletesearchparam', JSON.stringify(params)); - } - } - focusFirstField() { - this._focusNextField(1); + this._focusNextField(0); } - focusLastField() { - const tabbableFields = this.querySelectorAll('*[ztabindex]:not([disabled=true])'); - const last = tabbableFields[tabbableFields.length - 1]; - - if (last.classList.contains('zotero-focusable')) { - last.focus(); - } - // Fields need to be clicked - else { - last.click(); - } - } focusField(fieldName) { let field = this.querySelector(`[fieldname="${fieldName}"][ztabindex]`); if (!field) return false; return this._focusNextField(field.getAttribute('ztabindex')); } + + getFocusedTextArea() { + let input = this._infoTable.querySelector('input[data-initial-value], textarea[data-initial-value]'); + if (input) { + return input.closest('editable-text'); + } + return null; + } /** * Advance the field focus forward or backward @@ -2359,15 +2332,11 @@ return false; } - // Drop-down and creator buttons need to be focused - if (next.id == 'item-type-menu' || next.classList.contains('zotero-focusable')) { - next.focus(); - } - // Fields need to be clicked - else { - next.click(); - } + // Ensure the node is visible + next.style.visibility = "visible"; + next.focus(); + next.style.visibility = ""; // 1) next.parentNode is always null for some reason // 2) For some reason it's necessary to scroll to the next element when // moving forward for the target element to be fully in view @@ -2385,17 +2354,37 @@ } updateLastFocused(ev) { - if (ev.target.classList.contains('zotero-focusable')) { - this._lastTabIndex = parseInt(ev.target.getAttribute('ztabindex')); + let ztabindex = parseInt(ev.target.getAttribute('ztabindex')); + if (ztabindex) { + this._lastTabIndex = ztabindex; } } async blurOpenField() { - var activeField = this._infoTable.querySelector('input, textarea'); + var activeField = this.getFocusedTextArea(); if (!activeField) { return false; } - return this.blurHandler(activeField); + return activeField.blur(); + } + + handlePopupOpening(event, popup) { + let isRightClick = event.type == 'contextmenu'; + if (!isRightClick) { + event.target.style.visibility = "visible"; + } + // On click, we have x/y coordinates so use that + // On keyboard click, open it next to the target + if (event.screenX) { + popup.openPopupAtScreen( + event.screenX + (isRightClick ? 0 : -5), + event.screenY + (isRightClick ? 0 : 5), + true + ); + } + else { + popup.openPopup(event.target); + } } /** diff --git a/chrome/content/zotero/elements/tagsBox.js b/chrome/content/zotero/elements/tagsBox.js index 1c82cc15c7..905be31822 100644 --- a/chrome/content/zotero/elements/tagsBox.js +++ b/chrome/content/zotero/elements/tagsBox.js @@ -351,7 +351,7 @@ itemID: this._item.id || '' }; valueElement.autocomplete = { - ignoreBlurWhileSearching: true, + ignoreBlurWhileSearching: false, popup: 'PopupAutoComplete', search: 'zotero', searchParam: JSON.stringify(params), diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl index e3a6b1f5e1..41794b48dc 100644 --- a/chrome/locale/en-US/zotero/zotero.ftl +++ b/chrome/locale/en-US/zotero/zotero.ftl @@ -9,6 +9,13 @@ zotero-tabs-menu-filter = zotero-tabs-menu-close-button = .tooltiptext = Close Tab +item-creator-moveDown = + .label = Move Down +item-creator-moveToTop = + .label = Move to Top +item-creator-moveUp = + .label = Move Up + item-menu-viewAttachment = .label = Open { $attachmentType -> @@ -22,6 +29,13 @@ item-menu-viewAttachment = [window] New Window *[other] Reader } +itembox-button-openLink = + .title = Open Link + .aria-label = Open Link + +itembox-button-options = + .aria-label = Open Context Menu + .title = Open Context Menu import-window = .title = Import diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index ad08fef613..2718b7c880 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -411,11 +411,8 @@ pane.item.changeType.text = Are you sure you want to change the item type?\n\nT pane.item.defaultFirstName = first pane.item.defaultLastName = last pane.item.defaultFullName = full name -pane.item.switchFieldMode.one = Switch to single field -pane.item.switchFieldMode.two = Switch to two fields -pane.item.creator.moveToTop = Move to Top -pane.item.creator.moveUp = Move Up -pane.item.creator.moveDown = Move Down +pane.item.switchFieldMode.one = Switch to Single Field +pane.item.switchFieldMode.two = Switch to Two Fields pane.item.notes.allNotes = All Notes pane.item.notes.untitled = Untitled Note pane.item.notes.delete.confirm = Are you sure you want to delete this note? diff --git a/scss/components/_clicky.scss b/scss/components/_clicky.scss index ee95afec53..3f400b36a8 100644 --- a/scss/components/_clicky.scss +++ b/scss/components/_clicky.scss @@ -15,7 +15,7 @@ /* Minus and plus buttons with clicky glow effect */ .zotero-clicky-minus, .zotero-clicky-plus { - margin: 0 !important; + margin: 0; width: 20px; height: 20px; border-radius: 2px; @@ -29,11 +29,33 @@ .zotero-clicky-minus { @include svgicon-menu("minus-circle", "universal", "16"); border: 0px !important; + margin: 0; } .zotero-clicky-plus { @include svgicon-menu("plus-circle", "universal", "16"); border: 0px !important; + margin: 0; +} + +.zotero-clicky-grippy { + @include svgicon-menu("grip", "universal", "16"); + margin: 0; +} + +.zotero-clicky-options { + @include svgicon-menu("options", "universal", "16"); + margin: 0; +} + +.zotero-clicky-open-link { + @include svgicon-menu("open-link", "universal", "16"); + margin: 0; +} + +.zotero-clicky-merge { + @include svgicon-menu("merge", "universal", "16"); + margin: 0; } .zotero-clicky-cross { diff --git a/scss/elements/_editableText.scss b/scss/elements/_editableText.scss index 1bb8dd16de..8439a4ce48 100644 --- a/scss/elements/_editableText.scss +++ b/scss/elements/_editableText.scss @@ -40,9 +40,11 @@ editable-text { span { visibility: hidden; - margin: 1px; + margin: 0; + border: 1px solid transparent; width: fit-content; white-space: nowrap; + padding: var(--editable-text-padding-block) var(--editable-text-padding-inline); } &:not([nowrap])::after { @@ -83,7 +85,7 @@ editable-text { &:hover:not(:read-only, :focus) { border-radius: 5px; - border: 1px solid var(--fill-tertiary); + background-color: var(--fill-quinary); box-shadow: 0 0 0 1px var(--fill-quinary); } diff --git a/scss/elements/_itemBox.scss b/scss/elements/_itemBox.scss index 1e3fac858a..72184a0f54 100644 --- a/scss/elements/_itemBox.scss +++ b/scss/elements/_itemBox.scss @@ -1,100 +1,115 @@ item-box { - display: flex; - flex-direction: column; -} - -item-box .body { - --row-height: 1.5em; display: flex; min-width: 0; width: 100%; #item-box { - display: flex; - flex-direction: column; - margin-top: 8px; - align-items: start; width: 100%; } #info-table { display: grid; grid-template-columns: max-content 1fr; - // Make sure rows are at least --row-height tall, but no taller than their tallest child - // This lets rows expand for multi-line editors - grid-auto-rows: minmax(var(--row-height), max-content); - align-items: center; - width: 100%; - } - - tr { - display: contents; - } - - td { - display: flex; - min-width: 0; - align-self: stretch; - align-items: center; - margin-inline-end: 5px; - } - - td > input, .creator-name-box > input { - align-self: center; - // Fields have 3px borders; cancel them out - margin-top: -3px; - margin-bottom: -3px; - margin-inline-start: 0; - padding: 0; + column-gap: 10px; + row-gap: 2px; + width: inherit; } - td > input { - margin-inline-end: 5px; + .meta-row { + display: grid; + grid-template-columns: subgrid; + grid-column: span 2; + padding-inline-start: 8px; + padding-inline-end: 8px; + + .meta-data { + width: 0; + min-width: 100%; + display: flex; + toolbarbutton { + margin-inline-start: 4px; + } + + } + editable-text { + flex: 1; // stretch value field as much as possible + max-width: 100%; // stay within .meta-data when the itemBox is narrow + .input { + // keep input within editable-text when the itemBox is narrow + width: calc(100% - 2*var(--editable-text-padding-inline) - 1px) + } + // keep multiline fields as tall as they have to be unless they're focused + &[multiline] textarea:not(:focus) { + min-height: 1em; + } + } + + .meta-label { + display: flex; + font-weight: normal; + text-align: end; + &[fieldname^="creator"] { + justify-content: space-between; + align-items: center; + } + > label { + margin-top: 2px; + @include comfortable { + margin-top: 3px; + } + } + } + + .key { + width: 100%; + } + + toolbarbutton { + @include focus-ring; + + // needed to have the outline appear on all platforms + -moz-appearance: none; + + align-self: center; + // Make all buttons tigher to not stretch the rows + height: auto; + width: auto; + padding: 1px; + } } - th > label { - margin-top: 1px !important; - margin-bottom: 1px !important; - -moz-box-pack: start; - margin-inline-start: 1px !important; - margin-inline-end: 5px !important; - padding: 0 2px; + .meta-label > label, .creator-type-label, #more-creators-label { + color: var(--fill-secondary); } - td > [fieldname] { - width: 100%; + // All icons that are by default hidden + #info-table .show-on-hover { + visibility: hidden; } - .value { - min-height: 14px; - align-self: center; - padding-inline: 2px; + .drag-hidden-creator { + opacity: 0; } - - .value:not(.multiline) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .value.multiline { - white-space: pre-line; - } - - /*td > vbox > description - { - margin: 0 !important; - }*/ + // On hover of the meta-row, reveal all hidden icons + // unless there's .noHover class which keeps everything hidden + #info-table .meta-row:not(.noHover):hover .show-on-hover, + #info-table .meta-row:focus-within .show-on-hover { + visibility: visible; + } #item-type-menu { - height: 1.5em !important; - min-height: 1.5em !important; - padding: 0 2px !important; - margin: 0 !important; + @include focus-ring; + margin: 0; margin-inline-end: 5px !important; - max-height: 1.5em !important; flex: 1; + padding-inline-start: 5px; + + // Same padding as editable-text + @include comfortable { + padding-top: 3px; + padding-bottom: 3px; + } &::part(dropmarker) { display: none; @@ -103,10 +118,13 @@ item-box .body { &::part(label) { margin-inline-start: 0; } + + &[disabled] { + background: none; + } } #item-type-menu:not(:hover):not(:active) { - border: var(--material-border-transparent); background-color: transparent; } @@ -119,128 +137,95 @@ item-box .body { padding: 0 !important; } - /* DEBUG: this doesn't seem to work, unfortunately - label[singleField=false]:after - { - content:","; - } - */ - - /*textbox .textbox-input-box - { - margin: 0; - }*/ - textarea { font: inherit; resize: none; } - /* metadata field names */ - th { - display: flex; - height: var(--row-height); - align-self: start; - align-items: center; - justify-content: end; - font-weight: normal; - margin-inline-start: 5px !important; - margin-inline-end: 0 !important; - } #more-creators-label { font-weight: bold; } - /*row > label - { - border: 1px solid transparent; - } - - row label - { - -moz-user-focus: ignore; - }*/ - .pointer:hover, .pointer:hover > label { cursor: pointer !important; } /* creator type menu */ - + .creator-type-label { + @include focus-ring; + display: inline-block; + // undo the padding so that the name is pushed to the end + margin-inline-end: -4px; + padding-left: 4px; + padding-right: 4px; + + &:hover, &:focus { + .creator-type-dropmarker { + visibility: visible; + } + } + } .creator-type-label, .creator-type-value { -moz-box-align: center; + align-items: center; + display: flex; + } + + .creator-type-value > .zotero-clicky { + // Some spacing between creator buttons + margin-right: 2px; } .creator-name-box { flex: 1; - min-width: 0; display: flex; - align-items: baseline; - - & > input { - flex: 1; - min-width: 60%; + editable-text input { + min-width: 0; } - & > div { - flex-shrink: 1; - min-width: 10px; + // Margin adjusted by inline padding to have 4px between first and last name + *[fieldMode="0"]:first-child { + margin-inline-end: calc(max(0px, 4px - var(--editable-text-padding-inline))); } - - & > div:first-child[fieldMode="0"] { - // Cancel out padding before comma - margin-inline-end: -3px; + // Add comma when the last name is not focused + *[fieldMode="0"]:first-child:not(.focused) { + position: relative; + &::after { + content: ","; + position: absolute; + right: 0; + bottom: var(--editable-text-padding-block); + } } } - .creator-type-label > label - { - margin: 1px 0 !important; - margin-inline-end: 4px !important; - padding-inline-end: 2px !important; - } - .creator-type-dropmarker { display: inline-block; - margin: 0 1em 1px; - background-image: url('chrome://zotero/skin/arrow-down.gif'); - background-size: cover; - width: 11px; - height: 6px; - } - - .creator-name-box, .date-box > span { - margin: 1px 0 !important; - margin-inline-start: 1px !important; + background-image: url('chrome://zotero/skin/16/universal/chevron-12.svg'); + background-size: contain; + width: 8px; + height: 8px; + background-repeat: no-repeat; + padding-right: 4px; + align-self: center; + visibility: hidden; } .comma { - margin: 1px 0 !important; + margin-inline-end: calc(4px - var(--editable-text-padding-inline)); + align-self: center; } #zotero-date-field-status { - color: #666; + color: var(--fill-secondary); padding: 0 !important; - padding-inline-start: 1px !important; - padding-inline-end: 10px !important; + padding-inline-start: 5px !important; + padding-inline-end: 1px !important; white-space: nowrap; - } - - .zotero-field-toggle - { - width: 27px !important; - max-width: 27px !important; - min-width: 27px !important; - height: 14px; - margin: 0 !important; - margin-inline-end: 5px !important; - background-repeat: no-repeat !important; - background-position: center !important; - border-width: 0 !important; - border-radius: 4px !important; + align-self: center; } /* Merge pane in duplicates view */ @@ -267,7 +252,7 @@ item-box .body { } #retraction-details { - background: #fbf0f0; + @include light-dark(background, #fbf0f0, var(--material-background)); padding: .5em 1.5em; margin-top: 0; margin-bottom: 1em; diff --git a/scss/linux/elements/_itemBox.scss b/scss/linux/elements/_itemBox.scss index eac9e05f3c..898af8a121 100644 --- a/scss/linux/elements/_itemBox.scss +++ b/scss/linux/elements/_itemBox.scss @@ -1,5 +1,9 @@ item-box { - .zotero-clicky { - min-height: 17px; + + #item-type-menu { + padding: 1px; + @include comfortable { + padding: 2px; + } } } diff --git a/scss/mac/elements/_itemBox.scss b/scss/mac/elements/_itemBox.scss deleted file mode 100644 index ca06431c25..0000000000 --- a/scss/mac/elements/_itemBox.scss +++ /dev/null @@ -1,32 +0,0 @@ -item-box { - scrollbox - { - padding-top: 3px; - } - - th > label, .creator-type-label, #more-creators-label { - color: #7f7f7f; - } - - /*.zotero-field-toggle .toolbarbutton-text - { - visibility: hidden; - } - - .zotero-field-toggle .toolbarbutton-icon - { - margin: 0px !important; - }*/ - - .creator-type-dropmarker { - margin: 1px .2em 1px; - background-image: url('chrome://zotero/skin/mac/arrow-down.png'); - max-width: 7px; - max-height: 7px; - } -} - -/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */ -@media (min-resolution: 1.25dppx) { - item-box .creator-type-dropmarker { background-image: url('chrome://zotero/skin/mac/arrow-down@2x.png'); } -} diff --git a/scss/win/elements/_itemBox.scss b/scss/win/elements/_itemBox.scss index aa4dcea8eb..017be80c12 100644 --- a/scss/win/elements/_itemBox.scss +++ b/scss/win/elements/_itemBox.scss @@ -1,24 +1,7 @@ item-box { - row > hbox, - row > vbox - { - margin-top: 0 !important; - margin-bottom: 0 !important; - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - row vbox[fieldname] - { - margin-inline-start: 1px; - } - - .creator-type-label image { - margin-bottom: 0; - } @media (-moz-platform: windows-win10) { - td > input, td > textarea, .creator-name-box > input { + .meta-data > input, .meta-data > textarea, .creator-name-box > input { // Give text fields a consistent, native-ish look using Windows 11 // colors. Ideally, we only want to do this on Windows 11, but // Windows 11 can't currently be specifically targeted. @@ -36,7 +19,6 @@ item-box { #item-type-menu { - padding: 0 !important; padding-inline-start: 1px !important; margin: 0 !important; margin-inline-start: 1px !important; diff --git a/scss/zotero-mac.scss b/scss/zotero-mac.scss index 40843840a6..db24b86dd4 100644 --- a/scss/zotero-mac.scss +++ b/scss/zotero-mac.scss @@ -13,4 +13,3 @@ // Elements @import "mac/elements/attachmentBox"; -@import "mac/elements/itemBox"; diff --git a/test/tests/itemPaneTest.js b/test/tests/itemPaneTest.js index 0fafc273a1..5725da3355 100644 --- a/test/tests/itemPaneTest.js +++ b/test/tests/itemPaneTest.js @@ -16,14 +16,14 @@ describe("Item pane", function () { var id = yield item.saveTx(); var itemBox = doc.getElementById('zotero-editpane-item-box'); - var label = itemBox.querySelectorAll('[fieldname="title"]')[1]; - assert.equal(label.textContent, ''); + var label = itemBox.querySelectorAll('[fieldname="series"]')[1]; + assert.equal(label.value, ''); - item.setField('title', 'Test'); + item.setField('series', 'Test'); yield item.saveTx(); - label = itemBox.querySelectorAll('[fieldname="title"]')[1]; - assert.equal(label.textContent, 'Test'); + label = itemBox.querySelectorAll('[fieldname="series"]')[1]; + assert.equal(label.value, 'Test'); yield Zotero.Items.erase(id); }) @@ -41,10 +41,10 @@ describe("Item pane", function () { await item.saveTx(); var itemBox = doc.getElementById('zotero-editpane-item-box'); - var label = itemBox.querySelector('[fieldname="creator-0-lastName"]') - var parent = label.parentNode; + var lastName = itemBox.querySelector('#itembox-field-value-creator-0-lastName'); + var parent = lastName.closest(".creator-type-value"); assert.property(parent, 'oncontextmenu'); - assert.isFunction(label.parentNode.oncontextmenu); + assert.isFunction(parent.oncontextmenu); var menupopup = itemBox.querySelector('#zotero-creator-transform-menu'); // Fake a right-click @@ -73,13 +73,61 @@ describe("Item pane", function () { await item.saveTx(); var itemBox = doc.getElementById('zotero-editpane-item-box'); - var label = itemBox.querySelector('[fieldname="creator-0-lastName"]') - var firstlast = label.closest('.creator-name-box'); - firstlast.oncontextmenu(new MouseEvent('click', { bubbles: true, button: 2 })); + var label = itemBox.querySelector('#itembox-field-value-creator-0-lastName'); + var firstlast = label.closest('.creator-type-value'); + firstlast.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, button: 2 })); var menuitem = doc.getElementById('creator-transform-swap-names'); assert.isTrue(menuitem.hidden); }); + + it("should reorder creators", async function () { + var item = new Zotero.Item('book'); + item.setCreators([ + { + lastName: "One", + creatorType: "author" + }, + { + lastName: "Two", + creatorType: "author" + }, + { + lastName: "Three", + creatorType: "author" + } + ]); + await item.saveTx(); + + var itemBox = doc.getElementById('zotero-editpane-item-box'); + // Move One to the last spot + itemBox.moveCreator(0, null, 3); + await waitForItemEvent('modify'); + let thirdLastName = itemBox.querySelector("[fieldname='creator-2-lastName']").value; + assert.equal(thirdLastName, "One"); + + // Move One to the second spot + itemBox.moveCreator(2, null, 1); + await waitForItemEvent('modify'); + let secondLastname = itemBox.querySelector("[fieldname='creator-1-lastName']").value; + assert.equal(secondLastname, "One"); + + // Move Two down + itemBox.moveCreator(0, 'down'); + await waitForItemEvent('modify'); + secondLastname = itemBox.querySelector("[fieldname='creator-1-lastName']").value; + let firstLastName = itemBox.querySelector("[fieldname='creator-0-lastName']").value; + assert.equal(secondLastname, "Two"); + assert.equal(firstLastName, "One"); + + // Move Three up + itemBox.moveCreator(2, 'up'); + await waitForItemEvent('modify'); + secondLastname = itemBox.querySelector("[fieldname='creator-1-lastName']").value; + thirdLastName = itemBox.querySelector("[fieldname='creator-2-lastName']").value; + assert.equal(secondLastname, "Three"); + assert.equal(thirdLastName, "Two"); + }); // Note: This issue applies to all context menus in the item box (text transform, name swap), @@ -119,9 +167,7 @@ describe("Item pane", function () { var item = await createDataObject('item'); var itemBox = doc.getElementById('zotero-editpane-item-box'); - var label = itemBox.querySelector('div[fieldname="accessDate"].zotero-clicky'); - label.click(); - var textbox = itemBox.querySelector('input[fieldname="accessDate"]'); + var textbox = itemBox.querySelector('[fieldname="accessDate"]'); textbox.value = 'now'; // Blur events don't necessarily trigger if window doesn't have focus itemBox.hideEditor(textbox); @@ -148,11 +194,11 @@ describe("Item pane", function () { let itemBox = doc.getElementById('zotero-editpane-item-box'); - itemBox.querySelector('div[fieldname="creator-0-lastName"]').click(); + itemBox.querySelector('[fieldname="creator-0-lastName"]').click(); itemBox.hideEditor(itemBox.querySelector('input[fieldname="creator-0-lastName"]')); assert.equal( - itemBox.querySelector('div[fieldname="creator-0-lastName"]').getAttribute('fieldMode'), + itemBox.querySelector('[fieldname="creator-0-lastName"]').getAttribute('fieldMode'), '1' ); });