Merge pull request #20810 from calixteman/bug2010832

Add a UI to undo cut/delete and cancel a copy (bug 2021352, bug 2010832)
This commit is contained in:
calixteman 2026-03-09 11:36:37 +01:00 committed by GitHub
commit 4ef5ea9681
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 748 additions and 43 deletions

View File

@ -764,6 +764,7 @@ pdfjs-views-manager-status-warning-copy-label = Couldnt copy. Refresh page an
pdfjs-views-manager-status-warning-delete-label = Couldnt delete. Refresh page and try again.
pdfjs-views-manager-status-warning-save-label = Couldnt save. Refresh page and try again.
pdfjs-views-manager-status-undo-button-label = Undo
pdfjs-views-manager-status-done-button-label = Done
pdfjs-views-manager-status-close-button =
.title = Close
pdfjs-views-manager-status-close-button-label = Close

View File

@ -2406,6 +2406,8 @@ class WorkerTransport {
#copiedPageInfo = null;
#savedPageInfo = null;
constructor(
messageHandler,
loadingTask,
@ -2477,13 +2479,36 @@ class WorkerTransport {
return;
}
if (type === "cancelCopy") {
this.#copiedPageInfo = null;
return;
}
if (type === "delete") {
this.#savedPageInfo = {
pageCache: new Map(this.#pageCache),
pagePromises: new Map(this.#pagePromises),
};
for (const pageNum of pageNumbers) {
this.#pageCache.delete(pageNum - 1);
this.#pagePromises.delete(pageNum - 1);
}
}
if (type === "cancelDelete") {
if (this.#savedPageInfo) {
this.#pageCache = this.#savedPageInfo.pageCache;
this.#pagePromises = this.#savedPageInfo.pagePromises;
this.#savedPageInfo = null;
}
return;
}
if (type === "cleanSavedData") {
this.#savedPageInfo = null;
return;
}
const newPageCache = new Map();
const newPromiseCache = new Map();
const { pagesMapper } = this;

View File

@ -1082,6 +1082,8 @@ class PagesMapper {
*/
#copiedPageNumbers = null;
#savedData = null;
/**
* Gets the total number of pages.
* @returns {number} The number of pages.
@ -1253,6 +1255,13 @@ class PagesMapper {
const pageNumberToId = this.#pageNumberToId;
const prevIdToPageNumber = this.#idToPageNumber;
this.#savedData = {
pageNumberToId: pageNumberToId.slice(),
idToPageNumber: new Map(prevIdToPageNumber),
pageNumber: this.#pagesNumber,
prevPageNumbers: this.#prevPageNumbers.slice(),
};
this.pagesNumber -= pagesToDelete.length;
this.#init(false);
const newPageNumberToId = this.#pageNumberToId;
@ -1279,6 +1288,22 @@ class PagesMapper {
this.#updateListeners({ type: "delete", pageNumbers: pagesToDelete });
}
cancelDelete() {
if (this.#savedData) {
this.#pageNumberToId = this.#savedData.pageNumberToId;
this.#idToPageNumber = this.#savedData.idToPageNumber;
this.pagesNumber = this.#savedData.pageNumber;
this.#prevPageNumbers = this.#savedData.prevPageNumbers;
this.#savedData = null;
this.#updateListeners({ type: "cancelDelete" });
}
}
cleanSavedData() {
this.#savedData = null;
this.#updateListeners({ type: "cleanSavedData" });
}
/**
* Copies a set of pages while keeping IDnumber mappings in sync.
* @param {Uint32Array} pagesToCopy - Page numbers to copy (1-indexed).
@ -1292,6 +1317,12 @@ class PagesMapper {
this.#updateListeners({ type: "copy", pageNumbers: pagesToCopy });
}
cancelCopy() {
this.#copiedPageIds = null;
this.#copiedPageNumbers = null;
this.#updateListeners({ type: "cancelCopy" });
}
/**
* Pastes a set of pages while keeping IDnumber mappings in sync.
* @param {number} index - Zero-based insertion index in the page-number list.
@ -1323,6 +1354,7 @@ class PagesMapper {
this.#updateListeners({ type: "paste" });
this.#copiedPageIds = null;
this.#copiedPageNumbers = null;
}
/**
@ -1455,7 +1487,7 @@ class PagesMapper {
}
getMapping() {
return this.#pageNumberToId.subarray(0, this.pagesNumber);
return this.#pageNumberToId?.subarray(0, this.pagesNumber);
}
}

View File

@ -33,6 +33,7 @@ import {
showViewsManager,
waitAndClick,
waitForDOMMutation,
waitForTextToBe,
} from "./test_utils.mjs";
async function waitForThumbnailVisible(page, pageNums) {
@ -63,7 +64,7 @@ function waitForPagesEdited(page, type) {
return;
}
window.PDFViewerApplication.eventBus.off("pagesedited", listener);
resolve(Array.from(pagesMapper.getMapping()));
resolve(Array.from(pagesMapper.getMapping() || []));
};
window.PDFViewerApplication.eventBus.on("pagesedited", listener);
},
@ -164,7 +165,7 @@ describe("Reorganize Pages View", () => {
continue;
}
for (const node of mutation.addedNodes) {
if (node.classList.contains("dragMarker")) {
if (node.classList?.contains("dragMarker")) {
return true;
}
}
@ -180,7 +181,7 @@ describe("Reorganize Pages View", () => {
continue;
}
for (const node of mutation.removedNodes) {
if (node.classList.contains("dragMarker")) {
if (node.classList?.contains("dragMarker")) {
return true;
}
}
@ -559,7 +560,7 @@ describe("Reorganize Pages View", () => {
continue;
}
for (const node of mutation.addedNodes) {
if (node.classList.contains("dragMarker")) {
if (node.classList?.contains("dragMarker")) {
const rect = node.getBoundingClientRect();
return rect.width !== 0;
}
@ -1182,6 +1183,401 @@ describe("Reorganize Pages View", () => {
});
});
describe("Status label reflects number of checked thumbnails (bug 2010832)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"1",
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should update the status label when thumbnails are checked or unchecked", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
const labelSelector = "#viewsManagerStatusActionLabel";
// Initially no pages are selected.
await waitForTextToBe(page, labelSelector, "Select pages");
// Check thumbnail 1: label should read "1 selected".
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
// Check thumbnail 2: label should read "2 selected".
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(2)}) input`
);
await waitForTextToBe(page, labelSelector, `${FSI}2${PDI} selected`);
// Uncheck thumbnail 1: label should read "1 selected".
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
// Uncheck thumbnail 2: label should revert to "Select pages".
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(2)}) input`
);
await waitForTextToBe(page, labelSelector, "Select pages");
})
);
});
});
describe("Undo label reflects number of cut/deleted pages (bug 2010832)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"1",
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should show the correct undo label after cutting one or two pages", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
const undoLabelSelector = "#viewsManagerStatusUndoLabel";
// Cut 1 page and check the undo label.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
let handlePagesEdited = await waitForPagesEdited(page, "cut");
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionCut");
await awaitPromise(handlePagesEdited);
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
await waitForTextToBe(page, undoLabelSelector, "1 page cut");
// Undo the cut to restore the original state.
handlePagesEdited = await waitForPagesEdited(page);
await waitAndClick(page, "#viewsManagerStatusUndoButton");
await awaitPromise(handlePagesEdited);
// Cut 2 pages and check the undo label.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(3)}) input`
);
handlePagesEdited = await waitForPagesEdited(page, "cut");
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionCut");
await awaitPromise(handlePagesEdited);
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
await waitForTextToBe(
page,
undoLabelSelector,
`${FSI}2${PDI} pages cut`
);
})
);
});
it("should show the correct undo label after deleting one or two pages", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
const undoLabelSelector = "#viewsManagerStatusUndoLabel";
// Delete 1 page and check the undo label.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
let handlePagesEdited = await waitForPagesEdited(page);
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionDelete");
await awaitPromise(handlePagesEdited);
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
await waitForTextToBe(page, undoLabelSelector, "1 page deleted");
// Undo the deletion to restore the original state.
handlePagesEdited = await waitForPagesEdited(page);
await waitAndClick(page, "#viewsManagerStatusUndoButton");
await awaitPromise(handlePagesEdited);
// Delete 2 pages and check the undo label.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(3)}) input`
);
handlePagesEdited = await waitForPagesEdited(page);
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionDelete");
await awaitPromise(handlePagesEdited);
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
await waitForTextToBe(
page,
undoLabelSelector,
`${FSI}2${PDI} pages deleted`
);
})
);
});
});
describe("Closing the undo bar after a cut is equivalent to a delete (bug 2010832)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"1",
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should permanently remove the cut page when the undo bar is closed", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
// Cut page 1.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
let handlePagesEdited = await waitForPagesEdited(page, "cut");
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionCut");
await awaitPromise(handlePagesEdited);
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
// Close the undo bar instead of undoing.
handlePagesEdited = await waitForPagesEdited(page, "cleanSavedData");
await waitAndClick(page, "#viewsManagerStatusUndoCloseButton");
const pageIndices = await awaitPromise(handlePagesEdited);
// The result must equal a plain deletion of page 1.
const expected = [
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
];
expect(pageIndices)
.withContext(`In ${browserName}`)
.toEqual(expected);
await page.waitForSelector("#viewsManagerStatusUndo", {
hidden: true,
});
await waitForHavingContents(page, expected);
})
);
});
});
describe("Closing the undo bar after a delete effectively deletes the page (bug 2010832)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"1",
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should permanently remove the deleted page when the undo bar is closed", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
// Delete page 1.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
let handlePagesEdited = await waitForPagesEdited(page);
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionDelete");
await awaitPromise(handlePagesEdited);
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
// Close the undo bar instead of undoing.
handlePagesEdited = await waitForPagesEdited(page, "cleanSavedData");
await waitAndClick(page, "#viewsManagerStatusUndoCloseButton");
const pageIndices = await awaitPromise(handlePagesEdited);
// The page must be effectively deleted.
const expected = [
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
];
expect(pageIndices)
.withContext(`In ${browserName}`)
.toEqual(expected);
await page.waitForSelector("#viewsManagerStatusUndo", {
hidden: true,
});
await waitForHavingContents(page, expected);
})
);
});
});
describe("Clicking Done after copying removes paste buttons (bug 2010832)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"1",
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should show a Done button after copy and remove paste buttons when clicked", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
// Copy page 1.
await waitAndClick(
page,
`.thumbnail:has(${getThumbnailSelector(1)}) input`
);
const handlePagesEdited = await waitForPagesEdited(page, "copy");
await waitAndClick(page, "#viewsManagerStatusActionButton");
await waitAndClick(page, "#viewsManagerStatusActionCopy");
await awaitPromise(handlePagesEdited);
// The undo bar must appear with a "Done" label (not "Undo").
await page.waitForSelector("#viewsManagerStatusUndo", {
visible: true,
});
await waitForTextToBe(
page,
"#viewsManagerStatusUndoLabel",
"1 page copied"
);
await waitForTextToBe(
page,
"#viewsManagerStatusUndoButton span[data-l10n-id]",
"Done"
);
// The close button must be hidden for copy.
const closeHidden = await page.$eval(
"#viewsManagerStatusUndoCloseButton",
el => el.classList.contains("hidden")
);
expect(closeHidden).withContext(`In ${browserName}`).toBeTrue();
// Paste buttons must be present.
await page.waitForSelector("button.thumbnailPasteButton");
// Click Done and wait for the cancelCopy pagesedited event.
const handleCancelCopy = await waitForPagesEdited(page, "cancelCopy");
await waitAndClick(page, "#viewsManagerStatusUndoButton");
await awaitPromise(handleCancelCopy);
// Undo bar must be hidden and paste buttons must be gone.
await page.waitForSelector("#viewsManagerStatusUndo", {
hidden: true,
});
await page.waitForSelector("button.thumbnailPasteButton", {
hidden: true,
});
const pasteButtons = await page.$$("button.thumbnailPasteButton");
expect(pasteButtons.length).withContext(`In ${browserName}`).toBe(0);
})
);
});
});
describe("Extract some pages from a pdf", () => {
let pages;

View File

@ -890,6 +890,15 @@ function waitForNoElement(page, selector) {
);
}
function waitForTextToBe(page, selector, text) {
return page.waitForFunction(
(sel, str) => document.querySelector(sel)?.textContent.trim() === str,
{},
selector,
text
);
}
function isCanvasMonochrome(page, pageNumber, rectangle, color) {
return page.evaluate(
(rect, pageN, col) => {
@ -1071,6 +1080,7 @@ export {
waitForSelectedEditor,
waitForSerialized,
waitForStorageEntries,
waitForTextToBe,
waitForTimeout,
waitForUnselectedEditor,
};

View File

@ -589,8 +589,9 @@ const PDFViewerApplication = {
pdfScriptingManager.setViewer(pdfViewer);
if (appConfig.viewsManager?.thumbnailsView) {
const { viewsManager } = appConfig;
this.pdfThumbnailViewer = new PDFThumbnailViewer({
container: appConfig.viewsManager.thumbnailsView,
container: viewsManager.thumbnailsView,
eventBus,
renderingQueue,
linkService,
@ -600,8 +601,10 @@ const PDFViewerApplication = {
abortSignal,
enableHWA,
enableSplitMerge,
manageMenu: appConfig.viewsManager.manageMenu,
addFileButton: appConfig.viewsManager.viewsManagerAddFileButton,
statusBar: viewsManager.viewsManagerStatusBar,
undoBar: viewsManager.viewsManagerUndoBar,
manageMenu: viewsManager.manageMenu,
addFileButton: viewsManager.viewsManagerAddFileButton,
});
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
}

View File

@ -424,6 +424,8 @@ class PDFFindController {
#copiedExtractTextPromises = null;
#savedExtractTextPromises = null;
/**
* @param {PDFFindControllerOptions} options
*/
@ -1146,10 +1148,29 @@ class PDFFindController {
return;
}
if (type === "cancelCopy") {
this.#copiedExtractTextPromises = null;
return;
}
if (type === "delete") {
this.#savedExtractTextPromises = this._extractTextPromises;
}
if (type === "cancelDelete") {
this._extractTextPromises = this.#savedExtractTextPromises;
return;
}
if (type === "cleanSavedData") {
this.#savedExtractTextPromises = null;
return;
}
this.#onFindBarClose();
this._dirtyMatch = true;
const prevTextPromises = this._extractTextPromises;
const extractTextPromises = (this._extractTextPromises.length = []);
const extractTextPromises = (this._extractTextPromises = []);
for (let i = 1, ii = pagesMapper.length; i <= ii; i++) {
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
if (prevPageNumber < 0) {

View File

@ -68,6 +68,10 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
* rendering. The default value is `false`.
* @property {boolean} [enableSplitMerge] - Enables split and merge features.
* The default value is `false`.
* @property {Object} [statusBar] - The status bar elements to manage the status
* label and action when editing pages.
* @property {Object} [undoBar] - The undo bar elements to manage the undo
* action.
* @property {Object} [manageMenu] - The menu elements to manage saving edited
* PDF.
* @property {HTMLButtonElement} addFileButton - The button that opens a dialog
@ -123,6 +127,10 @@ class PDFThumbnailViewer {
#copiedThumbnails = null;
#savedThumbnails = null;
#deletedPageNumbers = null;
#copiedPageNumbers = null;
#boundPastePages = this.#pastePages.bind(this);
@ -139,6 +147,18 @@ class PDFThumbnailViewer {
hasSelectedPages: false,
};
#statusLabel = null;
#statusBar = null;
#undoBar = null;
#undoLabel = null;
#undoButton = null;
#undoCloseButton = null;
/**
* @param {PDFThumbnailViewerOptions} options
*/
@ -153,6 +173,8 @@ class PDFThumbnailViewer {
abortSignal,
enableHWA,
enableSplitMerge,
statusBar,
undoBar,
manageMenu,
addFileButton,
}) {
@ -166,6 +188,13 @@ class PDFThumbnailViewer {
this.pageColors = pageColors || null;
this.enableHWA = enableHWA || false;
this.#enableSplitMerge = enableSplitMerge || false;
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
this.#statusBar = statusBar?.viewsManagerStatusAction || null;
this.#undoBar = undoBar?.viewsManagerStatusUndo || null;
this.#undoLabel = undoBar?.viewsManagerStatusUndoLabel || null;
this.#undoButton = undoBar?.viewsManagerStatusUndoButton || null;
this.#undoCloseButton = undoBar?.viewsManagerStatusUndoCloseButton || null;
// TODO: uncomment when the "add file" feature is implemented.
// this.#addFileButton = addFileButton;
@ -183,7 +212,7 @@ class PDFThumbnailViewer {
this.#manageSaveAsButton = saveAs;
saveAs.addEventListener("click", this.#saveExtractedPages.bind(this));
this.#manageDeleteButton = del;
del.addEventListener("click", this.#deletePages.bind(this));
del.addEventListener("click", this.#deletePages.bind(this, "delete"));
this.#manageCopyButton = copy;
copy.addEventListener("click", this.#copyPages.bind(this));
this.#manageCutButton = cut;
@ -201,13 +230,19 @@ class PDFThumbnailViewer {
this.#cutPages();
break;
case "deletePage":
this.#deletePages();
this.#deletePages("delete");
break;
case "savePage":
this.#saveExtractedPages();
break;
}
});
this.#undoButton?.addEventListener("click", this.#undo.bind(this));
this.#undoCloseButton?.addEventListener(
"click",
this.#dismissUndo.bind(this)
);
} else {
manageMenu.button.hidden = true;
}
@ -492,17 +527,14 @@ class PDFThumbnailViewer {
#updateThumbnails(currentPageNumber) {
let newCurrentPageNumber = 0;
const pagesMapper = this.#pagesMapper;
this.container.replaceChildren();
const prevThumbnails = this._thumbnails;
const prevThumbnails = (this.#savedThumbnails = this._thumbnails);
const newThumbnails = (this._thumbnails = []);
const fragment = document.createDocumentFragment();
const isCut = this.#isCut;
const oldThumbnails = new Set(prevThumbnails);
for (let i = 1, ii = pagesMapper.pagesNumber; i <= ii; i++) {
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
if (prevPageNumber < 0) {
let thumbnail = this.#copiedThumbnails.get(-prevPageNumber);
oldThumbnails.delete(thumbnail);
thumbnail.checkbox.checked = false;
if (isCut) {
thumbnail.updateId(i);
@ -519,14 +551,10 @@ class PDFThumbnailViewer {
const newThumbnail = prevThumbnails[prevPageNumber - 1];
newThumbnails.push(newThumbnail);
newThumbnail.updateId(i);
oldThumbnails.delete(newThumbnail);
newThumbnail.checkbox.checked = false;
fragment.append(newThumbnail.div);
}
this.container.append(fragment);
for (const oldThumbnail of oldThumbnails) {
oldThumbnail.destroy();
}
this.container.replaceChildren(fragment);
return newCurrentPageNumber;
}
@ -676,6 +704,80 @@ class PDFThumbnailViewer {
}, 0);
}
#undo() {
if (this.#copiedThumbnails) {
// We undo a copy or a cut.
this.#copiedThumbnails = null;
this.#pagesMapper.cancelCopy();
this.#clearSelection();
this.#toggleMenuEntries(false);
this.#updateStatus("select");
this.#togglePasteMode(false);
this.eventBus.dispatch("pagesedited", {
source: this,
pagesMapper: this.#pagesMapper,
type: "cancelCopy",
});
}
this.#isCut = false;
if (this.#savedThumbnails) {
const fragment = document.createDocumentFragment();
for (let i = 1, ii = this.#savedThumbnails.length; i <= ii; i++) {
const thumbnail = this.#savedThumbnails[i - 1];
thumbnail.updateId(i);
thumbnail.checkbox.checked = false;
fragment.append(thumbnail.div);
}
this.container.replaceChildren(fragment);
this._thumbnails = this.#savedThumbnails;
this.#savedThumbnails = null;
this.#pagesMapper.cancelDelete();
this.eventBus.dispatch("pagesedited", {
source: this,
pagesMapper: this.#pagesMapper,
type: "cancelDelete",
});
}
}
#dismissUndo() {
this.#copiedThumbnails = null;
if (this.#deletedPageNumbers) {
for (const pageNumber of this.#deletedPageNumbers) {
this.#savedThumbnails[pageNumber - 1].destroy();
}
this.#deletedPageNumbers = null;
this.#savedThumbnails = null;
}
this.#isCut = false;
this.#updateStatus("select");
this.#togglePasteMode(false);
this.#pagesMapper.cleanSavedData();
this.eventBus.dispatch("pagesedited", {
source: this,
pagesMapper: this.#pagesMapper,
type: "cleanSavedData",
});
}
#togglePasteMode(enable) {
if (enable) {
this.container.classList.add("pasteMode");
for (const thumbnail of this._thumbnails) {
thumbnail.addPasteButton(this.#boundPastePages);
}
} else {
this.container.classList.remove("pasteMode");
for (const thumbnail of this._thumbnails) {
thumbnail.removePasteButton();
}
}
}
#saveExtractedPages() {
this.eventBus.dispatch("saveextractedpages", {
source: this,
@ -686,12 +788,13 @@ class PDFThumbnailViewer {
}
#copyPages(clearSelection = true) {
this.#updateStatus(this.#isCut ? "cut" : "copy");
const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from(
this.#selectedPages
).sort((a, b) => a - b));
const pagesMapper = this.#pagesMapper;
pagesMapper.copyPages(pageNumbersToCopy);
this.#copiedThumbnails ||= new Map();
this.#copiedThumbnails = new Map();
for (const pageNumber of pageNumbersToCopy) {
this.#copiedThumbnails.set(pageNumber, this._thumbnails[pageNumber - 1]);
}
@ -704,10 +807,7 @@ class PDFThumbnailViewer {
if (clearSelection) {
this.#clearSelection();
}
for (const thumbnail of this._thumbnails) {
thumbnail.addPasteButton(this.#boundPastePages);
}
this.container.classList.add("pasteMode");
this.#togglePasteMode(true);
this.#toggleMenuEntries(false);
}
@ -718,10 +818,7 @@ class PDFThumbnailViewer {
}
#pastePages(index) {
this.container.classList.remove("pasteMode");
for (const thumbnail of this._thumbnails) {
thumbnail.removePasteButton();
}
this.#togglePasteMode(false);
this.#toggleMenuEntries(true);
const pagesMapper = this.#pagesMapper;
@ -744,6 +841,7 @@ class PDFThumbnailViewer {
this.#copiedThumbnails = null;
this.#isCut = false;
this.#updateMenuEntries();
this.#updateStatus("select");
this.#updateCurrentPage(currentPageNumber);
}
@ -753,11 +851,16 @@ class PDFThumbnailViewer {
if (selectedPages.size === 0) {
return;
}
if (type === "delete") {
this.#updateStatus("delete");
}
const pagesMapper = this.#pagesMapper;
let currentPageNumber = selectedPages.has(this._currentPageNumber)
? 0
: this._currentPageNumber;
const pagesToDelete = Uint32Array.from(selectedPages).sort((a, b) => a - b);
const pagesToDelete = (this.#deletedPageNumbers = Uint32Array.from(
selectedPages
).sort((a, b) => a - b));
pagesMapper.deletePages(pagesToDelete);
currentPageNumber = this.#updateThumbnails(currentPageNumber);
@ -793,6 +896,64 @@ class PDFThumbnailViewer {
!enable;
}
#updateStatus(type) {
if (!this.#statusBar || !this.#undoBar) {
return;
}
const count = this.#selectedPages?.size || 0;
if (type === "select") {
this.#statusLabel.setAttribute(
"data-l10n-id",
count
? "pdfjs-views-manager-pages-status-action-label"
: "pdfjs-views-manager-pages-status-none-action-label"
);
if (count) {
this.#statusLabel.setAttribute(
"data-l10n-args",
JSON.stringify({ count })
);
} else {
this.#statusLabel.removeAttribute("data-l10n-args");
}
this.#statusBar.classList.toggle("hidden", false);
this.#undoBar.classList.toggle("hidden", true);
return;
}
let l10nId;
switch (type) {
case "copy":
l10nId = "pdfjs-views-manager-pages-status-undo-copy-label";
break;
case "cut":
l10nId = "pdfjs-views-manager-status-undo-cut-label";
break;
case "delete":
l10nId = "pdfjs-views-manager-pages-status-undo-delete-label";
break;
}
this.#undoLabel.setAttribute("data-l10n-id", l10nId);
this.#undoLabel.setAttribute("data-l10n-args", JSON.stringify({ count }));
if (type === "copy") {
this.#undoButton.firstElementChild.setAttribute(
"data-l10n-id",
"pdfjs-views-manager-status-done-button-label"
);
this.#undoCloseButton.classList.toggle("hidden", true);
} else {
this.#undoButton.firstElementChild.setAttribute(
"data-l10n-id",
"pdfjs-views-manager-status-undo-button-label"
);
this.#undoCloseButton.classList.toggle("hidden", false);
}
this.#statusBar.classList.toggle("hidden", true);
this.#undoBar.classList.toggle("hidden", false);
}
#moveDraggedContainer(dx, dy) {
if (this.#isOneColumnView) {
dx = 0;
@ -1047,6 +1208,7 @@ class PDFThumbnailViewer {
set.delete(pageNumber);
}
this.#updateMenuEntries();
this.#updateStatus("select");
}
#addDragListeners() {

View File

@ -290,6 +290,10 @@ class PDFViewer {
#copiedPageViews = null;
#savedPageViews = null;
#deletedPageNumbers = null;
/**
* @param {PDFViewerOptions} options
*/
@ -1187,11 +1191,42 @@ class PDFViewer {
return;
}
if (type === "cancelCopy") {
this.#copiedPageViews = null;
return;
}
const isCut = type === "cut";
if (isCut || type === "delete") {
for (const pageNum of pageNumbers) {
this._pages[pageNum - 1].deleteMe(isCut);
this.#savedPageViews = this._pages;
this.#deletedPageNumbers = pageNumbers;
}
if (type === "cancelDelete") {
const viewerElement =
this._scrollMode === ScrollMode.PAGE ? null : this.viewer;
if (viewerElement) {
const fragment = document.createDocumentFragment();
for (let i = 0, ii = this.#savedPageViews.length; i < ii; i++) {
const page = this.#savedPageViews[i];
page.updatePageNumber(i + 1);
fragment.append(page.div);
}
viewerElement.replaceChildren(fragment);
}
this._pages = this.#savedPageViews;
this.#savedPageViews = null;
this.#deletedPageNumbers = null;
return;
}
if (type === "cleanSavedData") {
for (const pageNum of this.#deletedPageNumbers) {
this.#savedPageViews[pageNum - 1].deleteMe();
}
this.#savedPageViews = null;
this.#deletedPageNumbers = null;
return;
}
this._currentPageNumber = 0;
@ -1221,14 +1256,11 @@ class PDFViewer {
const viewerElement =
this._scrollMode === ScrollMode.PAGE ? null : this.viewer;
if (viewerElement) {
viewerElement.replaceChildren();
const fragment = document.createDocumentFragment();
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
const { div } = newPages[i];
div.setAttribute("data-page-number", i + 1);
for (const { div } of newPages) {
fragment.append(div);
}
viewerElement.append(fragment);
viewerElement.replaceChildren(fragment);
}
setTimeout(() => {

View File

@ -230,7 +230,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
</div>
<div id="viewsManagerStatusUndo" class="hidden">
<span class="viewsManagerStatusLabel" data-l10n-id="pdfjs-views-manager-status-undo-cut-label" data-l10n-args='{"count": 0}'></span>
<span id="viewsManagerStatusUndoLabel" class="viewsManagerStatusLabel"></span>
<div>
<button id="viewsManagerStatusUndoButton" class="viewsManagerButton" type="button" tabindex="0">
<span data-l10n-id="pdfjs-views-manager-status-undo-button-label"></span>
@ -247,7 +247,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
</div>
<div id="viewsManagerStatusWarning" class="hidden">
<span class="viewsManagerStatusLabel"></span>
<span id="viewsManagerStatusWarningLabel" class="viewsManagerStatusLabel"></span>
<button
id="viewsManagerStatusWarningCloseButton"
class="toolbarButton viewsManagerButton viewsCloseButton"
@ -259,7 +259,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</button>
</div>
<div id="viewsManagerStatusWaiting" class="hidden">
<span class="viewsManagerStatusLabel"></span>
<span id="viewsManagerStatusWaitingLabel" class="viewsManagerStatusLabel"></span>
<button
id="viewsManagerStatusWaitingCloseButton"
class="toolbarButton viewsManagerButton viewsCloseButton"

View File

@ -134,6 +134,28 @@ function getViewerConfiguration() {
"viewsManagerHeaderLabel"
),
viewsManagerStatus: document.getElementById("viewsManagerStatus"),
viewsManagerStatusBar: {
viewsManagerStatusAction: document.getElementById(
"viewsManagerStatusAction"
),
viewsManagerStatusActionLabel: document.getElementById(
"viewsManagerStatusActionLabel"
),
},
viewsManagerUndoBar: {
viewsManagerStatusUndo: document.getElementById(
"viewsManagerStatusUndo"
),
viewsManagerStatusUndoLabel: document.getElementById(
"viewsManagerStatusUndoLabel"
),
viewsManagerStatusUndoButton: document.getElementById(
"viewsManagerStatusUndoButton"
),
viewsManagerStatusUndoCloseButton: document.getElementById(
"viewsManagerStatusUndoCloseButton"
),
},
manageMenu: {
button: document.getElementById("viewsManagerStatusActionButton"),
menu: document.getElementById("viewsManagerStatusActionOptions"),

View File

@ -226,10 +226,11 @@
}
&.viewsCloseButton {
width: 32px;
height: 32px;
width: 26px;
height: 26px;
padding: 4px;
border-radius: 8px;
background: none;
&::before {
mask-image: var(--close-button-icon);
@ -402,7 +403,7 @@
align-self: stretch;
background-color: var(--status-actions-bg);
> span.selected::before {
> span[data-l10n-args]::before {
content: "";
display: inline-block;
width: var(--icon-size);