mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-26 01:55:48 +02:00
Add the UI for merging PDFs (bug 2028071)
This commit is contained in:
parent
96debf0c81
commit
8c9b819b4e
@ -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.
|
# 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.
|
# It's spelled uppercase for English, but it can be translated as usual.
|
||||||
pdfjs-new-badge-content = NEW
|
pdfjs-new-badge-content = NEW
|
||||||
|
|
||||||
|
pdfjs-views-manager-waiting-for-file = Uploading file…
|
||||||
|
|||||||
@ -560,8 +560,138 @@ class PDFEditor {
|
|||||||
* excluded ranges (inclusive) or indices.
|
* excluded ranges (inclusive) or indices.
|
||||||
* @property {Array<number>} [pageIndices]
|
* @property {Array<number>} [pageIndices]
|
||||||
* position of the pages in the final document.
|
* 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.
|
* Extract pages from the given documents.
|
||||||
* @param {Array<PageInfo>} pageInfos
|
* @param {Array<PageInfo>} pageInfos
|
||||||
@ -574,6 +704,9 @@ class PDFEditor {
|
|||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async extractPages(pageInfos, annotationStorage, handler, task) {
|
async extractPages(pageInfos, annotationStorage, handler, task) {
|
||||||
|
if (pageInfos.some(info => info.insertAfter !== undefined)) {
|
||||||
|
pageInfos = this.#resolveInsertAfterIndices(pageInfos);
|
||||||
|
}
|
||||||
const promises = [];
|
const promises = [];
|
||||||
let newIndex = 0;
|
let newIndex = 0;
|
||||||
this.isSingleFile =
|
this.isSingleFile =
|
||||||
@ -610,57 +743,12 @@ class PDFEditor {
|
|||||||
const documentData = new DocumentData(document);
|
const documentData = new DocumentData(document);
|
||||||
allDocumentData.push(documentData);
|
allDocumentData.push(documentData);
|
||||||
promises.push(this.#collectDocumentData(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;
|
let pageIndex = 0;
|
||||||
for (let i = 0, ii = document.numPages; i < ii; i++) {
|
for (const i of this.#getFilteredPageIndices({
|
||||||
if (deletedIndices?.has(i)) {
|
document,
|
||||||
continue;
|
includePages,
|
||||||
}
|
excludePages,
|
||||||
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;
|
|
||||||
}
|
|
||||||
let newPageIndex;
|
let newPageIndex;
|
||||||
if (pageIndices) {
|
if (pageIndices) {
|
||||||
newPageIndex = pageIndices[pageIndex++];
|
newPageIndex = pageIndices[pageIndex++];
|
||||||
|
|||||||
@ -40,6 +40,9 @@ import {
|
|||||||
waitForTextToBe,
|
waitForTextToBe,
|
||||||
waitForTooltipToBe,
|
waitForTooltipToBe,
|
||||||
} from "./test_utils.mjs";
|
} from "./test_utils.mjs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const __dirname = import.meta.dirname;
|
||||||
|
|
||||||
async function waitForThumbnailVisible(page, pageNums) {
|
async function waitForThumbnailVisible(page, pageNums) {
|
||||||
await showViewsManager(page);
|
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`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -901,3 +901,4 @@
|
|||||||
!issue21068.pdf
|
!issue21068.pdf
|
||||||
!recursiveCompositGlyf.pdf
|
!recursiveCompositGlyf.pdf
|
||||||
!issue19634.pdf
|
!issue19634.pdf
|
||||||
|
!three_pages_with_number.pdf
|
||||||
|
|||||||
BIN
test/pdfs/three_pages_with_number.pdf
Executable file
BIN
test/pdfs/three_pages_with_number.pdf
Executable file
Binary file not shown.
@ -7018,6 +7018,116 @@ small scripts as well as for`);
|
|||||||
expect(newPdfDoc.numPages).toEqual(2);
|
expect(newPdfDoc.numPages).toEqual(2);
|
||||||
await passwordAcceptedLoadingTask.destroy();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
25
web/app.js
25
web/app.js
@ -375,6 +375,7 @@ const PDFViewerApplication = {
|
|||||||
enableGuessAltText: x => x === "true",
|
enableGuessAltText: x => x === "true",
|
||||||
enableNewBadge: x => x === "true",
|
enableNewBadge: x => x === "true",
|
||||||
enablePermissions: x => x === "true",
|
enablePermissions: x => x === "true",
|
||||||
|
enableMerge: x => x === "true",
|
||||||
enableSplitMerge: x => x === "true",
|
enableSplitMerge: x => x === "true",
|
||||||
enableUpdatedAddImage: x => x === "true",
|
enableUpdatedAddImage: x => x === "true",
|
||||||
highlightEditorColors: x => x,
|
highlightEditorColors: x => x,
|
||||||
@ -461,6 +462,7 @@ const PDFViewerApplication = {
|
|||||||
foreground: AppOptions.get("pageColorsForeground"),
|
foreground: AppOptions.get("pageColorsForeground"),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
const enableMerge = AppOptions.get("enableMerge");
|
||||||
const enableSplitMerge = AppOptions.get("enableSplitMerge");
|
const enableSplitMerge = AppOptions.get("enableSplitMerge");
|
||||||
|
|
||||||
let altTextManager;
|
let altTextManager;
|
||||||
@ -601,11 +603,13 @@ const PDFViewerApplication = {
|
|||||||
pageColors,
|
pageColors,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
enableSplitMerge,
|
enableSplitMerge,
|
||||||
|
enableMerge,
|
||||||
enableNewBadge: AppOptions.get("enableNewBadge"),
|
enableNewBadge: AppOptions.get("enableNewBadge"),
|
||||||
statusBar: viewsManager.viewsManagerStatusBar,
|
statusBar: viewsManager.viewsManagerStatusBar,
|
||||||
undoBar: viewsManager.viewsManagerUndoBar,
|
undoBar: viewsManager.viewsManagerUndoBar,
|
||||||
manageMenu: viewsManager.manageMenu,
|
manageMenu: viewsManager.manageMenu,
|
||||||
addFileButton: viewsManager.viewsManagerAddFileButton,
|
waitingBar: viewsManager.viewsManagerWaitingBar,
|
||||||
|
addFileComponent: viewsManager.viewsManagerAddFile,
|
||||||
});
|
});
|
||||||
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
||||||
}
|
}
|
||||||
@ -765,6 +769,7 @@ const PDFViewerApplication = {
|
|||||||
elements: appConfig.viewsManager,
|
elements: appConfig.viewsManager,
|
||||||
eventBus,
|
eventBus,
|
||||||
l10n,
|
l10n,
|
||||||
|
enableMerge,
|
||||||
enableSplitMerge,
|
enableSplitMerge,
|
||||||
globalAbortSignal: abortSignal,
|
globalAbortSignal: abortSignal,
|
||||||
});
|
});
|
||||||
@ -2217,6 +2222,7 @@ const PDFViewerApplication = {
|
|||||||
}
|
}
|
||||||
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
|
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
|
||||||
eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts);
|
eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts);
|
||||||
|
eventBus._on("saveandload", this.onSaveAndLoad.bind(this), opts);
|
||||||
},
|
},
|
||||||
|
|
||||||
bindWindowEvents() {
|
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) {
|
_accumulateTicks(ticks, prop) {
|
||||||
// If the direction changed, reset the accumulated ticks.
|
// If the direction changed, reset the accumulated ticks.
|
||||||
if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {
|
if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {
|
||||||
|
|||||||
@ -232,6 +232,11 @@ const defaultOptions = {
|
|||||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
enableMerge: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
|
},
|
||||||
enableNewAltTextWhenAddingImage: {
|
enableNewAltTextWhenAddingImage: {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
value: true,
|
value: true,
|
||||||
|
|||||||
@ -66,6 +66,8 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
|
|||||||
* events.
|
* events.
|
||||||
* @property {boolean} [enableNewBadge] - Enables the "new" badge for the split
|
* @property {boolean} [enableNewBadge] - Enables the "new" badge for the split
|
||||||
* and merge features.
|
* and merge features.
|
||||||
|
* @property {boolean} [enableMerge] - Enables the merge feature.
|
||||||
|
* The default value is `false`.
|
||||||
* @property {boolean} [enableSplitMerge] - Enables split and merge features.
|
* @property {boolean} [enableSplitMerge] - Enables split and merge features.
|
||||||
* The default value is `false`.
|
* The default value is `false`.
|
||||||
* @property {Object} [statusBar] - The status bar elements to manage the status
|
* @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.
|
* action.
|
||||||
* @property {Object} [manageMenu] - The menu elements to manage saving edited
|
* @property {Object} [manageMenu] - The menu elements to manage saving edited
|
||||||
* PDF.
|
* PDF.
|
||||||
* @property {HTMLButtonElement} addFileButton - The button that opens a dialog
|
* @property {Object} [waitingBar] - The waiting bar elements shown during
|
||||||
* to add a PDF file to merge with the current one.
|
* 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.
|
* 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 {
|
class PDFThumbnailViewer {
|
||||||
static #draggingScaleFactor = 0;
|
static #draggingScaleFactor = 0;
|
||||||
|
|
||||||
|
#enableMerge = false;
|
||||||
|
|
||||||
#enableSplitMerge = false;
|
#enableSplitMerge = false;
|
||||||
|
|
||||||
#dragAC = null;
|
#dragAC = null;
|
||||||
@ -157,6 +164,8 @@ class PDFThumbnailViewer {
|
|||||||
|
|
||||||
#undoCloseButton = null;
|
#undoCloseButton = null;
|
||||||
|
|
||||||
|
#waitingBar = null;
|
||||||
|
|
||||||
#isInPasteMode = false;
|
#isInPasteMode = false;
|
||||||
|
|
||||||
#hasUndoBarVisible = false;
|
#hasUndoBarVisible = false;
|
||||||
@ -175,12 +184,14 @@ class PDFThumbnailViewer {
|
|||||||
maxCanvasDim,
|
maxCanvasDim,
|
||||||
pageColors,
|
pageColors,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
|
enableMerge,
|
||||||
enableSplitMerge,
|
enableSplitMerge,
|
||||||
enableNewBadge,
|
enableNewBadge,
|
||||||
statusBar,
|
statusBar,
|
||||||
undoBar,
|
undoBar,
|
||||||
|
waitingBar,
|
||||||
manageMenu,
|
manageMenu,
|
||||||
addFileButton,
|
addFileComponent,
|
||||||
}) {
|
}) {
|
||||||
this.scrollableContainer = container.parentElement;
|
this.scrollableContainer = container.parentElement;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
@ -190,6 +201,7 @@ class PDFThumbnailViewer {
|
|||||||
this.maxCanvasPixels = maxCanvasPixels;
|
this.maxCanvasPixels = maxCanvasPixels;
|
||||||
this.maxCanvasDim = maxCanvasDim;
|
this.maxCanvasDim = maxCanvasDim;
|
||||||
this.pageColors = pageColors || null;
|
this.pageColors = pageColors || null;
|
||||||
|
this.#enableMerge = enableMerge || false;
|
||||||
this.#enableSplitMerge = enableSplitMerge || false;
|
this.#enableSplitMerge = enableSplitMerge || false;
|
||||||
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
|
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
|
||||||
this.#deselectButton =
|
this.#deselectButton =
|
||||||
@ -199,13 +211,11 @@ class PDFThumbnailViewer {
|
|||||||
this.#undoLabel = undoBar?.viewsManagerStatusUndoLabel || null;
|
this.#undoLabel = undoBar?.viewsManagerStatusUndoLabel || null;
|
||||||
this.#undoButton = undoBar?.viewsManagerStatusUndoButton || null;
|
this.#undoButton = undoBar?.viewsManagerStatusUndoButton || null;
|
||||||
this.#undoCloseButton = undoBar?.viewsManagerStatusUndoCloseButton || null;
|
this.#undoCloseButton = undoBar?.viewsManagerStatusUndoCloseButton || null;
|
||||||
|
this.#waitingBar = waitingBar || null;
|
||||||
// TODO: uncomment when the "add file" feature is implemented.
|
|
||||||
// this.#addFileButton = addFileButton;
|
|
||||||
|
|
||||||
if (this.#enableSplitMerge && manageMenu) {
|
if (this.#enableSplitMerge && manageMenu) {
|
||||||
const {
|
const {
|
||||||
button,
|
button: menuButton,
|
||||||
menu,
|
menu,
|
||||||
copy,
|
copy,
|
||||||
cut,
|
cut,
|
||||||
@ -217,19 +227,19 @@ class PDFThumbnailViewer {
|
|||||||
const newSpan = document.createElement("span");
|
const newSpan = document.createElement("span");
|
||||||
newSpan.setAttribute("data-l10n-id", "pdfjs-new-badge-content");
|
newSpan.setAttribute("data-l10n-id", "pdfjs-new-badge-content");
|
||||||
newSpan.classList.add("newBadge");
|
newSpan.classList.add("newBadge");
|
||||||
button.parentElement.before(newSpan);
|
menuButton.parentElement.before(newSpan);
|
||||||
this.#newBadge = newSpan;
|
this.#newBadge = newSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.eventBus.on(
|
this.eventBus.on(
|
||||||
"pagesloaded",
|
"pagesloaded",
|
||||||
() => {
|
() => {
|
||||||
button.disabled = false;
|
menuButton.disabled = false;
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
this._manageMenu = new Menu(menu, button, [
|
this._manageMenu = new Menu(menu, menuButton, [
|
||||||
copy,
|
copy,
|
||||||
cut,
|
cut,
|
||||||
del,
|
del,
|
||||||
@ -248,7 +258,7 @@ class PDFThumbnailViewer {
|
|||||||
cut.addEventListener("click", this.#cutPages.bind(this));
|
cut.addEventListener("click", this.#cutPages.bind(this));
|
||||||
|
|
||||||
this.#toggleMenuEntries(false);
|
this.#toggleMenuEntries(false);
|
||||||
button.disabled = true;
|
menuButton.disabled = true;
|
||||||
|
|
||||||
this.eventBus.on("editingaction", ({ name }) => {
|
this.eventBus.on("editingaction", ({ name }) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@ -301,6 +311,63 @@ class PDFThumbnailViewer {
|
|||||||
this.#updateStatus("select");
|
this.#updateStatus("select");
|
||||||
});
|
});
|
||||||
this.#deselectButton.classList.toggle("hidden", true);
|
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 {
|
} else {
|
||||||
manageMenu.button.hidden = true;
|
manageMenu.button.hidden = true;
|
||||||
}
|
}
|
||||||
@ -466,6 +533,9 @@ class PDFThumbnailViewer {
|
|||||||
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
||||||
thumbnailView.toggleCurrent(/* isCurrent = */ true);
|
thumbnailView.toggleCurrent(/* isCurrent = */ true);
|
||||||
this.container.append(fragment);
|
this.container.append(fragment);
|
||||||
|
this.eventBus.dispatch("thumbnailsloaded", {
|
||||||
|
source: this,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(reason => {
|
.catch(reason => {
|
||||||
console.error("Unable to initialize thumbnail viewer", reason);
|
console.error("Unable to initialize thumbnail viewer", reason);
|
||||||
@ -830,6 +900,37 @@ class PDFThumbnailViewer {
|
|||||||
return size > 0 && size < this._thumbnails.length;
|
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) {
|
#togglePasteMode(enable) {
|
||||||
this.#isInPasteMode = enable;
|
this.#isInPasteMode = enable;
|
||||||
if (enable) {
|
if (enable) {
|
||||||
@ -996,21 +1097,7 @@ class PDFThumbnailViewer {
|
|||||||
? "pdfjs-views-manager-pages-status-action-label"
|
? "pdfjs-views-manager-pages-status-action-label"
|
||||||
: "pdfjs-views-manager-pages-status-none-action-label"
|
: "pdfjs-views-manager-pages-status-none-action-label"
|
||||||
);
|
);
|
||||||
if (count) {
|
this.#toggleBar("status", "", count ? { count } : null);
|
||||||
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;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1026,8 +1113,7 @@ class PDFThumbnailViewer {
|
|||||||
l10nId = "pdfjs-views-manager-pages-status-undo-delete-label";
|
l10nId = "pdfjs-views-manager-pages-status-undo-delete-label";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.#undoLabel.setAttribute("data-l10n-id", l10nId);
|
this.#toggleBar("undo", l10nId, { count });
|
||||||
this.#undoLabel.setAttribute("data-l10n-args", JSON.stringify({ count }));
|
|
||||||
|
|
||||||
if (type === "copy") {
|
if (type === "copy") {
|
||||||
this.#undoButton.firstElementChild.setAttribute(
|
this.#undoButton.firstElementChild.setAttribute(
|
||||||
@ -1042,10 +1128,6 @@ class PDFThumbnailViewer {
|
|||||||
);
|
);
|
||||||
this.#undoCloseButton.classList.toggle("hidden", false);
|
this.#undoCloseButton.classList.toggle("hidden", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#statusBar.classList.toggle("hidden", true);
|
|
||||||
this.#undoBar.classList.toggle("hidden", false);
|
|
||||||
this.#hasUndoBarVisible = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#moveDraggedContainer(dx, dy) {
|
#moveDraggedContainer(dx, dy) {
|
||||||
|
|||||||
@ -162,6 +162,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
hidden="true"
|
hidden="true"
|
||||||
>
|
>
|
||||||
<span data-l10n-id="pdfjs-views-manager-add-file-button-label"></span>
|
<span data-l10n-id="pdfjs-views-manager-add-file-button-label"></span>
|
||||||
|
<input id="viewsManagerAddFilePicker" type="file" accept="application/pdf" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="viewsManagerCurrentOutlineButton"
|
id="viewsManagerCurrentOutlineButton"
|
||||||
|
|||||||
@ -124,9 +124,10 @@ function getViewerConfiguration() {
|
|||||||
outlinesView: document.getElementById("outlinesView"),
|
outlinesView: document.getElementById("outlinesView"),
|
||||||
attachmentsView: document.getElementById("attachmentsView"),
|
attachmentsView: document.getElementById("attachmentsView"),
|
||||||
layersView: document.getElementById("layersView"),
|
layersView: document.getElementById("layersView"),
|
||||||
viewsManagerAddFileButton: document.getElementById(
|
viewsManagerAddFile: {
|
||||||
"viewsManagerAddFileButton"
|
button: document.getElementById("viewsManagerAddFileButton"),
|
||||||
),
|
picker: document.getElementById("viewsManagerAddFilePicker"),
|
||||||
|
},
|
||||||
viewsManagerCurrentOutlineButton: document.getElementById(
|
viewsManagerCurrentOutlineButton: document.getElementById(
|
||||||
"viewsManagerCurrentOutlineButton"
|
"viewsManagerCurrentOutlineButton"
|
||||||
),
|
),
|
||||||
@ -159,6 +160,13 @@ function getViewerConfiguration() {
|
|||||||
"viewsManagerStatusUndoCloseButton"
|
"viewsManagerStatusUndoCloseButton"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
viewsManagerWaitingBar: {
|
||||||
|
container: document.getElementById("viewsManagerStatusWaiting"),
|
||||||
|
closeButton: document.getElementById(
|
||||||
|
"viewsManagerStatusWaitingCloseButton"
|
||||||
|
),
|
||||||
|
label: document.getElementById("viewsManagerStatusWaitingLabel"),
|
||||||
|
},
|
||||||
manageMenu: {
|
manageMenu: {
|
||||||
button: document.getElementById("viewsManagerStatusActionButton"),
|
button: document.getElementById("viewsManagerStatusActionButton"),
|
||||||
menu: document.getElementById("viewsManagerStatusActionOptions"),
|
menu: document.getElementById("viewsManagerStatusActionOptions"),
|
||||||
|
|||||||
@ -355,8 +355,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#viewsManagerAddFileButton {
|
#viewsManagerAddFileButton {
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
background: var(--button-no-bg);
|
background: var(--button-no-bg);
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@ -369,6 +367,10 @@
|
|||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-image: var(--views-manager-add-file-button-icon);
|
mask-image: var(--views-manager-add-file-button-icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewsManagerCurrentOutlineButton {
|
#viewsManagerCurrentOutlineButton {
|
||||||
@ -388,17 +390,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#viewsManagerStatus {
|
#viewsManagerStatus {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
align-self: stretch;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: auto;
|
|
||||||
border: 1px solid var(--status-border-color);
|
border: 1px solid var(--status-border-color);
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
display: unset !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewsManagerStatusLabel {
|
.viewsManagerStatusLabel {
|
||||||
|
|||||||
@ -89,7 +89,7 @@ class ViewsManager extends Sidebar {
|
|||||||
outlinesView,
|
outlinesView,
|
||||||
attachmentsView,
|
attachmentsView,
|
||||||
layersView,
|
layersView,
|
||||||
viewsManagerAddFileButton,
|
viewsManagerAddFile: { button: viewsManagerAddFileButton },
|
||||||
viewsManagerCurrentOutlineButton,
|
viewsManagerCurrentOutlineButton,
|
||||||
viewsManagerSelectorButton,
|
viewsManagerSelectorButton,
|
||||||
viewsManagerSelectorOptions,
|
viewsManagerSelectorOptions,
|
||||||
@ -98,6 +98,7 @@ class ViewsManager extends Sidebar {
|
|||||||
},
|
},
|
||||||
eventBus,
|
eventBus,
|
||||||
l10n,
|
l10n,
|
||||||
|
enableMerge = false,
|
||||||
enableSplitMerge = false,
|
enableSplitMerge = false,
|
||||||
globalAbortSignal,
|
globalAbortSignal,
|
||||||
}) {
|
}) {
|
||||||
@ -149,6 +150,10 @@ class ViewsManager extends Sidebar {
|
|||||||
viewsManagerStatus.hidden = true;
|
viewsManagerStatus.hidden = true;
|
||||||
}
|
}
|
||||||
this._enableSplitMerge = enableSplitMerge;
|
this._enableSplitMerge = enableSplitMerge;
|
||||||
|
this._enableMerge = enableMerge;
|
||||||
|
if (!enableMerge) {
|
||||||
|
viewsManagerAddFileButton.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.menu = new Menu(
|
this.menu = new Menu(
|
||||||
viewsManagerSelectorOptions,
|
viewsManagerSelectorOptions,
|
||||||
@ -260,10 +265,10 @@ class ViewsManager extends Sidebar {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._enableSplitMerge) {
|
this.viewsManagerStatus.hidden =
|
||||||
this.viewsManagerStatus.hidden = view !== SidebarView.THUMBS;
|
!this._enableSplitMerge || view !== SidebarView.THUMBS;
|
||||||
}
|
this.viewsManagerAddFileButton.hidden =
|
||||||
this.viewsManagerAddFileButton.hidden = view !== SidebarView.THUMBS;
|
!this._enableMerge || view !== SidebarView.THUMBS;
|
||||||
this.viewsManagerCurrentOutlineButton.hidden = view !== SidebarView.OUTLINE;
|
this.viewsManagerCurrentOutlineButton.hidden = view !== SidebarView.OUTLINE;
|
||||||
this.viewsManagerHeaderLabel.setAttribute(
|
this.viewsManagerHeaderLabel.setAttribute(
|
||||||
"data-l10n-id",
|
"data-l10n-id",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user