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();
|
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)) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
Loading…
Reference in a new issue