Merge pull request #20586 from marco-c/commentundo

Bug 1999154 - Add the ability to undo comment deletion
This commit is contained in:
calixteman 2026-01-26 15:52:15 +01:00 committed by GitHub
commit 48df8a5ea2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 259 additions and 4 deletions

View File

@ -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 =

View File

@ -314,6 +314,20 @@ class Comment {
this.#deleted = false;
}
/**
* Restore the comment data (used for undo).
* @param {Object} data - The comment data to restore.
* @param {string} data.text - The comment text.
* @param {string|null} data.richText - The rich text content.
* @param {Date|null} data.date - The original date.
*/
restoreData({ text, richText, date }) {
this.#text = text;
this.#richText = richText;
this.#date = date;
this.#deleted = false;
}
setInitialText(text, richText = null) {
this.#initialText = text;
this.data = text;

View File

@ -1220,9 +1220,14 @@ class AnnotationEditor {
};
}
set comment(text) {
set comment(value) {
this.#comment ||= new Comment(this);
this.#comment.data = text;
if (typeof value === "object" && value !== null) {
// Restore full comment data (used for undo).
this.#comment.restoreData(value);
} else {
this.#comment.data = value;
}
if (this.hasComment) {
this.removeCommentButtonFromToolbar();
this.addStandaloneCommentButton();

View File

@ -1177,6 +1177,23 @@ 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 {Object} savedData - The comment data to save for undo.
*/
deleteComment(editor, savedData) {
const undo = () => {
editor.comment = savedData;
};
const cmd = () => {
this._editorUndoBar?.show(undo, "comment");
this.toggleComment(/* editor = */ null);
editor.comment = null;
};
this.addCommands({ cmd, undo, mustExec: true });
}
toggleComment(editor, isSelected, visibility = undefined) {
this.#commentManager?.toggleCommentPopup(editor, isSelected, visibility);
}

View File

@ -24,6 +24,8 @@ import {
highlightSpan,
kbModifierDown,
kbModifierUp,
kbRedo,
kbUndo,
loadAndWait,
scrollIntoView,
selectEditor,
@ -965,4 +967,213 @@ 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 });
// Capture the date before deletion
const dateBefore = await page.evaluate(
() =>
document.querySelector("#commentPopup .commentPopupTime")
?.textContent
);
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);
// Check that the date is preserved
const dateAfter = await page.evaluate(
() =>
document.querySelector("#commentPopup .commentPopupTime")
?.textContent
);
expect(dateAfter)
.withContext(`In ${browserName}`)
.toEqual(dateBefore);
})
);
});
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 kbUndo(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);
})
);
});
it("must check that the comment popup is hidden after redo", 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 redo";
await editComment(page, editorSelector, comment);
// Show the popup by clicking the comment button
await waitAndClick(
page,
`${editorSelector} .annotationCommentButton`
);
await page.waitForSelector("#commentPopup", { visible: true });
// Delete the comment
await waitAndClick(page, "button.commentPopupDelete");
await page.waitForSelector("#editorUndoBar", { visible: true });
// Undo the deletion
await kbUndo(page);
await page.waitForSelector("#editorUndoBar", { hidden: true });
// Show the popup again by clicking the comment button
await waitAndClick(
page,
`${editorSelector} .annotationCommentButton`
);
await page.waitForSelector("#commentPopup", { visible: true });
// Redo the deletion - popup should be hidden
await kbRedo(page);
await page.waitForSelector("#commentPopup", { hidden: true });
})
);
});
});
});

View File

@ -982,9 +982,15 @@ class CommentPopup {
},
},
});
this.#editor.comment = null;
this.#editor.focus();
const editor = this.#editor;
const savedData = editor.comment;
this.destroy();
if (savedData?.text) {
editor._uiManager.deleteComment(editor, savedData);
} else {
editor.comment = null;
}
editor.focus();
});
del.addEventListener("contextmenu", noContextMenu);
buttons.append(edit, del);

View File

@ -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",
});