diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index d9cef7dec267b..e19333a61ab4c 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -43,6 +43,8 @@ import { PixelsPerInch } from "../display_utils.js"; class AnnotationEditorLayer { #boundClick; + #boundMouseover; + #editors = new Map(); #uiManager; @@ -83,6 +85,7 @@ class AnnotationEditorLayer { this.pageIndex = options.pageIndex; this.div = options.div; this.#boundClick = this.click.bind(this); + this.#boundMouseover = this.mouseover.bind(this); for (const editor of this.#uiManager.getEditors(options.pageIndex)) { this.add(editor); @@ -91,13 +94,45 @@ class AnnotationEditorLayer { this.#uiManager.addLayer(this); } + /** + * The mode has changed: it must be updated. + * @param {number} mode + */ + updateMode(mode) { + if (mode === AnnotationEditorType.INK) { + // We want to have the ink editor covering all of the page without having + // to click to create it: it must be here when we start to draw. + this.div.addEventListener("mouseover", this.#boundMouseover); + this.div.removeEventListener("click", this.#boundClick); + } else { + this.div.removeEventListener("mouseover", this.#boundMouseover); + } + } + + /** + * Mouseover callback. + * @param {MouseEvent} event + */ + mouseover(event) { + if (event.target === this.div && event.buttons === 0) { + // The div is the target so there is no ink editor, hence we can + // create a new one. + // event.buttons === 0 is here to avoid adding a new ink editor + // when we drop an editor. + const editor = this.#createAndAddNewEditor(event); + editor.setInBackground(); + } + } + /** * Add some commands into the CommandManager (undo/redo stuff). * @param {function} cmd * @param {function} undo + * @param {boolean} mustExec - If true the command is executed after having + * been added. */ - addCommands(cmd, undo) { - this.#uiManager.addCommands(cmd, undo); + addCommands(cmd, undo, mustExec) { + this.#uiManager.addCommands(cmd, undo, mustExec); } /** @@ -178,6 +213,17 @@ class AnnotationEditorLayer { * @param {AnnotationEditor} editor */ setActiveEditor(editor) { + const currentActive = this.#uiManager.getActive(); + if (currentActive === editor) { + return; + } + + this.#uiManager.setActiveEditor(editor); + + if (currentActive && currentActive !== editor) { + currentActive.commitOrRemove(); + } + if (editor) { this.unselectAll(); this.div.removeEventListener("click", this.#boundClick); @@ -185,7 +231,6 @@ class AnnotationEditorLayer { this.#uiManager.allowClick = false; this.div.addEventListener("click", this.#boundClick); } - this.#uiManager.setActiveEditor(editor); } attach(editor) { @@ -212,7 +257,6 @@ class AnnotationEditorLayer { if (this.#uiManager.isActive(editor) || this.#editors.size === 0) { this.setActiveEditor(null); this.#uiManager.allowClick = true; - this.div.focus(); } } @@ -279,7 +323,22 @@ class AnnotationEditorLayer { editor.remove(); }; - this.addCommands(cmd, undo); + this.addCommands(cmd, undo, true); + } + + /** + * Add a new editor and make this addition undoable. + * @param {AnnotationEditor} editor + */ + addUndoableEditor(editor) { + const cmd = () => { + this.addOrRebuild(editor); + }; + const undo = () => { + editor.remove(); + }; + + this.addCommands(cmd, undo, false); } /** @@ -306,16 +365,11 @@ class AnnotationEditorLayer { } /** - * Mouseclick callback. + * Create and add a new editor. * @param {MouseEvent} event - * @returns {undefined} + * @returns {AnnotationEditor} */ - click(event) { - if (!this.#uiManager.allowClick) { - this.#uiManager.allowClick = true; - return; - } - + #createAndAddNewEditor(event) { const id = this.getNextId(); const editor = this.#createNewEditor({ parent: this, @@ -324,8 +378,24 @@ class AnnotationEditorLayer { y: event.offsetY, }); if (editor) { - this.addANewEditor(editor); + this.add(editor); } + + return editor; + } + + /** + * Mouseclick callback. + * @param {MouseEvent} event + * @returns {undefined} + */ + click(event) { + if (!this.#uiManager.allowClick) { + this.#uiManager.allowClick = true; + return; + } + + this.#createAndAddNewEditor(event); } /** diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index c086ea97269b9..6de6c7cedc999 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -53,6 +53,20 @@ class AnnotationEditor { this.isAttachedToDOM = false; } + /** + * This editor will be behind the others. + */ + setInBackground() { + this.div.classList.add("background"); + } + + /** + * This editor will be in the foreground. + */ + setInForeground() { + this.div.classList.remove("background"); + } + /** * onfocus callback. */ @@ -81,12 +95,16 @@ class AnnotationEditor { event.preventDefault(); + this.commitOrRemove(); + this.parent.setActiveEditor(null); + } + + commitOrRemove() { if (this.isEmpty()) { this.remove(); } else { this.commit(); } - this.parent.setActiveEditor(null); } /** @@ -156,7 +174,6 @@ class AnnotationEditor { this.div = document.createElement("div"); this.div.className = this.name; this.div.setAttribute("id", this.id); - this.div.draggable = true; this.div.tabIndex = 100; const [tx, ty] = this.getInitialTranslation(); diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 14cb1739058ab..68c65ef370a1f 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -33,6 +33,8 @@ class FreeTextEditor extends AnnotationEditor { #contentHTML = ""; + #hasAlreadyBeenCommitted = false; + #fontSize; static _freeTextDefaultContent = ""; @@ -168,6 +170,13 @@ class FreeTextEditor extends AnnotationEditor { * @returns {undefined} */ commit() { + if (!this.#hasAlreadyBeenCommitted) { + // This editor has something and it's the first time + // it's commited so we can it in the undo/redo stack. + this.#hasAlreadyBeenCommitted = true; + this.parent.addUndoableEditor(this); + } + this.disableEditMode(); this.#contentHTML = this.editorDiv.innerHTML; this.#content = this.#extractText().trimEnd(); diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index cb8c4ea4d3242..e9bc81232e6a3 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -21,11 +21,11 @@ import { fitCurve } from "./fit_curve/fit_curve.js"; * Basic draw editor in order to generate an Ink annotation. */ class InkEditor extends AnnotationEditor { - #aspectRatio; + #aspectRatio = 0; - #baseHeight; + #baseHeight = 0; - #baseWidth; + #baseWidth = 0; #boundCanvasMousemove; @@ -35,9 +35,9 @@ class InkEditor extends AnnotationEditor { #boundCanvasMousedown; - #disableEditing; + #disableEditing = false; - #observer; + #observer = null; constructor(params) { super({ ...params, name: "inkEditor" }); @@ -48,10 +48,6 @@ class InkEditor extends AnnotationEditor { this.currentPath = []; this.scaleFactor = 1; this.translationX = this.translationY = 0; - this.#baseWidth = this.#baseHeight = 0; - this.#aspectRatio = 0; - this.#disableEditing = false; - this.#observer = null; this.x = 0; this.y = 0; @@ -113,8 +109,6 @@ class InkEditor extends AnnotationEditor { return; } - super.remove(); - // Destroy the canvas. this.canvas.width = this.canvas.heigth = 0; this.canvas.remove(); @@ -122,11 +116,13 @@ class InkEditor extends AnnotationEditor { this.#observer.disconnect(); this.#observer = null; + + super.remove(); } /** @inheritdoc */ enableEditMode() { - if (this.#disableEditing) { + if (this.#disableEditing || this.canvas === null) { return; } @@ -145,7 +141,7 @@ class InkEditor extends AnnotationEditor { super.disableEditMode(); this.canvas.style.cursor = "auto"; - this.div.draggable = true; + this.div.draggable = !this.isEmpty(); this.div.classList.remove("editing"); this.canvas.removeEventListener("mousedown", this.#boundCanvasMousedown); @@ -154,6 +150,7 @@ class InkEditor extends AnnotationEditor { /** @inheritdoc */ onceAdded() { + this.div.draggable = !this.isEmpty(); this.div.focus(); } @@ -238,11 +235,15 @@ class InkEditor extends AnnotationEditor { if (this.paths.length === 0) { this.remove(); } else { + if (!this.canvas) { + this.#createCanvas(); + this.#createObserver(); + } this.#fitToContent(); } }; - this.parent.addCommands(cmd, undo); + this.parent.addCommands(cmd, undo, true); } /** @@ -273,8 +274,12 @@ class InkEditor extends AnnotationEditor { if (this.#disableEditing) { return; } + this.disableEditMode(); + // This editor must be on top of the main ink editor. + this.setInForeground(); + this.#disableEditing = true; this.div.classList.add("disabled"); @@ -297,6 +302,10 @@ class InkEditor extends AnnotationEditor { return; } + // We want to draw on top of any other editors. + // Since it's the last child, there's no need to give it a higher z-index. + this.setInForeground(); + event.stopPropagation(); this.canvas.addEventListener("mouseleave", this.#boundCanvasMouseleave); @@ -324,6 +333,10 @@ class InkEditor extends AnnotationEditor { if (this.isInEditMode() && this.currentPath.length !== 0) { event.stopPropagation(); this.#endDrawing(event); + + // Since the ink editor covers all of the page and we want to be able + // to select another editor, we just put this one in the background. + this.setInBackground(); } } @@ -334,6 +347,7 @@ class InkEditor extends AnnotationEditor { */ canvasMouseleave(event) { this.#endDrawing(event); + this.setInBackground(); } /** diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 61fc17bf7928f..731f98a78e3cb 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -63,8 +63,9 @@ class CommandManager { * Add a new couple of commands to be used in case of redo/undo. * @param {function} cmd * @param {function} undo + * @param {boolean} mustExec */ - add(cmd, undo) { + add(cmd, undo, mustExec) { const save = [cmd, undo]; const next = (this.#position + 1) % this.#maxSize; if (next !== this.#start) { @@ -79,7 +80,10 @@ class CommandManager { this.#position = this.#commands.length - 1; } this.#setCommands(save); - cmd(); + + if (mustExec) { + cmd(); + } } /** @@ -316,6 +320,9 @@ class AnnotationEditorUIManager { this.#disableAll(); } else { this.#enableAll(); + for (const layer of this.#allLayers) { + layer.updateMode(mode); + } } } @@ -409,9 +416,10 @@ class AnnotationEditorUIManager { * Add a command to execute (cmd) and another one to undo it. * @param {function} cmd * @param {function} undo + * @param {boolean} mustExec */ - addCommands(cmd, undo) { - this.#commandManager.add(cmd, undo); + addCommands(cmd, undo, mustExec) { + this.#commandManager.add(cmd, undo, mustExec); } /** @@ -460,7 +468,7 @@ class AnnotationEditorUIManager { } }; - this.addCommands(cmd, undo); + this.addCommands(cmd, undo, true); } else { if (!this.#activeEditor) { return; @@ -474,7 +482,7 @@ class AnnotationEditorUIManager { }; } - this.addCommands(cmd, undo); + this.addCommands(cmd, undo, true); } /** @@ -501,7 +509,7 @@ class AnnotationEditorUIManager { layer.addOrRebuild(editor); }; - this.addCommands(cmd, undo); + this.addCommands(cmd, undo, true); } } @@ -522,7 +530,7 @@ class AnnotationEditorUIManager { editor.remove(); }; - this.addCommands(cmd, undo); + this.addCommands(cmd, undo, true); } /** diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 7fe9b43d632db..56e627ee3a2e2 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -98,6 +98,11 @@ overflow: auto; width: 100%; height: 100%; + z-index: 1; +} + +.annotationEditorLayer .background { + z-index: 0; } .annotationEditorLayer .inkEditor:focus {