mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-04 14:15:48 +02:00
Merge pull request #20586 from marco-c/commentundo
Bug 1999154 - Add the ability to undo comment deletion
This commit is contained in:
commit
48df8a5ea2
@ -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-ink = Drawing removed
|
||||||
pdfjs-editor-undo-bar-message-stamp = Image removed
|
pdfjs-editor-undo-bar-message-stamp = Image removed
|
||||||
pdfjs-editor-undo-bar-message-signature = Signature removed
|
pdfjs-editor-undo-bar-message-signature = Signature removed
|
||||||
|
pdfjs-editor-undo-bar-message-comment = Comment removed
|
||||||
# Variables:
|
# Variables:
|
||||||
# $count (Number) - the number of removed annotations.
|
# $count (Number) - the number of removed annotations.
|
||||||
pdfjs-editor-undo-bar-message-multiple =
|
pdfjs-editor-undo-bar-message-multiple =
|
||||||
|
|||||||
@ -314,6 +314,20 @@ class Comment {
|
|||||||
this.#deleted = false;
|
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) {
|
setInitialText(text, richText = null) {
|
||||||
this.#initialText = text;
|
this.#initialText = text;
|
||||||
this.data = text;
|
this.data = text;
|
||||||
|
|||||||
@ -1220,9 +1220,14 @@ class AnnotationEditor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
set comment(text) {
|
set comment(value) {
|
||||||
this.#comment ||= new Comment(this);
|
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) {
|
if (this.hasComment) {
|
||||||
this.removeCommentButtonFromToolbar();
|
this.removeCommentButtonFromToolbar();
|
||||||
this.addStandaloneCommentButton();
|
this.addStandaloneCommentButton();
|
||||||
|
|||||||
@ -1177,6 +1177,23 @@ class AnnotationEditorUIManager {
|
|||||||
this.#commentManager?.removeComments([editor.uid]);
|
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) {
|
toggleComment(editor, isSelected, visibility = undefined) {
|
||||||
this.#commentManager?.toggleCommentPopup(editor, isSelected, visibility);
|
this.#commentManager?.toggleCommentPopup(editor, isSelected, visibility);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import {
|
|||||||
highlightSpan,
|
highlightSpan,
|
||||||
kbModifierDown,
|
kbModifierDown,
|
||||||
kbModifierUp,
|
kbModifierUp,
|
||||||
|
kbRedo,
|
||||||
|
kbUndo,
|
||||||
loadAndWait,
|
loadAndWait,
|
||||||
scrollIntoView,
|
scrollIntoView,
|
||||||
selectEditor,
|
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 });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -982,9 +982,15 @@ class CommentPopup {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.#editor.comment = null;
|
const editor = this.#editor;
|
||||||
this.#editor.focus();
|
const savedData = editor.comment;
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
if (savedData?.text) {
|
||||||
|
editor._uiManager.deleteComment(editor, savedData);
|
||||||
|
} else {
|
||||||
|
editor.comment = null;
|
||||||
|
}
|
||||||
|
editor.focus();
|
||||||
});
|
});
|
||||||
del.addEventListener("contextmenu", noContextMenu);
|
del.addEventListener("contextmenu", noContextMenu);
|
||||||
buttons.append(edit, del);
|
buttons.append(edit, del);
|
||||||
|
|||||||
@ -40,6 +40,7 @@ class EditorUndoBar {
|
|||||||
stamp: "pdfjs-editor-undo-bar-message-stamp",
|
stamp: "pdfjs-editor-undo-bar-message-stamp",
|
||||||
ink: "pdfjs-editor-undo-bar-message-ink",
|
ink: "pdfjs-editor-undo-bar-message-ink",
|
||||||
signature: "pdfjs-editor-undo-bar-message-signature",
|
signature: "pdfjs-editor-undo-bar-message-signature",
|
||||||
|
comment: "pdfjs-editor-undo-bar-message-comment",
|
||||||
_multiple: "pdfjs-editor-undo-bar-message-multiple",
|
_multiple: "pdfjs-editor-undo-bar-message-multiple",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user