Skip to content

Commit

Permalink
Merge pull request #16761 from calixteman/editor_add_new_with_keyboard
Browse files Browse the repository at this point in the history
[Editor] Add the possibility to create a new editor in using the keyboard (bug 1853424)
  • Loading branch information
calixteman committed Oct 6, 2023
2 parents 2453b79 + ea5eafa commit 905ad1f
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 9 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
54 changes: 48 additions & 6 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 = false) {
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 @@ -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
Expand All @@ -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.
*/
Expand Down Expand Up @@ -1736,7 +1778,7 @@ class AnnotationEditorUIManager {
return (
this.getActive()?.shouldGetKeyboardEvents() ||
(this.#selectedEditors.size === 1 &&
this.#selectedEditors.values().next().value.shouldGetKeyboardEvents())
this.firstSelectedEditor.shouldGetKeyboardEvents())
);
}

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 905ad1f

Please sign in to comment.