1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 14:48:08 +02:00

Merge pull request #18900 from ryzokuken/popup-undo-annotation

Annotation deletion popup (bug 1899731)
This commit is contained in:
calixteman 2024-12-03 16:53:48 +01:00 committed by GitHub
commit a5ce71247b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1249 additions and 92 deletions

View file

@ -1148,6 +1148,7 @@ function buildComponents(defines, dir) {
"web/images/messageBar_*.svg",
"web/images/toolbarButton-{editorHighlight,menuArrow}.svg",
"web/images/cursor-*.svg",
"web/images/secondaryToolbarButton-documentProperties.svg",
];
return ordered([

View file

@ -503,3 +503,24 @@ pdfjs-editor-alt-text-settings-editor-title = Alt text editor
pdfjs-editor-alt-text-settings-show-dialog-button-label = Show alt text editor right away when adding an image
pdfjs-editor-alt-text-settings-show-dialog-description = Helps you make sure all your images have alt text.
pdfjs-editor-alt-text-settings-close-button = Close
## "Annotations removed" bar
pdfjs-editor-undo-bar-message-highlight = Highlight removed
pdfjs-editor-undo-bar-message-freetext = Text removed
pdfjs-editor-undo-bar-message-ink = Drawing removed
pdfjs-editor-undo-bar-message-stamp = Image removed
# Variables:
# $count (Number) - the number of removed annotations.
pdfjs-editor-undo-bar-message-multiple =
{ $count ->
[one] { $count } annotation removed
*[other] { $count } annotations removed
}
pdfjs-editor-undo-bar-undo-button =
.title = Undo
pdfjs-editor-undo-bar-undo-button-label = Undo
pdfjs-editor-undo-bar-close-button =
.title = Close
pdfjs-editor-undo-bar-close-button-label = Close

View file

@ -676,6 +676,7 @@ class DrawingEditor extends AnnotationEditor {
signal,
});
parent.toggleDrawing();
uiManager._editorUndoBar?.hide();
if (this._currentDraw) {
parent.drawLayer.updateProperties(

View file

@ -1142,6 +1142,8 @@ class AnnotationEditor {
bindEvents(this, this.div, ["pointerdown"]);
this._uiManager._editorUndoBar?.hide();
return this.div;
}

View file

@ -620,6 +620,8 @@ class AnnotationEditorUIManager {
#editorsToRescale = new Set();
_editorUndoBar = null;
#enableHighlightFloatingButton = false;
#enableUpdatedAddImage = false;
@ -829,7 +831,8 @@ class AnnotationEditorUIManager {
enableHighlightFloatingButton,
enableUpdatedAddImage,
enableNewAltTextWhenAddingImage,
mlManager
mlManager,
editorUndoBar
) {
const signal = (this._signal = this.#abortController.signal);
this.#container = container;
@ -864,6 +867,7 @@ class AnnotationEditorUIManager {
rotation: 0,
};
this.isShiftKeyDown = false;
this._editorUndoBar = editorUndoBar || null;
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
Object.defineProperty(this, "reset", {
@ -904,6 +908,7 @@ class AnnotationEditorUIManager {
clearTimeout(this.#translationTimeoutId);
this.#translationTimeoutId = null;
}
this._editorUndoBar?.destroy();
}
combinedSignal(ac) {
@ -1656,6 +1661,8 @@ class AnnotationEditorUIManager {
this.setEditingState(false);
this.#disableAll();
this._editorUndoBar?.hide();
this.#updateModeCapability.resolve();
return;
}
@ -2038,6 +2045,7 @@ class AnnotationEditorUIManager {
hasSomethingToRedo: true,
isEmpty: this.#isEmpty(),
});
this._editorUndoBar?.hide();
}
/**
@ -2099,6 +2107,10 @@ class AnnotationEditorUIManager {
? [drawingEditor]
: [...this.#selectedEditors];
const cmd = () => {
this._editorUndoBar?.show(
undo,
editors.length === 1 ? editors[0].editorType : editors.length
);
for (const editor of editors) {
editor.remove();
}

View file

@ -3670,4 +3670,125 @@ describe("FreeText Editor", () => {
);
});
});
describe("Undo deletion popup has the expected behaviour", () => {
let pages;
const editorSelector = getEditorSelector(0);
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that deleting a FreeText editor can be undone using the undo button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 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`);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorUndoBarUndoButton");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
})
);
});
it("must check that the undo deletion popup displays the correct message", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 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`);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForFunction(() => {
const messageElement = document.querySelector(
"#editorUndoBarMessage"
);
return messageElement && messageElement.textContent.trim() !== "";
});
const message = await page.waitForSelector("#editorUndoBarMessage");
const messageText = await page.evaluate(
el => el.textContent,
message
);
expect(messageText).toContain("Text removed");
})
);
});
it("must check that the popup disappears when a new textbox is created", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, {
visible: true,
});
await page.type(`${editorSelector} .internal`, data);
await page.keyboard.press("Escape");
await page.waitForSelector(`${editorSelector} .overlay.enabled`);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
rect = await getRect(page, ".annotationEditorLayer");
const newData = "This is a new text box!";
await page.mouse.click(rect.x + 150, rect.y + 150);
await page.waitForSelector(getEditorSelector(1), {
visible: true,
});
await page.type(`${getEditorSelector(1)} .internal`, newData);
await page.keyboard.press("Escape");
await page.waitForSelector(
`${getEditorSelector(1)} .overlay.enabled`
);
await waitForSerialized(page, 1);
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
});
});

View file

@ -26,6 +26,7 @@ import {
kbBigMoveUp,
kbFocusNext,
kbFocusPrevious,
kbSave,
kbSelectAll,
kbUndo,
loadAndWait,
@ -37,6 +38,11 @@ import {
waitForSelectedEditor,
waitForSerialized,
} from "./test_utils.mjs";
import { fileURLToPath } from "url";
import fs from "fs";
import path from "path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const selectAll = async page => {
await kbSelectAll(page);
@ -2165,4 +2171,446 @@ describe("Highlight Editor", () => {
);
});
});
describe("Undo deletion popup has the expected behaviour", () => {
let pages;
const editorSelector = getEditorSelector(0);
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
null,
null,
{
highlightEditorColors:
"yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000",
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that deleting a highlight can be undone using the undo button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorUndoBarUndoButton");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
await page.waitForSelector(
`.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]`
);
})
);
});
it("must check that the popup disappears when the undo button is clicked", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorUndoBarUndoButton");
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when the close button is clicked", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.waitForSelector("#editorUndoBarCloseButton");
await page.click("#editorUndoBarCloseButton");
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when a new annotation is created", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
const newRect = await getSpanRectFromText(page, 1, "Introduction");
const newX = newRect.x + newRect.width / 2;
const newY = newRect.y + newRect.height / 2;
await page.mouse.click(newX, newY, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(1));
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when the print dialog is opened", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.evaluate(() => window.print());
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when the user clicks on the print button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#printButton");
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when the save dialog is opened", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await kbSave(page);
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when an option from the secondaryToolbar is used", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#secondaryToolbarToggleButton");
await page.click("#lastPage");
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when highlight mode is disabled", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await switchToHighlight(page, /* disable */ true);
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the popup disappears when a PDF is drag-and-dropped", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
const pdfPath = path.join(__dirname, "../pdfs/basicapi.pdf");
const pdfData = fs.readFileSync(pdfPath).toString("base64");
const dataTransfer = await page.evaluateHandle(data => {
const transfer = new DataTransfer();
const view = Uint8Array.from(atob(data), code =>
code.charCodeAt(0)
);
const file = new File([view], "basicapi.pdf", {
type: "application/pdf",
});
transfer.items.add(file);
return transfer;
}, pdfData);
const dropSelector = "#viewer";
await page.evaluate(
(transfer, selector) => {
const dropTarget = document.querySelector(selector);
const event = new DragEvent("dragstart", {
dataTransfer: transfer,
});
dropTarget.dispatchEvent(event);
},
dataTransfer,
dropSelector
);
await page.evaluate(
(transfer, selector) => {
const dropTarget = document.querySelector(selector);
const event = new DragEvent("drop", {
dataTransfer: transfer,
bubbles: true,
});
dropTarget.dispatchEvent(event);
},
dataTransfer,
dropSelector
);
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
it("must check that the undo deletion popup displays the correct message", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForFunction(() => {
const messageElement = document.querySelector(
"#editorUndoBarMessage"
);
return messageElement && messageElement.textContent.trim() !== "";
});
const message = await page.waitForSelector("#editorUndoBarMessage");
const messageText = await page.evaluate(
el => el.textContent,
message
);
expect(messageText).toContain("Highlight removed");
})
);
});
it("must display correct message for multiple highlights", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
let rect = await getSpanRectFromText(page, 1, "Abstract");
let x = rect.x + rect.width / 2;
let y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
rect = await getSpanRectFromText(page, 1, "Languages");
x = rect.x + rect.width / 2;
y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(1));
await selectAll(page);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForFunction(() => {
const messageElement = document.querySelector(
"#editorUndoBarMessage"
);
return messageElement && messageElement.textContent.trim() !== "";
});
const message = await page.waitForSelector("#editorUndoBarMessage");
const messageText = await page.evaluate(
el => el.textContent,
message
);
// Cleans the message text by removing all non-ASCII characters.
// It eliminates any invisible characters such as directional marks
// that interfere with string comparisons
const cleanMessage = messageText.replaceAll(/\P{ASCII}/gu, "");
expect(cleanMessage).toContain(`2 annotations removed`);
})
);
});
it("must work properly when selecting undo by keyboard", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.focus("#editorUndoBarUndoButton"); // we have to simulate focus like this to avoid the wait
await page.keyboard.press("Enter");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
await page.waitForSelector(
`.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]`
);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.focus("#editorUndoBarUndoButton"); // we have to simulate focus like this to avoid the wait
await page.keyboard.press(" ");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
await page.waitForSelector(
`.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]`
);
})
);
});
it("must dismiss itself when user presses space/enter key and undo key isn't focused", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.focus("#editorUndoBar");
await page.keyboard.press("Enter");
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
});
});

View file

@ -829,4 +829,130 @@ describe("Ink Editor", () => {
);
});
});
describe("Undo deletion popup has the expected behaviour", () => {
let pages;
const editorSelector = getEditorSelector(0);
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that deleting a drawing can be undone using the undo button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
const xStart = rect.x + 300;
const yStart = rect.y + 300;
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(xStart, yStart);
await page.mouse.down();
await page.mouse.move(xStart + 50, yStart + 50);
await page.mouse.up();
await awaitPromise(clickHandle);
await commit(page);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorUndoBarUndoButton");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
})
);
});
it("must check that the undo deletion popup displays the correct message", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
const xStart = rect.x + 300;
const yStart = rect.y + 300;
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(xStart, yStart);
await page.mouse.down();
await page.mouse.move(xStart + 50, yStart + 50);
await page.mouse.up();
await awaitPromise(clickHandle);
await commit(page);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForFunction(() => {
const messageElement = document.querySelector(
"#editorUndoBarMessage"
);
return messageElement && messageElement.textContent.trim() !== "";
});
const message = await page.waitForSelector("#editorUndoBarMessage");
const messageText = await page.evaluate(
el => el.textContent,
message
);
expect(messageText).toContain("Drawing removed");
})
);
});
it("must check that the popup disappears when a new drawing is created", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
const xStart = rect.x + 300;
const yStart = rect.y + 300;
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(xStart, yStart);
await page.mouse.down();
await page.mouse.move(xStart + 50, yStart + 50);
await page.mouse.up();
await awaitPromise(clickHandle);
await commit(page);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
const newRect = await getRect(page, ".annotationEditorLayer");
const newXStart = newRect.x + 300;
const newYStart = newRect.y + 300;
const newClickHandle = await waitForPointerUp(page);
await page.mouse.move(newXStart, newYStart);
await page.mouse.down();
await page.mouse.move(newXStart + 50, newYStart + 50);
await page.mouse.up();
await awaitPromise(newClickHandle);
await commit(page);
await page.waitForSelector(getEditorSelector(1));
await waitForSerialized(page, 1);
await page.waitForSelector(getEditorSelector(1));
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
});
});

View file

@ -1530,4 +1530,98 @@ describe("Stamp Editor", () => {
}
});
});
describe("Undo deletion popup has the expected behaviour", () => {
let pages;
const editorSelector = getEditorSelector(0);
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that deleting an image can be undone using the undo button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
const selector = editorSelector;
await copyImage(page, "../images/firefox_logo.png", 0);
await page.waitForSelector(selector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${selector} button.delete`);
await page.click(`${selector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorUndoBarUndoButton");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
await page.waitForSelector(`${selector} canvas`);
})
);
});
it("must check that the undo deletion popup displays the correct message", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
const selector = editorSelector;
await copyImage(page, "../images/firefox_logo.png", 0);
await page.waitForSelector(selector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${selector} button.delete`);
await page.click(`${selector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForFunction(() => {
const messageElement = document.querySelector(
"#editorUndoBarMessage"
);
return messageElement && messageElement.textContent.trim() !== "";
});
const message = await page.waitForSelector("#editorUndoBarMessage");
const messageText = await page.evaluate(
el => el.textContent,
message
);
expect(messageText).toContain("Image removed");
})
);
});
it("must check that the popup disappears when a new image is inserted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
const selector = editorSelector;
await copyImage(page, "../images/firefox_logo.png", 0);
await page.waitForSelector(selector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorStampAddImage");
const newInput = await page.$("#stampEditorFileInput");
await newInput.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
await waitForImage(page, getEditorSelector(1));
await waitForSerialized(page, 1);
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
});
});

View file

@ -756,6 +756,12 @@ async function kbFocusPrevious(page) {
await awaitPromise(handle);
}
async function kbSave(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("s");
await page.keyboard.up(modifier);
}
async function switchToEditor(name, page, disable = false) {
const modeChangedHandle = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
@ -842,6 +848,7 @@ export {
kbModifierDown,
kbModifierUp,
kbRedo,
kbSave,
kbSelectAll,
kbUndo,
loadAndWait,

View file

@ -69,6 +69,7 @@ import { AltTextManager } from "web-alt_text_manager";
import { AnnotationEditorParams } from "web-annotation_editor_params";
import { CaretBrowsingMode } from "./caret_browsing.js";
import { DownloadManager } from "web-download_manager";
import { EditorUndoBar } from "./editor_undo_bar.js";
import { OverlayManager } from "./overlay_manager.js";
import { PasswordPrompt } from "./password_prompt.js";
import { PDFAttachmentViewer } from "web-pdf_attachment_viewer";
@ -192,6 +193,7 @@ const PDFViewerApplication = {
_isCtrlKeyDown: false,
_caretBrowsing: null,
_isScrolling: false,
editorUndoBar: null,
// Called once when the document is loaded.
async initialize(appConfig) {
@ -461,6 +463,10 @@ const PDFViewerApplication = {
: null;
}
if (appConfig.editorUndoBar) {
this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus);
}
const enableHWA = AppOptions.get("enableHWA");
const pdfViewer = new PDFViewer({
container,
@ -470,6 +476,7 @@ const PDFViewerApplication = {
linkService: pdfLinkService,
downloadManager,
altTextManager,
editorUndoBar: this.editorUndoBar,
findController,
scriptingManager:
AppOptions.get("enableScripting") && pdfScriptingManager,
@ -2732,7 +2739,7 @@ function onTouchEnd(evt) {
this._isPinching = false;
}
function onClick(evt) {
function closeSecondaryToolbar(evt) {
if (!this.secondaryToolbar?.isOpen) {
return;
}
@ -2749,6 +2756,20 @@ function onClick(evt) {
}
}
function closeEditorUndoBar(evt) {
if (!this.editorUndoBar?.isOpen) {
return;
}
if (this.appConfig.secondaryToolbar?.toolbar.contains(evt.target)) {
this.editorUndoBar.hide();
}
}
function onClick(evt) {
closeSecondaryToolbar.call(this, evt);
closeEditorUndoBar.call(this, evt);
}
function onKeyUp(evt) {
// evt.ctrlKey is false hence we use evt.key.
if (evt.key === "Control") {
@ -2759,6 +2780,20 @@ function onKeyUp(evt) {
function onKeyDown(evt) {
this._isCtrlKeyDown = evt.key === "Control";
if (
this.editorUndoBar?.isOpen &&
evt.keyCode !== 9 &&
evt.keyCode !== 16 &&
!(
(evt.keyCode === 13 || evt.keyCode === 32) &&
getActiveOrFocusedElement() === this.appConfig.editorUndoBar.undoButton
)
) {
// Hide undo bar on keypress except for Shift, Tab, Shift+Tab.
// Also avoid hiding if the undo button is triggered.
this.editorUndoBar.hide();
}
if (this.overlayManager.active) {
return;
}

View file

@ -270,29 +270,17 @@
}
.messageBar {
--message-bar-warning-icon: url(images/messageBar_warning.svg);
--closing-button-icon: url(images/messageBar_closingButton.svg);
--message-bar-bg-color: #ffebcd;
--message-bar-fg-color: #15141a;
--message-bar-border-color: rgb(0 0 0 / 0.08);
--message-bar-icon: url(images/messageBar_warning.svg);
--message-bar-icon-color: #cd411e;
--message-bar-close-button-border-radius: 4px;
--message-bar-close-button-border: none;
--message-bar-close-button-color: var(--text-primary-color);
--message-bar-close-button-hover-bg-color: rgb(21 20 26 / 0.14);
--message-bar-close-button-active-bg-color: rgb(21 20 26 / 0.21);
--message-bar-close-button-focus-bg-color: rgb(21 20 26 / 0.07);
--message-bar-close-button-color-hover: var(--text-primary-color);
@media (prefers-color-scheme: dark) {
--message-bar-bg-color: #5a3100;
--message-bar-fg-color: #fbfbfe;
--message-bar-border-color: rgb(255 255 255 / 0.08);
--message-bar-icon-color: #e49c49;
--message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14);
--message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21);
--message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07);
}
@media screen and (forced-colors: active) {
@ -300,43 +288,14 @@
--message-bar-fg-color: CanvasText;
--message-bar-border-color: CanvasText;
--message-bar-icon-color: CanvasText;
--message-bar-close-button-color: ButtonText;
--message-bar-close-button-border: 1px solid ButtonText;
--message-bar-close-button-hover-bg-color: ButtonText;
--message-bar-close-button-active-bg-color: ButtonText;
--message-bar-close-button-focus-bg-color: ButtonText;
--message-bar-close-button-color-hover: HighlightText;
}
display: flex;
position: relative;
padding: 12px 8px 12px 0;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 8px;
align-self: stretch;
border-radius: 4px;
border: 1px solid var(--message-bar-border-color);
background: var(--message-bar-bg-color);
color: var(--message-bar-fg-color);
> div {
display: flex;
padding-inline-start: 16px;
align-items: flex-start;
gap: 8px;
align-self: stretch;
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--message-bar-warning-icon);
mask-size: cover;
background-color: var(--message-bar-icon-color);
&::before,
> div {
margin-block: 4px;
}
> div {
@ -356,50 +315,6 @@
}
}
}
.closeButton {
position: absolute;
width: 32px;
height: 32px;
inset-inline-end: 8px;
inset-block-start: 8px;
background: none;
border-radius: var(--message-bar-close-button-border-radius);
border: var(--message-bar-close-button-border);
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--closing-button-icon);
mask-size: cover;
background-color: var(--message-bar-close-button-color);
}
&:is(:hover, :active, :focus)::before {
background-color: var(--message-bar-close-button-color-hover);
}
&:hover {
background-color: var(--message-bar-close-button-hover-bg-color);
}
&:active {
background-color: var(--message-bar-close-button-active-bg-color);
}
&:focus {
background-color: var(--message-bar-close-button-focus-bg-color);
}
> span {
display: inline-block;
width: 0;
height: 0;
overflow: hidden;
}
}
}
.toggler {

128
web/editor_undo_bar.js Normal file
View file

@ -0,0 +1,128 @@
/* Copyright 2024 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 "pdfjs-lib";
class EditorUndoBar {
#closeButton = null;
#container;
#eventBus = null;
#focusTimeout = null;
#initController = null;
isOpen = false;
#message;
#showController = null;
#undoButton;
static #l10nMessages = Object.freeze({
highlight: "pdfjs-editor-undo-bar-message-highlight",
freetext: "pdfjs-editor-undo-bar-message-freetext",
stamp: "pdfjs-editor-undo-bar-message-stamp",
ink: "pdfjs-editor-undo-bar-message-ink",
_multiple: "pdfjs-editor-undo-bar-message-multiple",
});
constructor({ container, message, undoButton, closeButton }, eventBus) {
this.#container = container;
this.#message = message;
this.#undoButton = undoButton;
this.#closeButton = closeButton;
this.#eventBus = eventBus;
}
destroy() {
this.#initController?.abort();
this.#initController = null;
this.hide();
}
show(undoAction, messageData) {
if (!this.#initController) {
this.#initController = new AbortController();
const opts = { signal: this.#initController.signal };
const boundHide = this.hide.bind(this);
this.#container.addEventListener("contextmenu", noContextMenu, opts);
this.#closeButton.addEventListener("click", boundHide, opts);
this.#eventBus._on("beforeprint", boundHide, opts);
this.#eventBus._on("download", boundHide, opts);
}
this.hide();
if (typeof messageData === "string") {
this.#message.setAttribute(
"data-l10n-id",
EditorUndoBar.#l10nMessages[messageData]
);
} else {
this.#message.setAttribute(
"data-l10n-id",
EditorUndoBar.#l10nMessages._multiple
);
this.#message.setAttribute(
"data-l10n-args",
JSON.stringify({ count: messageData })
);
}
this.isOpen = true;
this.#container.hidden = false;
this.#showController = new AbortController();
this.#undoButton.addEventListener(
"click",
() => {
undoAction();
this.hide();
},
{ signal: this.#showController.signal }
);
// Without the setTimeout, VoiceOver will read out the document title
// instead of the popup label.
this.#focusTimeout = setTimeout(() => {
this.#container.focus();
this.#focusTimeout = null;
}, 100);
}
hide() {
if (!this.isOpen) {
return;
}
this.isOpen = false;
this.#container.hidden = true;
this.#showController?.abort();
this.#showController = null;
if (this.#focusTimeout) {
clearTimeout(this.#focusTimeout);
this.#focusTimeout = null;
}
}
}
export { EditorUndoBar };

221
web/message_bar.css Normal file
View file

@ -0,0 +1,221 @@
/* Copyright 2024 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.
*/
.messageBar {
--closing-button-icon: url(images/messageBar_closingButton.svg);
--message-bar-close-button-color: var(--text-primary-color);
--message-bar-close-button-color-hover: var(--text-primary-color);
--message-bar-close-button-border-radius: 4px;
--message-bar-close-button-border: none;
--message-bar-close-button-hover-bg-color: rgb(21 20 26 / 0.14);
--message-bar-close-button-active-bg-color: rgb(21 20 26 / 0.21);
--message-bar-close-button-focus-bg-color: rgb(21 20 26 / 0.07);
@media (prefers-color-scheme: dark) {
--message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14);
--message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21);
--message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07);
}
@media screen and (forced-colors: active) {
--message-bar-close-button-color: ButtonText;
--message-bar-close-button-border: 1px solid ButtonText;
--message-bar-close-button-hover-bg-color: ButtonText;
--message-bar-close-button-active-bg-color: ButtonText;
--message-bar-close-button-focus-bg-color: ButtonText;
--message-bar-close-button-color-hover: HighlightText;
}
display: flex;
position: relative;
padding: 8px 8px 8px 16px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
user-select: none;
border-radius: 4px;
border: 1px solid var(--message-bar-border-color);
background: var(--message-bar-bg-color);
color: var(--message-bar-fg-color);
> div {
display: flex;
align-items: flex-start;
gap: 8px;
align-self: stretch;
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--message-bar-icon);
mask-size: cover;
background-color: var(--message-bar-icon-color);
flex-shrink: 0;
}
}
button {
cursor: pointer;
&:focus-visible {
outline: var(--focus-ring-outline);
outline-offset: 2px;
}
}
.closeButton {
width: 32px;
height: 32px;
background: none;
border-radius: var(--message-bar-close-button-border-radius);
border: var(--message-bar-close-button-border);
display: flex;
align-items: center;
justify-content: center;
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--closing-button-icon);
mask-size: cover;
background-color: var(--message-bar-close-button-color);
}
&:is(:hover, :active, :focus)::before {
background-color: var(--message-bar-close-button-color-hover);
}
&:hover {
background-color: var(--message-bar-close-button-hover-bg-color);
}
&:active {
background-color: var(--message-bar-close-button-active-bg-color);
}
&:focus {
background-color: var(--message-bar-close-button-focus-bg-color);
}
> span {
display: inline-block;
width: 0;
height: 0;
overflow: hidden;
}
}
}
#editorUndoBar {
--text-primary-color: #15141a;
--message-bar-icon: url(images/secondaryToolbarButton-documentProperties.svg);
--message-bar-icon-color: #0060df;
--message-bar-bg-color: #deeafc;
--message-bar-fg-color: var(--text-primary-color);
--message-bar-border-color: rgb(0 0 0 / 0.08);
--undo-button-bg-color: rgb(21 20 26 / 0.07);
--undo-button-bg-color-hover: rgb(21 20 26 / 0.14);
--undo-button-bg-color-active: rgb(21 20 26 / 0.21);
--undo-button-fg-color: var(--message-bar-fg-color);
--undo-button-fg-color-hover: var(--undo-button-fg-color);
--undo-button-fg-color-active: var(--undo-button-fg-color);
--focus-ring-color: #0060df;
--focus-ring-outline: 2px solid var(--focus-ring-color);
@media (prefers-color-scheme: dark) {
--text-primary-color: #fbfbfe;
--message-bar-icon-color: #73a7f3;
--message-bar-bg-color: #003070;
--message-bar-border-color: rgb(255 255 255 / 0.08);
--undo-button-bg-color: rgb(255 255 255 / 0.08);
--undo-button-bg-color-hover: rgb(255 255 255 / 0.14);
--undo-button-bg-color-active: rgb(255 255 255 / 0.21);
}
@media screen and (forced-colors: active) {
--text-primary-color: CanvasText;
--message-bar-icon-color: CanvasText;
--message-bar-bg-color: Canvas;
--message-bar-border-color: CanvasText;
--undo-button-bg-color: ButtonText;
--undo-button-bg-color-hover: SelectedItem;
--undo-button-bg-color-active: SelectedItem;
--undo-button-fg-color: ButtonFace;
--undo-button-fg-color-hover: SelectedItemText;
--undo-button-fg-color-active: SelectedItemText;
--focus-ring-color: CanvasText;
}
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
padding-block: 8px;
padding-inline: 16px 8px;
font: menu;
font-size: 15px;
cursor: default;
button {
cursor: pointer;
}
#editorUndoBarUndoButton {
border-radius: 4px;
font-weight: 590;
line-height: 19.5px;
color: var(--undo-button-fg-color);
border: none;
padding: 4px 16px;
margin-inline-start: 8px;
height: 32px;
background-color: var(--undo-button-bg-color);
&:hover {
background-color: var(--undo-button-bg-color-hover);
}
&:active {
background-color: var(--undo-button-bg-color-active);
}
}
> div {
align-items: center;
}
}

View file

@ -12,6 +12,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import url(message_bar.css);
@import url(dialog.css);
@import url(text_layer_builder.css);
@import url(annotation_layer_builder.css);

View file

@ -214,6 +214,8 @@ class PDFViewer {
#containerTopLeft = null;
#editorUndoBar = null;
#enableHWA = false;
#enableHighlightFloatingButton = false;
@ -281,6 +283,7 @@ class PDFViewer {
this.downloadManager = options.downloadManager || null;
this.findController = options.findController || null;
this.#altTextManager = options.altTextManager || null;
this.#editorUndoBar = options.editorUndoBar || null;
if (this.findController) {
this.findController.onIsPageVisible = pageNumber =>
@ -907,7 +910,8 @@ class PDFViewer {
this.#enableHighlightFloatingButton,
this.#enableUpdatedAddImage,
this.#enableNewAltTextWhenAddingImage,
this.#mlManager
this.#mlManager,
this.#editorUndoBar
);
eventBus.dispatch("annotationeditoruimanager", {
source: this,

View file

@ -688,6 +688,20 @@ See https://github.com/adobe-type-tools/cmap-resources
<!--#endif-->
</div> <!-- dialogContainer -->
<div id="editorUndoBar" class="messageBar" role="status" aria-labelledby="editorUndoBarMessage" tabindex="-1" hidden>
<div>
<div>
<span id="editorUndoBarMessage" class="description"></span>
</div>
<button id="editorUndoBarUndoButton" class="undoButton" type="button" tabindex="0" title="Undo" data-l10n-id="pdfjs-editor-undo-bar-undo-button">
<span data-l10n-id="pdfjs-editor-undo-bar-undo-button-label">Undo</span>
</button>
<button id="editorUndoBarCloseButton" class="closeButton" type="button" tabindex="0" title="Close" data-l10n-id="pdfjs-editor-undo-bar-close-button">
<span data-l10n-id="pdfjs-editor-undo-bar-close-button-label">Close</span>
</button>
</div>
</div> <!-- editorUndoBar -->
</div> <!-- outerContainer -->
<div id="printContainer"></div>
</body>

View file

@ -223,6 +223,12 @@ function getViewerConfiguration() {
editorHighlightShowAll: document.getElementById("editorHighlightShowAll"),
},
printContainer: document.getElementById("printContainer"),
editorUndoBar: {
container: document.getElementById("editorUndoBar"),
message: document.getElementById("editorUndoBarMessage"),
undoButton: document.getElementById("editorUndoBarUndoButton"),
closeButton: document.getElementById("editorUndoBarCloseButton"),
},
};
}