From 93b09f6320fcc0f2c648165b68dcac198e6ee349 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 26 Jul 2023 22:11:55 +0200 Subject: [PATCH] [Editor] Add the possibility to move an empty freetext editor with the keyboard (bug 1845088) --- src/display/editor/freetext.js | 58 +++++++++- src/display/editor/tools.js | 14 ++- test/integration/freetext_editor_spec.js | 139 ++++++++++++++++++----- 3 files changed, 175 insertions(+), 36 deletions(-) diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 8baf88ddf..c837725de 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -24,7 +24,11 @@ import { shadow, Util, } from "../../shared/util.js"; -import { bindEvents, KeyboardManager } from "./tools.js"; +import { + AnnotationEditorUIManager, + bindEvents, + KeyboardManager, +} from "./tools.js"; import { AnnotationEditor } from "./editor.js"; import { FreeTextAnnotationElement } from "../annotation_layer.js"; @@ -61,6 +65,9 @@ class FreeTextEditor extends AnnotationEditor { static _defaultFontSize = 10; static get _keyboardManager() { + const arrowChecker = self => self.isEmpty(); + const small = AnnotationEditorUIManager.TRANSLATE_SMALL; + const big = AnnotationEditorUIManager.TRANSLATE_BIG; return shadow( this, "_keyboardManager", @@ -77,6 +84,46 @@ class FreeTextEditor extends AnnotationEditor { ["ctrl+Enter", "mac+meta+Enter", "Escape", "mac+Escape"], FreeTextEditor.prototype.commitOrRemove, ], + [ + ["ArrowLeft", "mac+ArrowLeft"], + FreeTextEditor.prototype._translateEmpty, + { args: [-small, 0], checker: arrowChecker }, + ], + [ + ["ctrl+ArrowLeft", "mac+shift+ArrowLeft"], + FreeTextEditor.prototype._translateEmpty, + { args: [-big, 0], checker: arrowChecker }, + ], + [ + ["ArrowRight", "mac+ArrowRight"], + FreeTextEditor.prototype._translateEmpty, + { args: [small, 0], checker: arrowChecker }, + ], + [ + ["ctrl+ArrowRight", "mac+shift+ArrowRight"], + FreeTextEditor.prototype._translateEmpty, + { args: [big, 0], checker: arrowChecker }, + ], + [ + ["ArrowUp", "mac+ArrowUp"], + FreeTextEditor.prototype._translateEmpty, + { args: [0, -small], checker: arrowChecker }, + ], + [ + ["ctrl+ArrowUp", "mac+shift+ArrowUp"], + FreeTextEditor.prototype._translateEmpty, + { args: [0, -big], checker: arrowChecker }, + ], + [ + ["ArrowDown", "mac+ArrowDown"], + FreeTextEditor.prototype._translateEmpty, + { args: [0, small], checker: arrowChecker }, + ], + [ + ["ctrl+ArrowDown", "mac+shift+ArrowDown"], + FreeTextEditor.prototype._translateEmpty, + { args: [0, big], checker: arrowChecker }, + ], ]) ); } @@ -209,6 +256,15 @@ class FreeTextEditor extends AnnotationEditor { }); } + /** + * Helper to translate the editor with the keyboard when it's empty. + * @param {number} x in page units. + * @param {number} y in page units. + */ + _translateEmpty(x, y) { + this._uiManager.translateSelectedEditors(x, y, /* noCommit = */ true); + } + /** @inheritdoc */ getInitialTranslation() { // The start of the base line is where the user clicked. diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 2dc1ed02d..6dd5a9e2c 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -561,9 +561,9 @@ class AnnotationEditorUIManager { #container = null; - static #TRANSLATE_SMALL = 1; // page units. + static TRANSLATE_SMALL = 1; // page units. - static #TRANSLATE_BIG = 10; // page units. + static TRANSLATE_BIG = 10; // page units. static get _keyboardManager() { const arrowChecker = self => { @@ -576,8 +576,8 @@ class AnnotationEditorUIManager { self.hasSomethingToControl() ); }; - const small = this.#TRANSLATE_SMALL; - const big = this.#TRANSLATE_BIG; + const small = this.TRANSLATE_SMALL; + const big = this.TRANSLATE_BIG; return shadow( this, "_keyboardManager", @@ -1379,8 +1379,10 @@ class AnnotationEditorUIManager { }); } - translateSelectedEditors(x, y) { - this.commitOrRemove(); + translateSelectedEditors(x, y, noCommit = false) { + if (!noCommit) { + this.commitOrRemove(); + } if (!this.hasSelection) { return; } diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index 4f4650480..69855fcf8 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -44,6 +44,17 @@ const copyPaste = async page => { await promise; }; +const clearAll = async page => { + await page.keyboard.down("Control"); + await page.keyboard.press("a"); + await page.keyboard.up("Control"); + await page.waitForTimeout(10); + await page.keyboard.down("Control"); + await page.keyboard.press("Backspace"); + await page.keyboard.up("Control"); + await page.waitForTimeout(10); +}; + describe("FreeText Editor", () => { describe("FreeText", () => { let pages; @@ -140,17 +151,7 @@ describe("FreeText Editor", () => { it("must clear all", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.keyboard.down("Control"); - await page.keyboard.press("a"); - await page.keyboard.up("Control"); - - await page.waitForTimeout(10); - - await page.keyboard.down("Control"); - await page.keyboard.press("Backspace"); - await page.keyboard.up("Control"); - - await page.waitForTimeout(10); + await clearAll(page); for (const n of [0, 1, 2]) { const hasEditor = await page.evaluate(sel => { @@ -285,6 +286,7 @@ describe("FreeText Editor", () => { // Commit. await page.keyboard.press("Escape"); + await page.waitForTimeout(10); const ariaOwns = await page.$eval(".textLayer", el => { const span = el.querySelector(`span[pdfjs="true"]`); @@ -306,17 +308,7 @@ describe("FreeText Editor", () => { return { x, y }; }); - await page.keyboard.down("Control"); - await page.keyboard.press("a"); - await page.keyboard.up("Control"); - - await page.waitForTimeout(10); - - await page.keyboard.down("Control"); - await page.keyboard.press("Backspace"); - await page.keyboard.up("Control"); - - await page.waitForTimeout(10); + await clearAll(page); const data = "Hello PDF.js World !!"; await page.mouse.click(rect.x + 100, rect.y + 100); @@ -377,13 +369,7 @@ describe("FreeText Editor", () => { return { x, y }; }); - await page.keyboard.down("Control"); - await page.keyboard.press("a"); - await page.keyboard.up("Control"); - - await page.keyboard.down("Control"); - await page.keyboard.press("Backspace"); - await page.keyboard.up("Control"); + await clearAll(page); await page.mouse.click(rect.x + 200, rect.y + 100); @@ -1918,5 +1904,100 @@ describe("FreeText Editor", () => { }) ); }); + + it("must check the position of an empty freetext", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await clearAll(page); + + const rect = await page.$eval(".annotationEditorLayer", el => { + const { x, y } = el.getBoundingClientRect(); + return { x, y }; + }); + + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.type(`${getEditorSelector(1)} .internal`, data); + + const editorRect = await page.$eval(getEditorSelector(1), el => { + const { x, y, width, height } = el.getBoundingClientRect(); + return { + x, + y, + width, + height, + }; + }); + + // Commit. + await page.mouse.click( + editorRect.x, + editorRect.y + 2 * editorRect.height + ); + await page.waitForTimeout(10); + + const [pageX, pageY] = await getFirstSerialized(page, x => x.rect); + + await clearAll(page); + await page.mouse.click(rect.x + 100, rect.y + 100); + + for (let i = 0; i < 20; i++) { + await page.keyboard.press("ArrowRight"); + await page.waitForTimeout(1); + } + await page.waitForTimeout(10); + + for (let i = 0; i < 2; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.up("Control"); + await page.waitForTimeout(1); + } + await page.waitForTimeout(10); + + for (let i = 0; i < 20; i++) { + await page.keyboard.press("ArrowLeft"); + await page.waitForTimeout(1); + } + await page.waitForTimeout(10); + + for (let i = 0; i < 2; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowUp"); + await page.keyboard.up("Control"); + await page.waitForTimeout(1); + } + await page.waitForTimeout(10); + + for (let i = 0; i < 2; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowRight"); + await page.keyboard.up("Control"); + await page.waitForTimeout(1); + } + await page.waitForTimeout(10); + + for (let i = 0; i < 2; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowLeft"); + await page.keyboard.up("Control"); + await page.waitForTimeout(1); + } + await page.waitForTimeout(10); + + await page.type(`${getEditorSelector(2)} .internal`, data); + await page.keyboard.press("Escape"); + await page.waitForTimeout(10); + + const [newX, newY] = await getFirstSerialized(page, x => x.rect); + expect(Math.round(newX)) + .withContext(`In ${browserName}`) + .toEqual(Math.round(pageX)); + expect(Math.round(newY)) + .withContext(`In ${browserName}`) + .toEqual(Math.round(pageY)); + }) + ); + }); }); });