diff --git a/gulpfile.mjs b/gulpfile.mjs index 539e199cff6d3..57c8c66d16461 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -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([ diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index fbff0d86a9c36..3e4a3510cf83d 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -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 diff --git a/src/display/editor/draw.js b/src/display/editor/draw.js index 3906fc4ee9f6e..d5f1c4bb02835 100644 --- a/src/display/editor/draw.js +++ b/src/display/editor/draw.js @@ -676,6 +676,7 @@ class DrawingEditor extends AnnotationEditor { signal, }); parent.toggleDrawing(); + uiManager._editorUndoBar?.hide(); if (this._currentDraw) { parent.drawLayer.updateProperties( diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 026d225742e18..50aff8624d838 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1142,6 +1142,8 @@ class AnnotationEditor { bindEvents(this, this.div, ["pointerdown"]); + this._uiManager._editorUndoBar?.hide(); + return this.div; } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index d608324885a28..cb1d9e24b74bf 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -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(); } diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 0c3f9d7242a7d..085c75e6a6c8e 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -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 }); + }) + ); + }); + }); }); diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index c6cbd1f8a30c3..19bcf30ca45c0 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -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 }); + }) + ); + }); + }); }); diff --git a/test/integration/ink_editor_spec.mjs b/test/integration/ink_editor_spec.mjs index 1f908fde78488..8754ccfd2f291 100644 --- a/test/integration/ink_editor_spec.mjs +++ b/test/integration/ink_editor_spec.mjs @@ -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 }); + }) + ); + }); + }); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index d09d9c3c37a0a..60197b5ddc96f 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -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 }); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 6138035b05122..c52f5257bb299 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -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, diff --git a/web/app.js b/web/app.js index aa4bba7558d5d..e45c954b97048 100644 --- a/web/app.js +++ b/web/app.js @@ -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; } diff --git a/web/dialog.css b/web/dialog.css index c36393fc35011..3a5ba15d44932 100644 --- a/web/dialog.css +++ b/web/dialog.css @@ -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 { diff --git a/web/editor_undo_bar.js b/web/editor_undo_bar.js new file mode 100644 index 0000000000000..aa064e3841f0c --- /dev/null +++ b/web/editor_undo_bar.js @@ -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 }; diff --git a/web/message_bar.css b/web/message_bar.css new file mode 100644 index 0000000000000..4a948d2eda055 --- /dev/null +++ b/web/message_bar.css @@ -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; + } +} diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index ec7bceb69a9aa..115a8f8532b82 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -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); diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 6f05d56fe7c09..e968db3f14810 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -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, diff --git a/web/viewer.html b/web/viewer.html index dc8020d320af6..bee574ad9fdde 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -688,6 +688,20 @@ +
+