diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index d1cecc3bf..2024f9189 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -21,8 +21,8 @@ /** @typedef {import("../../web/interfaces").IL10n} IL10n */ import { AnnotationEditorType, shadow } from "../../shared/util.js"; -import { bindEvents, KeyboardManager } from "./tools.js"; import { binarySearchFirstItem } from "../display_utils.js"; +import { bindEvents } from "./tools.js"; import { FreeTextEditor } from "./freetext.js"; import { InkEditor } from "./ink.js"; @@ -61,33 +61,6 @@ class AnnotationEditorLayer { static _initialized = false; - static _keyboardManager = new KeyboardManager([ - [["ctrl+a", "mac+meta+a"], AnnotationEditorLayer.prototype.selectAll], - [["ctrl+c", "mac+meta+c"], AnnotationEditorLayer.prototype.copy], - [["ctrl+v", "mac+meta+v"], AnnotationEditorLayer.prototype.paste], - [["ctrl+x", "mac+meta+x"], AnnotationEditorLayer.prototype.cut], - [["ctrl+z", "mac+meta+z"], AnnotationEditorLayer.prototype.undo], - [ - ["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"], - AnnotationEditorLayer.prototype.redo, - ], - [ - [ - "Backspace", - "alt+Backspace", - "ctrl+Backspace", - "shift+Backspace", - "mac+Backspace", - "mac+alt+Backspace", - "mac+ctrl+Backspace", - "Delete", - "ctrl+Delete", - "shift+Delete", - ], - AnnotationEditorLayer.prototype.delete, - ], - ]); - /** * @param {AnnotationEditorLayerOptions} options */ @@ -205,62 +178,6 @@ class AnnotationEditorLayer { this.#uiManager.addCommands(params); } - /** - * Undo the last command. - */ - undo() { - this.#uiManager.undo(); - } - - /** - * Redo the last command. - */ - redo() { - this.#uiManager.redo(); - } - - /** - * Suppress the selected editor or all editors. - */ - delete() { - this.#uiManager.delete(); - } - - /** - * Copy the selected editor. - */ - copy() { - this.#uiManager.copy(); - } - - /** - * Cut the selected editor. - */ - cut() { - this.#uiManager.cut(); - } - - /** - * Paste a previously copied editor. - */ - paste() { - this.#uiManager.paste(); - } - - /** - * Select all the editors. - */ - selectAll() { - this.#uiManager.selectAll(); - } - - /** - * Unselect all the editors. - */ - unselectAll() { - this.#uiManager.unselectAll(); - } - /** * Enable pointer events on the main div in order to enable * editor creation. @@ -299,7 +216,7 @@ class AnnotationEditorLayer { } if (editor) { - this.unselectAll(); + this.#uiManager.unselectAll(); } } @@ -691,16 +608,6 @@ class AnnotationEditorLayer { event.preventDefault(); } - /** - * Keydown callback. - * @param {KeyboardEvent} event - */ - keydown(event) { - if (!this.#uiManager.getActive()?.shouldGetKeyboardEvents()) { - AnnotationEditorLayer._keyboardManager.exec(this, event); - } - } - /** * Destroy the main editor. */ @@ -741,7 +648,7 @@ class AnnotationEditorLayer { */ render(parameters) { this.viewport = parameters.viewport; - bindEvents(this, this.div, ["dragover", "drop", "keydown"]); + bindEvents(this, this.div, ["dragover", "drop"]); this.setDimensions(); for (const editor of this.#uiManager.getEditors(this.pageIndex)) { this.add(editor); diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index bdea9c3e9..8fe3761f0 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -23,13 +23,15 @@ import { LINE_FACTOR, Util, } from "../../shared/util.js"; +import { bindEvents, KeyboardManager } from "./tools.js"; import { AnnotationEditor } from "./editor.js"; -import { bindEvents } from "./tools.js"; /** * Basic text editor in order to create a FreeTex annotation. */ class FreeTextEditor extends AnnotationEditor { + #boundEditorDivKeydown = this.editorDivKeydown.bind(this); + #color; #content = ""; @@ -50,6 +52,13 @@ class FreeTextEditor extends AnnotationEditor { static _defaultFontSize = 10; + static _keyboardManager = new KeyboardManager([ + [ + ["ctrl+Enter", "mac+meta+Enter", "Escape", "mac+Escape"], + FreeTextEditor.prototype.commitOrRemove, + ], + ]); + constructor(params) { super({ ...params, name: "freeTextEditor" }); this.#color = @@ -210,6 +219,7 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.contentEditable = true; this.div.draggable = false; this.div.removeAttribute("tabIndex"); + this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown); } /** @inheritdoc */ @@ -220,6 +230,7 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.contentEditable = false; this.div.draggable = true; this.div.tabIndex = 0; + this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown); } /** @inheritdoc */ @@ -311,13 +322,17 @@ class FreeTextEditor extends AnnotationEditor { * onkeydown callback. * @param {MouseEvent} event */ - keyup(event) { - if (event.key === "Enter") { + keydown(event) { + if (event.target === this.div && event.key === "Enter") { this.enableEditMode(); this.editorDiv.focus(); } } + editorDivKeydown(event) { + FreeTextEditor._keyboardManager.exec(this, event); + } + /** @inheritdoc */ disableEditing() { this.editorDiv.setAttribute("role", "comment"); @@ -376,7 +391,7 @@ class FreeTextEditor extends AnnotationEditor { // TODO: implement paste callback. // The goal is to sanitize and have something suitable for this // editor. - bindEvents(this, this.div, ["dblclick", "keyup"]); + bindEvents(this, this.div, ["dblclick", "keydown"]); if (this.width) { // This editor was created in using copy (ctrl+c). diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index f76e147bb..212fae6ff 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -261,12 +261,12 @@ class KeyboardManager { /** * Execute a callback, if any, for a given keyboard event. - * The page is used as `this` in the callback. - * @param {AnnotationEditorLayer} page. + * The self is used as `this` in the callback. + * @param {Object} self. * @param {KeyboardEvent} event * @returns */ - exec(page, event) { + exec(self, event) { if (!this.allKeys.has(event.key)) { return; } @@ -274,7 +274,7 @@ class KeyboardManager { if (!callback) { return; } - callback.bind(page)(); + callback.bind(self)(); event.preventDefault(); } } @@ -422,6 +422,8 @@ class AnnotationEditorUIManager { #previousActiveEditor = null; + #boundKeydown = this.keydown.bind(this); + #boundOnEditingAction = this.onEditingAction.bind(this); #boundOnPageChanging = this.onPageChanging.bind(this); @@ -437,7 +439,37 @@ class AnnotationEditorUIManager { hasSelectedEditor: false, }; - constructor(eventBus) { + #container = null; + + static _keyboardManager = new KeyboardManager([ + [["ctrl+a", "mac+meta+a"], AnnotationEditorUIManager.prototype.selectAll], + [["ctrl+c", "mac+meta+c"], AnnotationEditorUIManager.prototype.copy], + [["ctrl+v", "mac+meta+v"], AnnotationEditorUIManager.prototype.paste], + [["ctrl+x", "mac+meta+x"], AnnotationEditorUIManager.prototype.cut], + [["ctrl+z", "mac+meta+z"], AnnotationEditorUIManager.prototype.undo], + [ + ["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"], + AnnotationEditorUIManager.prototype.redo, + ], + [ + [ + "Backspace", + "alt+Backspace", + "ctrl+Backspace", + "shift+Backspace", + "mac+Backspace", + "mac+alt+Backspace", + "mac+ctrl+Backspace", + "Delete", + "ctrl+Delete", + "shift+Delete", + ], + AnnotationEditorUIManager.prototype.delete, + ], + ]); + + constructor(container, eventBus) { + this.#container = container; this.#eventBus = eventBus; this.#eventBus._on("editingaction", this.#boundOnEditingAction); this.#eventBus._on("pagechanging", this.#boundOnPageChanging); @@ -445,6 +477,7 @@ class AnnotationEditorUIManager { } destroy() { + this.#removeKeyboardManager(); this.#eventBus._off("editingaction", this.#boundOnEditingAction); this.#eventBus._off("pagechanging", this.#boundOnPageChanging); this.#eventBus._off("textlayerrendered", this.#boundOnTextLayerRendered); @@ -468,6 +501,26 @@ class AnnotationEditorUIManager { layer?.onTextLayerRendered(); } + #addKeyboardManager() { + // The keyboard events are caught at the container level in order to be able + // to execute some callbacks even if the current page doesn't have focus. + this.#container.addEventListener("keydown", this.#boundKeydown); + } + + #removeKeyboardManager() { + this.#container.removeEventListener("keydown", this.#boundKeydown); + } + + /** + * Keydown callback. + * @param {KeyboardEvent} event + */ + keydown(event) { + if (!this.getActive()?.shouldGetKeyboardEvents()) { + AnnotationEditorUIManager._keyboardManager.exec(this, event); + } + } + /** * Execute an action for a given name. * For example, the user can click on the "Undo" entry in the context menu @@ -517,6 +570,7 @@ class AnnotationEditorUIManager { */ setEditingState(isEditing) { if (isEditing) { + this.#addKeyboardManager(); this.#dispatchUpdateStates({ isEditing: this.#mode !== AnnotationEditorType.NONE, isEmpty: this.#isEmpty(), @@ -526,6 +580,7 @@ class AnnotationEditorUIManager { hasEmptyClipboard: this.#clipboardManager.isEmpty(), }); } else { + this.#removeKeyboardManager(); this.#dispatchUpdateStates({ isEditing: false, }); diff --git a/web/base_viewer.js b/web/base_viewer.js index ff8bab134..c3ae98a80 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -734,6 +734,7 @@ class BaseViewer { }); this.#annotationEditorUIManager = new AnnotationEditorUIManager( + this.container, this.eventBus ); if (mode !== AnnotationEditorType.NONE) {