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:
parent
1394381257
commit
c6799bc3c2
8 changed files with 251 additions and 481 deletions
|
@ -334,27 +334,6 @@
|
|||
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") {
|
||||
// No actions on right/left on header buttons
|
||||
if (["ArrowRight", "ArrowLeft"].includes(event.key)) {
|
||||
|
|
|
@ -193,6 +193,14 @@
|
|||
input.addEventListener('mousedown', this._handleMouseDown);
|
||||
input.addEventListener('dragover', this._handleDragOver);
|
||||
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 selectionStart = this._input?.selectionStart;
|
||||
|
@ -333,12 +341,15 @@
|
|||
if (event.key === 'Enter') {
|
||||
if (this.multiline === event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('escape_enter'));
|
||||
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') {
|
||||
this.dispatchEvent(new CustomEvent('escape_enter'));
|
||||
let initialValue = this._input.dataset.initialValue ?? '';
|
||||
this.setAttribute('value', initialValue);
|
||||
this._input.value = initialValue;
|
||||
|
@ -346,6 +357,22 @@
|
|||
}
|
||||
};
|
||||
|
||||
_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) => {
|
||||
this.setAttribute("mousedown", true);
|
||||
// Prevent a right-click from focusing the input when unfocused
|
||||
|
|
|
@ -47,11 +47,8 @@
|
|||
this._editableFields = [];
|
||||
this._fieldAlternatives = {};
|
||||
this._fieldOrder = [];
|
||||
this._tabIndexMinCreators = 100;
|
||||
this._tabIndexMaxFields = 0;
|
||||
this._initialVisibleCreators = 5;
|
||||
this._draggedCreator = false;
|
||||
this._ztabindex = 0;
|
||||
this._selectField = null;
|
||||
}
|
||||
|
||||
|
@ -116,7 +113,6 @@
|
|||
);
|
||||
typeBox.setAttribute('typeid', typeID);
|
||||
|
||||
this._lastTabIndex = -1;
|
||||
this.modifyCreator(index, fields);
|
||||
if (this.saveOnEdit) {
|
||||
await this.blurOpenField();
|
||||
|
@ -226,6 +222,23 @@
|
|||
() => 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');
|
||||
Zotero.Prefs.registerObserver('fontSize', () => {
|
||||
this._forceRenderAll();
|
||||
|
@ -327,7 +340,6 @@
|
|||
}
|
||||
|
||||
this._item = val;
|
||||
this._lastTabIndex = null;
|
||||
this.scrollToTop();
|
||||
}
|
||||
|
||||
|
@ -457,14 +469,6 @@
|
|||
if (id != this.item.id) {
|
||||
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();
|
||||
break;
|
||||
}
|
||||
|
@ -484,8 +488,6 @@
|
|||
|
||||
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;
|
||||
|
||||
//
|
||||
|
@ -498,13 +500,6 @@
|
|||
// Item type menu
|
||||
this.addItemTypeMenu();
|
||||
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 = [];
|
||||
|
||||
// Manual field order
|
||||
|
@ -653,7 +648,6 @@
|
|||
if (!(Zotero.ItemFields.isLong(fieldName) || Zotero.ItemFields.isMultiline(fieldName))) {
|
||||
optionsButton.classList.add("no-display");
|
||||
}
|
||||
optionsButton.setAttribute("ztabindex", ++this._ztabindex);
|
||||
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
|
||||
// eslint-disable-next-line no-loop-func
|
||||
let triggerPopup = (e) => {
|
||||
|
@ -672,20 +666,14 @@
|
|||
};
|
||||
// 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) {
|
||||
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') {
|
||||
if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
|
||||
var button = document.createXULElement("toolbarbutton");
|
||||
button.className = 'zotero-field-version-button zotero-clicky-merge';
|
||||
button.setAttribute('type', 'menu');
|
||||
|
@ -715,13 +703,11 @@
|
|||
rowData.appendChild(button);
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
|
@ -778,11 +764,6 @@
|
|||
for (let i = 0; i < max; i++) {
|
||||
let data = this.item.getCreator(i);
|
||||
this.addCreatorRow(data, data.creatorTypeID);
|
||||
|
||||
// Display "+" button on all but last row
|
||||
if (i == max - 2) {
|
||||
this.disableCreatorAddButtons();
|
||||
}
|
||||
}
|
||||
if (this._draggedCreator) {
|
||||
this._draggedCreator = false;
|
||||
|
@ -802,8 +783,6 @@
|
|||
// Additional creators not displayed
|
||||
if (num > max) {
|
||||
this.addMoreCreatorsRow(num - max);
|
||||
|
||||
this.disableCreatorAddButtons();
|
||||
}
|
||||
else {
|
||||
// If we didn't start with creators truncated,
|
||||
|
@ -815,20 +794,14 @@
|
|||
if (this._addCreatorRow) {
|
||||
this.addCreatorRow(false, this.item.getCreator(max - 1).creatorTypeID, true);
|
||||
this._addCreatorRow = false;
|
||||
this.disableCreatorAddButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.editable && Zotero.CreatorTypes.itemTypeHasCreators(this.item.itemTypeID)) {
|
||||
// Add default row
|
||||
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) {
|
||||
let creatorTypeLabels = this.querySelectorAll(".creator-type-label");
|
||||
|
@ -866,11 +839,16 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
this._refreshed = true;
|
||||
// Add tabindex=0 to all focusable element
|
||||
this.querySelectorAll("[ztabindex]").forEach((node) => {
|
||||
node.setAttribute("tabindex", 0);
|
||||
});
|
||||
|
||||
this._ensureButtonsFocusable();
|
||||
|
||||
// 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
|
||||
this.querySelectorAll("menupopup").forEach((popup) => {
|
||||
popup.hidePopup();
|
||||
|
@ -896,24 +874,27 @@
|
|||
else {
|
||||
var menulist = document.createXULElement("menulist", { is: "menulist-item-types" });
|
||||
menulist.id = "item-type-menu";
|
||||
menulist.className = "zotero-clicky";
|
||||
menulist.className = "zotero-clicky keyboard-clickable";
|
||||
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);
|
||||
// This is instead of setting disabled=true so that the menu is not excluded
|
||||
// from tab navigation. For <input>s, we just set readonly=true but it is not
|
||||
// a valid property for menulist.
|
||||
menulist.addEventListener("popupshowing", (e) => {
|
||||
if (!this._editable) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
menulist.setAttribute("aria-labelledby", "itembox-field-itemType-label");
|
||||
this.itemTypeMenu = menulist;
|
||||
rowData.appendChild(menulist);
|
||||
}
|
||||
this.itemTypeMenu.setAttribute('ztabindex', '1');
|
||||
this.itemTypeMenu.disabled = !this.showTypeMenu;
|
||||
this.itemTypeMenu.setAttribute("aria-disabled", !this._editable);
|
||||
row.appendChild(labelWrapper);
|
||||
row.appendChild(rowData);
|
||||
this._infoTable.appendChild(row);
|
||||
|
@ -967,8 +948,10 @@
|
|||
let labelWrapper = document.createElement('div');
|
||||
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.setAttribute('tabindex', -1);
|
||||
rowLabel.appendChild(grippy);
|
||||
|
||||
if (this.editable) {
|
||||
|
@ -989,7 +972,7 @@
|
|||
|
||||
labelWrapper.setAttribute('role', 'button');
|
||||
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 (!this.editable || this.item.numCreators() < 2) {
|
||||
|
@ -1016,7 +999,6 @@
|
|||
this.createValueElement(
|
||||
lastName,
|
||||
fieldName,
|
||||
++this._ztabindex
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -1026,7 +1008,6 @@
|
|||
this.createValueElement(
|
||||
firstName,
|
||||
fieldName,
|
||||
++this._ztabindex
|
||||
)
|
||||
);
|
||||
firstNameElem.placeholder = this._defaultFirstName;
|
||||
|
@ -1039,41 +1020,27 @@
|
|||
// Minus (-) button
|
||||
var removeButton = document.createXULElement('toolbarbutton');
|
||||
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'));
|
||||
// If default first row, don't let user remove it
|
||||
if (defaultRow || !this.editable) {
|
||||
this.disableButton(removeButton);
|
||||
}
|
||||
else {
|
||||
removeButton.addEventListener("click", () => {
|
||||
this.removeCreator(rowIndex, rowData.parentNode);
|
||||
});
|
||||
}
|
||||
removeButton.addEventListener("command", () => this.removeCreator(rowIndex, rowData.parentNode));
|
||||
rowData.appendChild(removeButton);
|
||||
|
||||
// Plus (+) button
|
||||
var addButton = document.createXULElement('toolbarbutton');
|
||||
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'));
|
||||
// If row isn't saved, don't let user add more
|
||||
if (unsaved || !this.editable) {
|
||||
this.disableButton(addButton);
|
||||
}
|
||||
else {
|
||||
this._enablePlusButton(addButton, typeID, fieldMode);
|
||||
}
|
||||
addButton.addEventListener("command", () => this.addCreatorRow(null, typeID, true));
|
||||
rowData.appendChild(addButton);
|
||||
|
||||
// Options button that opens creator transform menu
|
||||
let optionsButton = document.createXULElement("toolbarbutton");
|
||||
if (!this.editable) {
|
||||
optionsButton.style.visibility = "hidden";
|
||||
this.disableButton(optionsButton);
|
||||
optionsButton.disabled = true;
|
||||
}
|
||||
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");
|
||||
let triggerPopup = (e) => {
|
||||
document.popupNode = firstlast;
|
||||
|
@ -1086,28 +1053,18 @@
|
|||
rowData.appendChild(optionsButton);
|
||||
|
||||
if (this.editable) {
|
||||
optionsButton.addEventListener("click", triggerPopup);
|
||||
optionsButton.addEventListener("command", 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++;
|
||||
|
||||
if (!this.editable) {
|
||||
removeButton.hidden = true;
|
||||
addButton.hidden = true;
|
||||
optionsButton.hidden = true;
|
||||
}
|
||||
|
||||
let row = this.addDynamicRow(rowLabel, rowData, true);
|
||||
|
||||
this._updateCreatorButtonsStatus();
|
||||
this._ensureButtonsFocusable();
|
||||
|
||||
/**
|
||||
* Events handling creator drag-drop reordering
|
||||
*/
|
||||
|
@ -1173,6 +1130,8 @@
|
|||
this.addAutocompleteToElement(lastNameElem);
|
||||
this.addAutocompleteToElement(firstNameElem);
|
||||
|
||||
row.addEventListener("keydown", e => this.handleCreatorRowKeyDown(e));
|
||||
lastNameElem.addEventListener("paste", e => this.handleCreatorPaste(e));
|
||||
// Focus new rows
|
||||
if (unsaved && !defaultRow) {
|
||||
lastNameElem.focus();
|
||||
|
@ -1186,7 +1145,7 @@
|
|||
var rowData = document.createElement('div');
|
||||
rowData.className = "meta-data";
|
||||
rowData.id = 'more-creators-label';
|
||||
rowData.setAttribute("ztabindex", ++this._ztabindex);
|
||||
rowData.setAttribute("tabindex", 0);
|
||||
rowData.addEventListener('click', () => {
|
||||
this._displayAllCreators = true;
|
||||
this._forceRenderAll();
|
||||
|
@ -1248,10 +1207,6 @@
|
|||
delete lastName.style.width;
|
||||
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
|
||||
firstName.hidden = true;
|
||||
|
||||
|
@ -1264,13 +1219,6 @@
|
|||
}
|
||||
// Clear first name value after it was moved to the full name field
|
||||
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
|
||||
else {
|
||||
|
@ -1278,9 +1226,6 @@
|
|||
lastName.setAttribute('fieldMode', '0');
|
||||
|
||||
lastName.placeholder = this._defaultLastName;
|
||||
// Add firstname field to tabindex
|
||||
tab = parseInt(lastName.getAttribute('ztabindex'));
|
||||
firstName.setAttribute('ztabindex', tab + 1);
|
||||
|
||||
if (!initial) {
|
||||
// Move all but last word to first name field and show it
|
||||
|
@ -1426,26 +1371,15 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
disableButton(button) {
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('onclick', false);
|
||||
// Toolbarbuttons required tabindex=0 to be properly focusable via tab
|
||||
_ensureButtonsFocusable() {
|
||||
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) {
|
||||
// In duplicates/trash mode return nothing
|
||||
|
@ -1455,8 +1389,6 @@
|
|||
let openLink = document.createXULElement("toolbarbutton");
|
||||
openLink.className = "zotero-clicky zotero-clicky-open-link show-on-hover no-display";
|
||||
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");
|
||||
return openLink;
|
||||
}
|
||||
|
@ -1485,23 +1417,15 @@
|
|||
if (this._fieldIsClickable(fieldName)) {
|
||||
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);
|
||||
|
@ -1557,18 +1481,19 @@
|
|||
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 (!this.item.hasCreatorAt(index)) {
|
||||
labelToDelete.parentNode.removeChild(labelToDelete);
|
||||
|
||||
// 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);
|
||||
creatorRow.remove();
|
||||
|
||||
this._creatorCount--;
|
||||
this.querySelector(`#${this._selectField}`)?.focus();
|
||||
this._updateCreatorButtonsStatus();
|
||||
return;
|
||||
}
|
||||
await this.blurOpenField();
|
||||
|
@ -1676,15 +1601,140 @@
|
|||
// 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);
|
||||
this.handleCreatorAutoCompleteSelect(elem);
|
||||
};
|
||||
// Tab/Shift-Tab
|
||||
elem.addEventListener('change', () => {
|
||||
this.handleCreatorAutoCompleteSelect(elem, true);
|
||||
this.handleCreatorAutoCompleteSelect(elem);
|
||||
});
|
||||
}
|
||||
elem.autocomplete = {
|
||||
completeSelectedIndex: true,
|
||||
ignoreBlurWhileSearching: false,
|
||||
search: 'zotero',
|
||||
searchParam: JSON.stringify(params),
|
||||
popup: 'PopupAutoComplete',
|
||||
};
|
||||
}
|
||||
|
||||
if (creatorField == 'lastName') {
|
||||
elem.addEventListener('paste', (event) => {
|
||||
|
||||
/**
|
||||
* Save a multiple-field selection for the creator autocomplete
|
||||
* (e.g. "Shakespeare, William")
|
||||
*/
|
||||
handleCreatorAutoCompleteSelect(textbox) {
|
||||
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++) {
|
||||
if (controller.getCommentAt(i) == textbox.value) {
|
||||
id = controller.getLabelAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No result selected
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var [creatorID, numFields] = id.split('-');
|
||||
|
||||
// If result uses two fields, save both
|
||||
if (numFields == 2) {
|
||||
// Manually clear autocomplete controller's reference to
|
||||
// textbox to prevent error next time around
|
||||
inputField.mController.input = null;
|
||||
|
||||
var [_field, creatorIndex, creatorField]
|
||||
= textbox.getAttribute('fieldname').split('-');
|
||||
|
||||
var creator = Zotero.Creators.get(creatorID);
|
||||
|
||||
var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName';
|
||||
|
||||
// Update this textbox
|
||||
textbox.value = creator[creatorField];
|
||||
|
||||
// Update the other label
|
||||
let label;
|
||||
if (otherField == 'firstName') {
|
||||
label = textbox.nextSibling;
|
||||
}
|
||||
else if (otherField == 'lastName') {
|
||||
label = textbox.previousSibling;
|
||||
}
|
||||
|
||||
label.value = creator[otherField];
|
||||
|
||||
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;
|
||||
this.item.saveTx().then(() => {
|
||||
this.ignoreBlur = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise let the autocomplete popup handle matters
|
||||
}
|
||||
|
||||
// Handle Shift-Enter on creator input field
|
||||
handleCreatorRowKeyDown(event) {
|
||||
let target = event.target.closest("editable-text");
|
||||
if (!target) return;
|
||||
|
||||
if (event.key == "Enter" && event.shiftKey) {
|
||||
event.stopPropagation();
|
||||
console.log("Target ", target);
|
||||
// Value has changed - focus empty creator row at the bottom
|
||||
if (target.initialValue != target.value) {
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle adding multiple creator rows via paste
|
||||
handleCreatorPaste(event) {
|
||||
let target = event.target.closest('editable-text');
|
||||
var fieldName = target.getAttribute('fieldname');
|
||||
let creatorTypeID = parseInt(
|
||||
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/);
|
||||
|
@ -1692,14 +1742,6 @@
|
|||
// 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);
|
||||
|
||||
|
@ -1742,196 +1784,14 @@
|
|||
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++;
|
||||
}
|
||||
}
|
||||
// Select the last field added
|
||||
this._selectField = `itembox-field-value-creator-${currentIndex}-lastName`;
|
||||
this._addCreatorRow = (creatorsToShift == 0);
|
||||
|
||||
if (this.saveOnEdit) {
|
||||
this.item.saveTx();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
elem.autocomplete = {
|
||||
completeSelectedIndex: true,
|
||||
ignoreBlurWhileSearching: false,
|
||||
search: 'zotero',
|
||||
searchParam: JSON.stringify(params)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a multiple-field selection for the creator autocomplete
|
||||
* (e.g. "Shakespeare, William")
|
||||
*/
|
||||
handleCreatorAutoCompleteSelect(textbox, stayFocused) {
|
||||
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++) {
|
||||
if (controller.getCommentAt(i) == textbox.value) {
|
||||
id = controller.getLabelAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No result selected
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var [creatorID, numFields] = id.split('-');
|
||||
|
||||
// If result uses two fields, save both
|
||||
if (numFields == 2) {
|
||||
// Manually clear autocomplete controller's reference to
|
||||
// textbox to prevent error next time around
|
||||
inputField.mController.input = null;
|
||||
|
||||
var [_field, creatorIndex, creatorField]
|
||||
= textbox.getAttribute('fieldname').split('-');
|
||||
|
||||
if (stayFocused) {
|
||||
this._lastTabIndex = parseInt(textbox.getAttribute('ztabindex'));
|
||||
this._tabDirection = false;
|
||||
}
|
||||
|
||||
var creator = Zotero.Creators.get(creatorID);
|
||||
|
||||
var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName';
|
||||
|
||||
// Update this textbox
|
||||
textbox.value = creator[creatorField];
|
||||
|
||||
// Update the other label
|
||||
let label;
|
||||
if (otherField == 'firstName') {
|
||||
label = textbox.nextSibling;
|
||||
}
|
||||
else if (otherField == 'lastName') {
|
||||
label = textbox.previousSibling;
|
||||
}
|
||||
|
||||
label.value = creator[otherField];
|
||||
|
||||
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;
|
||||
this.item.saveTx().then(() => {
|
||||
this.ignoreBlur = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise let the autocomplete popup handle matters
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
var target = event.target;
|
||||
if ((event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === ' ')
|
||||
&& target.classList.contains('zotero-clicky')) {
|
||||
event.preventDefault();
|
||||
target.click();
|
||||
|
||||
setTimeout(() => {
|
||||
this._creatorTypeMenu.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: 'ArrowDown', keyCode: 40, charCode: 0 })
|
||||
);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
let tree;
|
||||
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;
|
||||
}
|
||||
|
||||
// Shift-enter adds new creator row
|
||||
if (fieldname.indexOf('creator-') == 0 && event.shiftKey) {
|
||||
// Value hasn't changed
|
||||
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 (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(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 {
|
||||
let focused = this._focusNextField(++this._lastTabIndex);
|
||||
if (focused) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hideEditor(textbox) {
|
||||
|
@ -1946,8 +1806,6 @@
|
|||
|
||||
Zotero.debug(`Hiding editor for ${textbox.getAttribute('fieldname')}`);
|
||||
|
||||
this._lastTabIndex = -1;
|
||||
|
||||
// Prevent autocomplete breakage in Firefox 3
|
||||
if (textbox.mController) {
|
||||
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) {
|
||||
var typeID = row.querySelector('[typeid]').getAttribute('typeid');
|
||||
var [label1, label2] = row.querySelectorAll('editable-text');
|
||||
|
@ -2188,7 +2070,6 @@
|
|||
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
|
||||
|
@ -2283,9 +2164,7 @@
|
|||
}
|
||||
|
||||
focusField(fieldName) {
|
||||
let field = this.querySelector(`editable-text[fieldname="${fieldName}"]`);
|
||||
if (!field) return false;
|
||||
return this._focusNextField(field.getAttribute('ztabindex'));
|
||||
this.querySelector(`editable-text[fieldname="${fieldName}"]`)?.focus();
|
||||
}
|
||||
|
||||
getTitleField() {
|
||||
|
@ -2301,84 +2180,6 @@
|
|||
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() {
|
||||
var activeField = this.getFocusedTextArea();
|
||||
if (!activeField) {
|
||||
|
|
|
@ -199,7 +199,7 @@
|
|||
this._header = this.querySelector('#zotero-item-pane-header');
|
||||
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._paneHiddenOb = new MutationObserver(this._handlePaneStatus);
|
||||
|
@ -225,7 +225,7 @@
|
|||
}
|
||||
|
||||
destroy() {
|
||||
this._container.removeEventListener("keypress", this._handleKeypress);
|
||||
this._container.removeEventListener("keydown", this._handleKeydown);
|
||||
this._paneParent.removeEventListener('scroll', this._handleContainerScroll);
|
||||
|
||||
this._paneHiddenOb.disconnect();
|
||||
|
@ -535,7 +535,7 @@
|
|||
};
|
||||
|
||||
// Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
|
||||
_handleKeypress = (event) => {
|
||||
_handleKeydown = (event) => {
|
||||
let stopEvent = () => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -552,21 +552,16 @@
|
|||
stopEvent();
|
||||
return;
|
||||
}
|
||||
// Tab tavigation between entries and buttons within library, related and notes boxes
|
||||
if (event.key == "Tab" && event.target.closest(".box")) {
|
||||
let next = null;
|
||||
if (event.key == "Tab" && !event.shiftKey) {
|
||||
next = event.target.nextElementSibling;
|
||||
// On Escape/Enter on editable-text, return focus to the item tree or reader
|
||||
if (event.key == "Escape" || (event.key == "Enter" && event.target.classList.contains('input'))) {
|
||||
if (isLibraryTab) {
|
||||
document.getElementById('item-tree-main-default').focus();
|
||||
}
|
||||
if (event.key == "Tab" && event.shiftKey) {
|
||||
next = event.target.parentNode.previousElementSibling?.lastChild;
|
||||
else {
|
||||
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||
if (reader) {
|
||||
reader.focus();
|
||||
}
|
||||
// Force the element to be visible before focusing
|
||||
if (next) {
|
||||
next.style.visibility = "visible";
|
||||
next.focus();
|
||||
next.style.removeProperty("visibility");
|
||||
stopEvent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -327,6 +327,11 @@
|
|||
if (!row.parentElement) {
|
||||
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 focusField = false;
|
||||
|
||||
|
@ -348,13 +353,6 @@
|
|||
if (focusField) {
|
||||
focusField.focus();
|
||||
}
|
||||
// Return focus to items pane
|
||||
else {
|
||||
var tree = document.getElementById('zotero-items-tree');
|
||||
if (tree) {
|
||||
tree.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -768,31 +768,7 @@ var Zotero_Tabs = new function () {
|
|||
Services.focus.MOVEFOCUS_BACKWARD, 0);
|
||||
return;
|
||||
}
|
||||
// If no item is selected, focus items list.
|
||||
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
|
||||
// Focus the last field of itemPane
|
||||
// We do that by moving focus backwards from the element following the pane, because Services.focus doesn't
|
||||
// support MOVEFOCUS_LAST on subtrees
|
||||
Services.focus.moveFocus(window, document.getElementById("zotero-context-splitter"),
|
||||
|
|
|
@ -2147,7 +2147,7 @@ var ZoteroPane = new function()
|
|||
await original.saveTx({ skipDateModifiedUpdate: true });
|
||||
await duplicate.saveTx();
|
||||
|
||||
document.getElementById('zotero-editpane-item-box').focusField('title');
|
||||
ZoteroPane.itemPane.querySelector("item-box").getTitleField().focus();
|
||||
return duplicate;
|
||||
};
|
||||
|
||||
|
|
|
@ -123,13 +123,6 @@
|
|||
row-gap: 2px;
|
||||
width: inherit;
|
||||
|
||||
.show-on-hover {
|
||||
visibility: hidden;
|
||||
&.no-display {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
|
@ -139,14 +132,15 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
// On hover of the meta-row, reveal all hidden icons
|
||||
// unless there's .noHover class which keeps everything hidden
|
||||
&:not(.noHover):hover .show-on-hover,
|
||||
&:focus-within .show-on-hover {
|
||||
visibility: visible;
|
||||
display: revert;
|
||||
&:not(:hover):not(:focus-within) .show-on-hover,
|
||||
&.noHover .show-on-hover {
|
||||
clip-path: inset(50%);
|
||||
&.no-display {
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-data {
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
|
|
Loading…
Reference in a new issue