diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index 3b3760605..9c6636de7 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -561,6 +561,7 @@ pdfjs-editor-undo-bar-message-freetext = Text removed pdfjs-editor-undo-bar-message-ink = Drawing removed pdfjs-editor-undo-bar-message-stamp = Image removed pdfjs-editor-undo-bar-message-signature = Signature removed +pdfjs-editor-undo-bar-message-comment = Comment removed # Variables: # $count (Number) - the number of removed annotations. pdfjs-editor-undo-bar-message-multiple = diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 84d2f23c2..feafa2003 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1177,6 +1177,22 @@ class AnnotationEditorUIManager { this.#commentManager?.removeComments([editor.uid]); } + /** + * Delete a comment from an editor with undo support. + * @param {AnnotationEditor} editor - The editor whose comment to delete. + * @param {string} savedComment - The comment text to save for undo. + */ + deleteComment(editor, savedComment) { + const undo = () => { + editor.comment = savedComment; + }; + const cmd = () => { + this._editorUndoBar?.show(undo, "comment"); + editor.comment = null; + }; + this.addCommands({ cmd, undo, mustExec: true }); + } + toggleComment(editor, isSelected, visibility = undefined) { this.#commentManager?.toggleCommentPopup(editor, isSelected, visibility); } diff --git a/test/integration/comment_spec.mjs b/test/integration/comment_spec.mjs index fc97906ac..36bebb3b6 100644 --- a/test/integration/comment_spec.mjs +++ b/test/integration/comment_spec.mjs @@ -965,4 +965,159 @@ describe("Comment", () => { ); }); }); + + describe("Undo deletion popup for comments (bug 1999154)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".annotationEditorLayer", + "page-fit", + null, + { enableComment: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a comment can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + await highlightSpan(page, 1, "Abstract"); + const editorSelector = getEditorSelector(0); + const comment = "Test comment for undo"; + await editComment(page, editorSelector, comment); + + // Stay in highlight mode - don't disable it + await waitAndClick( + page, + `${editorSelector} .annotationCommentButton` + ); + + await page.waitForSelector("#commentPopup", { visible: true }); + await waitAndClick(page, "button.commentPopupDelete"); + + await page.waitForSelector("#editorUndoBar", { visible: true }); + await page.waitForSelector("#editorUndoBarUndoButton", { + visible: true, + }); + await page.click("#editorUndoBarUndoButton"); + + // Check that the comment is restored by hovering to show the popup + await page.hover(`${editorSelector} .annotationCommentButton`); + await page.waitForSelector("#commentPopup", { visible: true }); + const popupText = await page.evaluate( + () => + document.querySelector("#commentPopup .commentPopupText") + ?.textContent + ); + expect(popupText).withContext(`In ${browserName}`).toEqual(comment); + }) + ); + }); + + it("must check that the undo deletion popup displays 'Comment removed' message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + await highlightSpan(page, 1, "Abstract"); + const editorSelector = getEditorSelector(0); + await editComment(page, editorSelector, "Test comment"); + + // Stay in highlight mode - don't disable it + await waitAndClick( + page, + `${editorSelector} .annotationCommentButton` + ); + + await page.waitForSelector("#commentPopup", { visible: true }); + await waitAndClick(page, "button.commentPopupDelete"); + + 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) + .withContext(`In ${browserName}`) + .toContain("Comment removed"); + }) + ); + }); + + it("must check that the undo bar closes when clicking the close button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + await highlightSpan(page, 1, "Abstract"); + const editorSelector = getEditorSelector(0); + await editComment(page, editorSelector, "Test comment"); + + // Stay in highlight mode - don't disable it + await waitAndClick( + page, + `${editorSelector} .annotationCommentButton` + ); + + await page.waitForSelector("#commentPopup", { visible: true }); + await waitAndClick(page, "button.commentPopupDelete"); + + await page.waitForSelector("#editorUndoBar", { visible: true }); + await waitAndClick(page, "#editorUndoBarCloseButton"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that deleting a comment can be undone using Ctrl+Z", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + await highlightSpan(page, 1, "Abstract"); + const editorSelector = getEditorSelector(0); + const comment = "Test comment for Ctrl+Z undo"; + await editComment(page, editorSelector, comment); + + // Stay in highlight mode - don't disable it + await waitAndClick( + page, + `${editorSelector} .annotationCommentButton` + ); + + await page.waitForSelector("#commentPopup", { visible: true }); + await waitAndClick(page, "button.commentPopupDelete"); + + await page.waitForSelector("#editorUndoBar", { visible: true }); + + // Use Ctrl+Z to undo + await kbModifierDown(page); + await page.keyboard.press("z"); + await kbModifierUp(page); + + // The undo bar should be hidden after undo + await page.waitForSelector("#editorUndoBar", { hidden: true }); + + // Check that the comment is restored by hovering to show the popup + await page.hover(`${editorSelector} .annotationCommentButton`); + await page.waitForSelector("#commentPopup", { visible: true }); + const popupText = await page.evaluate( + () => + document.querySelector("#commentPopup .commentPopupText") + ?.textContent + ); + expect(popupText).withContext(`In ${browserName}`).toEqual(comment); + }) + ); + }); + }); }); diff --git a/web/comment_manager.js b/web/comment_manager.js index 0b7bb268d..438d85f78 100644 --- a/web/comment_manager.js +++ b/web/comment_manager.js @@ -982,9 +982,15 @@ class CommentPopup { }, }, }); - this.#editor.comment = null; - this.#editor.focus(); + const savedComment = this.#editor.comment?.text; + const editor = this.#editor; this.destroy(); + if (savedComment) { + editor._uiManager.deleteComment(editor, savedComment); + } else { + editor.comment = null; + } + editor.focus(); }); del.addEventListener("contextmenu", noContextMenu); buttons.append(edit, del); diff --git a/web/editor_undo_bar.js b/web/editor_undo_bar.js index 096547062..1cb3ed90a 100644 --- a/web/editor_undo_bar.js +++ b/web/editor_undo_bar.js @@ -40,6 +40,7 @@ class EditorUndoBar { stamp: "pdfjs-editor-undo-bar-message-stamp", ink: "pdfjs-editor-undo-bar-message-ink", signature: "pdfjs-editor-undo-bar-message-signature", + comment: "pdfjs-editor-undo-bar-message-comment", _multiple: "pdfjs-editor-undo-bar-message-multiple", });