diff --git a/gulpfile.mjs b/gulpfile.mjs index 2de6f1f8be4ff5..cbdc8fe7334212 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -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([ diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 6fb2b4534d6699..4229381430ac0f 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -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"; /** @@ -62,6 +63,8 @@ class AnnotationEditor { #boundFocusout = this.focusout.bind(this); + #editToolbar = null; + #focusedResizerName = ""; #hasBeenClicked = false; @@ -1034,6 +1037,22 @@ class AnnotationEditor { this.#altTextWasFromKeyBoard = false; } + addEditToolbar() { + if (this.#editToolbar || this.#isInEditMode) { + return; + } + this.#editToolbar = new EditorToolbar(this, this._uiManager); + this.div.append(this.#editToolbar.render()); + } + + removeEditToolbar() { + if (!this.#editToolbar) { + return; + } + this.#editToolbar.remove(); + this.#editToolbar = null; + } + getClientDimensions() { return this.div.getBoundingClientRect(); } @@ -1386,6 +1405,7 @@ class AnnotationEditor { this.#moveInDOMTimeout = null; } this.#stopResizing(); + this.removeEditToolbar(); } /** @@ -1543,6 +1563,8 @@ class AnnotationEditor { select() { this.makeResizable(); this.div?.classList.add("selectedEditor"); + this.addEditToolbar(); + this.#editToolbar?.show(); } /** @@ -1556,6 +1578,7 @@ class AnnotationEditor { // go. this._uiManager.currentLayer.div.focus(); } + this.#editToolbar?.hide(); } /** diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index ee2ca03ebc759e..cc52887069092e 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -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); diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js new file mode 100644 index 00000000000000..41ccf94990706c --- /dev/null +++ b/src/display/editor/toolbar.js @@ -0,0 +1,80 @@ +/* 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; + + #uiManager; + + constructor(editor, uiManager) { + this.#editor = editor; + this.#uiManager = uiManager; + } + + 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(); + } + + hide() { + this.#toolbar.classList.add("hidden"); + } + + show() { + this.#toolbar.classList.remove("hidden"); + } + + #addDeleteButton() { + const button = document.createElement("button"); + button.className = "delete"; + button.tabIndex = 0; + button.addEventListener("contextmenu", noContextMenu); + button.addEventListener( + "click", + () => { + event.preventDefault(); + this.#uiManager.deleteEditor(this.#editor); + }, + { capture: true } + ); + this.#buttons.append(button); + } + + remove() { + this.#toolbar.remove(); + } +} + +export { EditorToolbar }; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index b8ac29c114237a..1ae76489ad6eac 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1561,6 +1561,21 @@ class AnnotationEditorUIManager { this.addCommands({ cmd, undo, mustExec: true }); } + /** + * Delete the given editor. + * @param {AnnotationEditor} editor + */ + deleteEditor(editor) { + const cmd = () => { + editor.remove(); + }; + const undo = () => { + this.#addEditorToLayer(editor); + }; + + this.addCommands({ cmd, undo, mustExec: true }); + } + commitOrRemove() { // An editor is being edited so just commit it. this.#activeEditor?.commitOrRemove(); diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index a3acdeba3c61b2..59f7552b697138 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -3053,4 +3053,61 @@ describe("FreeText Editor", () => { ); }); }); + + describe("Delete a freetext in using the bin 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, + }); + }) + ); + }); + }); }); diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 5f0047e7d12ed2..92719f31b8a113 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -188,6 +188,101 @@ border: var(--focus-outline-around); } } + + .editToolbar { + --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); + --editor-toolbar-bg-color: #f0f0f4; + --editor-toolbar-fg-color: #2e2e56; + --editor-toolbar-border-color: #8f8f9d; + --editor-toolbar-hover-bg-color: #e0e0e6; + --editor-toolbar-active-bg-color: #cfcfd8; + --editor-toolbar-focus-outline-color: #0060df; + --editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2); + + @media (prefers-color-scheme: dark) { + --editor-toolbar-bg-color: #2b2a33; + --editor-toolbar-fg-color: #fbfbfe; + --editor-toolbar-border-color: #2b2a33; + --editor-toolbar-hover-bg-color: #52525e; + --editor-toolbar-active-bg-color: #5b5b66; + --editor-toolbar-focus-outline-color: #0df; + } + + @media screen and (forced-colors: active) { + --editor-toolbar-bg-color: ButtonFace; + --editor-toolbar-fg-color: ButtonText; + --editor-toolbar-border-color: ButtonText; + --editor-toolbar-hover-bg-color: AccentColor; + --editor-toolbar-active-bg-color: ButtonFace; + --editor-toolbar-focus-outline-color: ButtonBorder; + --editor-toolbar-shadow: none; + } + + display: flex; + width: fit-content; + height: 28px; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: default; + + position: absolute; + inset-inline-end: 0; + inset-block-start: calc(100% + 6px); + + border-radius: 4px; + background-color: var(--editor-toolbar-bg-color); + border: 1px solid var(--editor-toolbar-border-color); + box-shadow: var(--editor-toolbar-shadow); + + &.hidden { + display: none; + } + + .buttons { + display: flex; + padding: 0 2px; + justify-content: center; + align-items: center; + gap: 4px; + + .delete { + width: 24px; + height: 24px; + cursor: pointer; + border: none; + background-color: transparent; + + &::before { + content: ""; + mask-image: var(--editor-toolbar-delete-image); + mask-repeat: no-repeat; + mask-position: center; + display: inline-block; + background-color: var(--editor-toolbar-fg-color); + width: 100%; + height: 100%; + } + } + + > * { + &:hover { + border-radius: 2px; + background-color: var(--editor-toolbar-hover-bg-color); + } + + &:active { + border-radius: 2px; + background-color: var(--editor-toolbar-active-bg-color); + } + + &:focus-visible { + border-radius: 3px; + outline: 2px solid var(--editor-toolbar-focus-outline-color); + } + } + } + } } .annotationEditorLayer .freeTextEditor { @@ -409,6 +504,21 @@ } } } + + .editToolbar { + rotate: 270deg; + + &:dir(ltr) { + inset-inline-start: calc(100% + 6px); + inset-block-start: 0; + } + + &:dir(rtl) { + inset-inline-end: calc(100% + 6px); + inset-block-end: 0; + inset-block-start: unset; + } + } } & @@ -429,6 +539,13 @@ inset-block-start: -8px; } } + + .editToolbar { + rotate: 180deg; + inset-inline-start: 0; + inset-block-end: calc(100% + 6px); + inset-block-start: unset; + } } & @@ -459,6 +576,21 @@ } } } + + .editToolbar { + rotate: 90deg; + + &:dir(ltr) { + inset-inline-end: calc(100% + 6px); + inset-block-end: 0; + inset-block-start: unset; + } + + &:dir(rtl) { + inset-inline-start: calc(100% + 6px); + inset-block-start: 0; + } + } } } @@ -492,6 +624,7 @@ &:dir(ltr) { transform-origin: 0 100%; } + &:dir(rtl) { transform-origin: 100% 100%; } @@ -500,6 +633,7 @@ &:dir(ltr) { transform-origin: 0 0; } + &:dir(rtl) { transform-origin: 100% 0; } @@ -804,6 +938,7 @@ outline-offset: 0; border-color: transparent; } + &:disabled { pointer-events: none; opacity: 0.4; diff --git a/web/images/editor-toolbar-delete.svg b/web/images/editor-toolbar-delete.svg new file mode 100644 index 00000000000000..f84520d85a3406 --- /dev/null +++ b/web/images/editor-toolbar-delete.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file