Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editor] Add a toolbar to selected editors with a button to delete it (bug 1863763) #17243

Merged
merged 1 commit into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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