From ea5eafa265c2062cb0d7c0a04f05fa193b5939af Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 5 Oct 2023 19:25:36 +0200 Subject: [PATCH] [Editor] Add the possibility to create a new editor in using the keyboard (bug 1853424) When an editing button is disabled, focused and the user press Enter (or space), an editor is automatically added at the center of the current page. Next creations can be done in using the same keys within the focused page. --- src/display/editor/editor.js | 7 + src/display/editor/tools.js | 54 +++++++- test/integration/freetext_editor_spec.js | 155 +++++++++++++++++++++++ web/pdf_viewer.js | 4 +- web/toolbar.js | 7 +- 5 files changed, 218 insertions(+), 9 deletions(-) diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 04296b46f79ff..180c3c1b7437f 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -276,6 +276,13 @@ class AnnotationEditor { this.div?.classList.toggle("draggable", value); } + /** + * @returns {boolean} true if the editor handles the Enter key itself. + */ + get isEnterHandled() { + return true; + } + center() { const [pageWidth, pageHeight] = this.pageDimensions; switch (this.parentRotation) { diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index a03bb0bf46134..1b180ac8f5216 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -605,10 +605,8 @@ class AnnotationEditorUIManager { const arrowChecker = self => { // If the focused element is an input, we don't want to handle the arrow. // For example, sliders can be controlled with the arrow keys. - const { activeElement } = document; return ( - activeElement && - self.#container.contains(activeElement) && + self.#container.contains(document.activeElement) && self.hasSomethingToControl() ); }; @@ -650,6 +648,28 @@ class AnnotationEditorUIManager { ], proto.delete, ], + [ + ["Enter", "mac+Enter"], + proto.addNewEditorFromKeyboard, + { + // Those shortcuts can be used in the toolbar for some other actions + // like zooming, hence we need to check if the container has the + // focus. + checker: self => + self.#container.contains(document.activeElement) && + !self.isEnterHandled, + }, + ], + [ + [" ", "mac+ "], + proto.addNewEditorFromKeyboard, + { + // Those shortcuts can be used in the toolbar for some other actions + // like zooming, hence we need to check if the container has the + // focus. + checker: self => self.#container.contains(document.activeElement), + }, + ], [["Escape", "mac+Escape"], proto.unselectAll], [ ["ArrowLeft", "mac+ArrowLeft"], @@ -1147,8 +1167,10 @@ class AnnotationEditorUIManager { * Change the editor mode (None, FreeText, Ink, ...) * @param {number} mode * @param {string|null} editId + * @param {boolean} [isFromKeyboard] - true if the mode change is due to a + * keyboard action. */ - updateMode(mode, editId = null) { + updateMode(mode, editId = null, isFromKeyboard = false) { if (this.#mode === mode) { return; } @@ -1164,6 +1186,11 @@ class AnnotationEditorUIManager { for (const layer of this.#allLayers.values()) { layer.updateMode(mode); } + if (!editId && isFromKeyboard) { + this.addNewEditorFromKeyboard(); + return; + } + if (!editId) { return; } @@ -1176,6 +1203,10 @@ class AnnotationEditorUIManager { } } + addNewEditorFromKeyboard() { + this.currentLayer.addNewEditor(); + } + /** * Update the toolbar if it's required to reflect the tool currently used. * @param {number} mode @@ -1201,7 +1232,7 @@ class AnnotationEditorUIManager { return; } if (type === AnnotationEditorParamsType.CREATE) { - this.currentLayer.addNewEditor(type); + this.currentLayer.addNewEditor(); return; } @@ -1416,6 +1447,10 @@ class AnnotationEditorUIManager { return this.#selectedEditors.has(editor); } + get firstSelectedEditor() { + return this.#selectedEditors.values().next().value; + } + /** * Unselect an editor. * @param {AnnotationEditor} editor @@ -1432,6 +1467,13 @@ class AnnotationEditorUIManager { return this.#selectedEditors.size !== 0; } + get isEnterHandled() { + return ( + this.#selectedEditors.size === 1 && + this.firstSelectedEditor.isEnterHandled + ); + } + /** * Undo the last command. */ @@ -1736,7 +1778,7 @@ class AnnotationEditorUIManager { return ( this.getActive()?.shouldGetKeyboardEvents() || (this.#selectedEditors.size === 1 && - this.#selectedEditors.values().next().value.shouldGetKeyboardEvents()) + this.firstSelectedEditor.shouldGetKeyboardEvents()) ); } diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index 805f373772e5a..7a2889b760420 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -2808,4 +2808,159 @@ describe("FreeText Editor", () => { ); }); }); + + describe("Create editor with keyboard", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must create an editor from the toolbar", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.focus("#editorFreeText"); + await page.keyboard.press("Enter"); + + let selectorEditor = getEditorSelector(0); + await page.waitForSelector(selectorEditor, { + visible: true, + }); + + let xy = await getXY(page, selectorEditor); + for (let i = 0; i < 5; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowUp"); + await page.keyboard.up("Control"); + await waitForPositionChange(page, selectorEditor, xy); + xy = await getXY(page, selectorEditor); + } + + const data = "Hello PDF.js World !!"; + await page.type(`${selectorEditor} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${selectorEditor} .overlay.enabled`); + + let content = await page.$eval(selectorEditor, el => + el.innerText.trimEnd() + ); + + expect(content).withContext(`In ${browserName}`).toEqual(data); + + // Disable editing mode. + await page.click("#editorFreeText"); + await page.waitForSelector( + `.annotationEditorLayer:not(.freetextEditing)` + ); + + await page.focus("#editorFreeText"); + await page.keyboard.press(" "); + selectorEditor = getEditorSelector(1); + await page.waitForSelector(selectorEditor, { + visible: true, + }); + + xy = await getXY(page, selectorEditor); + for (let i = 0; i < 5; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.up("Control"); + await waitForPositionChange(page, selectorEditor, xy); + xy = await getXY(page, selectorEditor); + } + + await page.type(`${selectorEditor} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${selectorEditor} .overlay.enabled`); + + // Unselect. + await page.keyboard.press("Escape"); + await waitForUnselectedEditor(page, selectorEditor); + + content = await page.$eval(getEditorSelector(1), el => + el.innerText.trimEnd() + ); + + expect(content).withContext(`In ${browserName}`).toEqual(data); + }) + ); + }); + + it("must create an editor with keyboard", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.keyboard.press("Enter"); + let selectorEditor = getEditorSelector(2); + await page.waitForSelector(selectorEditor, { + visible: true, + }); + + let xy = await getXY(page, selectorEditor); + for (let i = 0; i < 10; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowLeft"); + await page.keyboard.up("Control"); + await waitForPositionChange(page, selectorEditor, xy); + xy = await getXY(page, selectorEditor); + } + + const data = "Hello PDF.js World !!"; + await page.type(`${selectorEditor} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${selectorEditor} .overlay.enabled`); + + // Unselect. + await page.keyboard.press("Escape"); + await waitForUnselectedEditor(page, selectorEditor); + + let content = await page.$eval(getEditorSelector(2), el => + el.innerText.trimEnd() + ); + + expect(content).withContext(`In ${browserName}`).toEqual(data); + + await page.keyboard.press(" "); + selectorEditor = getEditorSelector(3); + await page.waitForSelector(selectorEditor, { + visible: true, + }); + + xy = await getXY(page, selectorEditor); + for (let i = 0; i < 10; i++) { + await page.keyboard.down("Control"); + await page.keyboard.press("ArrowRight"); + await page.keyboard.up("Control"); + await waitForPositionChange(page, selectorEditor, xy); + xy = await getXY(page, selectorEditor); + } + + await page.type(`${selectorEditor} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${selectorEditor} .overlay.enabled`); + + // Unselect. + await page.keyboard.press("Escape"); + await waitForUnselectedEditor(page, selectorEditor); + + content = await page.$eval(selectorEditor, el => + el.innerText.trimEnd() + ); + + expect(content).withContext(`In ${browserName}`).toEqual(data); + }) + ); + }); + }); }); diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 94d9acc9a8c28..71e10f0bff89f 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -2208,7 +2208,7 @@ class PDFViewer { /** * @param {number} mode - AnnotationEditor mode (None, FreeText, Ink, ...) */ - set annotationEditorMode({ mode, editId = null }) { + set annotationEditorMode({ mode, editId = null, isFromKeyboard = false }) { if (!this.#annotationEditorUIManager) { throw new Error(`The AnnotationEditor is not enabled.`); } @@ -2227,7 +2227,7 @@ class PDFViewer { mode, }); - this.#annotationEditorUIManager.updateMode(mode, editId); + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); } // eslint-disable-next-line accessor-pairs diff --git a/web/toolbar.js b/web/toolbar.js index 550685e591ec6..879f57b2bf288 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -162,7 +162,12 @@ class Toolbar { for (const { element, eventName, eventDetails } of this.buttons) { element.addEventListener("click", evt => { if (eventName !== null) { - this.eventBus.dispatch(eventName, { source: this, ...eventDetails }); + this.eventBus.dispatch(eventName, { + source: this, + ...eventDetails, + // evt.detail is the number of clicks. + isFromKeyboard: evt.detail === 0, + }); } }); }