[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.
This commit is contained in:
Calixte Denizet 2023-10-05 19:25:36 +02:00
parent a60f90ae94
commit ea5eafa265
5 changed files with 218 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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