diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index c6bbc463d..b6e408c57 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -16,11 +16,12 @@ import { AnnotationEditorParamsType, AnnotationEditorType, + shadow, Util, } from "../../shared/util.js"; +import { bindEvents, KeyboardManager } from "./tools.js"; import { FreeOutliner, Outliner } from "./outliner.js"; import { AnnotationEditor } from "./editor.js"; -import { bindEvents } from "./tools.js"; import { ColorPicker } from "./color_picker.js"; import { noContextMenu } from "../display_utils.js"; @@ -28,6 +29,10 @@ import { noContextMenu } from "../display_utils.js"; * Basic draw editor in order to generate an Highlight annotation. */ class HighlightEditor extends AnnotationEditor { + #anchorNode = null; + + #anchorOffset = 0; + #boxes; #clipPathId = null; @@ -36,6 +41,10 @@ class HighlightEditor extends AnnotationEditor { #focusOutlines = null; + #focusNode = null; + + #focusOffset = 0; + #highlightDiv = null; #highlightOutlines = null; @@ -44,6 +53,8 @@ class HighlightEditor extends AnnotationEditor { #isFreeHighlight = false; + #boundKeydown = this.#keydown.bind(this); + #lastPoint = null; #opacity; @@ -72,6 +83,20 @@ class HighlightEditor extends AnnotationEditor { static _freeHighlightClipId = ""; + static get _keyboardManager() { + const proto = HighlightEditor.prototype; + return shadow( + this, + "_keyboardManager", + new KeyboardManager([ + [["ArrowLeft", "mac+ArrowLeft"], proto._moveCaret, { args: [0] }], + [["ArrowRight", "mac+ArrowRight"], proto._moveCaret, { args: [1] }], + [["ArrowUp", "mac+ArrowUp"], proto._moveCaret, { args: [2] }], + [["ArrowDown", "mac+ArrowDown"], proto._moveCaret, { args: [3] }], + ]) + ); + } + constructor(params) { super({ ...params, name: "highlightEditor" }); this.color = params.color || HighlightEditor._defaultColor; @@ -86,6 +111,10 @@ class HighlightEditor extends AnnotationEditor { this.#createFreeOutlines(params); this.#addToDrawLayer(); } else { + this.#anchorNode = params.anchorNode; + this.#anchorOffset = params.anchorOffset; + this.#focusNode = params.focusNode; + this.#focusOffset = params.focusOffset; this.#createOutlines(); this.#addToDrawLayer(); this.rotate(this.rotation); @@ -530,6 +559,8 @@ class HighlightEditor extends AnnotationEditor { const div = super.render(); if (this.#isFreeHighlight) { div.classList.add("free"); + } else { + this.div.addEventListener("keydown", this.#boundKeydown); } const highlightDiv = (this.#highlightDiv = document.createElement("div")); div.append(highlightDiv); @@ -552,6 +583,36 @@ class HighlightEditor extends AnnotationEditor { this.parent.drawLayer.removeClass(this.#outlineId, "hovered"); } + #keydown(event) { + HighlightEditor._keyboardManager.exec(this, event); + } + + _moveCaret(direction) { + this.parent.unselect(this); + switch (direction) { + case 0 /* left */: + case 2 /* up */: + this.#setCaret(/* start = */ true); + break; + case 1 /* right */: + case 3 /* down */: + this.#setCaret(/* start = */ false); + break; + } + } + + #setCaret(start) { + if (!this.#anchorNode) { + return; + } + const selection = window.getSelection(); + if (start) { + selection.setPosition(this.#anchorNode, this.#anchorOffset); + } else { + selection.setPosition(this.#focusNode, this.#focusOffset); + } + } + /** @inheritdoc */ select() { super.select(); @@ -563,6 +624,9 @@ class HighlightEditor extends AnnotationEditor { unselect() { super.unselect(); this.parent?.drawLayer.removeClass(this.#outlineId, "selected"); + if (!this.#isFreeHighlight) { + this.#setCaret(/* start = */ false); + } } #getRotation() { diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 0040d53f5..819081046 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -947,7 +947,7 @@ class AnnotationEditorUIManager { if (!selection || selection.isCollapsed) { return; } - const { anchorNode } = selection; + const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; const anchorElement = anchorNode.nodeType === Node.TEXT_NODE ? anchorNode.parentElement @@ -966,6 +966,10 @@ class AnnotationEditorUIManager { layer.createAndAddNewEditor({ x: 0, y: 0 }, false, { methodOfCreation, boxes, + anchorNode, + anchorOffset, + focusNode, + focusOffset, }); break; } diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index b368f0cd9..e67a35eaa 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1154,5 +1154,81 @@ describe("Highlight Editor", () => { }) ); }); + + it("must check that an highlight can be left with the keyboard", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#editorHighlight"); + await page.waitForSelector(".annotationEditorLayer.highlightEditing"); + + if (browserName === "chrome") { + // Unfortunately, we can't test this on Chrome because we can't set + // the caret browsing mode to true. + return; + } + + let rect = await getSpanRectFromText( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-" + ); + await page.mouse.click(rect.x + 5, rect.y + rect.height / 2); + await page.keyboard.down("Shift"); + for (let i = 0; i < 10; i++) { + await page.keyboard.press("ArrowRight"); + } + const focusOffset = await page.evaluate( + () => window.getSelection().focusOffset + ); + await page.keyboard.up("Shift"); + + await page.waitForSelector(getEditorSelector(0)); + + for (let i = 0; i < 5; i++) { + await page.keyboard.press("ArrowRight"); + } + + let offset = await page.evaluate( + () => window.getSelection().anchorOffset + ); + + expect(offset) + .withContext(`In ${browserName}`) + .toEqual(focusOffset + 4); + + rect = await getSpanRectFromText( + page, + 1, + "experience and enable a new generation of applications, virtual ma-" + ); + await page.mouse.click( + rect.x + rect.width / 2, + rect.y + rect.height / 2 + ); + await page.keyboard.down("Shift"); + for (let i = 0; i < 10; i++) { + await page.keyboard.press("ArrowRight"); + } + const anchorOffset = await page.evaluate( + () => window.getSelection().anchorOffset + ); + await page.keyboard.up("Shift"); + + await page.waitForSelector(getEditorSelector(1)); + + for (let i = 0; i < 5; i++) { + await page.keyboard.press("ArrowLeft"); + } + + offset = await page.evaluate( + () => window.getSelection().anchorOffset + ); + + expect(offset) + .withContext(`In ${browserName}`) + .toEqual(anchorOffset - 4); + }) + ); + }); }); });