[Editor] Add a toolbar to selected editors with a button to delete it (bug 1863763)
This commit is contained in:
parent
1b88aad0db
commit
334f0eb060
@ -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([
|
||||
|
@ -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.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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
||||
|
97
src/display/editor/toolbar.js
Normal file
97
src/display/editor/toolbar.js
Normal file
@ -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 };
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
@ -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,
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -188,6 +188,105 @@
|
||||
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;
|
||||
}
|
||||
|
||||
&:has(:focus-visible) {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.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 +508,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 +543,13 @@
|
||||
inset-block-start: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.editToolbar {
|
||||
rotate: 180deg;
|
||||
inset-inline-start: 0;
|
||||
inset-block-end: calc(100% + 6px);
|
||||
inset-block-start: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&
|
||||
@ -459,6 +580,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 +628,7 @@
|
||||
&:dir(ltr) {
|
||||
transform-origin: 0 100%;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
@ -500,6 +637,7 @@
|
||||
&:dir(ltr) {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
&:dir(rtl) {
|
||||
transform-origin: 100% 0;
|
||||
}
|
||||
@ -804,6 +942,7 @@
|
||||
outline-offset: 0;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
|
5
web/images/editor-toolbar-delete.svg
Normal file
5
web/images/editor-toolbar-delete.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11 3H13.6C14 3 14.3 3.3 14.3 3.6C14.3 3.9 14 4.2 13.7 4.2H13.3V14C13.3 15.1 12.4 16 11.3 16H4.80005C3.70005 16 2.80005 15.1 2.80005 14V4.2H2.40005C2.00005 4.2 1.80005 4 1.80005 3.6C1.80005 3.2 2.00005 3 2.40005 3H5.00005V2C5.00005 0.9 5.90005 0 7.00005 0H9.00005C10.1 0 11 0.9 11 2V3ZM6.90005 1.2L6.30005 1.8V3H9.80005V1.8L9.20005 1.2H6.90005ZM11.4 14.7L12 14.1V4.2H4.00005V14.1L4.60005 14.7H11.4ZM7.00005 12.4C7.00005 12.7 6.70005 13 6.40005 13C6.10005 13 5.80005 12.7 5.80005 12.4V7.6C5.70005 7.3 6.00005 7 6.40005 7C6.80005 7 7.00005 7.3 7.00005 7.6V12.4ZM10.2001 12.4C10.2001 12.7 9.90006 13 9.60006 13C9.30006 13 9.00006 12.7 9.00006 12.4V7.6C9.00006 7.3 9.30006 7 9.60006 7C9.90006 7 10.2001 7.3 10.2001 7.6V12.4Z"
|
||||
fill="black" />
|
||||
</svg>
|
After Width: | Height: | Size: 909 B |
Loading…
Reference in New Issue
Block a user