Skip to content

Commit

Permalink
[Editor] Add the possibility to create a new editor in using the keyb…
Browse files Browse the repository at this point in the history
…oard (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.
  • Loading branch information
calixteman committed Oct 5, 2023
1 parent a60f90a commit 40a0818
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 8 deletions.
7 changes: 7 additions & 0 deletions src/display/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
48 changes: 43 additions & 5 deletions src/display/editor/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
};
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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) {
if (this.#mode === mode) {
return;
}
Expand All @@ -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;
}
Expand All @@ -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
Expand All @@ -1201,7 +1232,7 @@ class AnnotationEditorUIManager {
return;
}
if (type === AnnotationEditorParamsType.CREATE) {
this.currentLayer.addNewEditor(type);
this.currentLayer.addNewEditor();
return;
}

Expand Down Expand Up @@ -1432,6 +1463,13 @@ class AnnotationEditorUIManager {
return this.#selectedEditors.size !== 0;
}

get isEnterHandled() {
return (
this.#selectedEditors.size === 1 &&
[...this.#selectedEditors][0].isEnterHandled
);
}

/**
* Undo the last command.
*/
Expand Down
155 changes: 155 additions & 0 deletions test/integration/freetext_editor_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
);
});
});
});
4 changes: 2 additions & 2 deletions web/pdf_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
}
Expand All @@ -2227,7 +2227,7 @@ class PDFViewer {
mode,
});

this.#annotationEditorUIManager.updateMode(mode, editId);
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
}

// eslint-disable-next-line accessor-pairs
Expand Down
7 changes: 6 additions & 1 deletion web/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
});
}
Expand Down

0 comments on commit 40a0818

Please sign in to comment.