Merge pull request #21060 from calixteman/implement_merge

Add the UI for merging PDFs (bug 2028071)
This commit is contained in:
calixteman 2026-04-13 21:37:02 +02:00 committed by GitHub
commit b82ceda22b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 496 additions and 98 deletions

View File

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

View File

@ -560,8 +560,138 @@ class PDFEditor {
* excluded ranges (inclusive) or indices.
* @property {Array<number>} [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<number>}
*/
#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<PageInfo>} pageInfos
* @returns {Array<PageInfo>}
*/
#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<PageInfo>} pageInfos
@ -574,6 +704,9 @@ class PDFEditor {
* @return {Promise<void>}
*/
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++];

View File

@ -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 12 come from the original document, then all 3 pages of
// the merged PDF, then pages 46 of the original shifted to the end.
await waitForHavingContents(page, [1, 2, 1, 2, 3, 3]);
await waitForTextToBe(
page,
"#viewsManagerStatusActionLabel",
`${FSI}3${PDI} selected`
);
})
);
});
});
});

View File

@ -901,3 +901,4 @@
!issue21068.pdf
!recursiveCompositGlyf.pdf
!issue19634.pdf
!three_pages_with_number.pdf

Binary file not shown.

View File

@ -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 05 ("1""6", base positions 05).
// 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();
});
});
});
});

View File

@ -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)) {

View File

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

View File

@ -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) {

View File

@ -162,6 +162,7 @@ See https://github.com/adobe-type-tools/cmap-resources
hidden="true"
>
<span data-l10n-id="pdfjs-views-manager-add-file-button-label"></span>
<input id="viewsManagerAddFilePicker" type="file" accept="application/pdf" />
</button>
<button
id="viewsManagerCurrentOutlineButton"

View File

@ -124,9 +124,10 @@ function getViewerConfiguration() {
outlinesView: document.getElementById("outlinesView"),
attachmentsView: document.getElementById("attachmentsView"),
layersView: document.getElementById("layersView"),
viewsManagerAddFileButton: document.getElementById(
"viewsManagerAddFileButton"
),
viewsManagerAddFile: {
button: document.getElementById("viewsManagerAddFileButton"),
picker: document.getElementById("viewsManagerAddFilePicker"),
},
viewsManagerCurrentOutlineButton: document.getElementById(
"viewsManagerCurrentOutlineButton"
),
@ -159,6 +160,13 @@ function getViewerConfiguration() {
"viewsManagerStatusUndoCloseButton"
),
},
viewsManagerWaitingBar: {
container: document.getElementById("viewsManagerStatusWaiting"),
closeButton: document.getElementById(
"viewsManagerStatusWaitingCloseButton"
),
label: document.getElementById("viewsManagerStatusWaitingLabel"),
},
manageMenu: {
button: document.getElementById("viewsManagerStatusActionButton"),
menu: document.getElementById("viewsManagerStatusActionOptions"),

View File

@ -355,8 +355,6 @@
}
#viewsManagerAddFileButton {
visibility: hidden;
background: var(--button-no-bg);
width: 32px;
height: 32px;
@ -369,6 +367,10 @@
mask-repeat: no-repeat;
mask-image: var(--views-manager-add-file-button-icon);
}
> input {
display: none;
}
}
#viewsManagerCurrentOutlineButton {
@ -388,17 +390,21 @@
}
#viewsManagerStatus {
display: flex;
align-items: center;
align-self: stretch;
justify-content: space-between;
width: auto;
display: grid;
width: 100%;
border: 1px solid var(--status-border-color);
> div {
min-height: 64px;
width: 100%;
padding-inline: 16px;
grid-area: 1 / 1;
box-sizing: border-box;
&.hidden {
visibility: hidden;
display: unset !important;
}
}
.viewsManagerStatusLabel {

View File

@ -89,7 +89,7 @@ class ViewsManager extends Sidebar {
outlinesView,
attachmentsView,
layersView,
viewsManagerAddFileButton,
viewsManagerAddFile: { button: viewsManagerAddFileButton },
viewsManagerCurrentOutlineButton,
viewsManagerSelectorButton,
viewsManagerSelectorOptions,
@ -98,6 +98,7 @@ class ViewsManager extends Sidebar {
},
eventBus,
l10n,
enableMerge = false,
enableSplitMerge = false,
globalAbortSignal,
}) {
@ -149,6 +150,10 @@ class ViewsManager extends Sidebar {
viewsManagerStatus.hidden = true;
}
this._enableSplitMerge = enableSplitMerge;
this._enableMerge = enableMerge;
if (!enableMerge) {
viewsManagerAddFileButton.hidden = true;
}
this.menu = new Menu(
viewsManagerSelectorOptions,
@ -260,10 +265,10 @@ class ViewsManager extends Sidebar {
return;
}
if (this._enableSplitMerge) {
this.viewsManagerStatus.hidden = view !== SidebarView.THUMBS;
}
this.viewsManagerAddFileButton.hidden = view !== SidebarView.THUMBS;
this.viewsManagerStatus.hidden =
!this._enableSplitMerge || view !== SidebarView.THUMBS;
this.viewsManagerAddFileButton.hidden =
!this._enableMerge || view !== SidebarView.THUMBS;
this.viewsManagerCurrentOutlineButton.hidden = view !== SidebarView.OUTLINE;
this.viewsManagerHeaderLabel.setAttribute(
"data-l10n-id",