Skip to content

Commit

Permalink
[Editor] Add a toolbar to selected editors with a button to delete it…
Browse files Browse the repository at this point in the history
… (bug 1863763)
  • Loading branch information
calixteman committed Nov 10, 2023
1 parent 1b88aad commit 334f0eb
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 3 deletions.
1 change: 1 addition & 0 deletions gulpfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@ function buildComponents(defines, dir) {
"web/images/annotation-*.svg",
"web/images/loading-icon.gif",
"web/images/altText_*.svg",
"web/images/editor-toolbar-*.svg",
];

return merge([
Expand Down
23 changes: 23 additions & 0 deletions src/display/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
KeyboardManager,
} from "./tools.js";
import { FeatureTest, shadow, unreachable } from "../../shared/util.js";
import { EditorToolbar } from "./toolbar.js";
import { noContextMenu } from "../display_utils.js";

/**
Expand Down Expand Up @@ -62,6 +63,8 @@ class AnnotationEditor {

#boundFocusout = this.focusout.bind(this);

#editToolbar = null;

#focusedResizerName = "";

#hasBeenClicked = false;
Expand Down Expand Up @@ -1034,6 +1037,22 @@ class AnnotationEditor {
this.#altTextWasFromKeyBoard = false;
}

addEditToolbar() {
if (this.#editToolbar || this.#isInEditMode) {
return;
}
this.#editToolbar = new EditorToolbar(this);
this.div.append(this.#editToolbar.render());
}

removeEditToolbar() {
if (!this.#editToolbar) {
return;
}
this.#editToolbar.remove();
this.#editToolbar = null;
}

getClientDimensions() {
return this.div.getBoundingClientRect();
}
Expand Down Expand Up @@ -1386,6 +1405,7 @@ class AnnotationEditor {
this.#moveInDOMTimeout = null;
}
this.#stopResizing();
this.removeEditToolbar();
}

/**
Expand Down Expand Up @@ -1543,6 +1563,8 @@ class AnnotationEditor {
select() {
this.makeResizable();
this.div?.classList.add("selectedEditor");
this.addEditToolbar();
this.#editToolbar?.show();
}

/**
Expand All @@ -1556,6 +1578,7 @@ class AnnotationEditor {
// go.
this._uiManager.currentLayer.div.focus();
}
this.#editToolbar?.hide();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/display/editor/ink.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ class InkEditor extends AnnotationEditor {
this.div.classList.add("disabled");

this.#fitToContent(/* firstTime = */ true);
this.makeResizable();
this.select();

this.parent.addInkEditorIfNeeded(/* isCommitting = */ true);

Expand Down
97 changes: 97 additions & 0 deletions src/display/editor/toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* Copyright 2023 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { noContextMenu } from "../display_utils.js";

class EditorToolbar {
#toolbar = null;

#editor;

#buttons = null;

constructor(editor) {
this.#editor = editor;
}

render() {
const editToolbar = (this.#toolbar = document.createElement("div"));
editToolbar.className = "editToolbar";
editToolbar.addEventListener("contextmenu", noContextMenu);
editToolbar.addEventListener("pointerdown", EditorToolbar.#pointerDown);

const buttons = (this.#buttons = document.createElement("div"));
buttons.className = "buttons";
editToolbar.append(buttons);

this.#addDeleteButton();

return editToolbar;
}

static #pointerDown(e) {
e.stopPropagation();
}

#focusIn(e) {
this.#editor._focusEventsAllowed = false;
e.preventDefault();
e.stopPropagation();
}

#focusOut(e) {
this.#editor._focusEventsAllowed = true;
e.preventDefault();
e.stopPropagation();
}

#addListenersToElement(element) {
// If we're clicking on a button with the keyboard or with
// the mouse, we don't want to trigger any focus events on
// the editor.
element.addEventListener("focusin", this.#focusIn.bind(this), {
capture: true,
});
element.addEventListener("focusout", this.#focusOut.bind(this), {
capture: true,
});
element.addEventListener("contextmenu", noContextMenu);
}

hide() {
this.#toolbar.classList.add("hidden");
}

show() {
this.#toolbar.classList.remove("hidden");
}

#addDeleteButton() {
const button = document.createElement("button");
button.className = "delete";
button.tabIndex = 0;
this.#addListenersToElement(button);
button.addEventListener("click", e => {
this.#editor._uiManager.delete();
});
this.#buttons.append(button);
}

remove() {
this.#toolbar.remove();
}
}

export { EditorToolbar };
5 changes: 3 additions & 2 deletions src/display/editor/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,9 @@ class AnnotationEditorUIManager {
// 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) &&
checker: (self, { target: el }) =>
!(el instanceof HTMLButtonElement) &&
self.#container.contains(el) &&
!self.isEnterHandled,
},
],
Expand Down
134 changes: 134 additions & 0 deletions test/integration/freetext_editor_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3053,4 +3053,138 @@ describe("FreeText Editor", () => {
);
});
});

describe("Delete a freetext in using the delete button", () => {
let pages;

beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});

afterAll(async () => {
await closePages(pages);
});

it("must check that a freetext is deleted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);

const rect = await page.$eval(".annotationEditorLayer", el => {
// With Chrome something is wrong when serializing a DomRect,
// hence we extract the values and just return them.
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.waitForSelector(getEditorSelector(0), {
visible: true,
});
await page.type(`${getEditorSelector(0)} .internal`, data);

// Commit.
await page.keyboard.press("Escape");
await page.waitForSelector(
`${getEditorSelector(0)} .overlay.enabled`
);

// Delete it in using the button.
await page.click(`${getEditorSelector(0)} button.delete`);
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
getEditorSelector(0)
);
await waitForStorageEntries(page, 0);

// Undo.
await kbUndo(page);
await waitForSerialized(page, 1);

await page.waitForSelector(getEditorSelector(0), {
visible: true,
});
})
);
});
});

describe("Delete two freetexts in using the delete button and the keyboard", () => {
let pages;

beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});

afterAll(async () => {
await closePages(pages);
});

it("must check that freetexts are deleted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);

const rect = await page.$eval(".annotationEditorLayer", el => {
// With Chrome something is wrong when serializing a DomRect,
// hence we extract the values and just return them.
const { x, y } = el.getBoundingClientRect();
return { x, y };
});

const data = "Hello PDF.js World !!";

for (let i = 1; i <= 2; i++) {
const editorSelector = getEditorSelector(i - 1);
await page.mouse.click(rect.x + i * 100, rect.y + i * 100);
await page.waitForSelector(editorSelector, {
visible: true,
});
await page.type(`${editorSelector} .internal`, data);

// Commit.
await page.keyboard.press("Escape");
await page.waitForSelector(`${editorSelector} .overlay.enabled`);
}

// Select the editor created previously.
const editorRect = await page.$eval(getEditorSelector(0), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2
);
await waitForSelectedEditor(page, getEditorSelector(0));

await selectAll(page);

// Delete it in using the button.
await page.focus(`${getEditorSelector(0)} button.delete`);
await page.keyboard.press("Enter");
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
getEditorSelector(0)
);
await waitForStorageEntries(page, 0);

// Undo.
await kbUndo(page);
await waitForSerialized(page, 2);

await page.waitForSelector(getEditorSelector(0), {
visible: true,
});

await page.waitForSelector(getEditorSelector(1), {
visible: true,
});
})
);
});
});
});
Loading

0 comments on commit 334f0eb

Please sign in to comment.