itembox focus edits and refactoring (#4096)

- removed ztabindex logic from itemBox. It is no longer needed, adds
  unnecessary complexity and is likely at the root of multiple glitches
  if a plugin inserts an arbitrary row that does not have ztabindex set.
- if a creator row is deleted when the focus is inside of the row, focus
  another creator row to not loose the focus.
- more centralized button handling in `_ensureButtonsFocusable` and
  `_updateCreatorButtonsStatus`
- refactoring of hidden toolbarbuttons css so that the icons are still
  hidden and don't occupy space (if desired) but are still visible for
  screen readers, so they are focusable without JS changing their
  visibility (this with ztabindex removal fixes vpat 24)
- removed `escape_enter` event from `editable-text`. It was a workaround
  to know when itemBox should move focus back to itemTree. Unhandled
  Enter on an input or Escape should focus itemTree (or reader) from
  anywhere in the itemPane/contextPane (not just itemBox), so that logic
  is moved to itemDetails.js. To avoid conflicts with Shift-Enter, do
  not propagate that event outside of multiline editable-text. Fixes:
  #3896
- removed not necessary keyboard nav handling from itemDetails.js. It
  was only needed for mac, and is redundant if "Keyboard navigation"
  setting is on
- using `keydown` instead of `keypress` for itemDetails keyboard nav
  handling because `Enter` `keypress` does not seem to get out of
  `editable-text` but `keydown` does.
- old handleKeyPress from itemBox is no longer relevant for most
  elements, so it is removed and substituted with a dedicated handler
  just for creator row.
- moved the creator's paste handler into its own dedicated function
  from the autocomplete handler (which was confusing)
- special handling for `enter` and `escape` events on `editable-text`
  with autocomplete to not stop event propagation, so that the events
  can bubble and be handled in `itemDetails`. It avoids some cases of
  the focus being lost and returned to the `window`. It was unnecessary
  earlier due to `escape_enter` workaround but only within itemBox and
  only within itemPane.
- removed explicit tab navigation handling from `collapsible-section`
  header. Currently, it may get stuck when buttons are hidden (e.g. in
  the trash mode). It was only added to enable keyboard navigation on
  mac before special "Keyboard navigation" setting was discovered (it
  was never an issue on windows), so now it's easier to just let mozilla
  handle it.
- always use `getTitleField` to find and focus the proper title field in
  itemBox
- on shift-tab from the focused tab, just move focus to the first
  focusable element before the splitter without any special handling for
  attachments, notes and duplicates pane as before. It ensures a more
  consistent and predictable keyboard navigation, especially now that
  itemPane is fairly keyboard accessible.

Fixes: #4076
This commit is contained in:
Bogdan Abaev 2024-05-07 10:42:20 -04:00 committed by Dan Stillman
parent 1394381257
commit c6799bc3c2
8 changed files with 251 additions and 481 deletions

View file

@ -334,27 +334,6 @@
event.stopPropagation(); event.stopPropagation();
}; };
// Tab/Shift-Tab from section header through header buttons
if (event.key === "Tab") {
let nextBtn;
if (tgt.classList.contains("head") && event.shiftKey) {
return;
}
if (tgt.classList.contains("head")) {
nextBtn = this._head.querySelector("toolbarbutton");
}
else {
nextBtn = event.shiftKey ? tgt.previousElementSibling : tgt.nextElementSibling;
}
if (nextBtn?.tagName == "popupset") {
nextBtn = this._head;
}
if (nextBtn) {
nextBtn.focus();
stopEvent();
}
}
if (event.target.tagName === "toolbarbutton") { if (event.target.tagName === "toolbarbutton") {
// No actions on right/left on header buttons // No actions on right/left on header buttons
if (["ArrowRight", "ArrowLeft"].includes(event.key)) { if (["ArrowRight", "ArrowLeft"].includes(event.key)) {

View file

@ -193,6 +193,14 @@
input.addEventListener('mousedown', this._handleMouseDown); input.addEventListener('mousedown', this._handleMouseDown);
input.addEventListener('dragover', this._handleDragOver); input.addEventListener('dragover', this._handleDragOver);
input.addEventListener('drop', this._handleDrop); input.addEventListener('drop', this._handleDrop);
if (autocompleteEnabled) {
// Even through this may run multiple times on editable-text, the listener
// is added only once because we pass the reference to the same exact function.
this.addEventListener('keydown', this._captureAutocompleteKeydown, true);
}
else {
this.removeEventListener('keydown', this._captureAutocompleteKeydown, true);
}
let focused = this.focused; let focused = this.focused;
let selectionStart = this._input?.selectionStart; let selectionStart = this._input?.selectionStart;
@ -333,18 +341,37 @@
if (event.key === 'Enter') { if (event.key === 'Enter') {
if (this.multiline === event.shiftKey) { if (this.multiline === event.shiftKey) {
event.preventDefault(); event.preventDefault();
this.dispatchEvent(new CustomEvent('escape_enter'));
this._input.blur(); this._input.blur();
} }
// Do not let out shift-enter event on multiline, since it should never do
// anything but add a linebreak to textarea
if (this.multiline && !event.shiftKey) {
event.stopPropagation();
}
} }
else if (event.key === 'Escape') { else if (event.key === 'Escape') {
this.dispatchEvent(new CustomEvent('escape_enter'));
let initialValue = this._input.dataset.initialValue ?? ''; let initialValue = this._input.dataset.initialValue ?? '';
this.setAttribute('value', initialValue); this.setAttribute('value', initialValue);
this._input.value = initialValue; this._input.value = initialValue;
this._input.blur(); this._input.blur();
} }
}; };
_captureAutocompleteKeydown = (event) => {
// On Enter or Escape, mozilla stops propagation of the event which may interfere with out handling
// of the focus. E.g. the event should be allowed to reach itemDetails from itemBox so that focus
// can be moved to the itemTree or the reader.
// https://searchfox.org/mozilla-central/source/toolkit/content/widgets/autocomplete-input.js#564
// To avoid it, capture Enter and Escape keydown events and handle them without stopping propagation.
if (this._input.autocomplete !== "on" || !["Enter", "Escape"].includes(event.key)) return;
event.preventDefault();
if (event.key == "Enter") {
this._input.handleEnter();
}
else if (event.key == "Escape") {
this._input.mController.handleEscape();
}
};
_handleMouseDown = (event) => { _handleMouseDown = (event) => {
this.setAttribute("mousedown", true); this.setAttribute("mousedown", true);

View file

@ -47,11 +47,8 @@
this._editableFields = []; this._editableFields = [];
this._fieldAlternatives = {}; this._fieldAlternatives = {};
this._fieldOrder = []; this._fieldOrder = [];
this._tabIndexMinCreators = 100;
this._tabIndexMaxFields = 0;
this._initialVisibleCreators = 5; this._initialVisibleCreators = 5;
this._draggedCreator = false; this._draggedCreator = false;
this._ztabindex = 0;
this._selectField = null; this._selectField = null;
} }
@ -116,7 +113,6 @@
); );
typeBox.setAttribute('typeid', typeID); typeBox.setAttribute('typeid', typeID);
this._lastTabIndex = -1;
this.modifyCreator(index, fields); this.modifyCreator(index, fields);
if (this.saveOnEdit) { if (this.saveOnEdit) {
await this.blurOpenField(); await this.blurOpenField();
@ -226,6 +222,23 @@
() => Zotero.Utilities.Internal.copyTextToClipboard(this._linkMenu.dataset.link) () => Zotero.Utilities.Internal.copyTextToClipboard(this._linkMenu.dataset.link)
); );
// Save the last focused element, so that the focus can go back to it if
// the table is refreshed
this._infoTable.addEventListener("focusin", (e) => {
let target = e.target.closest("[fieldname], [tabindex], [focusable]");
if (target?.id) {
this._selectField = target.id;
}
});
// If the focus leaves the itemBox, clear the last focused element
this._infoTable.addEventListener("focusout", (e) => {
let destination = e.relatedTarget;
if (!(destination && this._infoTable.contains(destination))) {
this._selectField = null;
}
});
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox'); this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox');
Zotero.Prefs.registerObserver('fontSize', () => { Zotero.Prefs.registerObserver('fontSize', () => {
this._forceRenderAll(); this._forceRenderAll();
@ -327,7 +340,6 @@
} }
this._item = val; this._item = val;
this._lastTabIndex = null;
this.scrollToTop(); this.scrollToTop();
} }
@ -457,14 +469,6 @@
if (id != this.item.id) { if (id != this.item.id) {
continue; continue;
} }
let activeArea = this.getFocusedTextArea();
// Re-select currently active area after refresh.
if (activeArea) {
this._selectField = activeArea.getAttribute("fieldname");
}
if (document.activeElement == this.itemTypeMenu) {
this._selectField = "item-type-menu";
}
this._forceRenderAll(); this._forceRenderAll();
break; break;
} }
@ -484,8 +488,6 @@
if (this._isAlreadyRendered()) return; if (this._isAlreadyRendered()) return;
// Init tab index to begin after all creator rows
this._ztabindex = this._tabIndexMinCreators * (this.item.numCreators() || 1);
delete this._linkMenu.dataset.link; delete this._linkMenu.dataset.link;
// //
@ -498,13 +500,6 @@
// Item type menu // Item type menu
this.addItemTypeMenu(); this.addItemTypeMenu();
this.updateItemTypeMenuSelection(); this.updateItemTypeMenuSelection();
this.itemTypeMenu.disabled = !this.showTypeMenu;
// Re-focus item type menu if it was focused before refresh
if (this._selectField == "item-type-menu") {
Services.focus.setFocus(this.itemTypeMenu, Services.focus.FLAG_SHOWRING);
this._selectField = null;
this._lastTabIndex = null;
}
var fieldNames = []; var fieldNames = [];
// Manual field order // Manual field order
@ -653,7 +648,6 @@
if (!(Zotero.ItemFields.isLong(fieldName) || Zotero.ItemFields.isMultiline(fieldName))) { if (!(Zotero.ItemFields.isLong(fieldName) || Zotero.ItemFields.isMultiline(fieldName))) {
optionsButton.classList.add("no-display"); optionsButton.classList.add("no-display");
} }
optionsButton.setAttribute("ztabindex", ++this._ztabindex);
optionsButton.setAttribute('data-l10n-id', "itembox-button-options"); optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
// eslint-disable-next-line no-loop-func // eslint-disable-next-line no-loop-func
let triggerPopup = (e) => { let triggerPopup = (e) => {
@ -672,20 +666,14 @@
}; };
// Same popup triggered for right-click and options button click // Same popup triggered for right-click and options button click
optionsButton.addEventListener("click", triggerPopup); optionsButton.addEventListener("click", triggerPopup);
optionsButton.addEventListener('keypress', event => this.handleKeyPress(event));
rowData.appendChild(optionsButton); rowData.appendChild(optionsButton);
rowData.oncontextmenu = triggerPopup; rowData.oncontextmenu = triggerPopup;
} }
this.addDynamicRow(rowLabel, rowData); this.addDynamicRow(rowLabel, rowData);
if (fieldName && this._selectField == fieldName) {
valueElement.focus();
this._selectField = null;
}
// In field merge mode, add a button to switch field versions // In field merge mode, add a button to switch field versions
else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') { if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
var button = document.createXULElement("toolbarbutton"); var button = document.createXULElement("toolbarbutton");
button.className = 'zotero-field-version-button zotero-clicky-merge'; button.className = 'zotero-field-version-button zotero-clicky-merge';
button.setAttribute('type', 'menu'); button.setAttribute('type', 'menu');
@ -715,13 +703,11 @@
rowData.appendChild(button); rowData.appendChild(button);
} }
} }
this._tabIndexMaxFields = this._ztabindex; // Save the last tab index
// //
// Creators // Creators
// //
this._ztabindex = 1; // Reset tab index to 1, since creators go before other fields
// Creator type menu // Creator type menu
if (this.editable) { if (this.editable) {
while (this._creatorTypeMenu.hasChildNodes()) { while (this._creatorTypeMenu.hasChildNodes()) {
@ -778,11 +764,6 @@
for (let i = 0; i < max; i++) { for (let i = 0; i < max; i++) {
let data = this.item.getCreator(i); let data = this.item.getCreator(i);
this.addCreatorRow(data, data.creatorTypeID); this.addCreatorRow(data, data.creatorTypeID);
// Display "+" button on all but last row
if (i == max - 2) {
this.disableCreatorAddButtons();
}
} }
if (this._draggedCreator) { if (this._draggedCreator) {
this._draggedCreator = false; this._draggedCreator = false;
@ -802,8 +783,6 @@
// Additional creators not displayed // Additional creators not displayed
if (num > max) { if (num > max) {
this.addMoreCreatorsRow(num - max); this.addMoreCreatorsRow(num - max);
this.disableCreatorAddButtons();
} }
else { else {
// If we didn't start with creators truncated, // If we didn't start with creators truncated,
@ -815,20 +794,14 @@
if (this._addCreatorRow) { if (this._addCreatorRow) {
this.addCreatorRow(false, this.item.getCreator(max - 1).creatorTypeID, true); this.addCreatorRow(false, this.item.getCreator(max - 1).creatorTypeID, true);
this._addCreatorRow = false; this._addCreatorRow = false;
this.disableCreatorAddButtons();
} }
} }
} }
else if (this.editable && Zotero.CreatorTypes.itemTypeHasCreators(this.item.itemTypeID)) { else if (this.editable && Zotero.CreatorTypes.itemTypeHasCreators(this.item.itemTypeID)) {
// Add default row // Add default row
this.addCreatorRow(false, false, true, true); this.addCreatorRow(false, false, true, true);
this.disableCreatorAddButtons();
} }
// Move to next or previous field if (shift-)tab was pressed
if (this._lastTabIndex && this._lastTabIndex != -1) {
this._focusNextField(this._lastTabIndex);
}
if (this._showCreatorTypeGuidance) { if (this._showCreatorTypeGuidance) {
let creatorTypeLabels = this.querySelectorAll(".creator-type-label"); let creatorTypeLabels = this.querySelectorAll(".creator-type-label");
@ -866,11 +839,16 @@
} }
}); });
} }
this._refreshed = true;
// Add tabindex=0 to all focusable element this._ensureButtonsFocusable();
this.querySelectorAll("[ztabindex]").forEach((node) => {
node.setAttribute("tabindex", 0); // Set focus on the last focused field
}); if (this._selectField) {
let refocusField = this.querySelector(`#${this._selectField}`);
if (refocusField) {
refocusField.focus();
}
}
// Make sure that any opened popup closes // Make sure that any opened popup closes
this.querySelectorAll("menupopup").forEach((popup) => { this.querySelectorAll("menupopup").forEach((popup) => {
popup.hidePopup(); popup.hidePopup();
@ -896,24 +874,27 @@
else { else {
var menulist = document.createXULElement("menulist", { is: "menulist-item-types" }); var menulist = document.createXULElement("menulist", { is: "menulist-item-types" });
menulist.id = "item-type-menu"; menulist.id = "item-type-menu";
menulist.className = "zotero-clicky"; menulist.className = "zotero-clicky keyboard-clickable";
menulist.addEventListener('command', (event) => { menulist.addEventListener('command', (event) => {
this.changeTypeTo(event.target.value, menulist); this.changeTypeTo(event.target.value, menulist);
}); });
menulist.addEventListener('focus', () => { menulist.addEventListener('focus', () => {
this.ensureElementIsVisible(menulist); this.ensureElementIsVisible(menulist);
}); });
menulist.addEventListener('keypress', (event) => { // This is instead of setting disabled=true so that the menu is not excluded
if (event.keyCode == event.DOM_VK_TAB) { // from tab navigation. For <input>s, we just set readonly=true but it is not
this.handleKeyPress(event); // a valid property for menulist.
menulist.addEventListener("popupshowing", (e) => {
if (!this._editable) {
e.preventDefault();
e.stopPropagation();
} }
}); });
menulist.setAttribute("aria-labelledby", "itembox-field-itemType-label"); menulist.setAttribute("aria-labelledby", "itembox-field-itemType-label");
this.itemTypeMenu = menulist; this.itemTypeMenu = menulist;
rowData.appendChild(menulist); rowData.appendChild(menulist);
} }
this.itemTypeMenu.setAttribute('ztabindex', '1'); this.itemTypeMenu.setAttribute("aria-disabled", !this._editable);
this.itemTypeMenu.disabled = !this.showTypeMenu;
row.appendChild(labelWrapper); row.appendChild(labelWrapper);
row.appendChild(rowData); row.appendChild(rowData);
this._infoTable.appendChild(row); this._infoTable.appendChild(row);
@ -967,8 +948,10 @@
let labelWrapper = document.createElement('div'); let labelWrapper = document.createElement('div');
let grippy = document.createXULElement('toolbarbutton'); let grippy = document.createXULElement('toolbarbutton');
labelWrapper.className = 'creator-type-label'; labelWrapper.className = 'creator-type-label keyboard-clickable';
labelWrapper.setAttribute("tabindex", 0);
grippy.className = "zotero-clicky zotero-clicky-grippy show-on-hover"; grippy.className = "zotero-clicky zotero-clicky-grippy show-on-hover";
grippy.setAttribute('tabindex', -1);
rowLabel.appendChild(grippy); rowLabel.appendChild(grippy);
if (this.editable) { if (this.editable) {
@ -989,7 +972,7 @@
labelWrapper.setAttribute('role', 'button'); labelWrapper.setAttribute('role', 'button');
labelWrapper.setAttribute('aria-describedby', 'creator-type-label-inner'); labelWrapper.setAttribute('aria-describedby', 'creator-type-label-inner');
labelWrapper.setAttribute('ztabindex', ++this._ztabindex); labelWrapper.setAttribute('id', `creator-${rowIndex}-label`);
// If not editable or only 1 creator row, hide grippy // If not editable or only 1 creator row, hide grippy
if (!this.editable || this.item.numCreators() < 2) { if (!this.editable || this.item.numCreators() < 2) {
@ -1016,7 +999,6 @@
this.createValueElement( this.createValueElement(
lastName, lastName,
fieldName, fieldName,
++this._ztabindex
) )
); );
@ -1026,7 +1008,6 @@
this.createValueElement( this.createValueElement(
firstName, firstName,
fieldName, fieldName,
++this._ztabindex
) )
); );
firstNameElem.placeholder = this._defaultFirstName; firstNameElem.placeholder = this._defaultFirstName;
@ -1039,41 +1020,27 @@
// Minus (-) button // Minus (-) button
var removeButton = document.createXULElement('toolbarbutton'); var removeButton = document.createXULElement('toolbarbutton');
removeButton.setAttribute("class", "zotero-clicky zotero-clicky-minus show-on-hover no-display"); removeButton.setAttribute("class", "zotero-clicky zotero-clicky-minus show-on-hover no-display");
removeButton.setAttribute('ztabindex', ++this._ztabindex); removeButton.setAttribute('id', `creator-${rowIndex}-remove`);
removeButton.setAttribute('tooltiptext', Zotero.getString('general.delete')); removeButton.setAttribute('tooltiptext', Zotero.getString('general.delete'));
// If default first row, don't let user remove it removeButton.addEventListener("command", () => this.removeCreator(rowIndex, rowData.parentNode));
if (defaultRow || !this.editable) {
this.disableButton(removeButton);
}
else {
removeButton.addEventListener("click", () => {
this.removeCreator(rowIndex, rowData.parentNode);
});
}
rowData.appendChild(removeButton); rowData.appendChild(removeButton);
// Plus (+) button // Plus (+) button
var addButton = document.createXULElement('toolbarbutton'); var addButton = document.createXULElement('toolbarbutton');
addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus show-on-hover no-display"); addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus show-on-hover no-display");
addButton.setAttribute('ztabindex', ++this._ztabindex); addButton.setAttribute('id', `creator-${rowIndex}-add`);
addButton.setAttribute('tooltiptext', Zotero.getString('general.create')); addButton.setAttribute('tooltiptext', Zotero.getString('general.create'));
// If row isn't saved, don't let user add more addButton.addEventListener("command", () => this.addCreatorRow(null, typeID, true));
if (unsaved || !this.editable) {
this.disableButton(addButton);
}
else {
this._enablePlusButton(addButton, typeID, fieldMode);
}
rowData.appendChild(addButton); rowData.appendChild(addButton);
// Options button that opens creator transform menu // Options button that opens creator transform menu
let optionsButton = document.createXULElement("toolbarbutton"); let optionsButton = document.createXULElement("toolbarbutton");
if (!this.editable) { if (!this.editable) {
optionsButton.style.visibility = "hidden"; optionsButton.style.visibility = "hidden";
this.disableButton(optionsButton); optionsButton.disabled = true;
} }
optionsButton.className = "zotero-clicky zotero-clicky-options show-on-hover no-display"; optionsButton.className = "zotero-clicky zotero-clicky-options show-on-hover no-display";
optionsButton.setAttribute('ztabindex', ++this._ztabindex); optionsButton.setAttribute('id', `creator-${rowIndex}-options`);
optionsButton.setAttribute('data-l10n-id', "itembox-button-options"); optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
let triggerPopup = (e) => { let triggerPopup = (e) => {
document.popupNode = firstlast; document.popupNode = firstlast;
@ -1086,28 +1053,18 @@
rowData.appendChild(optionsButton); rowData.appendChild(optionsButton);
if (this.editable) { if (this.editable) {
optionsButton.addEventListener("click", triggerPopup); optionsButton.addEventListener("command", triggerPopup);
rowData.oncontextmenu = triggerPopup; rowData.oncontextmenu = triggerPopup;
} }
if (!this.preventFocus) {
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));
}
}
this._creatorCount++; this._creatorCount++;
if (!this.editable) {
removeButton.hidden = true;
addButton.hidden = true;
optionsButton.hidden = true;
}
let row = this.addDynamicRow(rowLabel, rowData, true); let row = this.addDynamicRow(rowLabel, rowData, true);
this._updateCreatorButtonsStatus();
this._ensureButtonsFocusable();
/** /**
* Events handling creator drag-drop reordering * Events handling creator drag-drop reordering
*/ */
@ -1173,6 +1130,8 @@
this.addAutocompleteToElement(lastNameElem); this.addAutocompleteToElement(lastNameElem);
this.addAutocompleteToElement(firstNameElem); this.addAutocompleteToElement(firstNameElem);
row.addEventListener("keydown", e => this.handleCreatorRowKeyDown(e));
lastNameElem.addEventListener("paste", e => this.handleCreatorPaste(e));
// Focus new rows // Focus new rows
if (unsaved && !defaultRow) { if (unsaved && !defaultRow) {
lastNameElem.focus(); lastNameElem.focus();
@ -1186,7 +1145,7 @@
var rowData = document.createElement('div'); var rowData = document.createElement('div');
rowData.className = "meta-data"; rowData.className = "meta-data";
rowData.id = 'more-creators-label'; rowData.id = 'more-creators-label';
rowData.setAttribute("ztabindex", ++this._ztabindex); rowData.setAttribute("tabindex", 0);
rowData.addEventListener('click', () => { rowData.addEventListener('click', () => {
this._displayAllCreators = true; this._displayAllCreators = true;
this._forceRenderAll(); this._forceRenderAll();
@ -1248,10 +1207,6 @@
delete lastName.style.width; delete lastName.style.width;
delete lastName.style.maxWidth; delete lastName.style.maxWidth;
// Remove firstname field from tabindex
tab = parseInt(firstName.getAttribute('ztabindex'));
firstName.setAttribute('ztabindex', -1);
// Hide first name field and prepend to last name field // Hide first name field and prepend to last name field
firstName.hidden = true; firstName.hidden = true;
@ -1264,13 +1219,6 @@
} }
// Clear first name value after it was moved to the full name field // Clear first name value after it was moved to the full name field
firstName.value = ""; firstName.value = "";
// If one of the creator fields is open, leave it open after swap
let activeField = this.getFocusedTextArea();
if (activeField == firstName || activeField == lastName) {
this._lastTabIndex = parseInt(lastName.getAttribute('ztabindex'));
this._tabDirection = false;
}
} }
// Switch to two-field mode // Switch to two-field mode
else { else {
@ -1278,9 +1226,6 @@
lastName.setAttribute('fieldMode', '0'); lastName.setAttribute('fieldMode', '0');
lastName.placeholder = this._defaultLastName; lastName.placeholder = this._defaultLastName;
// Add firstname field to tabindex
tab = parseInt(lastName.getAttribute('ztabindex'));
firstName.setAttribute('ztabindex', tab + 1);
if (!initial) { if (!initial) {
// Move all but last word to first name field and show it // Move all but last word to first name field and show it
@ -1426,26 +1371,15 @@
return false; return false;
} }
disableButton(button) { // Toolbarbuttons required tabindex=0 to be properly focusable via tab
button.setAttribute('disabled', true); _ensureButtonsFocusable() {
button.setAttribute('onclick', false); this.querySelectorAll("toolbarbutton").forEach((btn) => {
} if (!btn.getAttribute('tabindex')) {
btn.setAttribute("tabindex", 0);
_enablePlusButton(button, creatorTypeID, _fieldMode) { }
button.removeAttribute('disabled'); });
button.onclick = () => {
this.disableButton(button);
this.addCreatorRow(null, creatorTypeID, true);
};
}
disableCreatorAddButtons() {
// Disable the "+" button on all creator rows
var elems = this._infoTable.getElementsByClassName('zotero-clicky-plus');
for (let elem of elems) {
this.disableButton(elem);
}
} }
createOpenLinkIcon(value) { createOpenLinkIcon(value) {
// In duplicates/trash mode return nothing // In duplicates/trash mode return nothing
@ -1455,8 +1389,6 @@
let openLink = document.createXULElement("toolbarbutton"); let openLink = document.createXULElement("toolbarbutton");
openLink.className = "zotero-clicky zotero-clicky-open-link show-on-hover no-display"; openLink.className = "zotero-clicky zotero-clicky-open-link show-on-hover no-display";
openLink.addEventListener("click", event => ZoteroPane.loadURI(value, event)); openLink.addEventListener("click", event => ZoteroPane.loadURI(value, event));
openLink.addEventListener('keypress', event => this.handleKeyPress(event));
openLink.setAttribute("ztabindex", ++this._ztabindex);
openLink.setAttribute('data-l10n-id', "item-button-view-online"); openLink.setAttribute('data-l10n-id', "item-button-view-online");
return openLink; return openLink;
} }
@ -1485,23 +1417,15 @@
if (this._fieldIsClickable(fieldName)) { if (this._fieldIsClickable(fieldName)) {
valueElement.addEventListener("focus", e => this.showEditor(e.target)); valueElement.addEventListener("focus", e => this.showEditor(e.target));
valueElement.addEventListener("blur", e => this.hideEditor(e.target)); valueElement.addEventListener("blur", e => this.hideEditor(e.target));
valueElement.addEventListener("escape_enter", (_) => {
setTimeout(() => {
document.getElementById('item-tree-main-default').focus();
});
});
} }
else { else {
valueElement.setAttribute('readonly', true); valueElement.setAttribute('readonly', true);
} }
valueElement.setAttribute('ztabindex', ++this._ztabindex);
valueElement.setAttribute('id', `itembox-field-value-${fieldName}`); valueElement.setAttribute('id', `itembox-field-value-${fieldName}`);
valueElement.setAttribute('fieldname', fieldName); valueElement.setAttribute('fieldname', fieldName);
valueElement.setAttribute('tight', true); valueElement.setAttribute('tight', true);
valueElement.addEventListener("focus", e => this.updateLastFocused(e));
valueElement.addEventListener("keypress", e => this.handleKeyPress(e));
switch (fieldName) { switch (fieldName) {
case 'itemType': case 'itemType':
valueElement.setAttribute('itemTypeID', valueText); valueElement.setAttribute('itemTypeID', valueText);
@ -1557,18 +1481,19 @@
return valueElement; return valueElement;
} }
async removeCreator(index, labelToDelete) { async removeCreator(index, creatorRow) {
// Move focus to another creator row
if (creatorRow.contains(document.activeElement)) {
let nextCreatorIndex = index ? index - 1 : 0;
this._selectField = `itembox-field-value-creator-${nextCreatorIndex}-lastName`;
}
// If unsaved row, just remove element // If unsaved row, just remove element
if (!this.item.hasCreatorAt(index)) { if (!this.item.hasCreatorAt(index)) {
labelToDelete.parentNode.removeChild(labelToDelete); creatorRow.remove();
// 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('.meta-row'));
this._enablePlusButton(button, creatorFields.creatorTypeID, creatorFields.fieldMode);
this._creatorCount--; this._creatorCount--;
this.querySelector(`#${this._selectField}`)?.focus();
this._updateCreatorButtonsStatus();
return; return;
} }
await this.blurOpenField(); await this.blurOpenField();
@ -1676,93 +1601,19 @@
// https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/toolkit/content/widgets/autocomplete-input.js#372 // 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 // https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/browser/components/search/content/searchbar.js#791
elem.onTextEntered = () => { elem.onTextEntered = () => {
this.handleCreatorAutoCompleteSelect(elem, true); this.handleCreatorAutoCompleteSelect(elem);
}; };
// Tab/Shift-Tab // Tab/Shift-Tab
elem.addEventListener('change', () => { elem.addEventListener('change', () => {
this.handleCreatorAutoCompleteSelect(elem, true); this.handleCreatorAutoCompleteSelect(elem);
}); });
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 = { elem.autocomplete = {
completeSelectedIndex: true, completeSelectedIndex: true,
ignoreBlurWhileSearching: false, ignoreBlurWhileSearching: false,
search: 'zotero', search: 'zotero',
searchParam: JSON.stringify(params) searchParam: JSON.stringify(params),
popup: 'PopupAutoComplete',
}; };
} }
@ -1771,7 +1622,7 @@
* Save a multiple-field selection for the creator autocomplete * Save a multiple-field selection for the creator autocomplete
* (e.g. "Shakespeare, William") * (e.g. "Shakespeare, William")
*/ */
handleCreatorAutoCompleteSelect(textbox, stayFocused) { handleCreatorAutoCompleteSelect(textbox) {
let inputField = textbox.querySelector("input"); let inputField = textbox.querySelector("input");
if (!inputField) { if (!inputField) {
return; return;
@ -1791,7 +1642,7 @@
if (!id) { if (!id) {
return; return;
} }
var [creatorID, numFields] = id.split('-'); var [creatorID, numFields] = id.split('-');
// If result uses two fields, save both // If result uses two fields, save both
@ -1803,11 +1654,6 @@
var [_field, creatorIndex, creatorField] var [_field, creatorIndex, creatorField]
= textbox.getAttribute('fieldname').split('-'); = textbox.getAttribute('fieldname').split('-');
if (stayFocused) {
this._lastTabIndex = parseInt(textbox.getAttribute('ztabindex'));
this._tabDirection = false;
}
var creator = Zotero.Creators.get(creatorID); var creator = Zotero.Creators.get(creatorID);
var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName'; var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName';
@ -1844,93 +1690,107 @@
// Otherwise let the autocomplete popup handle matters // Otherwise let the autocomplete popup handle matters
} }
handleKeyPress(event) { // Handle Shift-Enter on creator input field
var target = event.target; handleCreatorRowKeyDown(event) {
if ((event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === ' ') let target = event.target.closest("editable-text");
&& target.classList.contains('zotero-clicky')) { if (!target) return;
event.preventDefault();
target.click();
setTimeout(() => { if (event.key == "Enter" && event.shiftKey) {
this._creatorTypeMenu.dispatchEvent( event.stopPropagation();
new KeyboardEvent("keydown", { key: 'ArrowDown', keyCode: 40, charCode: 0 }) console.log("Target ", target);
); // Value has changed - focus empty creator row at the bottom
}, 0); if (target.initialValue != target.value) {
return; this._addCreatorRow = true;
this.blurOpenField();
return;
}
// Value hasn't changed
Zotero.debug("Value hasn't changed");
let row = target.closest('.meta-row');
// Next row is a creator - focus that
let nextRow = row.nextSibling;
if (nextRow.querySelector(".creator-type-value")) {
nextRow.querySelector("editable-text").focus();
return;
}
// Next row is a "More creators" label - click that
let moreCreators = nextRow.querySelector("#more-creators-label");
if (moreCreators) {
moreCreators.click();
this._selectField = `itembox-field-value-creator-${this._creatorCount}-lastName`;
}
var creatorFields = this.getCreatorFields(row);
// Do nothing from the last empty row
if (creatorFields.lastName == "" && creatorFields.firstName == "") return;
this.addCreatorRow(false, creatorFields.creatorTypeID, true);
this._selectField = `itembox-field-value-creator-${this._creatorCount - 1}-lastName`;
} }
}
let tree; // Handle adding multiple creator rows via paste
switch (event.key) { handleCreatorPaste(event) {
case "Enter": let target = event.target.closest('editable-text');
var valueField = target.closest("editable-text"); var fieldName = target.getAttribute('fieldname');
if (!valueField) { let creatorTypeID = parseInt(
return; target.closest('.meta-row').querySelector('.meta-label').getAttribute('typeid')
);
var [field, creatorIndex, creatorField] = fieldName.split('-');
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();
// 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
});
} }
var fieldname = valueField.getAttribute('fieldname');
// Use shift-enter as the save action for the larger fields // Shift existing creators
if (Zotero.ItemFields.isMultiline(fieldname) && !event.shiftKey) { for (let i = initNumCreators - 1; i >= creatorIndex; i--) {
return; let shiftedCreatorData = this.item.getCreator(i);
this.item.setCreator(nameArray.length + i, shiftedCreatorData);
} }
}
// Shift-enter adds new creator row
if (fieldname.indexOf('creator-') == 0 && event.shiftKey) { let currentIndex = creatorIndex;
// Value hasn't changed let newCreator = { creatorTypeID };
if (valueField.initialValue == valueField.value) { // Add the creators in lastNameArray one at a time
Zotero.debug("Value hasn't changed"); for (let tempName of nameArray) {
let row = target.closest('.meta-row'); // Check for tab to determine creator name format
// If + button is disabled, just focus next creator row newCreator.fieldMode = (tempName.indexOf('\t') == -1) ? 1 : 0;
if (row.querySelector(".zotero-clicky-plus").disabled) { if (newCreator.fieldMode == 0) {
let moreCreators = row.nextSibling.querySelector("#more-creators-label"); newCreator.lastName = tempName.split('\t')[0];
if (moreCreators) { newCreator.firstName = tempName.split('\t')[1];
moreCreators.click();
}
else {
row.nextSibling.querySelector("editable-text").focus();
}
}
else {
var creatorFields = this.getCreatorFields(row);
this.addCreatorRow(false, creatorFields.creatorTypeID, true);
}
}
// Value has changed
else {
this._tabDirection = 1;
this._addCreatorRow = true;
this.blurOpenField();
}
}
return;
case "Escape":
// Return focus to items pane
tree = document.getElementById('item-tree-main-default');
if (tree) {
tree.focus();
}
return;
case "Tab":
this.updateLastFocused(event);
if (event.shiftKey) {
// Shift-tab from the item type
if (this._lastTabIndex === 1) {
return;
}
event.preventDefault();
event.stopPropagation();
this._focusNextField(this._lastTabIndex, true);
} }
else { else {
let focused = this._focusNextField(++this._lastTabIndex); newCreator.lastName = tempName;
if (focused) { newCreator.firstName = '';
event.preventDefault();
event.stopPropagation();
}
} }
this.modifyCreator(currentIndex, newCreator);
currentIndex++;
}
// Select the last field added
this._selectField = `itembox-field-value-creator-${currentIndex}-lastName`;
this._addCreatorRow = (creatorsToShift == 0);
if (this.saveOnEdit) {
this.item.saveTx();
}
} }
} }
@ -1946,8 +1806,6 @@
Zotero.debug(`Hiding editor for ${textbox.getAttribute('fieldname')}`); Zotero.debug(`Hiding editor for ${textbox.getAttribute('fieldname')}`);
this._lastTabIndex = -1;
// Prevent autocomplete breakage in Firefox 3 // Prevent autocomplete breakage in Firefox 3
if (textbox.mController) { if (textbox.mController) {
textbox.mController.input = null; textbox.mController.input = null;
@ -2081,6 +1939,30 @@
} }
} }
// Make sure that irrelevant creators +/- buttons are disabled
_updateCreatorButtonsStatus() {
let creatorValues = [...this.querySelectorAll(".creator-type-value")];
let row;
for (let creatorValue of creatorValues) {
row = creatorValue.closest(".meta-row");
let { lastName, firstName } = this.getCreatorFields(row);
let isEmpty = lastName == "" && firstName == "";
let isNextRowCreator = row.nextSibling.querySelector(".creator-type-value");
let isDefaultEmptyRow = isEmpty && creatorValues.length == 1;
if (!this.editable) {
row.querySelector(".zotero-clicky-plus").hidden = true;
row.querySelector(".zotero-clicky-minus").hidden = true;
row.querySelector(".zotero-clicky-options").hidden = true;
return;
}
row.querySelector(".zotero-clicky-plus").disabled = isEmpty || isNextRowCreator;
row.querySelector(".zotero-clicky-minus").disabled = isDefaultEmptyRow;
}
}
getCreatorFields(row) { getCreatorFields(row) {
var typeID = row.querySelector('[typeid]').getAttribute('typeid'); var typeID = row.querySelector('[typeid]').getAttribute('typeid');
var [label1, label2] = row.querySelectorAll('editable-text'); var [label1, label2] = row.querySelectorAll('editable-text');
@ -2188,7 +2070,6 @@
return; return;
} }
this._draggedCreator = true; this._draggedCreator = true;
this._lastTabIndex = null;
// Due to some kind of drag-drop API issue, // Due to some kind of drag-drop API issue,
// after creator is dropped, the hover effect often stays at // after creator is dropped, the hover effect often stays at
// the row's old location. To workaround that, set noHover class to block all // the row's old location. To workaround that, set noHover class to block all
@ -2283,9 +2164,7 @@
} }
focusField(fieldName) { focusField(fieldName) {
let field = this.querySelector(`editable-text[fieldname="${fieldName}"]`); this.querySelector(`editable-text[fieldname="${fieldName}"]`)?.focus();
if (!field) return false;
return this._focusNextField(field.getAttribute('ztabindex'));
} }
getTitleField() { getTitleField() {
@ -2301,84 +2180,6 @@
return null; return null;
} }
/**
* Advance the field focus forward or backward
*
* Note: We're basically replicating the built-in tabindex functionality,
* which doesn't work well with the weird label/textbox stuff we're doing.
* (The textbox being tabbed away from is deleted before the blur()
* completes, so it doesn't know where it's supposed to go next.)
*/
_focusNextField(tabindex, back) {
var box = this._infoTable;
tabindex = parseInt(tabindex);
// Get all fields with ztabindex attributes
var tabbableFields = box.querySelectorAll('*[ztabindex]:not([disabled=true])');
if (!tabbableFields.length) {
Zotero.debug("No tabbable fields found");
return false;
}
var next;
if (back) {
Zotero.debug('Looking for previous tabindex before ' + tabindex, 4);
for (let i = tabbableFields.length - 1; i >= 0; i--) {
let field = tabbableFields[i];
let tabIndexHere = parseInt(field.getAttribute('ztabindex'));
if (tabIndexHere !== -1 && tabIndexHere < tabindex) {
next = tabbableFields[i];
break;
}
}
}
else {
Zotero.debug('Looking for tabindex ' + tabindex, 4);
for (var pos = 0; pos < tabbableFields.length; pos++) {
let field = tabbableFields[pos];
let tabIndexHere = parseInt(field.getAttribute('ztabindex'));
if (tabIndexHere !== -1 && tabIndexHere >= tabindex) {
next = tabbableFields[pos];
break;
}
}
}
if (!next) {
Zotero.debug("Next field not found");
return false;
}
// Ensure the node is visible
next.style.visibility = "visible";
next.style.display = "block";
next.focus();
next.style.removeProperty("visibility");
next.style.removeProperty("display");
// 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
let visElem;
if (!back && tabbableFields[pos + 1]) {
Zotero.debug("Scrolling to next field");
visElem = tabbableFields[pos + 1];
}
else {
visElem = next;
}
this.ensureElementIsVisible(visElem);
return true;
}
updateLastFocused(ev) {
let ztabindex = parseInt(ev.target.getAttribute('ztabindex'));
if (ztabindex) {
this._lastTabIndex = ztabindex;
}
}
async blurOpenField() { async blurOpenField() {
var activeField = this.getFocusedTextArea(); var activeField = this.getFocusedTextArea();
if (!activeField) { if (!activeField) {

View file

@ -199,7 +199,7 @@
this._header = this.querySelector('#zotero-item-pane-header'); this._header = this.querySelector('#zotero-item-pane-header');
this._paneParent = this.querySelector('#zotero-view-item'); this._paneParent = this.querySelector('#zotero-view-item');
this._container.addEventListener("keypress", this._handleKeypress); this._container.addEventListener("keydown", this._handleKeydown);
this._paneParent.addEventListener('scroll', this._handleContainerScroll); this._paneParent.addEventListener('scroll', this._handleContainerScroll);
this._paneHiddenOb = new MutationObserver(this._handlePaneStatus); this._paneHiddenOb = new MutationObserver(this._handlePaneStatus);
@ -225,7 +225,7 @@
} }
destroy() { destroy() {
this._container.removeEventListener("keypress", this._handleKeypress); this._container.removeEventListener("keydown", this._handleKeydown);
this._paneParent.removeEventListener('scroll', this._handleContainerScroll); this._paneParent.removeEventListener('scroll', this._handleContainerScroll);
this._paneHiddenOb.disconnect(); this._paneHiddenOb.disconnect();
@ -535,7 +535,7 @@
}; };
// Keyboard navigation within the itemPane. Also handles contextPane keyboard nav // Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
_handleKeypress = (event) => { _handleKeydown = (event) => {
let stopEvent = () => { let stopEvent = () => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -552,21 +552,16 @@
stopEvent(); stopEvent();
return; return;
} }
// Tab tavigation between entries and buttons within library, related and notes boxes // On Escape/Enter on editable-text, return focus to the item tree or reader
if (event.key == "Tab" && event.target.closest(".box")) { if (event.key == "Escape" || (event.key == "Enter" && event.target.classList.contains('input'))) {
let next = null; if (isLibraryTab) {
if (event.key == "Tab" && !event.shiftKey) { document.getElementById('item-tree-main-default').focus();
next = event.target.nextElementSibling;
} }
if (event.key == "Tab" && event.shiftKey) { else {
next = event.target.parentNode.previousElementSibling?.lastChild; let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
} if (reader) {
// Force the element to be visible before focusing reader.focus();
if (next) { }
next.style.visibility = "visible";
next.focus();
next.style.removeProperty("visibility");
stopEvent();
} }
} }
}; };

View file

@ -327,6 +327,11 @@
if (!row.parentElement) { if (!row.parentElement) {
return; return;
} }
// Do not propagate event to itemDetails that would send focus to itemTree or reader
// because a new empty row will be created and focused in saveTag
if (row.getAttribute("isNew")) {
event.stopPropagation();
}
let blurOnly = false; let blurOnly = false;
let focusField = false; let focusField = false;
@ -348,13 +353,6 @@
if (focusField) { if (focusField) {
focusField.focus(); focusField.focus();
} }
// Return focus to items pane
else {
var tree = document.getElementById('zotero-items-tree');
if (tree) {
tree.focus();
}
}
} }
}; };

View file

@ -768,31 +768,7 @@ var Zotero_Tabs = new function () {
Services.focus.MOVEFOCUS_BACKWARD, 0); Services.focus.MOVEFOCUS_BACKWARD, 0);
return; return;
} }
// If no item is selected, focus items list. // Focus the last field of itemPane
if (ZoteroPane.itemPane.mode == "message") {
document.getElementById("item-tree-main-default").focus();
return;
}
let selected = ZoteroPane.getSelectedItems();
// If the selected collection row is duplicates, just focus on the
// itemTree until the merge pane is keyboard accessible
// If multiple items selected, focus on itemTree as well.
let collectionRow = ZoteroPane.collectionsView.selectedTreeRow;
if (collectionRow.isDuplicates() || selected.length !== 1) {
document.getElementById("item-tree-main-default").focus();
return;
}
// Special treatment for notes and attachments in itemPane
selected = selected[0];
if (selected.isNote()) {
document.getElementById("zotero-note-editor").focus();
return;
}
if (selected.isAttachment()) {
document.getElementById("attachment-note-editor").focus();
return;
}
// For regular items, focus the last field
// We do that by moving focus backwards from the element following the pane, because Services.focus doesn't // We do that by moving focus backwards from the element following the pane, because Services.focus doesn't
// support MOVEFOCUS_LAST on subtrees // support MOVEFOCUS_LAST on subtrees
Services.focus.moveFocus(window, document.getElementById("zotero-context-splitter"), Services.focus.moveFocus(window, document.getElementById("zotero-context-splitter"),

View file

@ -2147,7 +2147,7 @@ var ZoteroPane = new function()
await original.saveTx({ skipDateModifiedUpdate: true }); await original.saveTx({ skipDateModifiedUpdate: true });
await duplicate.saveTx(); await duplicate.saveTx();
document.getElementById('zotero-editpane-item-box').focusField('title'); ZoteroPane.itemPane.querySelector("item-box").getTitleField().focus();
return duplicate; return duplicate;
}; };

View file

@ -123,13 +123,6 @@
row-gap: 2px; row-gap: 2px;
width: inherit; width: inherit;
.show-on-hover {
visibility: hidden;
&.no-display {
display: none;
}
}
.meta-row { .meta-row {
display: grid; display: grid;
grid-template-columns: subgrid; grid-template-columns: subgrid;
@ -139,14 +132,15 @@
display: none; display: none;
} }
// On hover of the meta-row, reveal all hidden icons &:not(:hover):not(:focus-within) .show-on-hover,
// unless there's .noHover class which keeps everything hidden &.noHover .show-on-hover {
&:not(.noHover):hover .show-on-hover, clip-path: inset(50%);
&:focus-within .show-on-hover { &.no-display {
visibility: visible; width: 0;
display: revert; height: 0;
padding: 0;
}
} }
.meta-data { .meta-data {
width: 0; width: 0;
min-width: 100%; min-width: 100%;