[Editor] Move the keyboard manager at the container level

- This way, the keyboard callbacks are called even if the page has not
the focus, hence the user doesn't have to guess that they have to click
on the page which is a bit painful especially in Ink mode.
- Add two keyboard shortcuts to commit a Freetext editor (ctrl+enter and
escape).
This commit is contained in:
Calixte Denizet 2022-07-19 20:54:17 +02:00
parent 7a4b72ed11
commit e1f28d3504
4 changed files with 83 additions and 105 deletions

View File

@ -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);

View File

@ -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).

View File

@ -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,
});

View File

@ -734,6 +734,7 @@ class BaseViewer {
});
this.#annotationEditorUIManager = new AnnotationEditorUIManager(
this.container,
this.eventBus
);
if (mode !== AnnotationEditorType.NONE) {