mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-25 17:45:48 +02:00
Merge pull request #20670 from calixteman/reorg_delete
Add support for deleting, cutting, copying and pasting pages (bug 2010830, 2010831)
This commit is contained in:
commit
f609ee8a0c
@ -766,3 +766,4 @@ pdfjs-views-manager-status-undo-button-label = Undo
|
|||||||
pdfjs-views-manager-status-close-button =
|
pdfjs-views-manager-status-close-button =
|
||||||
.title = Close
|
.title = Close
|
||||||
pdfjs-views-manager-status-close-button-label = Close
|
pdfjs-views-manager-status-close-button-label = Close
|
||||||
|
pdfjs-views-manager-paste-button-label = Paste
|
||||||
|
|||||||
@ -2405,6 +2405,8 @@ class WorkerTransport {
|
|||||||
|
|
||||||
#passwordCapability = null;
|
#passwordCapability = null;
|
||||||
|
|
||||||
|
#copiedPageInfo = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
messageHandler,
|
messageHandler,
|
||||||
loadingTask,
|
loadingTask,
|
||||||
@ -2464,11 +2466,42 @@ class WorkerTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateCaches() {
|
#updateCaches({ type, pageNumbers }) {
|
||||||
|
if (type === "copy") {
|
||||||
|
this.#copiedPageInfo = new Map();
|
||||||
|
for (const pageNum of pageNumbers) {
|
||||||
|
this.#copiedPageInfo.set(pageNum, {
|
||||||
|
proxy: this.#pageCache.get(pageNum - 1) || null,
|
||||||
|
promise: this.#pagePromises.get(pageNum - 1) || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "delete") {
|
||||||
|
for (const pageNum of pageNumbers) {
|
||||||
|
this.#pageCache.delete(pageNum - 1);
|
||||||
|
this.#pagePromises.delete(pageNum - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newPageCache = new Map();
|
const newPageCache = new Map();
|
||||||
const newPromiseCache = new Map();
|
const newPromiseCache = new Map();
|
||||||
for (let i = 0, ii = this.pagesMapper.pagesNumber; i < ii; i++) {
|
const { pagesMapper } = this;
|
||||||
const prevPageIndex = this.pagesMapper.getPrevPageNumber(i + 1) - 1;
|
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
|
||||||
|
const prevPageNumber = pagesMapper.getPrevPageNumber(i + 1);
|
||||||
|
if (prevPageNumber < 0) {
|
||||||
|
const { proxy, promise } =
|
||||||
|
this.#copiedPageInfo?.get(-prevPageNumber) || {};
|
||||||
|
if (proxy) {
|
||||||
|
newPageCache.set(i, proxy);
|
||||||
|
}
|
||||||
|
if (promise) {
|
||||||
|
newPromiseCache.set(i, promise);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const prevPageIndex = prevPageNumber - 1;
|
||||||
const page = this.#pageCache.get(prevPageIndex);
|
const page = this.#pageCache.get(prevPageIndex);
|
||||||
if (page) {
|
if (page) {
|
||||||
newPageCache.set(i, page);
|
newPageCache.set(i, page);
|
||||||
@ -3001,7 +3034,11 @@ class WorkerTransport {
|
|||||||
num: ref.num,
|
num: ref.num,
|
||||||
gen: ref.gen,
|
gen: ref.gen,
|
||||||
});
|
});
|
||||||
return this.pagesMapper.getPageNumber(index + 1) - 1;
|
const pageNumber = this.pagesMapper.getPageNumber(index + 1);
|
||||||
|
if (pageNumber === 0) {
|
||||||
|
throw new Error("GetPageIndex: page has been removed.");
|
||||||
|
}
|
||||||
|
return pageNumber - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnnotations(pageIndex, intent) {
|
getAnnotations(pageIndex, intent) {
|
||||||
@ -3150,9 +3187,13 @@ class WorkerTransport {
|
|||||||
}
|
}
|
||||||
const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`;
|
const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`;
|
||||||
const pageIndex = this.#pageRefCache.get(refStr);
|
const pageIndex = this.#pageRefCache.get(refStr);
|
||||||
return pageIndex >= 0
|
if (pageIndex >= 0) {
|
||||||
? this.pagesMapper.getPageNumber(pageIndex + 1)
|
const pageNumber = this.pagesMapper.getPageNumber(pageIndex + 1);
|
||||||
: null;
|
if (pageNumber !== 0) {
|
||||||
|
return pageNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1046,7 +1046,7 @@ function makePathFromDrawOPS(data) {
|
|||||||
class PagesMapper {
|
class PagesMapper {
|
||||||
/**
|
/**
|
||||||
* Maps page IDs to their corresponding page numbers.
|
* Maps page IDs to their corresponding page numbers.
|
||||||
* @type {Uint32Array|null}
|
* @type {Map<number, Array<number>>|null}
|
||||||
*/
|
*/
|
||||||
#idToPageNumber = null;
|
#idToPageNumber = null;
|
||||||
|
|
||||||
@ -1058,9 +1058,9 @@ class PagesMapper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Previous mapping of page IDs to page numbers.
|
* Previous mapping of page IDs to page numbers.
|
||||||
* @type {Uint32Array|null}
|
* @type {Int32Array|null}
|
||||||
*/
|
*/
|
||||||
#prevIdToPageNumber = null;
|
#prevPageNumbers = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total number of pages.
|
* The total number of pages.
|
||||||
@ -1074,6 +1074,19 @@ class PagesMapper {
|
|||||||
*/
|
*/
|
||||||
#listeners = [];
|
#listeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps page numbers to their corresponding page IDs (used in copy
|
||||||
|
* operations).
|
||||||
|
* @type {Uint32Array|null}
|
||||||
|
*/
|
||||||
|
#copiedPageIds = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps page IDs to their corresponding page numbers, used in copy operations.
|
||||||
|
* @type {Uint32Array|null}
|
||||||
|
*/
|
||||||
|
#copiedPageNumbers = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the total number of pages.
|
* Gets the total number of pages.
|
||||||
* @returns {number} The number of pages.
|
* @returns {number} The number of pages.
|
||||||
@ -1092,16 +1105,33 @@ class PagesMapper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#pagesNumber = n;
|
this.#pagesNumber = n;
|
||||||
if (n === 0) {
|
this.#reset();
|
||||||
this.#pageNumberToId = null;
|
|
||||||
this.#idToPageNumber = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the page mappings to their default state, where page IDs equal page
|
||||||
|
* numbers (1-indexed). This is called when the number of pages changes, or
|
||||||
|
* when the current mapping matches the default mapping after a move
|
||||||
|
* operation.
|
||||||
|
*/
|
||||||
|
#reset() {
|
||||||
|
this.#pageNumberToId = null;
|
||||||
|
this.#idToPageNumber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener function that will be called whenever the page mappings
|
||||||
|
* are updated.
|
||||||
|
* @param {function} listener
|
||||||
|
*/
|
||||||
addListener(listener) {
|
addListener(listener) {
|
||||||
this.#listeners.push(listener);
|
this.#listeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously added listener function.
|
||||||
|
* @param {function} listener
|
||||||
|
*/
|
||||||
removeListener(listener) {
|
removeListener(listener) {
|
||||||
const index = this.#listeners.indexOf(listener);
|
const index = this.#listeners.indexOf(listener);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@ -1109,28 +1139,56 @@ class PagesMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateListeners() {
|
/**
|
||||||
|
* Calls all registered listener functions to notify them of changes to the
|
||||||
|
* page mappings.
|
||||||
|
* @param {Object} data - An object containing information about the update.
|
||||||
|
*/
|
||||||
|
#updateListeners(data) {
|
||||||
for (const listener of this.#listeners) {
|
for (const listener of this.#listeners) {
|
||||||
listener();
|
listener(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the page mappings if they haven't been initialized yet.
|
||||||
|
* @param {boolean} mustInit
|
||||||
|
*/
|
||||||
#init(mustInit) {
|
#init(mustInit) {
|
||||||
if (this.#pageNumberToId) {
|
if (this.#pageNumberToId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = this.#pagesNumber;
|
const n = this.#pagesNumber;
|
||||||
|
|
||||||
// Allocate a single array for better memory locality.
|
const pageNumberToId = (this.#pageNumberToId = new Uint32Array(n));
|
||||||
const array = new Uint32Array(3 * n);
|
this.#prevPageNumbers = new Int32Array(pageNumberToId);
|
||||||
const pageNumberToId = (this.#pageNumberToId = array.subarray(0, n));
|
const idToPageNumber = (this.#idToPageNumber = new Map());
|
||||||
const idToPageNumber = (this.#idToPageNumber = array.subarray(n, 2 * n));
|
|
||||||
if (mustInit) {
|
if (mustInit) {
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
pageNumberToId[i] = idToPageNumber[i] = i + 1;
|
pageNumberToId[i - 1] = i;
|
||||||
|
idToPageNumber.set(i, [i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the mapping from page IDs to page numbers based on the current
|
||||||
|
* mapping from page numbers to page IDs. This should be called after any
|
||||||
|
* changes to the page-number-to-ID mapping to keep the two mappings in sync.
|
||||||
|
*/
|
||||||
|
#updateIdToPageNumber() {
|
||||||
|
const idToPageNumber = this.#idToPageNumber;
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
idToPageNumber.clear();
|
||||||
|
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
|
||||||
|
const id = pageNumberToId[i];
|
||||||
|
const pageNumbers = idToPageNumber.get(id);
|
||||||
|
if (pageNumbers) {
|
||||||
|
pageNumbers.push(i + 1);
|
||||||
|
} else {
|
||||||
|
idToPageNumber.set(id, [i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#prevIdToPageNumber = array.subarray(2 * n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1145,7 +1203,6 @@ class PagesMapper {
|
|||||||
this.#init(true);
|
this.#init(true);
|
||||||
const pageNumberToId = this.#pageNumberToId;
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
const idToPageNumber = this.#idToPageNumber;
|
const idToPageNumber = this.#idToPageNumber;
|
||||||
this.#prevIdToPageNumber.set(idToPageNumber);
|
|
||||||
const movedCount = pagesToMove.length;
|
const movedCount = pagesToMove.length;
|
||||||
const mappedPagesToMove = new Uint32Array(movedCount);
|
const mappedPagesToMove = new Uint32Array(movedCount);
|
||||||
let removedBeforeTarget = 0;
|
let removedBeforeTarget = 0;
|
||||||
@ -1182,17 +1239,118 @@ class PagesMapper {
|
|||||||
// Finally insert the moved pages.
|
// Finally insert the moved pages.
|
||||||
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
|
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
|
||||||
|
|
||||||
let hasChanged = false;
|
this.#setPrevPageNumbers(idToPageNumber, null);
|
||||||
for (let i = 0, ii = pagesNumber; i < ii; i++) {
|
this.#updateIdToPageNumber();
|
||||||
const id = pageNumberToId[i];
|
this.#updateListeners({ type: "move" });
|
||||||
hasChanged ||= id !== i + 1;
|
|
||||||
idToPageNumber[id - 1] = i + 1;
|
|
||||||
}
|
|
||||||
this.#updateListeners();
|
|
||||||
|
|
||||||
if (!hasChanged) {
|
if (pageNumberToId.every((id, i) => id === i + 1)) {
|
||||||
// Reset.
|
this.#reset();
|
||||||
this.pagesNumber = 0;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a set of pages while keeping ID→number mappings in sync.
|
||||||
|
* @param {Array<number>} pagesToDelete - Page numbers to delete (1-indexed).
|
||||||
|
* These must be unique and sorted in ascending order.
|
||||||
|
*/
|
||||||
|
deletePages(pagesToDelete) {
|
||||||
|
this.#init(true);
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
const prevIdToPageNumber = this.#idToPageNumber;
|
||||||
|
|
||||||
|
this.pagesNumber -= pagesToDelete.length;
|
||||||
|
this.#init(false);
|
||||||
|
const newPageNumberToId = this.#pageNumberToId;
|
||||||
|
|
||||||
|
let sourceIndex = 0;
|
||||||
|
let destIndex = 0;
|
||||||
|
for (const pageNumber of pagesToDelete) {
|
||||||
|
const pageIndex = pageNumber - 1;
|
||||||
|
if (pageIndex !== sourceIndex) {
|
||||||
|
newPageNumberToId.set(
|
||||||
|
pageNumberToId.subarray(sourceIndex, pageIndex),
|
||||||
|
destIndex
|
||||||
|
);
|
||||||
|
destIndex += pageIndex - sourceIndex;
|
||||||
|
}
|
||||||
|
sourceIndex = pageIndex + 1;
|
||||||
|
}
|
||||||
|
if (sourceIndex < pageNumberToId.length) {
|
||||||
|
newPageNumberToId.set(pageNumberToId.subarray(sourceIndex), destIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#setPrevPageNumbers(prevIdToPageNumber, null);
|
||||||
|
this.#updateIdToPageNumber();
|
||||||
|
this.#updateListeners({ type: "delete", pageNumbers: pagesToDelete });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a set of pages while keeping ID→number mappings in sync.
|
||||||
|
* @param {Uint32Array} pagesToCopy - Page numbers to copy (1-indexed).
|
||||||
|
*/
|
||||||
|
copyPages(pagesToCopy) {
|
||||||
|
this.#init(true);
|
||||||
|
this.#copiedPageNumbers = pagesToCopy;
|
||||||
|
this.#copiedPageIds = pagesToCopy.map(
|
||||||
|
pageNumber => this.#pageNumberToId[pageNumber - 1]
|
||||||
|
);
|
||||||
|
this.#updateListeners({ type: "copy", pageNumbers: pagesToCopy });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pastes a set of pages while keeping ID→number mappings in sync.
|
||||||
|
* @param {number} index - Zero-based insertion index in the page-number list.
|
||||||
|
*/
|
||||||
|
pastePages(index) {
|
||||||
|
this.#init(true);
|
||||||
|
const pageNumberToId = this.#pageNumberToId;
|
||||||
|
const prevIdToPageNumber = this.#idToPageNumber;
|
||||||
|
const copiedPageNumbers = this.#copiedPageNumbers;
|
||||||
|
|
||||||
|
const copiedPageMapping = new Map();
|
||||||
|
let base = index;
|
||||||
|
for (const pageNumber of copiedPageNumbers) {
|
||||||
|
copiedPageMapping.set(++base, pageNumber);
|
||||||
|
}
|
||||||
|
this.pagesNumber += copiedPageNumbers.length;
|
||||||
|
this.#init(false);
|
||||||
|
const newPageNumberToId = this.#pageNumberToId;
|
||||||
|
|
||||||
|
newPageNumberToId.set(pageNumberToId.subarray(0, index), 0);
|
||||||
|
newPageNumberToId.set(this.#copiedPageIds, index);
|
||||||
|
newPageNumberToId.set(
|
||||||
|
pageNumberToId.subarray(index),
|
||||||
|
index + copiedPageNumbers.length
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#setPrevPageNumbers(prevIdToPageNumber, copiedPageMapping);
|
||||||
|
this.#updateIdToPageNumber();
|
||||||
|
this.#updateListeners({ type: "paste" });
|
||||||
|
|
||||||
|
this.#copiedPageIds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the previous page numbers based on the current page-number-to-ID
|
||||||
|
* mapping and the provided previous ID-to-page-number mapping.
|
||||||
|
* This is used to keep track of the original page numbers for each page ID.
|
||||||
|
* @param {Map<number, Array<number>} prevIdToPageNumber
|
||||||
|
* @param {Map<number, number>|null} copiedPageMapping
|
||||||
|
*/
|
||||||
|
#setPrevPageNumbers(prevIdToPageNumber, copiedPageMapping) {
|
||||||
|
const prevPageNumbers = this.#prevPageNumbers;
|
||||||
|
const newPageNumberToId = this.#pageNumberToId;
|
||||||
|
const idsIndices = new Map();
|
||||||
|
for (let i = 0, ii = this.#pagesNumber; i < ii; i++) {
|
||||||
|
const oldPageNumber = copiedPageMapping?.get(i + 1);
|
||||||
|
if (oldPageNumber) {
|
||||||
|
prevPageNumbers[i] = -oldPageNumber;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id = newPageNumberToId[i];
|
||||||
|
const j = idsIndices.get(id) || 0;
|
||||||
|
prevPageNumbers[i] = prevIdToPageNumber.get(id)?.[j];
|
||||||
|
idsIndices.set(id, j + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1209,25 +1367,75 @@ class PagesMapper {
|
|||||||
* @returns {Object} An object containing the page indices.
|
* @returns {Object} An object containing the page indices.
|
||||||
*/
|
*/
|
||||||
getPageMappingForSaving() {
|
getPageMappingForSaving() {
|
||||||
// Saving is index-based.
|
const idToPageNumber = this.#idToPageNumber;
|
||||||
return {
|
|
||||||
pageIndices: this.#idToPageNumber
|
// idToPageNumber maps used 1-based IDs to 1-based page numbers.
|
||||||
? this.#idToPageNumber.map(x => x - 1)
|
// For example if the final pdf contains page 3 twice and they are moved at
|
||||||
: null,
|
// page 1 and 4, then it contains:
|
||||||
};
|
// pageNumberToId = [3, ., ., 3, ...,]
|
||||||
|
// idToPageNumber = {3: [1, 4], ...}
|
||||||
|
// In such a case we need to take a page 3 from the original pdf and take
|
||||||
|
// page 3 from a "copy".
|
||||||
|
// So we need to pass to the api something like:
|
||||||
|
// [ {
|
||||||
|
// document: null // this pdf
|
||||||
|
// includePages: [ 2, ... ], // page 3 is at index 2
|
||||||
|
// pageIndices: [0, ...], // page 3 will be at index 0 in the new pdf
|
||||||
|
// }, {
|
||||||
|
// document: null // this pdf
|
||||||
|
// includePages: [ 2, ... ], // page 3 is at index 2
|
||||||
|
// pageIndices: [3, ...], // page 3 will be at index 3 in the new pdf
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
let nCopy = 0;
|
||||||
|
for (const pageNumbers of idToPageNumber.values()) {
|
||||||
|
nCopy = Math.max(nCopy, pageNumbers.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractParams = new Array(nCopy);
|
||||||
|
for (let i = 0; i < nCopy; i++) {
|
||||||
|
extractParams[i] = {
|
||||||
|
document: null,
|
||||||
|
pageIndices: [],
|
||||||
|
includePages: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, pageNumbers] of idToPageNumber) {
|
||||||
|
for (let i = 0, ii = pageNumbers.length; i < ii; i++) {
|
||||||
|
extractParams[i].includePages.push([id - 1, pageNumbers[i] - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { includePages, pageIndices } of extractParams) {
|
||||||
|
includePages.sort((a, b) => a[0] - b[0]);
|
||||||
|
for (let i = 0, ii = includePages.length; i < ii; i++) {
|
||||||
|
pageIndices.push(includePages[i][1]);
|
||||||
|
includePages[i] = includePages[i][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the previous page number for a given page number.
|
||||||
|
* @param {number} pageNumber
|
||||||
|
* @returns {number} The previous page number for the given page number, or 0
|
||||||
|
* if no mapping exists.
|
||||||
|
*/
|
||||||
getPrevPageNumber(pageNumber) {
|
getPrevPageNumber(pageNumber) {
|
||||||
return this.#prevIdToPageNumber[this.#pageNumberToId[pageNumber - 1] - 1];
|
return this.#prevPageNumbers[pageNumber - 1] ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the page number for a given page ID.
|
* Gets the page number for a given page ID.
|
||||||
* @param {number} id - The page ID (1-indexed).
|
* @param {number} id - The page ID (1-indexed).
|
||||||
* @returns {number} The page number, or the ID itself if no mapping exists.
|
* @returns {number} The page number, or 0 if no mapping exists.
|
||||||
*/
|
*/
|
||||||
getPageNumber(id) {
|
getPageNumber(id) {
|
||||||
return this.#idToPageNumber?.[id - 1] ?? id;
|
return this.#idToPageNumber ? (this.#idToPageNumber.get(id)?.[0] ?? 0) : id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -145,7 +145,44 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePageIndex(newPageIndex) {
|
updatePageIndex(newPageIndex) {
|
||||||
|
for (const editor of this.#allEditorsIterator) {
|
||||||
|
editor.updatePageIndex(newPageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
this.pageIndex = newPageIndex;
|
this.pageIndex = newPageIndex;
|
||||||
|
this.#uiManager.addLayer(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones all annotation editors from another layer into this layer.
|
||||||
|
* This is typically used when duplicating a page - the editors from the
|
||||||
|
* source page are serialized and then deserialized into the new page's layer.
|
||||||
|
*
|
||||||
|
* @param {AnnotationEditorLayer} clonedFrom - The source annotation editor
|
||||||
|
* layer to clone editors from. If null or undefined, no action is taken.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when all editors have been
|
||||||
|
* cloned and added to this layer.
|
||||||
|
*/
|
||||||
|
async setClonedFrom(clonedFrom) {
|
||||||
|
if (!clonedFrom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const promises = [];
|
||||||
|
for (const editor of clonedFrom.#allEditorsIterator) {
|
||||||
|
const serialized = editor.serialize(/* isForCopying = */ true);
|
||||||
|
if (!serialized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
serialized.isCopy = false;
|
||||||
|
promises.push(
|
||||||
|
this.deserialize(serialized).then(deserialized => {
|
||||||
|
if (deserialized) {
|
||||||
|
this.addOrRebuild(deserialized);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEmpty() {
|
get isEmpty() {
|
||||||
|
|||||||
@ -948,7 +948,6 @@ class AnnotationEditorUIManager {
|
|||||||
evt => this.updateParams(evt.type, evt.value),
|
evt => this.updateParams(evt.type, evt.value),
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
eventBus._on("pagesedited", this.onPagesEdited.bind(this), { signal });
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"pointerdown",
|
"pointerdown",
|
||||||
() => {
|
() => {
|
||||||
@ -1264,30 +1263,20 @@ class AnnotationEditorUIManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPagesEdited({ pagesMapper }) {
|
|
||||||
for (const editor of this.#allEditors.values()) {
|
|
||||||
editor.updatePageIndex(
|
|
||||||
pagesMapper.getPrevPageNumber(editor.pageIndex + 1) - 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const allLayers = this.#allLayers;
|
|
||||||
const newAllLayers = (this.#allLayers = new Map());
|
|
||||||
for (const [pageIndex, layer] of allLayers) {
|
|
||||||
const prevPageIndex = pagesMapper.getPrevPageNumber(pageIndex + 1) - 1;
|
|
||||||
if (prevPageIndex === -1) {
|
|
||||||
// TODO: handle the case where the deletion of the page has been undone.
|
|
||||||
layer.destroy();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
newAllLayers.set(prevPageIndex, layer);
|
|
||||||
layer.updatePageIndex(prevPageIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageChanging({ pageNumber }) {
|
onPageChanging({ pageNumber }) {
|
||||||
this.#currentPageIndex = pageNumber - 1;
|
this.#currentPageIndex = pageNumber - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deletePage(id) {
|
||||||
|
for (const editor of this.getEditors(id)) {
|
||||||
|
editor.remove();
|
||||||
|
}
|
||||||
|
this.#allLayers.delete(id);
|
||||||
|
if (this.#currentPageIndex === id) {
|
||||||
|
this.#currentPageIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
focusMainContainer() {
|
focusMainContainer() {
|
||||||
this.#container.focus();
|
this.#container.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,19 +18,21 @@ import {
|
|||||||
clearInput,
|
clearInput,
|
||||||
closePages,
|
closePages,
|
||||||
createPromise,
|
createPromise,
|
||||||
|
createPromiseWithArgs,
|
||||||
dragAndDrop,
|
dragAndDrop,
|
||||||
getAnnotationSelector,
|
getAnnotationSelector,
|
||||||
getRect,
|
getRect,
|
||||||
getThumbnailSelector,
|
getThumbnailSelector,
|
||||||
loadAndWait,
|
loadAndWait,
|
||||||
scrollIntoView,
|
scrollIntoView,
|
||||||
|
waitAndClick,
|
||||||
waitForDOMMutation,
|
waitForDOMMutation,
|
||||||
} from "./test_utils.mjs";
|
} from "./test_utils.mjs";
|
||||||
|
|
||||||
async function waitForThumbnailVisible(page, pageNums) {
|
async function waitForThumbnailVisible(page, pageNums) {
|
||||||
await page.click("#viewsManagerToggleButton");
|
await page.click("#viewsManagerToggleButton");
|
||||||
|
|
||||||
const thumbSelector = "#thumbnailsView .thumbnailImage";
|
const thumbSelector = "#thumbnailsView .thumbnailImageContainer > img";
|
||||||
await page.waitForSelector(thumbSelector, { visible: true });
|
await page.waitForSelector(thumbSelector, { visible: true });
|
||||||
if (!pageNums) {
|
if (!pageNums) {
|
||||||
return null;
|
return null;
|
||||||
@ -45,18 +47,22 @@ async function waitForThumbnailVisible(page, pageNums) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForPagesEdited(page) {
|
function waitForPagesEdited(page, type) {
|
||||||
return createPromise(page, resolve => {
|
return createPromiseWithArgs(
|
||||||
window.PDFViewerApplication.eventBus.on(
|
page,
|
||||||
"pagesedited",
|
resolve => {
|
||||||
({ pagesMapper }) => {
|
const listener = ({ pagesMapper, type: ty }) => {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
if (args[0] && args[0] !== ty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.PDFViewerApplication.eventBus.off("pagesedited", listener);
|
||||||
resolve(Array.from(pagesMapper.getMapping()));
|
resolve(Array.from(pagesMapper.getMapping()));
|
||||||
},
|
};
|
||||||
{
|
window.PDFViewerApplication.eventBus.on("pagesedited", listener);
|
||||||
once: true,
|
},
|
||||||
}
|
[type]
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForHavingContents(page, expected) {
|
async function waitForHavingContents(page, expected) {
|
||||||
@ -533,7 +539,8 @@ describe("Reorganize Pages View", () => {
|
|||||||
await page.waitForSelector("#thumbnailsViewMenu", { visible: true });
|
await page.waitForSelector("#thumbnailsViewMenu", { visible: true });
|
||||||
await page.click("#thumbnailsViewMenu");
|
await page.click("#thumbnailsViewMenu");
|
||||||
|
|
||||||
const thumbSelector = "#thumbnailsView .thumbnailImage";
|
const thumbSelector =
|
||||||
|
"#thumbnailsView .thumbnailImageContainer > img";
|
||||||
await page.waitForSelector(thumbSelector, { visible: true });
|
await page.waitForSelector(thumbSelector, { visible: true });
|
||||||
const rect1 = await getRect(page, getThumbnailSelector(1));
|
const rect1 = await getRect(page, getThumbnailSelector(1));
|
||||||
const rect2 = await getRect(page, getThumbnailSelector(2));
|
const rect2 = await getRect(page, getThumbnailSelector(2));
|
||||||
@ -607,7 +614,7 @@ describe("Reorganize Pages View", () => {
|
|||||||
window.PDFViewerApplication.eventBus.on(
|
window.PDFViewerApplication.eventBus.on(
|
||||||
"savepageseditedpdf",
|
"savepageseditedpdf",
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
resolve(Array.from(data.pageIndices));
|
resolve(Array.from(data[0].pageIndices));
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
once: true,
|
once: true,
|
||||||
@ -630,4 +637,174 @@ describe("Reorganize Pages View", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Delete some pages", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"page_with_number.pdf",
|
||||||
|
"#viewsManagerToggleButton",
|
||||||
|
"1",
|
||||||
|
null,
|
||||||
|
{ enableSplitMerge: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check that the pages are deleted", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await waitForThumbnailVisible(page, 1);
|
||||||
|
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
await waitAndClick(
|
||||||
|
page,
|
||||||
|
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||||
|
);
|
||||||
|
await waitAndClick(
|
||||||
|
page,
|
||||||
|
`.thumbnail:has(${getThumbnailSelector(3)}) input`
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePagesEdited = await waitForPagesEdited(page);
|
||||||
|
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||||
|
await waitAndClick(page, "#viewsManagerStatusActionDelete");
|
||||||
|
|
||||||
|
const pageIndices = await awaitPromise(handlePagesEdited);
|
||||||
|
const expected = [
|
||||||
|
2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
|
||||||
|
];
|
||||||
|
expect(pageIndices)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(expected);
|
||||||
|
await waitForHavingContents(page, expected);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Cut and paste some pages", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"page_with_number.pdf",
|
||||||
|
"#viewsManagerToggleButton",
|
||||||
|
"1",
|
||||||
|
null,
|
||||||
|
{ enableSplitMerge: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check that the pages has been cut and pasted correctly", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await waitForThumbnailVisible(page, 1);
|
||||||
|
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
await waitAndClick(
|
||||||
|
page,
|
||||||
|
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||||
|
);
|
||||||
|
await waitAndClick(
|
||||||
|
page,
|
||||||
|
`.thumbnail:has(${getThumbnailSelector(3)}) input`
|
||||||
|
);
|
||||||
|
|
||||||
|
let handlePagesEdited = await waitForPagesEdited(page, "cut");
|
||||||
|
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||||
|
await waitAndClick(page, "#viewsManagerStatusActionCut");
|
||||||
|
|
||||||
|
let pageIndices = await awaitPromise(handlePagesEdited);
|
||||||
|
let expected = [2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
|
||||||
|
expect(pageIndices)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(expected);
|
||||||
|
await waitForHavingContents(page, expected);
|
||||||
|
|
||||||
|
handlePagesEdited = await waitForPagesEdited(page);
|
||||||
|
await waitAndClick(page, `${getThumbnailSelector(1)}+button`);
|
||||||
|
pageIndices = await awaitPromise(handlePagesEdited);
|
||||||
|
expected = [
|
||||||
|
2, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
|
||||||
|
];
|
||||||
|
expect(pageIndices)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(expected);
|
||||||
|
await waitForHavingContents(page, expected);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Copy and paste some pages", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"page_with_number.pdf",
|
||||||
|
"#viewsManagerToggleButton",
|
||||||
|
"1",
|
||||||
|
null,
|
||||||
|
{ enableSplitMerge: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should check that the pages has been copied and pasted correctly", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await waitForThumbnailVisible(page, 1);
|
||||||
|
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
await waitAndClick(
|
||||||
|
page,
|
||||||
|
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||||
|
);
|
||||||
|
await waitAndClick(
|
||||||
|
page,
|
||||||
|
`.thumbnail:has(${getThumbnailSelector(3)}) input`
|
||||||
|
);
|
||||||
|
|
||||||
|
let handlePagesEdited = await waitForPagesEdited(page);
|
||||||
|
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||||
|
await waitAndClick(page, "#viewsManagerStatusActionCopy");
|
||||||
|
|
||||||
|
let pageIndices = await awaitPromise(handlePagesEdited);
|
||||||
|
let expected = [
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
|
||||||
|
];
|
||||||
|
expect(pageIndices)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(expected);
|
||||||
|
await waitForHavingContents(page, expected);
|
||||||
|
|
||||||
|
handlePagesEdited = await waitForPagesEdited(page);
|
||||||
|
await waitAndClick(page, `${getThumbnailSelector(2)}+button`);
|
||||||
|
pageIndices = await awaitPromise(handlePagesEdited);
|
||||||
|
expected = [
|
||||||
|
1, 2, 1, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
|
||||||
|
];
|
||||||
|
expect(pageIndices)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(expected);
|
||||||
|
await waitForHavingContents(page, expected);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -131,6 +131,15 @@ function createPromise(page, callback) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPromiseWithArgs(page, callback, args) {
|
||||||
|
return page.evaluateHandle(
|
||||||
|
// eslint-disable-next-line no-eval, no-shadow
|
||||||
|
(cb, args) => [new Promise(eval(`(${cb})`))],
|
||||||
|
callback.toString(),
|
||||||
|
args
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function awaitPromise(promise) {
|
function awaitPromise(promise) {
|
||||||
return promise.evaluate(([p]) => p);
|
return promise.evaluate(([p]) => p);
|
||||||
}
|
}
|
||||||
@ -253,7 +262,7 @@ function getAnnotationSelector(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getThumbnailSelector(pageNumber) {
|
function getThumbnailSelector(pageNumber) {
|
||||||
return `.thumbnailImage[data-l10n-args='{"page":${pageNumber}}']`;
|
return `.thumbnailImageContainer[data-l10n-args='{"page":${pageNumber}}']`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSpanRectFromText(page, pageNumber, text) {
|
async function getSpanRectFromText(page, pageNumber, text) {
|
||||||
@ -963,6 +972,7 @@ export {
|
|||||||
countSerialized,
|
countSerialized,
|
||||||
countStorageEntries,
|
countStorageEntries,
|
||||||
createPromise,
|
createPromise,
|
||||||
|
createPromiseWithArgs,
|
||||||
dragAndDrop,
|
dragAndDrop,
|
||||||
firstPageOnTop,
|
firstPageOnTop,
|
||||||
FSI,
|
FSI,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
function waitForThumbnailVisible(page, pageNum) {
|
function waitForThumbnailVisible(page, pageNum) {
|
||||||
return page.waitForSelector(
|
return page.waitForSelector(
|
||||||
`.thumbnailImage[data-l10n-args='{"page":${pageNum}}']`,
|
`.thumbnailImageContainer[data-l10n-args='{"page":${pageNum}}']`,
|
||||||
{ visible: true }
|
{ visible: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,7 +46,8 @@ describe("PDF Thumbnail View", () => {
|
|||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
await page.click("#viewsManagerToggleButton");
|
await page.click("#viewsManagerToggleButton");
|
||||||
|
|
||||||
const thumbSelector = "#thumbnailsView .thumbnailImage";
|
const thumbSelector =
|
||||||
|
"#thumbnailsView .thumbnailImageContainer > img";
|
||||||
await page.waitForSelector(thumbSelector, { visible: true });
|
await page.waitForSelector(thumbSelector, { visible: true });
|
||||||
|
|
||||||
await waitForThumbnailVisible(page, 1);
|
await waitForThumbnailVisible(page, 1);
|
||||||
@ -110,12 +111,15 @@ describe("PDF Thumbnail View", () => {
|
|||||||
|
|
||||||
for (const pageNum of [14, 1, 13, 2]) {
|
for (const pageNum of [14, 1, 13, 2]) {
|
||||||
await goToPage(page, pageNum);
|
await goToPage(page, pageNum);
|
||||||
const thumbSelector = `.thumbnailImage[data-l10n-args='{"page":${pageNum}}']`;
|
const thumbSelector = `.thumbnailImageContainer[data-l10n-args='{"page":${pageNum}}']`;
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
`.thumbnail ${thumbSelector}[aria-current="page"]`,
|
`.thumbnail ${thumbSelector}[aria-current="page"]`,
|
||||||
{ visible: true }
|
{ visible: true }
|
||||||
);
|
);
|
||||||
const src = await page.$eval(thumbSelector, el => el.src);
|
const src = await page.$eval(
|
||||||
|
`${thumbSelector} > img`,
|
||||||
|
el => el.src
|
||||||
|
);
|
||||||
expect(src)
|
expect(src)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toMatch(/^blob:http:/);
|
.toMatch(/^blob:http:/);
|
||||||
@ -167,7 +171,7 @@ describe("PDF Thumbnail View", () => {
|
|||||||
expect(
|
expect(
|
||||||
await isElementFocused(
|
await isElementFocused(
|
||||||
page,
|
page,
|
||||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
|
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
@ -177,7 +181,7 @@ describe("PDF Thumbnail View", () => {
|
|||||||
expect(
|
expect(
|
||||||
await isElementFocused(
|
await isElementFocused(
|
||||||
page,
|
page,
|
||||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":2}']`
|
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":2}']`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
@ -187,7 +191,7 @@ describe("PDF Thumbnail View", () => {
|
|||||||
expect(
|
expect(
|
||||||
await isElementFocused(
|
await isElementFocused(
|
||||||
page,
|
page,
|
||||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
|
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
@ -198,7 +202,7 @@ describe("PDF Thumbnail View", () => {
|
|||||||
expect(
|
expect(
|
||||||
await isElementFocused(
|
await isElementFocused(
|
||||||
page,
|
page,
|
||||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":3}']`
|
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":3}']`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
@ -214,7 +218,7 @@ describe("PDF Thumbnail View", () => {
|
|||||||
expect(
|
expect(
|
||||||
await isElementFocused(
|
await isElementFocused(
|
||||||
page,
|
page,
|
||||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":14}']`
|
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":14}']`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
@ -224,7 +228,7 @@ describe("PDF Thumbnail View", () => {
|
|||||||
expect(
|
expect(
|
||||||
await isElementFocused(
|
await isElementFocused(
|
||||||
page,
|
page,
|
||||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
|
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { GenericL10n } from "web-null_l10n";
|
|||||||
/**
|
/**
|
||||||
* @typedef {Object} AnnotationEditorLayerBuilderOptions
|
* @typedef {Object} AnnotationEditorLayerBuilderOptions
|
||||||
* @property {AnnotationEditorUIManager} [uiManager]
|
* @property {AnnotationEditorUIManager} [uiManager]
|
||||||
* @property {PDFPageProxy} pdfPage
|
* @property {number} pageIndex
|
||||||
* @property {L10n} [l10n]
|
* @property {L10n} [l10n]
|
||||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
@ -39,6 +39,7 @@ import { GenericL10n } from "web-null_l10n";
|
|||||||
* @property {TextLayer} [textLayer]
|
* @property {TextLayer} [textLayer]
|
||||||
* @property {DrawLayer} [drawLayer]
|
* @property {DrawLayer} [drawLayer]
|
||||||
* @property {function} [onAppend]
|
* @property {function} [onAppend]
|
||||||
|
* @property {AnnotationEditorLayer} [clonedFrom]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,11 +61,13 @@ class AnnotationEditorLayerBuilder {
|
|||||||
|
|
||||||
#uiManager;
|
#uiManager;
|
||||||
|
|
||||||
|
#clonedFrom = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AnnotationEditorLayerBuilderOptions} options
|
* @param {AnnotationEditorLayerBuilderOptions} options
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.pdfPage = options.pdfPage;
|
this.pageIndex = options.pageIndex;
|
||||||
this.accessibilityManager = options.accessibilityManager;
|
this.accessibilityManager = options.accessibilityManager;
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
@ -79,6 +82,12 @@ class AnnotationEditorLayerBuilder {
|
|||||||
this.#drawLayer = options.drawLayer || null;
|
this.#drawLayer = options.drawLayer || null;
|
||||||
this.#onAppend = options.onAppend || null;
|
this.#onAppend = options.onAppend || null;
|
||||||
this.#structTreeLayer = options.structTreeLayer || null;
|
this.#structTreeLayer = options.structTreeLayer || null;
|
||||||
|
this.#clonedFrom = options.clonedFrom || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePageIndex(newPageIndex) {
|
||||||
|
this.pageIndex = newPageIndex;
|
||||||
|
this.annotationEditorLayer?.updatePageIndex(newPageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,7 +122,7 @@ class AnnotationEditorLayerBuilder {
|
|||||||
div,
|
div,
|
||||||
structTreeLayer: this.#structTreeLayer,
|
structTreeLayer: this.#structTreeLayer,
|
||||||
accessibilityManager: this.accessibilityManager,
|
accessibilityManager: this.accessibilityManager,
|
||||||
pageIndex: this.pdfPage.pageNumber - 1,
|
pageIndex: this.pageIndex,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
viewport: clonedViewport,
|
viewport: clonedViewport,
|
||||||
annotationLayer: this.#annotationLayer,
|
annotationLayer: this.#annotationLayer,
|
||||||
@ -121,6 +130,11 @@ class AnnotationEditorLayerBuilder {
|
|||||||
drawLayer: this.#drawLayer,
|
drawLayer: this.#drawLayer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.annotationEditorLayer.setClonedFrom(
|
||||||
|
this.#clonedFrom?.annotationEditorLayer
|
||||||
|
);
|
||||||
|
this.#clonedFrom = null;
|
||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
viewport: clonedViewport,
|
viewport: clonedViewport,
|
||||||
div,
|
div,
|
||||||
|
|||||||
21
web/app.js
21
web/app.js
@ -2185,11 +2185,6 @@ const PDFViewerApplication = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
|
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
|
||||||
eventBus._on(
|
|
||||||
"beforepagesedited",
|
|
||||||
this.onBeforePagesEdited.bind(this),
|
|
||||||
opts
|
|
||||||
);
|
|
||||||
eventBus._on(
|
eventBus._on(
|
||||||
"savepageseditedpdf",
|
"savepageseditedpdf",
|
||||||
this.onSavePagesEditedPDF.bind(this),
|
this.onSavePagesEditedPDF.bind(this),
|
||||||
@ -2369,30 +2364,18 @@ const PDFViewerApplication = {
|
|||||||
await Promise.all([this.l10n?.destroy(), this.close()]);
|
await Promise.all([this.l10n?.destroy(), this.close()]);
|
||||||
},
|
},
|
||||||
|
|
||||||
onBeforePagesEdited(data) {
|
|
||||||
this.pdfViewer.onBeforePagesEdited(data);
|
|
||||||
},
|
|
||||||
|
|
||||||
onPagesEdited(data) {
|
onPagesEdited(data) {
|
||||||
this.pdfViewer.onPagesEdited(data);
|
this.pdfViewer.onPagesEdited(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
async onSavePagesEditedPDF({
|
async onSavePagesEditedPDF({ data: extractParams }) {
|
||||||
data: { includePages, excludePages, pageIndices },
|
|
||||||
}) {
|
|
||||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.pdfDocument) {
|
if (!this.pdfDocument) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pageInfo = {
|
const modifiedPdfBytes = await this.pdfDocument.extractPages(extractParams);
|
||||||
document: null, // For now, no merge.
|
|
||||||
includePages,
|
|
||||||
excludePages,
|
|
||||||
pageIndices,
|
|
||||||
};
|
|
||||||
const modifiedPdfBytes = await this.pdfDocument.extractPages([pageInfo]);
|
|
||||||
if (!modifiedPdfBytes) {
|
if (!modifiedPdfBytes) {
|
||||||
console.error(
|
console.error(
|
||||||
"Something wrong happened when saving the edited PDF.\nPlease file a bug."
|
"Something wrong happened when saving the edited PDF.\nPlease file a bug."
|
||||||
|
|||||||
@ -19,8 +19,6 @@ import { RenderingCancelledException } from "pdfjs-lib";
|
|||||||
class BasePDFPageView extends RenderableView {
|
class BasePDFPageView extends RenderableView {
|
||||||
#loadingId = null;
|
#loadingId = null;
|
||||||
|
|
||||||
#minDurationToUpdateCanvas = 0;
|
|
||||||
|
|
||||||
#renderError = null;
|
#renderError = null;
|
||||||
|
|
||||||
#renderingState = RenderingStates.INITIAL;
|
#renderingState = RenderingStates.INITIAL;
|
||||||
@ -56,7 +54,7 @@ class BasePDFPageView extends RenderableView {
|
|||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
this.enableOptimizedPartialRendering =
|
this.enableOptimizedPartialRendering =
|
||||||
options.enableOptimizedPartialRendering ?? false;
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
get renderingState() {
|
get renderingState() {
|
||||||
@ -116,14 +114,14 @@ class BasePDFPageView extends RenderableView {
|
|||||||
this.#showCanvas = isLastShow => {
|
this.#showCanvas = isLastShow => {
|
||||||
if (updateOnFirstShow) {
|
if (updateOnFirstShow) {
|
||||||
let tempCanvas = this.#tempCanvas;
|
let tempCanvas = this.#tempCanvas;
|
||||||
if (!isLastShow && this.#minDurationToUpdateCanvas > 0) {
|
if (!isLastShow && this.minDurationToUpdateCanvas > 0) {
|
||||||
// We draw on the canvas at 60fps (in using `requestAnimationFrame`),
|
// We draw on the canvas at 60fps (in using `requestAnimationFrame`),
|
||||||
// so if the canvas is large, updating it at 60fps can be a way too
|
// so if the canvas is large, updating it at 60fps can be a way too
|
||||||
// much and can cause some serious performance issues.
|
// much and can cause some serious performance issues.
|
||||||
// To avoid that we only update the canvas every
|
// To avoid that we only update the canvas every
|
||||||
// `this.#minDurationToUpdateCanvas` ms.
|
// `this.#minDurationToUpdateCanvas` ms.
|
||||||
|
|
||||||
if (Date.now() - this.#startTime < this.#minDurationToUpdateCanvas) {
|
if (Date.now() - this.#startTime < this.minDurationToUpdateCanvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!tempCanvas) {
|
if (!tempCanvas) {
|
||||||
|
|||||||
@ -422,6 +422,8 @@ class PDFFindController {
|
|||||||
|
|
||||||
#visitedPagesCount = 0;
|
#visitedPagesCount = 0;
|
||||||
|
|
||||||
|
#copiedExtractTextPromises = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PDFFindControllerOptions} options
|
* @param {PDFFindControllerOptions} options
|
||||||
*/
|
*/
|
||||||
@ -609,6 +611,7 @@ class PDFFindController {
|
|||||||
this._dirtyMatch = false;
|
this._dirtyMatch = false;
|
||||||
clearTimeout(this._findTimeout);
|
clearTimeout(this._findTimeout);
|
||||||
this._findTimeout = null;
|
this._findTimeout = null;
|
||||||
|
this.#copiedExtractTextPromises = null;
|
||||||
|
|
||||||
this._firstPageCapability = Promise.withResolvers();
|
this._firstPageCapability = Promise.withResolvers();
|
||||||
}
|
}
|
||||||
@ -1127,21 +1130,37 @@ class PDFFindController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#onPagesEdited({ pagesMapper }) {
|
#onPagesEdited({ pagesMapper, type, pageNumbers }) {
|
||||||
if (this._extractTextPromises.length === 0) {
|
if (this._extractTextPromises.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "copy") {
|
||||||
|
this.#copiedExtractTextPromises = new Map();
|
||||||
|
for (const pageNum of pageNumbers) {
|
||||||
|
this.#copiedExtractTextPromises.set(
|
||||||
|
pageNum,
|
||||||
|
this._extractTextPromises[pageNum - 1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.#onFindBarClose();
|
this.#onFindBarClose();
|
||||||
this._dirtyMatch = true;
|
this._dirtyMatch = true;
|
||||||
const prevTextPromises = this._extractTextPromises;
|
const prevTextPromises = this._extractTextPromises;
|
||||||
const extractTextPromises = (this._extractTextPromises.length = []);
|
const extractTextPromises = (this._extractTextPromises.length = []);
|
||||||
for (let i = 0, ii = pagesMapper.length; i < ii; i++) {
|
for (let i = 1, ii = pagesMapper.length; i <= ii; i++) {
|
||||||
const prevPageIndex = pagesMapper.getPrevPageNumber(i + 1) - 1;
|
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||||
if (prevPageIndex === -1) {
|
if (prevPageNumber < 0) {
|
||||||
|
extractTextPromises.push(
|
||||||
|
this.#copiedExtractTextPromises?.get(-prevPageNumber) ||
|
||||||
|
Promise.resolve()
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
extractTextPromises.push(
|
extractTextPromises.push(
|
||||||
prevTextPromises[prevPageIndex] || Promise.resolve()
|
prevTextPromises[prevPageNumber - 1] || Promise.resolve()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ class PDFLinkService {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
get pagesCount() {
|
get pagesCount() {
|
||||||
return this.pdfDocument ? this.pdfDocument.numPages : 0;
|
return this.pdfDocument?.pagesMapper.pagesNumber || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -103,6 +103,8 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|||||||
* @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from
|
* @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from
|
||||||
* text that look like URLs. The default value is `true`.
|
* text that look like URLs. The default value is `true`.
|
||||||
* @property {CommentManager} [commentManager] - The comment manager instance.
|
* @property {CommentManager} [commentManager] - The comment manager instance.
|
||||||
|
* @property {PDFPageView} [clonedFrom] - The page view that is cloned
|
||||||
|
* to.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULT_LAYER_PROPERTIES =
|
const DEFAULT_LAYER_PROPERTIES =
|
||||||
@ -166,6 +168,8 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
|
|
||||||
#layers = [null, null, null, null];
|
#layers = [null, null, null, null];
|
||||||
|
|
||||||
|
#clonedFrom = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PDFPageViewOptions} options
|
* @param {PDFPageViewOptions} options
|
||||||
*/
|
*/
|
||||||
@ -197,6 +201,7 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
|
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
|
||||||
this.#enableAutoLinking = options.enableAutoLinking !== false;
|
this.#enableAutoLinking = options.enableAutoLinking !== false;
|
||||||
this.#commentManager = options.commentManager || null;
|
this.#commentManager = options.commentManager || null;
|
||||||
|
this.#clonedFrom = options.clonedFrom || null;
|
||||||
|
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
@ -226,7 +231,6 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
div.setAttribute("data-l10n-id", "pdfjs-page-landmark");
|
div.setAttribute("data-l10n-id", "pdfjs-page-landmark");
|
||||||
div.setAttribute("data-l10n-args", JSON.stringify({ page: this.id }));
|
div.setAttribute("data-l10n-args", JSON.stringify({ page: this.id }));
|
||||||
this.div = div;
|
this.div = div;
|
||||||
|
|
||||||
this.#setDimensions();
|
this.#setDimensions();
|
||||||
container?.append(div);
|
container?.append(div);
|
||||||
|
|
||||||
@ -270,6 +274,35 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(id) {
|
||||||
|
const clone = new PDFPageView({
|
||||||
|
container: null,
|
||||||
|
eventBus: this.eventBus,
|
||||||
|
pagesColors: this.pageColors,
|
||||||
|
renderingQueue: this.renderingQueue,
|
||||||
|
enableOptimizedPartialRendering: this.enableOptimizedPartialRendering,
|
||||||
|
minDurationToUpdateCanvas: this.minDurationToUpdateCanvas,
|
||||||
|
defaultViewport: this.viewport,
|
||||||
|
id,
|
||||||
|
layerProperties: this.#layerProperties,
|
||||||
|
scale: this.scale,
|
||||||
|
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||||
|
textLayerMode: this.#textLayerMode,
|
||||||
|
annotationMode: this.#annotationMode,
|
||||||
|
imageResourcesPath: this.imageResourcesPath,
|
||||||
|
enableDetailCanvas: this.enableDetailCanvas,
|
||||||
|
maxCanvasPixels: this.maxCanvasPixels,
|
||||||
|
maxCanvasDim: this.maxCanvasDim,
|
||||||
|
capCanvasAreaFactor: this.capCanvasAreaFactor,
|
||||||
|
enableAutoLinking: this.#enableAutoLinking,
|
||||||
|
commentManager: this.#commentManager,
|
||||||
|
l10n: this.l10n,
|
||||||
|
clonedFrom: this,
|
||||||
|
});
|
||||||
|
clone.setPdfPage(this.pdfPage);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
#addLayer(div, name) {
|
#addLayer(div, name) {
|
||||||
const pos = LAYERS_ORDER.get(name);
|
const pos = LAYERS_ORDER.get(name);
|
||||||
const oldDiv = this.#layers[pos];
|
const oldDiv = this.#layers[pos];
|
||||||
@ -331,6 +364,7 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
this._textHighlighter.pageIdx = newPageNumber - 1;
|
this._textHighlighter.pageIdx = newPageNumber - 1;
|
||||||
// Don't update the page index for the draw layer, since it's just used as
|
// Don't update the page index for the draw layer, since it's just used as
|
||||||
// an identifier.
|
// an identifier.
|
||||||
|
this.annotationEditorLayer?.updatePageIndex(newPageNumber - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPdfPage(pdfPage) {
|
setPdfPage(pdfPage) {
|
||||||
@ -378,6 +412,15 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
this.pdfPage?.cleanup();
|
this.pdfPage?.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteMe(isCut) {
|
||||||
|
if (isCut) {
|
||||||
|
this.div.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.destroy();
|
||||||
|
this.#layerProperties.annotationEditorUIManager?.deletePage(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
hasEditableAnnotations() {
|
hasEditableAnnotations() {
|
||||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||||
}
|
}
|
||||||
@ -1140,17 +1183,19 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
) {
|
) {
|
||||||
this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({
|
this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({
|
||||||
uiManager: annotationEditorUIManager,
|
uiManager: annotationEditorUIManager,
|
||||||
pdfPage,
|
pageIndex: this.id - 1,
|
||||||
l10n,
|
l10n,
|
||||||
structTreeLayer: this.structTreeLayer,
|
structTreeLayer: this.structTreeLayer,
|
||||||
accessibilityManager: this._accessibilityManager,
|
accessibilityManager: this._accessibilityManager,
|
||||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||||
textLayer: this.textLayer,
|
textLayer: this.textLayer,
|
||||||
drawLayer: this.drawLayer.getDrawLayer(),
|
drawLayer: this.drawLayer.getDrawLayer(),
|
||||||
|
clonedFrom: this.#clonedFrom?.annotationEditorLayer,
|
||||||
onAppend: annotationEditorLayerDiv => {
|
onAppend: annotationEditorLayerDiv => {
|
||||||
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.#clonedFrom = null;
|
||||||
this.#renderAnnotationEditorLayer();
|
this.#renderAnnotationEditorLayer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -116,10 +116,10 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
|
|
||||||
this.placeholder = null;
|
this.placeholder = null;
|
||||||
|
|
||||||
const imageContainer = (this.div = document.createElement("div"));
|
const thumbnailContainer = (this.div = document.createElement("div"));
|
||||||
imageContainer.className = "thumbnail";
|
thumbnailContainer.className = "thumbnail";
|
||||||
imageContainer.setAttribute("page-number", id);
|
thumbnailContainer.setAttribute("page-number", id);
|
||||||
imageContainer.setAttribute("page-id", id);
|
thumbnailContainer.setAttribute("page-id", id);
|
||||||
|
|
||||||
if (enableSplitMerge) {
|
if (enableSplitMerge) {
|
||||||
const checkbox = (this.checkbox = document.createElement("input"));
|
const checkbox = (this.checkbox = document.createElement("input"));
|
||||||
@ -127,24 +127,80 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
checkbox.tabIndex = -1;
|
checkbox.tabIndex = -1;
|
||||||
checkbox.setAttribute("data-l10n-id", "pdfjs-thumb-page-checkbox");
|
checkbox.setAttribute("data-l10n-id", "pdfjs-thumb-page-checkbox");
|
||||||
checkbox.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
checkbox.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
imageContainer.append(checkbox);
|
thumbnailContainer.append(checkbox);
|
||||||
|
this.pasteButton = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageContainer = (this.imageContainer =
|
||||||
|
document.createElement("div"));
|
||||||
|
thumbnailContainer.append(imageContainer);
|
||||||
|
imageContainer.classList.add(
|
||||||
|
"thumbnailImageContainer",
|
||||||
|
"missingThumbnailImage"
|
||||||
|
);
|
||||||
|
imageContainer.role = "button";
|
||||||
|
imageContainer.tabIndex = -1;
|
||||||
|
imageContainer.draggable = false;
|
||||||
|
imageContainer.setAttribute("page-number", id);
|
||||||
|
|
||||||
const image = (this.image = document.createElement("img"));
|
const image = (this.image = document.createElement("img"));
|
||||||
image.classList.add("thumbnailImage", "missingThumbnailImage");
|
imageContainer.append(image);
|
||||||
image.role = "button";
|
|
||||||
image.tabIndex = -1;
|
|
||||||
image.draggable = false;
|
|
||||||
this.#updateDims();
|
this.#updateDims();
|
||||||
|
|
||||||
imageContainer.append(image);
|
container.append(thumbnailContainer);
|
||||||
container.append(imageContainer);
|
}
|
||||||
|
|
||||||
|
clone(container, id) {
|
||||||
|
const thumbnailView = new PDFThumbnailView({
|
||||||
|
container,
|
||||||
|
id,
|
||||||
|
eventBus: this.eventBus,
|
||||||
|
defaultViewport: this.viewport,
|
||||||
|
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||||
|
linkService: this.linkService,
|
||||||
|
renderingQueue: this.renderingQueue,
|
||||||
|
maxCanvasPixels: this.maxCanvasPixels,
|
||||||
|
maxCanvasDim: this.maxCanvasDim,
|
||||||
|
pageColors: this.pageColors,
|
||||||
|
enableSplitMerge: !!this.checkbox,
|
||||||
|
});
|
||||||
|
thumbnailView.setPdfPage(this.pdfPage);
|
||||||
|
const { imageContainer } = this;
|
||||||
|
if (!imageContainer.classList.contains("missingThumbnailImage")) {
|
||||||
|
thumbnailView.image.replaceWith(this.image.cloneNode(true));
|
||||||
|
thumbnailView.imageContainer.classList.remove("missingThumbnailImage");
|
||||||
|
}
|
||||||
|
return thumbnailView;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPasteButton(pasteCallback) {
|
||||||
|
if (this.pasteButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pasteButton = (this.pasteButton = document.createElement("button"));
|
||||||
|
pasteButton.classList.add("thumbnailPasteButton", "viewsManagerButton");
|
||||||
|
pasteButton.tabIndex = 0;
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.setAttribute("data-l10n-id", "pdfjs-views-manager-paste-button-label");
|
||||||
|
pasteButton.append(span);
|
||||||
|
pasteButton.addEventListener("click", () => {
|
||||||
|
pasteCallback(this.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.imageContainer.after(pasteButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSelected(isSelected) {
|
||||||
|
if (this.checkbox) {
|
||||||
|
this.checkbox.checked = isSelected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateId(newId) {
|
updateId(newId) {
|
||||||
this.id = newId;
|
this.id = newId;
|
||||||
this.renderingId = `thumbnail${newId}`;
|
this.renderingId = `thumbnail${newId}`;
|
||||||
this.div.setAttribute("page-number", newId);
|
this.div.setAttribute("page-number", newId);
|
||||||
|
this.imageContainer.setAttribute("page-number", newId);
|
||||||
// TODO: do we set the page label ?
|
// TODO: do we set the page label ?
|
||||||
this.setPageLabel(this.pageLabel);
|
this.setPageLabel(this.pageLabel);
|
||||||
}
|
}
|
||||||
@ -157,7 +213,7 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
|
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
|
||||||
this.scale = canvasWidth / width;
|
this.scale = canvasWidth / width;
|
||||||
|
|
||||||
this.image.style.height = `${canvasHeight}px`;
|
this.imageContainer.style.height = `${canvasHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get renderingState() {
|
get renderingState() {
|
||||||
@ -181,17 +237,23 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
this.renderingState = RenderingStates.INITIAL;
|
this.renderingState = RenderingStates.INITIAL;
|
||||||
this.#updateDims();
|
this.#updateDims();
|
||||||
|
|
||||||
const { image } = this;
|
const { image, imageContainer } = this;
|
||||||
const url = image.src;
|
const url = image.src;
|
||||||
if (url) {
|
if (url) {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
image.removeAttribute("data-l10n-id");
|
|
||||||
image.removeAttribute("data-l10n-args");
|
|
||||||
image.src = "";
|
image.src = "";
|
||||||
this.image.classList.add("missingThumbnailImage");
|
imageContainer.removeAttribute("data-l10n-id");
|
||||||
|
imageContainer.removeAttribute("data-l10n-args");
|
||||||
|
imageContainer.classList.add("missingThumbnailImage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.reset();
|
||||||
|
this.toggleCurrent(false);
|
||||||
|
this.div.remove();
|
||||||
|
}
|
||||||
|
|
||||||
update({ rotation = null }) {
|
update({ rotation = null }) {
|
||||||
if (typeof rotation === "number") {
|
if (typeof rotation === "number") {
|
||||||
this.rotation = rotation; // The rotation may be zero.
|
this.rotation = rotation; // The rotation may be zero.
|
||||||
@ -205,12 +267,13 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleCurrent(isCurrent) {
|
toggleCurrent(isCurrent) {
|
||||||
|
const { imageContainer } = this;
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
this.image.ariaCurrent = "page";
|
imageContainer.ariaCurrent = "page";
|
||||||
this.image.tabIndex = 0;
|
imageContainer.tabIndex = 0;
|
||||||
} else {
|
} else {
|
||||||
this.image.ariaCurrent = false;
|
imageContainer.ariaCurrent = false;
|
||||||
this.image.tabIndex = -1;
|
imageContainer.tabIndex = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,14 +320,14 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
throw new Error("#convertCanvasToImage: Rendering has not finished.");
|
throw new Error("#convertCanvasToImage: Rendering has not finished.");
|
||||||
}
|
}
|
||||||
const reducedCanvas = this.#reduceImage(canvas);
|
const reducedCanvas = this.#reduceImage(canvas);
|
||||||
const { image } = this;
|
const { imageContainer, image } = this;
|
||||||
const { promise, resolve } = Promise.withResolvers();
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
reducedCanvas.toBlob(resolve);
|
reducedCanvas.toBlob(resolve);
|
||||||
const blob = await promise;
|
const blob = await promise;
|
||||||
image.src = URL.createObjectURL(blob);
|
image.src = URL.createObjectURL(blob);
|
||||||
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
imageContainer.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
||||||
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
imageContainer.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
image.classList.remove("missingThumbnailImage");
|
imageContainer.classList.remove("missingThumbnailImage");
|
||||||
if (!FeatureTest.isOffscreenCanvasSupported) {
|
if (!FeatureTest.isOffscreenCanvasSupported) {
|
||||||
// Clean up the canvas element since it is no longer needed.
|
// Clean up the canvas element since it is no longer needed.
|
||||||
reducedCanvas.width = reducedCanvas.height = 0;
|
reducedCanvas.width = reducedCanvas.height = 0;
|
||||||
@ -465,7 +528,7 @@ class PDFThumbnailView extends RenderableView {
|
|||||||
*/
|
*/
|
||||||
setPageLabel(label) {
|
setPageLabel(label) {
|
||||||
this.pageLabel = typeof label === "string" ? label : null;
|
this.pageLabel = typeof label === "string" ? label : null;
|
||||||
this.image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
this.imageContainer.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
this.checkbox?.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
this.checkbox?.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,6 +114,18 @@ class PDFThumbnailViewer {
|
|||||||
|
|
||||||
#manageSaveAsButton = null;
|
#manageSaveAsButton = null;
|
||||||
|
|
||||||
|
#manageDeleteButton = null;
|
||||||
|
|
||||||
|
#manageCopyButton = null;
|
||||||
|
|
||||||
|
#manageCutButton = null;
|
||||||
|
|
||||||
|
#copiedThumbnails = null;
|
||||||
|
|
||||||
|
#copiedPageNumbers = null;
|
||||||
|
|
||||||
|
#isCut = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PDFThumbnailViewerOptions} options
|
* @param {PDFThumbnailViewerOptions} options
|
||||||
*/
|
*/
|
||||||
@ -143,6 +155,14 @@ class PDFThumbnailViewer {
|
|||||||
|
|
||||||
if (this.#enableSplitMerge && manageMenu) {
|
if (this.#enableSplitMerge && manageMenu) {
|
||||||
const { button, menu, copy, cut, delete: del, saveAs } = manageMenu;
|
const { button, menu, copy, cut, delete: del, saveAs } = manageMenu;
|
||||||
|
this.eventBus.on(
|
||||||
|
"pagesloaded",
|
||||||
|
() => {
|
||||||
|
button.disabled = false;
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]);
|
this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]);
|
||||||
this.#manageSaveAsButton = saveAs;
|
this.#manageSaveAsButton = saveAs;
|
||||||
saveAs.addEventListener("click", () => {
|
saveAs.addEventListener("click", () => {
|
||||||
@ -151,6 +171,15 @@ class PDFThumbnailViewer {
|
|||||||
data: this.#pagesMapper.getPageMappingForSaving(),
|
data: this.#pagesMapper.getPageMappingForSaving(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this.#manageDeleteButton = del;
|
||||||
|
del.addEventListener("click", this.#deletePages.bind(this));
|
||||||
|
this.#manageCopyButton = copy;
|
||||||
|
copy.addEventListener("click", this.#copyPages.bind(this));
|
||||||
|
this.#manageCutButton = cut;
|
||||||
|
cut.addEventListener("click", this.#cutPages.bind(this));
|
||||||
|
|
||||||
|
this.#toggleMenuEntries(false);
|
||||||
|
button.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
manageMenu.button.hidden = true;
|
manageMenu.button.hidden = true;
|
||||||
}
|
}
|
||||||
@ -191,7 +220,7 @@ class PDFThumbnailViewer {
|
|||||||
}
|
}
|
||||||
if (pageNumber !== this._currentPageNumber) {
|
if (pageNumber !== this._currentPageNumber) {
|
||||||
const prevThumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
const prevThumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
||||||
prevThumbnailView.toggleCurrent(/* isCurrent = */ false);
|
prevThumbnailView?.toggleCurrent(/* isCurrent = */ false);
|
||||||
thumbnailView.toggleCurrent(/* isCurrent = */ true);
|
thumbnailView.toggleCurrent(/* isCurrent = */ true);
|
||||||
this._currentPageNumber = pageNumber;
|
this._currentPageNumber = pageNumber;
|
||||||
}
|
}
|
||||||
@ -200,15 +229,11 @@ class PDFThumbnailViewer {
|
|||||||
// If the thumbnail isn't currently visible, scroll it into view.
|
// If the thumbnail isn't currently visible, scroll it into view.
|
||||||
if (views.length > 0) {
|
if (views.length > 0) {
|
||||||
let shouldScroll = false;
|
let shouldScroll = false;
|
||||||
if (
|
if (pageNumber <= first.id || pageNumber >= last.id) {
|
||||||
pageNumber <= this.#pagesMapper.getPageNumber(first.id) ||
|
|
||||||
pageNumber >= this.#pagesMapper.getPageNumber(last.id)
|
|
||||||
) {
|
|
||||||
shouldScroll = true;
|
shouldScroll = true;
|
||||||
} else {
|
} else {
|
||||||
for (const { id, percent } of views) {
|
for (const { id, percent } of views) {
|
||||||
const mappedPageNumber = this.#pagesMapper.getPageNumber(id);
|
if (id !== pageNumber) {
|
||||||
if (mappedPageNumber !== pageNumber) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
shouldScroll = percent < 100;
|
shouldScroll = percent < 100;
|
||||||
@ -403,24 +428,45 @@ class PDFThumbnailViewer {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateThumbnails() {
|
#updateThumbnails(currentPageNumber) {
|
||||||
|
let newCurrentPageNumber = 0;
|
||||||
const pagesMapper = this.#pagesMapper;
|
const pagesMapper = this.#pagesMapper;
|
||||||
this.container.replaceChildren();
|
this.container.replaceChildren();
|
||||||
const prevThumbnails = this._thumbnails;
|
const prevThumbnails = this._thumbnails;
|
||||||
const newThumbnails = (this._thumbnails = []);
|
const newThumbnails = (this._thumbnails = []);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
|
const isCut = this.#isCut;
|
||||||
const prevPageIndex = pagesMapper.getPrevPageNumber(i + 1) - 1;
|
const oldThumbnails = new Set(prevThumbnails);
|
||||||
if (prevPageIndex === -1) {
|
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);
|
||||||
|
fragment.append(thumbnail.div);
|
||||||
|
} else {
|
||||||
|
thumbnail = thumbnail.clone(fragment, i);
|
||||||
|
}
|
||||||
|
newThumbnails.push(thumbnail);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const newThumbnail = prevThumbnails[prevPageIndex];
|
if (prevPageNumber === currentPageNumber) {
|
||||||
|
newCurrentPageNumber = i;
|
||||||
|
}
|
||||||
|
const newThumbnail = prevThumbnails[prevPageNumber - 1];
|
||||||
newThumbnails.push(newThumbnail);
|
newThumbnails.push(newThumbnail);
|
||||||
newThumbnail.updateId(i + 1);
|
newThumbnail.updateId(i);
|
||||||
|
oldThumbnails.delete(newThumbnail);
|
||||||
newThumbnail.checkbox.checked = false;
|
newThumbnail.checkbox.checked = false;
|
||||||
fragment.append(newThumbnail.div);
|
fragment.append(newThumbnail.div);
|
||||||
}
|
}
|
||||||
this.container.append(fragment);
|
this.container.append(fragment);
|
||||||
|
for (const oldThumbnail of oldThumbnails) {
|
||||||
|
oldThumbnail.destroy();
|
||||||
|
}
|
||||||
|
return newCurrentPageNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
#onStartDragging(draggedThumbnail) {
|
#onStartDragging(draggedThumbnail) {
|
||||||
@ -443,18 +489,18 @@ class PDFThumbnailViewer {
|
|||||||
const thumbnail = this._thumbnails[selected - 1];
|
const thumbnail = this._thumbnails[selected - 1];
|
||||||
const placeholder = (thumbnail.placeholder =
|
const placeholder = (thumbnail.placeholder =
|
||||||
document.createElement("div"));
|
document.createElement("div"));
|
||||||
placeholder.classList.add("thumbnailImage", "placeholder");
|
placeholder.classList.add("thumbnailImageContainer", "placeholder");
|
||||||
const { div, image } = thumbnail;
|
const { div, imageContainer } = thumbnail;
|
||||||
div.classList.add("isDragging");
|
div.classList.add("isDragging");
|
||||||
placeholder.style.height = getComputedStyle(image).height;
|
placeholder.style.height = getComputedStyle(imageContainer).height;
|
||||||
image.after(placeholder);
|
imageContainer.after(placeholder);
|
||||||
if (selected !== startPageNumber) {
|
if (selected !== startPageNumber) {
|
||||||
image.classList.add("hidden");
|
imageContainer.classList.add("hidden");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (this.#selectedPages.size === 1) {
|
if (this.#selectedPages.size === 1) {
|
||||||
image.classList.add("draggingThumbnail");
|
imageContainer.classList.add("draggingThumbnail");
|
||||||
this.#draggedContainer = image;
|
this.#draggedContainer = imageContainer;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// For multiple selected thumbnails, only the one being dragged is shown
|
// For multiple selected thumbnails, only the one being dragged is shown
|
||||||
@ -463,13 +509,13 @@ class PDFThumbnailViewer {
|
|||||||
document.createElement("div"));
|
document.createElement("div"));
|
||||||
draggedContainer.classList.add(
|
draggedContainer.classList.add(
|
||||||
"draggingThumbnail",
|
"draggingThumbnail",
|
||||||
"thumbnailImage",
|
"thumbnailImageContainer",
|
||||||
"multiple"
|
"multiple"
|
||||||
);
|
);
|
||||||
draggedContainer.style.height = getComputedStyle(image).height;
|
draggedContainer.style.height = getComputedStyle(imageContainer).height;
|
||||||
image.replaceWith(draggedContainer);
|
imageContainer.replaceWith(draggedContainer);
|
||||||
image.classList.remove("thumbnailImage");
|
imageContainer.classList.remove("thumbnailImageContainer");
|
||||||
draggedContainer.append(image);
|
draggedContainer.append(imageContainer);
|
||||||
draggedContainer.setAttribute(
|
draggedContainer.setAttribute(
|
||||||
"data-multiple-count",
|
"data-multiple-count",
|
||||||
this.#selectedPages.size
|
this.#selectedPages.size
|
||||||
@ -490,17 +536,17 @@ class PDFThumbnailViewer {
|
|||||||
this.container.classList.remove("isDragging");
|
this.container.classList.remove("isDragging");
|
||||||
for (const selected of this.#selectedPages) {
|
for (const selected of this.#selectedPages) {
|
||||||
const thumbnail = this._thumbnails[selected - 1];
|
const thumbnail = this._thumbnails[selected - 1];
|
||||||
const { div, placeholder, image } = thumbnail;
|
const { div, placeholder, imageContainer } = thumbnail;
|
||||||
placeholder.remove();
|
placeholder.remove();
|
||||||
image.classList.remove("draggingThumbnail", "hidden");
|
imageContainer.classList.remove("draggingThumbnail", "hidden");
|
||||||
div.classList.remove("isDragging");
|
div.classList.remove("isDragging");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draggedContainer.classList.contains("multiple")) {
|
if (draggedContainer.classList.contains("multiple")) {
|
||||||
// Restore the dragged image to its thumbnail.
|
// Restore the dragged image to its thumbnail.
|
||||||
const originalImage = draggedContainer.firstElementChild;
|
const originalImageContainer = draggedContainer.firstElementChild;
|
||||||
draggedContainer.replaceWith(originalImage);
|
draggedContainer.replaceWith(originalImageContainer);
|
||||||
originalImage.classList.add("thumbnailImage");
|
originalImageContainer.classList.add("thumbnailImageContainer");
|
||||||
} else {
|
} else {
|
||||||
draggedContainer.style.translate = "";
|
draggedContainer.style.translate = "";
|
||||||
}
|
}
|
||||||
@ -515,45 +561,36 @@ class PDFThumbnailViewer {
|
|||||||
selectedPages.has(lastDraggedOverIndex + 2))
|
selectedPages.has(lastDraggedOverIndex + 2))
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
this._thumbnails[this._currentPageNumber - 1]?.toggleCurrent(
|
||||||
|
/* isCurrent = */ false
|
||||||
|
);
|
||||||
|
this._currentPageNumber = -1;
|
||||||
|
|
||||||
const newIndex = lastDraggedOverIndex + 1;
|
const newIndex = lastDraggedOverIndex + 1;
|
||||||
const pagesToMove = Array.from(selectedPages).sort((a, b) => a - b);
|
const pagesToMove = Array.from(selectedPages).sort((a, b) => a - b);
|
||||||
const pagesMapper = this.#pagesMapper;
|
const pagesMapper = this.#pagesMapper;
|
||||||
const currentPageId = pagesMapper.getPageId(this._currentPageNumber);
|
let currentPageNumber = isNaN(this.#pageNumberToRemove)
|
||||||
const newCurrentPageId = pagesMapper.getPageId(
|
? pagesToMove[0]
|
||||||
isNaN(this.#pageNumberToRemove)
|
: this.#pageNumberToRemove;
|
||||||
? pagesToMove[0]
|
|
||||||
: this.#pageNumberToRemove
|
|
||||||
);
|
|
||||||
|
|
||||||
this.eventBus.dispatch("beforepagesedited", {
|
|
||||||
source: this,
|
|
||||||
pagesMapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
pagesMapper.movePages(selectedPages, pagesToMove, newIndex);
|
pagesMapper.movePages(selectedPages, pagesToMove, newIndex);
|
||||||
|
|
||||||
this.#updateThumbnails();
|
currentPageNumber = this.#updateThumbnails(currentPageNumber);
|
||||||
|
|
||||||
this._currentPageNumber = pagesMapper.getPageNumber(currentPageId);
|
|
||||||
this.#computeThumbnailsPosition();
|
this.#computeThumbnailsPosition();
|
||||||
|
|
||||||
selectedPages.clear();
|
selectedPages.clear();
|
||||||
this.#pageNumberToRemove = NaN;
|
this.#pageNumberToRemove = NaN;
|
||||||
|
this.#updateMenuEntries();
|
||||||
|
|
||||||
const isIdentity = (this.#manageSaveAsButton.disabled =
|
this.eventBus.dispatch("pagesedited", {
|
||||||
!this.#pagesMapper.hasBeenAltered());
|
source: this,
|
||||||
if (!isIdentity) {
|
pagesMapper,
|
||||||
this.eventBus.dispatch("pagesedited", {
|
type: "move",
|
||||||
source: this,
|
});
|
||||||
pagesMapper,
|
|
||||||
index: newIndex,
|
|
||||||
pagesToMove,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCurrentPageNumber = pagesMapper.getPageNumber(newCurrentPageId);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.linkService.goToPage(newCurrentPageNumber);
|
this.forceRendering();
|
||||||
|
this.linkService.goToPage(currentPageNumber);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,6 +600,121 @@ class PDFThumbnailViewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#clearSelection() {
|
||||||
|
for (const pageNumber of this.#selectedPages) {
|
||||||
|
this._thumbnails[pageNumber - 1].toggleSelected(false);
|
||||||
|
}
|
||||||
|
this.#selectedPages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyPages(clearSelection = true) {
|
||||||
|
const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from(
|
||||||
|
this.#selectedPages
|
||||||
|
).sort((a, b) => a - b));
|
||||||
|
const pagesMapper = this.#pagesMapper;
|
||||||
|
pagesMapper.copyPages(pageNumbersToCopy);
|
||||||
|
this.#copiedThumbnails ||= new Map();
|
||||||
|
for (const pageNumber of pageNumbersToCopy) {
|
||||||
|
this.#copiedThumbnails.set(pageNumber, this._thumbnails[pageNumber - 1]);
|
||||||
|
}
|
||||||
|
this.eventBus.dispatch("pagesedited", {
|
||||||
|
source: this,
|
||||||
|
pagesMapper,
|
||||||
|
pageNumbers: pageNumbersToCopy,
|
||||||
|
type: "copy",
|
||||||
|
});
|
||||||
|
if (clearSelection) {
|
||||||
|
this.#clearSelection();
|
||||||
|
}
|
||||||
|
for (const thumbnail of this._thumbnails) {
|
||||||
|
thumbnail.addPasteButton(this.#pastePages.bind(this));
|
||||||
|
}
|
||||||
|
this.container.classList.add("pasteMode");
|
||||||
|
this.#toggleMenuEntries(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#cutPages() {
|
||||||
|
this.#isCut = true;
|
||||||
|
this.#copyPages(false);
|
||||||
|
this.#deletePages(/* type = */ "cut");
|
||||||
|
}
|
||||||
|
|
||||||
|
#pastePages(index) {
|
||||||
|
this.container.classList.remove("pasteMode");
|
||||||
|
this.#toggleMenuEntries(true);
|
||||||
|
|
||||||
|
const pagesMapper = this.#pagesMapper;
|
||||||
|
let currentPageNumber = this.#copiedPageNumbers.includes(
|
||||||
|
this._currentPageNumber
|
||||||
|
)
|
||||||
|
? 0
|
||||||
|
: this._currentPageNumber;
|
||||||
|
|
||||||
|
pagesMapper.pastePages(index);
|
||||||
|
currentPageNumber = this.#updateThumbnails(currentPageNumber);
|
||||||
|
|
||||||
|
this.eventBus.dispatch("pagesedited", {
|
||||||
|
source: this,
|
||||||
|
pagesMapper,
|
||||||
|
hasBeenCut: this.#isCut,
|
||||||
|
type: "paste",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#copiedThumbnails = null;
|
||||||
|
this.#isCut = false;
|
||||||
|
this.#updateMenuEntries();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.forceRendering();
|
||||||
|
this.linkService.goToPage(currentPageNumber || 1);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#deletePages(type = "delete") {
|
||||||
|
const selectedPages = this.#selectedPages;
|
||||||
|
if (selectedPages.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pagesMapper = this.#pagesMapper;
|
||||||
|
let currentPageNumber = selectedPages.has(this._currentPageNumber)
|
||||||
|
? 0
|
||||||
|
: this._currentPageNumber;
|
||||||
|
const pagesToDelete = Uint32Array.from(selectedPages).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
pagesMapper.deletePages(pagesToDelete);
|
||||||
|
currentPageNumber = this.#updateThumbnails(currentPageNumber);
|
||||||
|
selectedPages.clear();
|
||||||
|
this.#updateMenuEntries();
|
||||||
|
|
||||||
|
this.eventBus.dispatch("pagesedited", {
|
||||||
|
source: this,
|
||||||
|
pagesMapper,
|
||||||
|
pageNumbers: pagesToDelete,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.forceRendering();
|
||||||
|
this.linkService.goToPage(currentPageNumber || 1);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateMenuEntries() {
|
||||||
|
this.#manageSaveAsButton.disabled = !this.#pagesMapper.hasBeenAltered();
|
||||||
|
this.#manageDeleteButton.disabled =
|
||||||
|
this.#manageCopyButton.disabled =
|
||||||
|
this.#manageCutButton.disabled =
|
||||||
|
!this.#selectedPages?.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggleMenuEntries(enable) {
|
||||||
|
this.#manageSaveAsButton.disabled =
|
||||||
|
this.#manageDeleteButton.disabled =
|
||||||
|
this.#manageCopyButton.disabled =
|
||||||
|
this.#manageCutButton.disabled =
|
||||||
|
!enable;
|
||||||
|
}
|
||||||
|
|
||||||
#moveDraggedContainer(dx, dy) {
|
#moveDraggedContainer(dx, dy) {
|
||||||
this.#draggedImageOffsetX += dx;
|
this.#draggedImageOffsetX += dx;
|
||||||
this.#draggedImageOffsetY += dy;
|
this.#draggedImageOffsetY += dy;
|
||||||
@ -714,11 +866,11 @@ class PDFThumbnailViewer {
|
|||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
break;
|
break;
|
||||||
case "Home":
|
case "Home":
|
||||||
this._thumbnails[0].image.focus();
|
this._thumbnails[0].imageContainer.focus();
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
break;
|
break;
|
||||||
case "End":
|
case "End":
|
||||||
this._thumbnails.at(-1).image.focus();
|
this._thumbnails.at(-1).imageContainer.focus();
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
break;
|
break;
|
||||||
case "Enter":
|
case "Enter":
|
||||||
@ -749,6 +901,7 @@ class PDFThumbnailViewer {
|
|||||||
} else {
|
} else {
|
||||||
set.delete(pageNumber);
|
set.delete(pageNumber);
|
||||||
}
|
}
|
||||||
|
this.#updateMenuEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
#addDragListeners() {
|
#addDragListeners() {
|
||||||
@ -763,8 +916,9 @@ class PDFThumbnailViewer {
|
|||||||
pointerId: dragPointerId,
|
pointerId: dragPointerId,
|
||||||
} = e;
|
} = e;
|
||||||
if (
|
if (
|
||||||
|
this.#pagesMapper.copiedPageNumbers?.length > 0 ||
|
||||||
!isNaN(this.#lastDraggedOverIndex) ||
|
!isNaN(this.#lastDraggedOverIndex) ||
|
||||||
!draggedImage.classList.contains("thumbnailImage")
|
!draggedImage.classList.contains("thumbnailImageContainer")
|
||||||
) {
|
) {
|
||||||
// We're already handling a drag, or the target is not draggable.
|
// We're already handling a drag, or the target is not draggable.
|
||||||
return;
|
return;
|
||||||
@ -884,7 +1038,7 @@ class PDFThumbnailViewer {
|
|||||||
|
|
||||||
#goToPage(e) {
|
#goToPage(e) {
|
||||||
const { target } = e;
|
const { target } = e;
|
||||||
if (target.classList.contains("thumbnailImage")) {
|
if (target.classList.contains("thumbnailImageContainer")) {
|
||||||
const pageNumber = parseInt(
|
const pageNumber = parseInt(
|
||||||
target.parentElement.getAttribute("page-number"),
|
target.parentElement.getAttribute("page-number"),
|
||||||
10
|
10
|
||||||
@ -943,7 +1097,7 @@ class PDFThumbnailViewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nextThumbnail) {
|
if (nextThumbnail) {
|
||||||
nextThumbnail.image.focus();
|
nextThumbnail.imageContainer.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -289,6 +289,8 @@ class PDFViewer {
|
|||||||
|
|
||||||
#viewerAlert = null;
|
#viewerAlert = null;
|
||||||
|
|
||||||
|
#copiedPageViews = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PDFViewerOptions} options
|
* @param {PDFViewerOptions} options
|
||||||
*/
|
*/
|
||||||
@ -1173,23 +1175,44 @@ class PDFViewer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBeforePagesEdited({ pagesMapper }) {
|
onPagesEdited({ pagesMapper, type, hasBeenCut, pageNumbers }) {
|
||||||
await this._pagesCapability.promise;
|
if (type === "copy") {
|
||||||
this._currentPageId = pagesMapper.getPageId(this._currentPageNumber);
|
this.#copiedPageViews = new Map();
|
||||||
}
|
for (const pageNum of pageNumbers) {
|
||||||
|
this.#copiedPageViews.set(pageNum, this._pages[pageNum - 1]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onPagesEdited({ pagesMapper }) {
|
const isCut = type === "cut";
|
||||||
this._currentPageNumber = pagesMapper.getPageNumber(this._currentPageId);
|
if (isCut || type === "delete") {
|
||||||
|
for (const pageNum of pageNumbers) {
|
||||||
|
this._pages[pageNum - 1].deleteMe(isCut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentPageNumber = 0;
|
||||||
const prevPages = this._pages;
|
const prevPages = this._pages;
|
||||||
const newPages = (this._pages = []);
|
const newPages = (this._pages = []);
|
||||||
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
|
for (let i = 1, ii = pagesMapper.pagesNumber; i <= ii; i++) {
|
||||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i + 1) - 1;
|
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||||
if (prevPageNumber === -1) {
|
if (prevPageNumber < 0) {
|
||||||
|
let page = this.#copiedPageViews.get(-prevPageNumber);
|
||||||
|
if (hasBeenCut) {
|
||||||
|
page.updatePageNumber(i);
|
||||||
|
} else {
|
||||||
|
page = page.clone(i);
|
||||||
|
}
|
||||||
|
newPages.push(page);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const page = prevPages[prevPageNumber];
|
const page = prevPages[prevPageNumber - 1];
|
||||||
newPages[i] = page;
|
newPages.push(page);
|
||||||
page.updatePageNumber(i + 1);
|
page.updatePageNumber(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCut) {
|
||||||
|
this.#copiedPageViews = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewerElement =
|
const viewerElement =
|
||||||
@ -1204,6 +1227,7 @@ class PDFViewer {
|
|||||||
}
|
}
|
||||||
viewerElement.append(fragment);
|
viewerElement.append(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.forceRendering();
|
this.forceRendering();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -535,7 +535,7 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
> .thumbnailImage:hover {
|
> .thumbnailImageContainer:hover {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|
||||||
&:not([aria-current="page"]) {
|
&:not([aria-current="page"]) {
|
||||||
@ -557,6 +557,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.pasteMode {
|
||||||
|
> .thumbnail {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .thumbnailPasteButton {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
|
||||||
|
font: menu;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.pasteMode) > .thumbnail > .thumbnailPasteButton {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -567,14 +596,19 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
scroll-margin-top: 20px;
|
scroll-margin-top: 20px;
|
||||||
|
|
||||||
&:not(.isDragging)::after {
|
.thumbnailPasteButton {
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.isDragging) > .thumbnailImageContainer::after {
|
||||||
content: attr(page-number);
|
content: attr(page-number);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--image-page-number-bg);
|
background-color: var(--image-page-number-bg);
|
||||||
color: var(--image-page-number-fg);
|
color: var(--image-page-number-fg);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
inset-inline-end: calc(var(--thumbnail-width) / 2);
|
inset-inline-end: 50%;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -590,7 +624,8 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has([aria-current="page"]):not(.isDragging)::after {
|
&:has([aria-current="page"]):not(.isDragging)
|
||||||
|
> .thumbnailImageContainer::after {
|
||||||
background-color: var(--image-current-page-number-bg);
|
background-color: var(--image-current-page-number-bg);
|
||||||
color: var(--image-current-page-number-fg);
|
color: var(--image-current-page-number-fg);
|
||||||
}
|
}
|
||||||
@ -603,7 +638,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .thumbnailImage {
|
> .thumbnailImageContainer {
|
||||||
--thumbnail-dragging-scale: 1.4;
|
--thumbnail-dragging-scale: 1.4;
|
||||||
|
|
||||||
width: var(--thumbnail-width);
|
width: var(--thumbnail-width);
|
||||||
@ -613,6 +648,17 @@
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
outline: var(--image-outline);
|
outline: var(--image-outline);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: inherit;
|
||||||
|
border-radius: inherit;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.missingThumbnailImage {
|
&.missingThumbnailImage {
|
||||||
content-visibility: hidden;
|
content-visibility: hidden;
|
||||||
@ -658,7 +704,7 @@
|
|||||||
&.multiple {
|
&.multiple {
|
||||||
box-shadow: var(--image-multiple-dragging-shadow);
|
box-shadow: var(--image-multiple-dragging-shadow);
|
||||||
|
|
||||||
> img {
|
> .thumbnailImageContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user