diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index c281f1d10..3186b9a4d 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -783,3 +783,5 @@ pdfjs-views-manager-paste-button-after = # Badge used to promote a new feature in the UI, keep it as short as possible. # It's spelled uppercase for English, but it can be translated as usual. pdfjs-new-badge-content = NEW + +pdfjs-views-manager-waiting-for-file = Uploading file… diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 166eadd8a..51f5b5faf 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -560,8 +560,138 @@ class PDFEditor { * excluded ranges (inclusive) or indices. * @property {Array} [pageIndices] * position of the pages in the final document. + * @property {number} [insertAfter] + * 0-based index in the base sequential document after which to insert the + * pages. Sequential pageInfos (those without pageIndices) have their indices + * shifted to accommodate the insertion. Cannot be combined with pageIndices. */ + /** + * Return the document-local page indices that pass the include/exclude + * filters for the given pageInfo, in document order. + * @param {PageInfo} pageInfo + * @returns {Array} + */ + #getFilteredPageIndices({ document, includePages, excludePages }) { + if (!document) { + return []; + } + let keptIndices, keptRanges, deletedIndices, deletedRanges; + for (const page of includePages || []) { + if (Array.isArray(page)) { + (keptRanges ||= []).push(page); + } else { + (keptIndices ||= new Set()).add(page); + } + } + for (const page of excludePages || []) { + if (Array.isArray(page)) { + (deletedRanges ||= []).push(page); + } else { + (deletedIndices ||= new Set()).add(page); + } + } + const indices = []; + for (let i = 0, ii = document.numPages; i < ii; i++) { + if (deletedIndices?.has(i)) { + continue; + } + if (deletedRanges) { + let isDeleted = false; + for (const [start, end] of deletedRanges) { + if (i >= start && i <= end) { + isDeleted = true; + break; + } + } + if (isDeleted) { + continue; + } + } + let takePage = false; + if (keptIndices) { + takePage = keptIndices.has(i); + } + if (!takePage && keptRanges) { + for (const [start, end] of keptRanges) { + if (i >= start && i <= end) { + takePage = true; + break; + } + } + } + if (!takePage && !keptIndices && !keptRanges) { + takePage = true; + } + if (takePage) { + indices.push(i); + } + } + return indices; + } + + /** + * Resolve insertAfter pageInfos by converting them (and sequential pageInfos) + * to explicit pageIndices, shifting indices to accommodate each insertion. + * insertAfter values are relative to the base sequential sequence (i.e. the + * concatenation of pages from pageInfos that have neither pageIndices nor + * insertAfter), so they are independent of each other. + * @param {Array} pageInfos + * @returns {Array} + */ + #resolveInsertAfterIndices(pageInfos) { + // Single pass: build the base sequential sequence and collect insertAfter + // entries, computing each pageInfo's filtered page count only once and only + // for pageInfos that actually contribute pages. + const sequence = []; // each element is the index into pageInfos + const insertAfterList = []; + for (let i = 0; i < pageInfos.length; i++) { + const info = pageInfos[i]; + if (!info.document || info.pageIndices) { + continue; + } + const count = this.#getFilteredPageIndices(info).length; + if (info.insertAfter === undefined) { + for (let j = 0; j < count; j++) { + sequence.push(i); + } + } else { + insertAfterList.push({ i, insertAfter: info.insertAfter, count }); + } + } + + // Sort by insertAfter value so that each value is interpreted relative to + // the same base sequential sequence, then insert into the sequence. + // The offset accumulates the number of pages already inserted, converting + // base-relative positions to current-sequence positions. + insertAfterList.sort((a, b) => a.insertAfter - b.insertAfter); + let offset = 0; + for (const { i, insertAfter, count } of insertAfterList) { + const insertPos = insertAfter + 1 + offset; + sequence.splice(insertPos, 0, ...new Array(count).fill(i)); + offset += count; + } + + // Map each pageInfo index to its final positions in the sequence using a + // plain array (keys are dense integers so no need for a Map). + const pageIndicesArr = new Array(pageInfos.length); + for (let pos = 0; pos < sequence.length; pos++) { + const infoIdx = sequence[pos]; + (pageIndicesArr[infoIdx] ||= []).push(pos); + } + + // Return updated pageInfos: sequential and insertAfter pageInfos now have + // explicit pageIndices; already-indexed pageInfos are left unchanged. + return pageInfos.map((info, i) => { + if (!info.document || info.pageIndices) { + return info; + } + const newInfo = { ...info, pageIndices: pageIndicesArr[i] || [] }; + delete newInfo.insertAfter; + return newInfo; + }); + } + /** * Extract pages from the given documents. * @param {Array} pageInfos @@ -574,6 +704,9 @@ class PDFEditor { * @return {Promise} */ async extractPages(pageInfos, annotationStorage, handler, task) { + if (pageInfos.some(info => info.insertAfter !== undefined)) { + pageInfos = this.#resolveInsertAfterIndices(pageInfos); + } const promises = []; let newIndex = 0; this.isSingleFile = @@ -610,57 +743,12 @@ class PDFEditor { const documentData = new DocumentData(document); allDocumentData.push(documentData); promises.push(this.#collectDocumentData(documentData)); - let keptIndices, keptRanges, deletedIndices, deletedRanges; - for (const page of includePages || []) { - if (Array.isArray(page)) { - (keptRanges ||= []).push(page); - } else { - (keptIndices ||= new Set()).add(page); - } - } - for (const page of excludePages || []) { - if (Array.isArray(page)) { - (deletedRanges ||= []).push(page); - } else { - (deletedIndices ||= new Set()).add(page); - } - } let pageIndex = 0; - for (let i = 0, ii = document.numPages; i < ii; i++) { - if (deletedIndices?.has(i)) { - continue; - } - if (deletedRanges) { - let isDeleted = false; - for (const [start, end] of deletedRanges) { - if (i >= start && i <= end) { - isDeleted = true; - break; - } - } - if (isDeleted) { - continue; - } - } - - let takePage = false; - if (keptIndices) { - takePage = keptIndices.has(i); - } - if (!takePage && keptRanges) { - for (const [start, end] of keptRanges) { - if (i >= start && i <= end) { - takePage = true; - break; - } - } - } - if (!takePage && !keptIndices && !keptRanges) { - takePage = true; - } - if (!takePage) { - continue; - } + for (const i of this.#getFilteredPageIndices({ + document, + includePages, + excludePages, + })) { let newPageIndex; if (pageIndices) { newPageIndex = pageIndices[pageIndex++]; diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 2e0cb6de6..0310cc3dd 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -40,6 +40,9 @@ import { waitForTextToBe, waitForTooltipToBe, } from "./test_utils.mjs"; +import path from "path"; + +const __dirname = import.meta.dirname; async function waitForThumbnailVisible(page, pageNums) { await showViewsManager(page); @@ -3030,4 +3033,68 @@ describe("Reorganize Pages View", () => { ); }); }); + + describe("Merge PDF", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "three_pages_with_number.pdf", + "#viewsManagerToggleButton", + "1", + null, + { enableSplitMerge: true, enableMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should merge a PDF after the current page", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + + // Navigate to page 2 so the merged PDF is inserted after it. + await page.evaluate(() => { + window.PDFViewerApplication.page = 2; + }); + await page.waitForFunction( + () => window.PDFViewerApplication.page === 2 + ); + await waitAndClick(page, getThumbnailSelector(2)); + + const handleMerged = await createPromise(page, resolve => { + window.PDFViewerApplication.eventBus._on( + "thumbnailsloaded", + resolve, + { once: true } + ); + }); + + const picker = await page.$("#viewsManagerAddFilePicker"); + await picker.uploadFile( + path.join(__dirname, "../pdfs/three_pages_with_number.pdf") + ); + await awaitPromise(handleMerged); + + // Original 3 pages + 3 merged pages = 6 pages total. + await page.waitForFunction( + () => parseInt(document.getElementById("pageNumber").max, 10) === 6 + ); + + // Pages 1–2 come from the original document, then all 3 pages of + // the merged PDF, then pages 4–6 of the original shifted to the end. + await waitForHavingContents(page, [1, 2, 1, 2, 3, 3]); + + await waitForTextToBe( + page, + "#viewsManagerStatusActionLabel", + `${FSI}3${PDI} selected` + ); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 7ba0239d7..cbd9f5330 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -901,3 +901,4 @@ !issue21068.pdf !recursiveCompositGlyf.pdf !issue19634.pdf +!three_pages_with_number.pdf diff --git a/test/pdfs/three_pages_with_number.pdf b/test/pdfs/three_pages_with_number.pdf new file mode 100755 index 000000000..7df48a980 Binary files /dev/null and b/test/pdfs/three_pages_with_number.pdf differ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 0795f9f34..79952547c 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -7018,6 +7018,116 @@ small scripts as well as for`); expect(newPdfDoc.numPages).toEqual(2); await passwordAcceptedLoadingTask.destroy(); }); + + it("insertAfter places pages at the given position", async function () { + // page_with_number.pdf has 17 pages; text on page N (1-based) is "N". + // Sequential pageInfo contributes pages 1 and 3 (0-based) at base + // positions 0 and 1. The insertAfter pageInfo inserts page 2 after + // base position 0, so the final order should be: "1" · "2" · "3". + let loadingTask = getDocument( + buildGetDocumentParams("page_with_number.pdf") + ); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([ + { document: null, includePages: [0, 2] }, + { document: null, includePages: [1], insertAfter: 0 }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + expect(pdfDoc.numPages).toEqual(3); + + for (const [pageNum, expected] of [ + [1, "1"], + [2, "2"], + [3, "3"], + ]) { + const pdfPage = await pdfDoc.getPage(pageNum); + const { items } = await pdfPage.getTextContent(); + expect(mergeText(items)) + .withContext(`Page ${pageNum}`) + .toEqual(expected); + } + + await loadingTask.destroy(); + }); + + it("insertAfter shifts sequential pageInfos across multiple entries", async function () { + // Two separate sequential pageInfos (pages 1 and 3, 0-based) form + // the base sequence at positions 0 and 1. Page 2 is inserted after + // base position 0, so both sequential entries should be shifted and + // the final order should be: "1" · "2" · "3". + let loadingTask = getDocument( + buildGetDocumentParams("page_with_number.pdf") + ); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([ + { document: null, includePages: [0] }, + { document: null, includePages: [2] }, + { document: null, includePages: [1], insertAfter: 0 }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + expect(pdfDoc.numPages).toEqual(3); + + for (const [pageNum, expected] of [ + [1, "1"], + [2, "2"], + [3, "3"], + ]) { + const pdfPage = await pdfDoc.getPage(pageNum); + const { items } = await pdfPage.getTextContent(); + expect(mergeText(items)) + .withContext(`Page ${pageNum}`) + .toEqual(expected); + } + + await loadingTask.destroy(); + }); + + it("insertAfter without includePages inserts all pages", async function () { + // Sequential pageInfo uses pages 0–5 ("1"–"6", base positions 0–5). + // The insertAfter pageInfo has no includePages so all 17 pages are + // inserted after base position 4, landing between "5" and "6". + // Final order: "1"·"2"·"3"·"4"·"5" · "1"…"17" · "6" = 23 pages. + let loadingTask = getDocument( + buildGetDocumentParams("page_with_number.pdf") + ); + let pdfDoc = await loadingTask.promise; + const data = await pdfDoc.extractPages([ + { document: null, includePages: [0, 1, 2, 3, 4, 5] }, + { document: null, insertAfter: 4 }, + ]); + await loadingTask.destroy(); + + loadingTask = getDocument(data); + pdfDoc = await loadingTask.promise; + expect(pdfDoc.numPages).toEqual(23); + + // Last page of the first sequential chunk. + let pdfPage = await pdfDoc.getPage(5); + let { items } = await pdfPage.getTextContent(); + expect(mergeText(items)).withContext("Page 5").toEqual("5"); + + // First and last of the 17 inserted pages. + pdfPage = await pdfDoc.getPage(6); + ({ items } = await pdfPage.getTextContent()); + expect(mergeText(items)).withContext("Page 6").toEqual("1"); + + pdfPage = await pdfDoc.getPage(22); + ({ items } = await pdfPage.getTextContent()); + expect(mergeText(items)).withContext("Page 22").toEqual("17"); + + // Sequential page "6" shifted to the end. + pdfPage = await pdfDoc.getPage(23); + ({ items } = await pdfPage.getTextContent()); + expect(mergeText(items)).withContext("Page 23").toEqual("6"); + + await loadingTask.destroy(); + }); }); }); }); diff --git a/web/app.js b/web/app.js index ed78bd41e..859e22da1 100644 --- a/web/app.js +++ b/web/app.js @@ -375,6 +375,7 @@ const PDFViewerApplication = { enableGuessAltText: x => x === "true", enableNewBadge: x => x === "true", enablePermissions: x => x === "true", + enableMerge: x => x === "true", enableSplitMerge: x => x === "true", enableUpdatedAddImage: x => x === "true", highlightEditorColors: x => x, @@ -461,6 +462,7 @@ const PDFViewerApplication = { foreground: AppOptions.get("pageColorsForeground"), } : null; + const enableMerge = AppOptions.get("enableMerge"); const enableSplitMerge = AppOptions.get("enableSplitMerge"); let altTextManager; @@ -601,11 +603,13 @@ const PDFViewerApplication = { pageColors, abortSignal, enableSplitMerge, + enableMerge, enableNewBadge: AppOptions.get("enableNewBadge"), statusBar: viewsManager.viewsManagerStatusBar, undoBar: viewsManager.viewsManagerUndoBar, manageMenu: viewsManager.manageMenu, - addFileButton: viewsManager.viewsManagerAddFileButton, + waitingBar: viewsManager.viewsManagerWaitingBar, + addFileComponent: viewsManager.viewsManagerAddFile, }); renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer); } @@ -765,6 +769,7 @@ const PDFViewerApplication = { elements: appConfig.viewsManager, eventBus, l10n, + enableMerge, enableSplitMerge, globalAbortSignal: abortSignal, }); @@ -2217,6 +2222,7 @@ const PDFViewerApplication = { } eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts); eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts); + eventBus._on("saveandload", this.onSaveAndLoad.bind(this), opts); }, bindWindowEvents() { @@ -2419,6 +2425,23 @@ const PDFViewerApplication = { ); }, + async onSaveAndLoad({ data: extractParams }) { + if (!this.pdfDocument) { + return; + } + const modifiedPdfBytes = await this.pdfDocument.extractPages(extractParams); + if (!modifiedPdfBytes) { + console.error( + "Something wrong happened when saving the edited PDF.\nPlease file a bug." + ); + return; + } + this.open({ + data: modifiedPdfBytes, + filename: this._docFilename, + }); + }, + _accumulateTicks(ticks, prop) { // If the direction changed, reset the accumulated ticks. if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) { diff --git a/web/app_options.js b/web/app_options.js index d2154f1d7..d84ba9336 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -232,6 +232,11 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableMerge: { + /** @type {boolean} */ + value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableNewAltTextWhenAddingImage: { /** @type {boolean} */ value: true, diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index 46c146999..dcd2e4128 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -66,6 +66,8 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15; * events. * @property {boolean} [enableNewBadge] - Enables the "new" badge for the split * and merge features. + * @property {boolean} [enableMerge] - Enables the merge feature. + * 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 @@ -74,8 +76,11 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15; * action. * @property {Object} [manageMenu] - The menu elements to manage saving edited * PDF. - * @property {HTMLButtonElement} addFileButton - The button that opens a dialog - * to add a PDF file to merge with the current one. + * @property {Object} [waitingBar] - The waiting bar elements shown during + * long-running operations. + * @property {Object} [addFileComponent] - The file picker and button used to + * add a PDF file to merge with the current one. + */ /** * Viewer control to display thumbnails for pages in a PDF document. @@ -83,6 +88,8 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15; class PDFThumbnailViewer { static #draggingScaleFactor = 0; + #enableMerge = false; + #enableSplitMerge = false; #dragAC = null; @@ -157,6 +164,8 @@ class PDFThumbnailViewer { #undoCloseButton = null; + #waitingBar = null; + #isInPasteMode = false; #hasUndoBarVisible = false; @@ -175,12 +184,14 @@ class PDFThumbnailViewer { maxCanvasDim, pageColors, abortSignal, + enableMerge, enableSplitMerge, enableNewBadge, statusBar, undoBar, + waitingBar, manageMenu, - addFileButton, + addFileComponent, }) { this.scrollableContainer = container.parentElement; this.container = container; @@ -190,6 +201,7 @@ class PDFThumbnailViewer { this.maxCanvasPixels = maxCanvasPixels; this.maxCanvasDim = maxCanvasDim; this.pageColors = pageColors || null; + this.#enableMerge = enableMerge || false; this.#enableSplitMerge = enableSplitMerge || false; this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null; this.#deselectButton = @@ -199,13 +211,11 @@ class PDFThumbnailViewer { 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; + this.#waitingBar = waitingBar || null; if (this.#enableSplitMerge && manageMenu) { const { - button, + button: menuButton, menu, copy, cut, @@ -217,19 +227,19 @@ class PDFThumbnailViewer { const newSpan = document.createElement("span"); newSpan.setAttribute("data-l10n-id", "pdfjs-new-badge-content"); newSpan.classList.add("newBadge"); - button.parentElement.before(newSpan); + menuButton.parentElement.before(newSpan); this.#newBadge = newSpan; } this.eventBus.on( "pagesloaded", () => { - button.disabled = false; + menuButton.disabled = false; }, { once: true } ); - this._manageMenu = new Menu(menu, button, [ + this._manageMenu = new Menu(menu, menuButton, [ copy, cut, del, @@ -248,7 +258,7 @@ class PDFThumbnailViewer { cut.addEventListener("click", this.#cutPages.bind(this)); this.#toggleMenuEntries(false); - button.disabled = true; + menuButton.disabled = true; this.eventBus.on("editingaction", ({ name }) => { switch (name) { @@ -301,6 +311,63 @@ class PDFThumbnailViewer { this.#updateStatus("select"); }); this.#deselectButton.classList.toggle("hidden", true); + + if (this.#enableMerge && addFileComponent) { + const { picker, button } = addFileComponent; + picker.addEventListener("change", async () => { + const file = picker.files?.[0]; + if (!file) { + return; + } + if (file.type !== "application/pdf") { + const magic = await file.slice(0, 5).text(); + if (magic !== "%PDF-") { + return; + } + } + this.#toggleBar("waiting", "pdfjs-views-manager-waiting-for-file"); + const currentPageIndex = this._currentPageNumber - 1; + const buffer = await file.bytes(); + const pagesCount = this.#pagesMapper.pagesNumber; + const data = this.hasStructuralChanges() + ? this.getStructuralChanges() + : [{ document: null }]; + data.push({ + document: buffer, + insertAfter: currentPageIndex ?? -1, + }); + this.eventBus._on( + "thumbnailsloaded", + () => { + this.#toggleBar("status"); + const newPagesCount = this.#pagesMapper.pagesNumber; + const insertedPagesCount = newPagesCount - pagesCount; + for ( + let i = currentPageIndex + 1, + ii = currentPageIndex + 1 + insertedPagesCount; + i < ii; + i++ + ) { + this._thumbnails[i].checkbox.checked = true; + this.#selectPage(i + 1, true); + } + }, + { once: true } + ); + this.#reportTelemetry({ action: "merge" }); + this.eventBus.dispatch("saveandload", { + source: this, + data, + }); + }); + button.addEventListener("click", () => { + picker.click(); + }); + this.#waitingBar.closeButton?.addEventListener("click", () => { + this.#toggleBar("status"); + picker.value = ""; + }); + } } else { manageMenu.button.hidden = true; } @@ -466,6 +533,9 @@ class PDFThumbnailViewer { const thumbnailView = this._thumbnails[this._currentPageNumber - 1]; thumbnailView.toggleCurrent(/* isCurrent = */ true); this.container.append(fragment); + this.eventBus.dispatch("thumbnailsloaded", { + source: this, + }); }) .catch(reason => { console.error("Unable to initialize thumbnail viewer", reason); @@ -830,6 +900,37 @@ class PDFThumbnailViewer { return size > 0 && size < this._thumbnails.length; } + #toggleBar(type, message, args) { + this.#statusBar.classList.toggle("hidden", type !== "status"); + this.#waitingBar.container.classList.toggle("hidden", type !== "waiting"); + this.#undoBar.classList.toggle("hidden", type !== "undo"); + this.#hasUndoBarVisible = type === "undo"; + + switch (type) { + case "waiting": + this.#waitingBar.label.setAttribute("data-l10n-id", message); + break; + case "undo": + this.#undoLabel.setAttribute("data-l10n-id", message); + if (args) { + this.#undoLabel.setAttribute("data-l10n-args", JSON.stringify(args)); + } + break; + case "status": + if (args) { + this.#statusLabel.setAttribute( + "data-l10n-args", + JSON.stringify(args) + ); + } else { + this.#statusLabel.removeAttribute("data-l10n-args"); + } + this.#newBadge?.classList.toggle("hidden", !!args); + this.#deselectButton.classList.toggle("hidden", !args); + break; + } + } + #togglePasteMode(enable) { this.#isInPasteMode = enable; if (enable) { @@ -996,21 +1097,7 @@ class PDFThumbnailViewer { ? "pdfjs-views-manager-pages-status-action-label" : "pdfjs-views-manager-pages-status-none-action-label" ); - if (count) { - this.#newBadge?.classList.add("hidden"); - this.#statusLabel.setAttribute( - "data-l10n-args", - JSON.stringify({ count }) - ); - this.#deselectButton.classList.toggle("hidden", false); - } else { - this.#newBadge?.classList.remove("hidden"); - this.#statusLabel.removeAttribute("data-l10n-args"); - this.#deselectButton.classList.toggle("hidden", true); - } - this.#statusBar.classList.toggle("hidden", false); - this.#undoBar.classList.toggle("hidden", true); - this.#hasUndoBarVisible = false; + this.#toggleBar("status", "", count ? { count } : null); return; } @@ -1026,8 +1113,7 @@ class PDFThumbnailViewer { 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 })); + this.#toggleBar("undo", l10nId, { count }); if (type === "copy") { this.#undoButton.firstElementChild.setAttribute( @@ -1042,10 +1128,6 @@ class PDFThumbnailViewer { ); this.#undoCloseButton.classList.toggle("hidden", false); } - - this.#statusBar.classList.toggle("hidden", true); - this.#undoBar.classList.toggle("hidden", false); - this.#hasUndoBarVisible = true; } #moveDraggedContainer(dx, dy) { diff --git a/web/viewer.html b/web/viewer.html index 502bb5f6d..0d5d90c7d 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -162,6 +162,7 @@ See https://github.com/adobe-type-tools/cmap-resources hidden="true" > +