mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 14:54:04 +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 =
|
||||
.title = Close
|
||||
pdfjs-views-manager-status-close-button-label = Close
|
||||
pdfjs-views-manager-paste-button-label = Paste
|
||||
|
||||
@ -2405,6 +2405,8 @@ class WorkerTransport {
|
||||
|
||||
#passwordCapability = null;
|
||||
|
||||
#copiedPageInfo = null;
|
||||
|
||||
constructor(
|
||||
messageHandler,
|
||||
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 newPromiseCache = new Map();
|
||||
for (let i = 0, ii = this.pagesMapper.pagesNumber; i < ii; i++) {
|
||||
const prevPageIndex = this.pagesMapper.getPrevPageNumber(i + 1) - 1;
|
||||
const { pagesMapper } = this;
|
||||
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);
|
||||
if (page) {
|
||||
newPageCache.set(i, page);
|
||||
@ -3001,7 +3034,11 @@ class WorkerTransport {
|
||||
num: ref.num,
|
||||
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) {
|
||||
@ -3150,9 +3187,13 @@ class WorkerTransport {
|
||||
}
|
||||
const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`;
|
||||
const pageIndex = this.#pageRefCache.get(refStr);
|
||||
return pageIndex >= 0
|
||||
? this.pagesMapper.getPageNumber(pageIndex + 1)
|
||||
: null;
|
||||
if (pageIndex >= 0) {
|
||||
const pageNumber = this.pagesMapper.getPageNumber(pageIndex + 1);
|
||||
if (pageNumber !== 0) {
|
||||
return pageNumber;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1046,7 +1046,7 @@ function makePathFromDrawOPS(data) {
|
||||
class PagesMapper {
|
||||
/**
|
||||
* Maps page IDs to their corresponding page numbers.
|
||||
* @type {Uint32Array|null}
|
||||
* @type {Map<number, Array<number>>|null}
|
||||
*/
|
||||
#idToPageNumber = null;
|
||||
|
||||
@ -1058,9 +1058,9 @@ class PagesMapper {
|
||||
|
||||
/**
|
||||
* Previous mapping of page IDs to page numbers.
|
||||
* @type {Uint32Array|null}
|
||||
* @type {Int32Array|null}
|
||||
*/
|
||||
#prevIdToPageNumber = null;
|
||||
#prevPageNumbers = null;
|
||||
|
||||
/**
|
||||
* The total number of pages.
|
||||
@ -1074,6 +1074,19 @@ class PagesMapper {
|
||||
*/
|
||||
#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.
|
||||
* @returns {number} The number of pages.
|
||||
@ -1092,16 +1105,33 @@ class PagesMapper {
|
||||
return;
|
||||
}
|
||||
this.#pagesNumber = n;
|
||||
if (n === 0) {
|
||||
this.#pageNumberToId = null;
|
||||
this.#idToPageNumber = null;
|
||||
}
|
||||
this.#reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
this.#listeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a previously added listener function.
|
||||
* @param {function} listener
|
||||
*/
|
||||
removeListener(listener) {
|
||||
const index = this.#listeners.indexOf(listener);
|
||||
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) {
|
||||
listener();
|
||||
listener(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the page mappings if they haven't been initialized yet.
|
||||
* @param {boolean} mustInit
|
||||
*/
|
||||
#init(mustInit) {
|
||||
if (this.#pageNumberToId) {
|
||||
return;
|
||||
}
|
||||
const n = this.#pagesNumber;
|
||||
|
||||
// Allocate a single array for better memory locality.
|
||||
const array = new Uint32Array(3 * n);
|
||||
const pageNumberToId = (this.#pageNumberToId = array.subarray(0, n));
|
||||
const idToPageNumber = (this.#idToPageNumber = array.subarray(n, 2 * n));
|
||||
const pageNumberToId = (this.#pageNumberToId = new Uint32Array(n));
|
||||
this.#prevPageNumbers = new Int32Array(pageNumberToId);
|
||||
const idToPageNumber = (this.#idToPageNumber = new Map());
|
||||
if (mustInit) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
pageNumberToId[i] = idToPageNumber[i] = i + 1;
|
||||
for (let i = 1; i <= n; i++) {
|
||||
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);
|
||||
const pageNumberToId = this.#pageNumberToId;
|
||||
const idToPageNumber = this.#idToPageNumber;
|
||||
this.#prevIdToPageNumber.set(idToPageNumber);
|
||||
const movedCount = pagesToMove.length;
|
||||
const mappedPagesToMove = new Uint32Array(movedCount);
|
||||
let removedBeforeTarget = 0;
|
||||
@ -1182,17 +1239,118 @@ class PagesMapper {
|
||||
// Finally insert the moved pages.
|
||||
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
|
||||
|
||||
let hasChanged = false;
|
||||
for (let i = 0, ii = pagesNumber; i < ii; i++) {
|
||||
const id = pageNumberToId[i];
|
||||
hasChanged ||= id !== i + 1;
|
||||
idToPageNumber[id - 1] = i + 1;
|
||||
}
|
||||
this.#updateListeners();
|
||||
this.#setPrevPageNumbers(idToPageNumber, null);
|
||||
this.#updateIdToPageNumber();
|
||||
this.#updateListeners({ type: "move" });
|
||||
|
||||
if (!hasChanged) {
|
||||
// Reset.
|
||||
this.pagesNumber = 0;
|
||||
if (pageNumberToId.every((id, i) => id === i + 1)) {
|
||||
this.#reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
getPageMappingForSaving() {
|
||||
// Saving is index-based.
|
||||
return {
|
||||
pageIndices: this.#idToPageNumber
|
||||
? this.#idToPageNumber.map(x => x - 1)
|
||||
: null,
|
||||
};
|
||||
const idToPageNumber = this.#idToPageNumber;
|
||||
|
||||
// idToPageNumber maps used 1-based IDs to 1-based page numbers.
|
||||
// For example if the final pdf contains page 3 twice and they are moved at
|
||||
// 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) {
|
||||
return this.#prevIdToPageNumber[this.#pageNumberToId[pageNumber - 1] - 1];
|
||||
return this.#prevPageNumbers[pageNumber - 1] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the page number for a given page ID.
|
||||
* @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) {
|
||||
return this.#idToPageNumber?.[id - 1] ?? id;
|
||||
return this.#idToPageNumber ? (this.#idToPageNumber.get(id)?.[0] ?? 0) : id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -145,7 +145,44 @@ class AnnotationEditorLayer {
|
||||
}
|
||||
|
||||
updatePageIndex(newPageIndex) {
|
||||
for (const editor of this.#allEditorsIterator) {
|
||||
editor.updatePageIndex(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() {
|
||||
|
||||
@ -948,7 +948,6 @@ class AnnotationEditorUIManager {
|
||||
evt => this.updateParams(evt.type, evt.value),
|
||||
{ signal }
|
||||
);
|
||||
eventBus._on("pagesedited", this.onPagesEdited.bind(this), { signal });
|
||||
window.addEventListener(
|
||||
"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 }) {
|
||||
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() {
|
||||
this.#container.focus();
|
||||
}
|
||||
|
||||
@ -18,19 +18,21 @@ import {
|
||||
clearInput,
|
||||
closePages,
|
||||
createPromise,
|
||||
createPromiseWithArgs,
|
||||
dragAndDrop,
|
||||
getAnnotationSelector,
|
||||
getRect,
|
||||
getThumbnailSelector,
|
||||
loadAndWait,
|
||||
scrollIntoView,
|
||||
waitAndClick,
|
||||
waitForDOMMutation,
|
||||
} from "./test_utils.mjs";
|
||||
|
||||
async function waitForThumbnailVisible(page, pageNums) {
|
||||
await page.click("#viewsManagerToggleButton");
|
||||
|
||||
const thumbSelector = "#thumbnailsView .thumbnailImage";
|
||||
const thumbSelector = "#thumbnailsView .thumbnailImageContainer > img";
|
||||
await page.waitForSelector(thumbSelector, { visible: true });
|
||||
if (!pageNums) {
|
||||
return null;
|
||||
@ -45,18 +47,22 @@ async function waitForThumbnailVisible(page, pageNums) {
|
||||
);
|
||||
}
|
||||
|
||||
function waitForPagesEdited(page) {
|
||||
return createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"pagesedited",
|
||||
({ pagesMapper }) => {
|
||||
function waitForPagesEdited(page, type) {
|
||||
return createPromiseWithArgs(
|
||||
page,
|
||||
resolve => {
|
||||
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()));
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
window.PDFViewerApplication.eventBus.on("pagesedited", listener);
|
||||
},
|
||||
[type]
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForHavingContents(page, expected) {
|
||||
@ -533,7 +539,8 @@ describe("Reorganize Pages View", () => {
|
||||
await page.waitForSelector("#thumbnailsViewMenu", { visible: true });
|
||||
await page.click("#thumbnailsViewMenu");
|
||||
|
||||
const thumbSelector = "#thumbnailsView .thumbnailImage";
|
||||
const thumbSelector =
|
||||
"#thumbnailsView .thumbnailImageContainer > img";
|
||||
await page.waitForSelector(thumbSelector, { visible: true });
|
||||
const rect1 = await getRect(page, getThumbnailSelector(1));
|
||||
const rect2 = await getRect(page, getThumbnailSelector(2));
|
||||
@ -607,7 +614,7 @@ describe("Reorganize Pages View", () => {
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"savepageseditedpdf",
|
||||
({ data }) => {
|
||||
resolve(Array.from(data.pageIndices));
|
||||
resolve(Array.from(data[0].pageIndices));
|
||||
},
|
||||
{
|
||||
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) {
|
||||
return promise.evaluate(([p]) => p);
|
||||
}
|
||||
@ -253,7 +262,7 @@ function getAnnotationSelector(id) {
|
||||
}
|
||||
|
||||
function getThumbnailSelector(pageNumber) {
|
||||
return `.thumbnailImage[data-l10n-args='{"page":${pageNumber}}']`;
|
||||
return `.thumbnailImageContainer[data-l10n-args='{"page":${pageNumber}}']`;
|
||||
}
|
||||
|
||||
async function getSpanRectFromText(page, pageNumber, text) {
|
||||
@ -963,6 +972,7 @@ export {
|
||||
countSerialized,
|
||||
countStorageEntries,
|
||||
createPromise,
|
||||
createPromiseWithArgs,
|
||||
dragAndDrop,
|
||||
firstPageOnTop,
|
||||
FSI,
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
|
||||
function waitForThumbnailVisible(page, pageNum) {
|
||||
return page.waitForSelector(
|
||||
`.thumbnailImage[data-l10n-args='{"page":${pageNum}}']`,
|
||||
`.thumbnailImageContainer[data-l10n-args='{"page":${pageNum}}']`,
|
||||
{ visible: true }
|
||||
);
|
||||
}
|
||||
@ -46,7 +46,8 @@ describe("PDF Thumbnail View", () => {
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.click("#viewsManagerToggleButton");
|
||||
|
||||
const thumbSelector = "#thumbnailsView .thumbnailImage";
|
||||
const thumbSelector =
|
||||
"#thumbnailsView .thumbnailImageContainer > img";
|
||||
await page.waitForSelector(thumbSelector, { visible: true });
|
||||
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
@ -110,12 +111,15 @@ describe("PDF Thumbnail View", () => {
|
||||
|
||||
for (const pageNum of [14, 1, 13, 2]) {
|
||||
await goToPage(page, pageNum);
|
||||
const thumbSelector = `.thumbnailImage[data-l10n-args='{"page":${pageNum}}']`;
|
||||
const thumbSelector = `.thumbnailImageContainer[data-l10n-args='{"page":${pageNum}}']`;
|
||||
await page.waitForSelector(
|
||||
`.thumbnail ${thumbSelector}[aria-current="page"]`,
|
||||
{ visible: true }
|
||||
);
|
||||
const src = await page.$eval(thumbSelector, el => el.src);
|
||||
const src = await page.$eval(
|
||||
`${thumbSelector} > img`,
|
||||
el => el.src
|
||||
);
|
||||
expect(src)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toMatch(/^blob:http:/);
|
||||
@ -167,7 +171,7 @@ describe("PDF Thumbnail View", () => {
|
||||
expect(
|
||||
await isElementFocused(
|
||||
page,
|
||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
|
||||
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
@ -177,7 +181,7 @@ describe("PDF Thumbnail View", () => {
|
||||
expect(
|
||||
await isElementFocused(
|
||||
page,
|
||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":2}']`
|
||||
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":2}']`
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
@ -187,7 +191,7 @@ describe("PDF Thumbnail View", () => {
|
||||
expect(
|
||||
await isElementFocused(
|
||||
page,
|
||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
|
||||
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
@ -198,7 +202,7 @@ describe("PDF Thumbnail View", () => {
|
||||
expect(
|
||||
await isElementFocused(
|
||||
page,
|
||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":3}']`
|
||||
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":3}']`
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
@ -214,7 +218,7 @@ describe("PDF Thumbnail View", () => {
|
||||
expect(
|
||||
await isElementFocused(
|
||||
page,
|
||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":14}']`
|
||||
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":14}']`
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
@ -224,7 +228,7 @@ describe("PDF Thumbnail View", () => {
|
||||
expect(
|
||||
await isElementFocused(
|
||||
page,
|
||||
`#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
|
||||
`#thumbnailsView .thumbnailImageContainer[data-l10n-args='{"page":1}']`
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
|
||||
@ -31,7 +31,7 @@ import { GenericL10n } from "web-null_l10n";
|
||||
/**
|
||||
* @typedef {Object} AnnotationEditorLayerBuilderOptions
|
||||
* @property {AnnotationEditorUIManager} [uiManager]
|
||||
* @property {PDFPageProxy} pdfPage
|
||||
* @property {number} pageIndex
|
||||
* @property {L10n} [l10n]
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
@ -39,6 +39,7 @@ import { GenericL10n } from "web-null_l10n";
|
||||
* @property {TextLayer} [textLayer]
|
||||
* @property {DrawLayer} [drawLayer]
|
||||
* @property {function} [onAppend]
|
||||
* @property {AnnotationEditorLayer} [clonedFrom]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -60,11 +61,13 @@ class AnnotationEditorLayerBuilder {
|
||||
|
||||
#uiManager;
|
||||
|
||||
#clonedFrom = null;
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerBuilderOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.pdfPage = options.pdfPage;
|
||||
this.pageIndex = options.pageIndex;
|
||||
this.accessibilityManager = options.accessibilityManager;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
@ -79,6 +82,12 @@ class AnnotationEditorLayerBuilder {
|
||||
this.#drawLayer = options.drawLayer || null;
|
||||
this.#onAppend = options.onAppend || 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,
|
||||
structTreeLayer: this.#structTreeLayer,
|
||||
accessibilityManager: this.accessibilityManager,
|
||||
pageIndex: this.pdfPage.pageNumber - 1,
|
||||
pageIndex: this.pageIndex,
|
||||
l10n: this.l10n,
|
||||
viewport: clonedViewport,
|
||||
annotationLayer: this.#annotationLayer,
|
||||
@ -121,6 +130,11 @@ class AnnotationEditorLayerBuilder {
|
||||
drawLayer: this.#drawLayer,
|
||||
});
|
||||
|
||||
this.annotationEditorLayer.setClonedFrom(
|
||||
this.#clonedFrom?.annotationEditorLayer
|
||||
);
|
||||
this.#clonedFrom = null;
|
||||
|
||||
const parameters = {
|
||||
viewport: clonedViewport,
|
||||
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(
|
||||
"beforepagesedited",
|
||||
this.onBeforePagesEdited.bind(this),
|
||||
opts
|
||||
);
|
||||
eventBus._on(
|
||||
"savepageseditedpdf",
|
||||
this.onSavePagesEditedPDF.bind(this),
|
||||
@ -2369,30 +2364,18 @@ const PDFViewerApplication = {
|
||||
await Promise.all([this.l10n?.destroy(), this.close()]);
|
||||
},
|
||||
|
||||
onBeforePagesEdited(data) {
|
||||
this.pdfViewer.onBeforePagesEdited(data);
|
||||
},
|
||||
|
||||
onPagesEdited(data) {
|
||||
this.pdfViewer.onPagesEdited(data);
|
||||
},
|
||||
|
||||
async onSavePagesEditedPDF({
|
||||
data: { includePages, excludePages, pageIndices },
|
||||
}) {
|
||||
async onSavePagesEditedPDF({ data: extractParams }) {
|
||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||
return;
|
||||
}
|
||||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
const pageInfo = {
|
||||
document: null, // For now, no merge.
|
||||
includePages,
|
||||
excludePages,
|
||||
pageIndices,
|
||||
};
|
||||
const modifiedPdfBytes = await this.pdfDocument.extractPages([pageInfo]);
|
||||
const modifiedPdfBytes = await this.pdfDocument.extractPages(extractParams);
|
||||
if (!modifiedPdfBytes) {
|
||||
console.error(
|
||||
"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 {
|
||||
#loadingId = null;
|
||||
|
||||
#minDurationToUpdateCanvas = 0;
|
||||
|
||||
#renderError = null;
|
||||
|
||||
#renderingState = RenderingStates.INITIAL;
|
||||
@ -56,7 +54,7 @@ class BasePDFPageView extends RenderableView {
|
||||
this.renderingQueue = options.renderingQueue;
|
||||
this.enableOptimizedPartialRendering =
|
||||
options.enableOptimizedPartialRendering ?? false;
|
||||
this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||
}
|
||||
|
||||
get renderingState() {
|
||||
@ -116,14 +114,14 @@ class BasePDFPageView extends RenderableView {
|
||||
this.#showCanvas = isLastShow => {
|
||||
if (updateOnFirstShow) {
|
||||
let tempCanvas = this.#tempCanvas;
|
||||
if (!isLastShow && this.#minDurationToUpdateCanvas > 0) {
|
||||
if (!isLastShow && this.minDurationToUpdateCanvas > 0) {
|
||||
// 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
|
||||
// much and can cause some serious performance issues.
|
||||
// To avoid that we only update the canvas every
|
||||
// `this.#minDurationToUpdateCanvas` ms.
|
||||
|
||||
if (Date.now() - this.#startTime < this.#minDurationToUpdateCanvas) {
|
||||
if (Date.now() - this.#startTime < this.minDurationToUpdateCanvas) {
|
||||
return;
|
||||
}
|
||||
if (!tempCanvas) {
|
||||
|
||||
@ -422,6 +422,8 @@ class PDFFindController {
|
||||
|
||||
#visitedPagesCount = 0;
|
||||
|
||||
#copiedExtractTextPromises = null;
|
||||
|
||||
/**
|
||||
* @param {PDFFindControllerOptions} options
|
||||
*/
|
||||
@ -609,6 +611,7 @@ class PDFFindController {
|
||||
this._dirtyMatch = false;
|
||||
clearTimeout(this._findTimeout);
|
||||
this._findTimeout = null;
|
||||
this.#copiedExtractTextPromises = null;
|
||||
|
||||
this._firstPageCapability = Promise.withResolvers();
|
||||
}
|
||||
@ -1127,21 +1130,37 @@ class PDFFindController {
|
||||
}
|
||||
}
|
||||
|
||||
#onPagesEdited({ pagesMapper }) {
|
||||
#onPagesEdited({ pagesMapper, type, pageNumbers }) {
|
||||
if (this._extractTextPromises.length === 0) {
|
||||
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._dirtyMatch = true;
|
||||
const prevTextPromises = this._extractTextPromises;
|
||||
const extractTextPromises = (this._extractTextPromises.length = []);
|
||||
for (let i = 0, ii = pagesMapper.length; i < ii; i++) {
|
||||
const prevPageIndex = pagesMapper.getPrevPageNumber(i + 1) - 1;
|
||||
if (prevPageIndex === -1) {
|
||||
for (let i = 1, ii = pagesMapper.length; i <= ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||
if (prevPageNumber < 0) {
|
||||
extractTextPromises.push(
|
||||
this.#copiedExtractTextPromises?.get(-prevPageNumber) ||
|
||||
Promise.resolve()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
extractTextPromises.push(
|
||||
prevTextPromises[prevPageIndex] || Promise.resolve()
|
||||
prevTextPromises[prevPageNumber - 1] || Promise.resolve()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ class PDFLinkService {
|
||||
* @type {number}
|
||||
*/
|
||||
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
|
||||
* text that look like URLs. The default value is `true`.
|
||||
* @property {CommentManager} [commentManager] - The comment manager instance.
|
||||
* @property {PDFPageView} [clonedFrom] - The page view that is cloned
|
||||
* to.
|
||||
*/
|
||||
|
||||
const DEFAULT_LAYER_PROPERTIES =
|
||||
@ -166,6 +168,8 @@ class PDFPageView extends BasePDFPageView {
|
||||
|
||||
#layers = [null, null, null, null];
|
||||
|
||||
#clonedFrom = null;
|
||||
|
||||
/**
|
||||
* @param {PDFPageViewOptions} options
|
||||
*/
|
||||
@ -197,6 +201,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
|
||||
this.#enableAutoLinking = options.enableAutoLinking !== false;
|
||||
this.#commentManager = options.commentManager || null;
|
||||
this.#clonedFrom = options.clonedFrom || null;
|
||||
|
||||
this.l10n = options.l10n;
|
||||
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-args", JSON.stringify({ page: this.id }));
|
||||
this.div = div;
|
||||
|
||||
this.#setDimensions();
|
||||
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) {
|
||||
const pos = LAYERS_ORDER.get(name);
|
||||
const oldDiv = this.#layers[pos];
|
||||
@ -331,6 +364,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
this._textHighlighter.pageIdx = newPageNumber - 1;
|
||||
// Don't update the page index for the draw layer, since it's just used as
|
||||
// an identifier.
|
||||
this.annotationEditorLayer?.updatePageIndex(newPageNumber - 1);
|
||||
}
|
||||
|
||||
setPdfPage(pdfPage) {
|
||||
@ -378,6 +412,15 @@ class PDFPageView extends BasePDFPageView {
|
||||
this.pdfPage?.cleanup();
|
||||
}
|
||||
|
||||
deleteMe(isCut) {
|
||||
if (isCut) {
|
||||
this.div.remove();
|
||||
return;
|
||||
}
|
||||
this.destroy();
|
||||
this.#layerProperties.annotationEditorUIManager?.deletePage(this.id);
|
||||
}
|
||||
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
@ -1140,17 +1183,19 @@ class PDFPageView extends BasePDFPageView {
|
||||
) {
|
||||
this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({
|
||||
uiManager: annotationEditorUIManager,
|
||||
pdfPage,
|
||||
pageIndex: this.id - 1,
|
||||
l10n,
|
||||
structTreeLayer: this.structTreeLayer,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||
textLayer: this.textLayer,
|
||||
drawLayer: this.drawLayer.getDrawLayer(),
|
||||
clonedFrom: this.#clonedFrom?.annotationEditorLayer,
|
||||
onAppend: annotationEditorLayerDiv => {
|
||||
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
||||
},
|
||||
});
|
||||
this.#clonedFrom = null;
|
||||
this.#renderAnnotationEditorLayer();
|
||||
}
|
||||
});
|
||||
|
||||
@ -116,10 +116,10 @@ class PDFThumbnailView extends RenderableView {
|
||||
|
||||
this.placeholder = null;
|
||||
|
||||
const imageContainer = (this.div = document.createElement("div"));
|
||||
imageContainer.className = "thumbnail";
|
||||
imageContainer.setAttribute("page-number", id);
|
||||
imageContainer.setAttribute("page-id", id);
|
||||
const thumbnailContainer = (this.div = document.createElement("div"));
|
||||
thumbnailContainer.className = "thumbnail";
|
||||
thumbnailContainer.setAttribute("page-number", id);
|
||||
thumbnailContainer.setAttribute("page-id", id);
|
||||
|
||||
if (enableSplitMerge) {
|
||||
const checkbox = (this.checkbox = document.createElement("input"));
|
||||
@ -127,24 +127,80 @@ class PDFThumbnailView extends RenderableView {
|
||||
checkbox.tabIndex = -1;
|
||||
checkbox.setAttribute("data-l10n-id", "pdfjs-thumb-page-checkbox");
|
||||
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"));
|
||||
image.classList.add("thumbnailImage", "missingThumbnailImage");
|
||||
image.role = "button";
|
||||
image.tabIndex = -1;
|
||||
image.draggable = false;
|
||||
imageContainer.append(image);
|
||||
this.#updateDims();
|
||||
|
||||
imageContainer.append(image);
|
||||
container.append(imageContainer);
|
||||
container.append(thumbnailContainer);
|
||||
}
|
||||
|
||||
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) {
|
||||
this.id = newId;
|
||||
this.renderingId = `thumbnail${newId}`;
|
||||
this.div.setAttribute("page-number", newId);
|
||||
this.imageContainer.setAttribute("page-number", newId);
|
||||
// TODO: do we set the page label ?
|
||||
this.setPageLabel(this.pageLabel);
|
||||
}
|
||||
@ -157,7 +213,7 @@ class PDFThumbnailView extends RenderableView {
|
||||
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
|
||||
this.scale = canvasWidth / width;
|
||||
|
||||
this.image.style.height = `${canvasHeight}px`;
|
||||
this.imageContainer.style.height = `${canvasHeight}px`;
|
||||
}
|
||||
|
||||
get renderingState() {
|
||||
@ -181,17 +237,23 @@ class PDFThumbnailView extends RenderableView {
|
||||
this.renderingState = RenderingStates.INITIAL;
|
||||
this.#updateDims();
|
||||
|
||||
const { image } = this;
|
||||
const { image, imageContainer } = this;
|
||||
const url = image.src;
|
||||
if (url) {
|
||||
URL.revokeObjectURL(url);
|
||||
image.removeAttribute("data-l10n-id");
|
||||
image.removeAttribute("data-l10n-args");
|
||||
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 }) {
|
||||
if (typeof rotation === "number") {
|
||||
this.rotation = rotation; // The rotation may be zero.
|
||||
@ -205,12 +267,13 @@ class PDFThumbnailView extends RenderableView {
|
||||
}
|
||||
|
||||
toggleCurrent(isCurrent) {
|
||||
const { imageContainer } = this;
|
||||
if (isCurrent) {
|
||||
this.image.ariaCurrent = "page";
|
||||
this.image.tabIndex = 0;
|
||||
imageContainer.ariaCurrent = "page";
|
||||
imageContainer.tabIndex = 0;
|
||||
} else {
|
||||
this.image.ariaCurrent = false;
|
||||
this.image.tabIndex = -1;
|
||||
imageContainer.ariaCurrent = false;
|
||||
imageContainer.tabIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,14 +320,14 @@ class PDFThumbnailView extends RenderableView {
|
||||
throw new Error("#convertCanvasToImage: Rendering has not finished.");
|
||||
}
|
||||
const reducedCanvas = this.#reduceImage(canvas);
|
||||
const { image } = this;
|
||||
const { imageContainer, image } = this;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
reducedCanvas.toBlob(resolve);
|
||||
const blob = await promise;
|
||||
image.src = URL.createObjectURL(blob);
|
||||
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
||||
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||
image.classList.remove("missingThumbnailImage");
|
||||
imageContainer.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
|
||||
imageContainer.setAttribute("data-l10n-args", this.#pageL10nArgs);
|
||||
imageContainer.classList.remove("missingThumbnailImage");
|
||||
if (!FeatureTest.isOffscreenCanvasSupported) {
|
||||
// Clean up the canvas element since it is no longer needed.
|
||||
reducedCanvas.width = reducedCanvas.height = 0;
|
||||
@ -465,7 +528,7 @@ class PDFThumbnailView extends RenderableView {
|
||||
*/
|
||||
setPageLabel(label) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,6 +114,18 @@ class PDFThumbnailViewer {
|
||||
|
||||
#manageSaveAsButton = null;
|
||||
|
||||
#manageDeleteButton = null;
|
||||
|
||||
#manageCopyButton = null;
|
||||
|
||||
#manageCutButton = null;
|
||||
|
||||
#copiedThumbnails = null;
|
||||
|
||||
#copiedPageNumbers = null;
|
||||
|
||||
#isCut = false;
|
||||
|
||||
/**
|
||||
* @param {PDFThumbnailViewerOptions} options
|
||||
*/
|
||||
@ -143,6 +155,14 @@ class PDFThumbnailViewer {
|
||||
|
||||
if (this.#enableSplitMerge && 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.#manageSaveAsButton = saveAs;
|
||||
saveAs.addEventListener("click", () => {
|
||||
@ -151,6 +171,15 @@ class PDFThumbnailViewer {
|
||||
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 {
|
||||
manageMenu.button.hidden = true;
|
||||
}
|
||||
@ -191,7 +220,7 @@ class PDFThumbnailViewer {
|
||||
}
|
||||
if (pageNumber !== this._currentPageNumber) {
|
||||
const prevThumbnailView = this._thumbnails[this._currentPageNumber - 1];
|
||||
prevThumbnailView.toggleCurrent(/* isCurrent = */ false);
|
||||
prevThumbnailView?.toggleCurrent(/* isCurrent = */ false);
|
||||
thumbnailView.toggleCurrent(/* isCurrent = */ true);
|
||||
this._currentPageNumber = pageNumber;
|
||||
}
|
||||
@ -200,15 +229,11 @@ class PDFThumbnailViewer {
|
||||
// If the thumbnail isn't currently visible, scroll it into view.
|
||||
if (views.length > 0) {
|
||||
let shouldScroll = false;
|
||||
if (
|
||||
pageNumber <= this.#pagesMapper.getPageNumber(first.id) ||
|
||||
pageNumber >= this.#pagesMapper.getPageNumber(last.id)
|
||||
) {
|
||||
if (pageNumber <= first.id || pageNumber >= last.id) {
|
||||
shouldScroll = true;
|
||||
} else {
|
||||
for (const { id, percent } of views) {
|
||||
const mappedPageNumber = this.#pagesMapper.getPageNumber(id);
|
||||
if (mappedPageNumber !== pageNumber) {
|
||||
if (id !== pageNumber) {
|
||||
continue;
|
||||
}
|
||||
shouldScroll = percent < 100;
|
||||
@ -403,24 +428,45 @@ class PDFThumbnailViewer {
|
||||
));
|
||||
}
|
||||
|
||||
#updateThumbnails() {
|
||||
#updateThumbnails(currentPageNumber) {
|
||||
let newCurrentPageNumber = 0;
|
||||
const pagesMapper = this.#pagesMapper;
|
||||
this.container.replaceChildren();
|
||||
const prevThumbnails = this._thumbnails;
|
||||
const newThumbnails = (this._thumbnails = []);
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
|
||||
const prevPageIndex = pagesMapper.getPrevPageNumber(i + 1) - 1;
|
||||
if (prevPageIndex === -1) {
|
||||
const isCut = this.#isCut;
|
||||
const oldThumbnails = new Set(prevThumbnails);
|
||||
for (let i = 1, ii = pagesMapper.pagesNumber; i <= ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||
if (prevPageNumber < 0) {
|
||||
let thumbnail = this.#copiedThumbnails.get(-prevPageNumber);
|
||||
oldThumbnails.delete(thumbnail);
|
||||
thumbnail.checkbox.checked = false;
|
||||
if (isCut) {
|
||||
thumbnail.updateId(i);
|
||||
fragment.append(thumbnail.div);
|
||||
} else {
|
||||
thumbnail = thumbnail.clone(fragment, i);
|
||||
}
|
||||
newThumbnails.push(thumbnail);
|
||||
continue;
|
||||
}
|
||||
const newThumbnail = prevThumbnails[prevPageIndex];
|
||||
if (prevPageNumber === currentPageNumber) {
|
||||
newCurrentPageNumber = i;
|
||||
}
|
||||
const newThumbnail = prevThumbnails[prevPageNumber - 1];
|
||||
newThumbnails.push(newThumbnail);
|
||||
newThumbnail.updateId(i + 1);
|
||||
newThumbnail.updateId(i);
|
||||
oldThumbnails.delete(newThumbnail);
|
||||
newThumbnail.checkbox.checked = false;
|
||||
fragment.append(newThumbnail.div);
|
||||
}
|
||||
this.container.append(fragment);
|
||||
for (const oldThumbnail of oldThumbnails) {
|
||||
oldThumbnail.destroy();
|
||||
}
|
||||
return newCurrentPageNumber;
|
||||
}
|
||||
|
||||
#onStartDragging(draggedThumbnail) {
|
||||
@ -443,18 +489,18 @@ class PDFThumbnailViewer {
|
||||
const thumbnail = this._thumbnails[selected - 1];
|
||||
const placeholder = (thumbnail.placeholder =
|
||||
document.createElement("div"));
|
||||
placeholder.classList.add("thumbnailImage", "placeholder");
|
||||
const { div, image } = thumbnail;
|
||||
placeholder.classList.add("thumbnailImageContainer", "placeholder");
|
||||
const { div, imageContainer } = thumbnail;
|
||||
div.classList.add("isDragging");
|
||||
placeholder.style.height = getComputedStyle(image).height;
|
||||
image.after(placeholder);
|
||||
placeholder.style.height = getComputedStyle(imageContainer).height;
|
||||
imageContainer.after(placeholder);
|
||||
if (selected !== startPageNumber) {
|
||||
image.classList.add("hidden");
|
||||
imageContainer.classList.add("hidden");
|
||||
continue;
|
||||
}
|
||||
if (this.#selectedPages.size === 1) {
|
||||
image.classList.add("draggingThumbnail");
|
||||
this.#draggedContainer = image;
|
||||
imageContainer.classList.add("draggingThumbnail");
|
||||
this.#draggedContainer = imageContainer;
|
||||
continue;
|
||||
}
|
||||
// For multiple selected thumbnails, only the one being dragged is shown
|
||||
@ -463,13 +509,13 @@ class PDFThumbnailViewer {
|
||||
document.createElement("div"));
|
||||
draggedContainer.classList.add(
|
||||
"draggingThumbnail",
|
||||
"thumbnailImage",
|
||||
"thumbnailImageContainer",
|
||||
"multiple"
|
||||
);
|
||||
draggedContainer.style.height = getComputedStyle(image).height;
|
||||
image.replaceWith(draggedContainer);
|
||||
image.classList.remove("thumbnailImage");
|
||||
draggedContainer.append(image);
|
||||
draggedContainer.style.height = getComputedStyle(imageContainer).height;
|
||||
imageContainer.replaceWith(draggedContainer);
|
||||
imageContainer.classList.remove("thumbnailImageContainer");
|
||||
draggedContainer.append(imageContainer);
|
||||
draggedContainer.setAttribute(
|
||||
"data-multiple-count",
|
||||
this.#selectedPages.size
|
||||
@ -490,17 +536,17 @@ class PDFThumbnailViewer {
|
||||
this.container.classList.remove("isDragging");
|
||||
for (const selected of this.#selectedPages) {
|
||||
const thumbnail = this._thumbnails[selected - 1];
|
||||
const { div, placeholder, image } = thumbnail;
|
||||
const { div, placeholder, imageContainer } = thumbnail;
|
||||
placeholder.remove();
|
||||
image.classList.remove("draggingThumbnail", "hidden");
|
||||
imageContainer.classList.remove("draggingThumbnail", "hidden");
|
||||
div.classList.remove("isDragging");
|
||||
}
|
||||
|
||||
if (draggedContainer.classList.contains("multiple")) {
|
||||
// Restore the dragged image to its thumbnail.
|
||||
const originalImage = draggedContainer.firstElementChild;
|
||||
draggedContainer.replaceWith(originalImage);
|
||||
originalImage.classList.add("thumbnailImage");
|
||||
const originalImageContainer = draggedContainer.firstElementChild;
|
||||
draggedContainer.replaceWith(originalImageContainer);
|
||||
originalImageContainer.classList.add("thumbnailImageContainer");
|
||||
} else {
|
||||
draggedContainer.style.translate = "";
|
||||
}
|
||||
@ -515,45 +561,36 @@ class PDFThumbnailViewer {
|
||||
selectedPages.has(lastDraggedOverIndex + 2))
|
||||
)
|
||||
) {
|
||||
this._thumbnails[this._currentPageNumber - 1]?.toggleCurrent(
|
||||
/* isCurrent = */ false
|
||||
);
|
||||
this._currentPageNumber = -1;
|
||||
|
||||
const newIndex = lastDraggedOverIndex + 1;
|
||||
const pagesToMove = Array.from(selectedPages).sort((a, b) => a - b);
|
||||
const pagesMapper = this.#pagesMapper;
|
||||
const currentPageId = pagesMapper.getPageId(this._currentPageNumber);
|
||||
const newCurrentPageId = pagesMapper.getPageId(
|
||||
isNaN(this.#pageNumberToRemove)
|
||||
? pagesToMove[0]
|
||||
: this.#pageNumberToRemove
|
||||
);
|
||||
|
||||
this.eventBus.dispatch("beforepagesedited", {
|
||||
source: this,
|
||||
pagesMapper,
|
||||
});
|
||||
let currentPageNumber = isNaN(this.#pageNumberToRemove)
|
||||
? pagesToMove[0]
|
||||
: this.#pageNumberToRemove;
|
||||
|
||||
pagesMapper.movePages(selectedPages, pagesToMove, newIndex);
|
||||
|
||||
this.#updateThumbnails();
|
||||
|
||||
this._currentPageNumber = pagesMapper.getPageNumber(currentPageId);
|
||||
currentPageNumber = this.#updateThumbnails(currentPageNumber);
|
||||
this.#computeThumbnailsPosition();
|
||||
|
||||
selectedPages.clear();
|
||||
this.#pageNumberToRemove = NaN;
|
||||
this.#updateMenuEntries();
|
||||
|
||||
const isIdentity = (this.#manageSaveAsButton.disabled =
|
||||
!this.#pagesMapper.hasBeenAltered());
|
||||
if (!isIdentity) {
|
||||
this.eventBus.dispatch("pagesedited", {
|
||||
source: this,
|
||||
pagesMapper,
|
||||
index: newIndex,
|
||||
pagesToMove,
|
||||
});
|
||||
}
|
||||
this.eventBus.dispatch("pagesedited", {
|
||||
source: this,
|
||||
pagesMapper,
|
||||
type: "move",
|
||||
});
|
||||
|
||||
const newCurrentPageNumber = pagesMapper.getPageNumber(newCurrentPageId);
|
||||
setTimeout(() => {
|
||||
this.linkService.goToPage(newCurrentPageNumber);
|
||||
this.forceRendering();
|
||||
this.linkService.goToPage(currentPageNumber);
|
||||
}, 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) {
|
||||
this.#draggedImageOffsetX += dx;
|
||||
this.#draggedImageOffsetY += dy;
|
||||
@ -714,11 +866,11 @@ class PDFThumbnailViewer {
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "Home":
|
||||
this._thumbnails[0].image.focus();
|
||||
this._thumbnails[0].imageContainer.focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "End":
|
||||
this._thumbnails.at(-1).image.focus();
|
||||
this._thumbnails.at(-1).imageContainer.focus();
|
||||
stopEvent(e);
|
||||
break;
|
||||
case "Enter":
|
||||
@ -749,6 +901,7 @@ class PDFThumbnailViewer {
|
||||
} else {
|
||||
set.delete(pageNumber);
|
||||
}
|
||||
this.#updateMenuEntries();
|
||||
}
|
||||
|
||||
#addDragListeners() {
|
||||
@ -763,8 +916,9 @@ class PDFThumbnailViewer {
|
||||
pointerId: dragPointerId,
|
||||
} = e;
|
||||
if (
|
||||
this.#pagesMapper.copiedPageNumbers?.length > 0 ||
|
||||
!isNaN(this.#lastDraggedOverIndex) ||
|
||||
!draggedImage.classList.contains("thumbnailImage")
|
||||
!draggedImage.classList.contains("thumbnailImageContainer")
|
||||
) {
|
||||
// We're already handling a drag, or the target is not draggable.
|
||||
return;
|
||||
@ -884,7 +1038,7 @@ class PDFThumbnailViewer {
|
||||
|
||||
#goToPage(e) {
|
||||
const { target } = e;
|
||||
if (target.classList.contains("thumbnailImage")) {
|
||||
if (target.classList.contains("thumbnailImageContainer")) {
|
||||
const pageNumber = parseInt(
|
||||
target.parentElement.getAttribute("page-number"),
|
||||
10
|
||||
@ -943,7 +1097,7 @@ class PDFThumbnailViewer {
|
||||
}
|
||||
}
|
||||
if (nextThumbnail) {
|
||||
nextThumbnail.image.focus();
|
||||
nextThumbnail.imageContainer.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -289,6 +289,8 @@ class PDFViewer {
|
||||
|
||||
#viewerAlert = null;
|
||||
|
||||
#copiedPageViews = null;
|
||||
|
||||
/**
|
||||
* @param {PDFViewerOptions} options
|
||||
*/
|
||||
@ -1173,23 +1175,44 @@ class PDFViewer {
|
||||
});
|
||||
}
|
||||
|
||||
async onBeforePagesEdited({ pagesMapper }) {
|
||||
await this._pagesCapability.promise;
|
||||
this._currentPageId = pagesMapper.getPageId(this._currentPageNumber);
|
||||
}
|
||||
onPagesEdited({ pagesMapper, type, hasBeenCut, pageNumbers }) {
|
||||
if (type === "copy") {
|
||||
this.#copiedPageViews = new Map();
|
||||
for (const pageNum of pageNumbers) {
|
||||
this.#copiedPageViews.set(pageNum, this._pages[pageNum - 1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onPagesEdited({ pagesMapper }) {
|
||||
this._currentPageNumber = pagesMapper.getPageNumber(this._currentPageId);
|
||||
const isCut = type === "cut";
|
||||
if (isCut || type === "delete") {
|
||||
for (const pageNum of pageNumbers) {
|
||||
this._pages[pageNum - 1].deleteMe(isCut);
|
||||
}
|
||||
}
|
||||
|
||||
this._currentPageNumber = 0;
|
||||
const prevPages = this._pages;
|
||||
const newPages = (this._pages = []);
|
||||
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i + 1) - 1;
|
||||
if (prevPageNumber === -1) {
|
||||
for (let i = 1, ii = pagesMapper.pagesNumber; i <= ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||
if (prevPageNumber < 0) {
|
||||
let page = this.#copiedPageViews.get(-prevPageNumber);
|
||||
if (hasBeenCut) {
|
||||
page.updatePageNumber(i);
|
||||
} else {
|
||||
page = page.clone(i);
|
||||
}
|
||||
newPages.push(page);
|
||||
continue;
|
||||
}
|
||||
const page = prevPages[prevPageNumber];
|
||||
newPages[i] = page;
|
||||
page.updatePageNumber(i + 1);
|
||||
const page = prevPages[prevPageNumber - 1];
|
||||
newPages.push(page);
|
||||
page.updatePageNumber(i);
|
||||
}
|
||||
|
||||
if (!isCut) {
|
||||
this.#copiedPageViews = null;
|
||||
}
|
||||
|
||||
const viewerElement =
|
||||
@ -1204,6 +1227,7 @@ class PDFViewer {
|
||||
}
|
||||
viewerElement.append(fragment);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.forceRendering();
|
||||
});
|
||||
|
||||
@ -535,7 +535,7 @@
|
||||
cursor: grabbing;
|
||||
|
||||
> .thumbnail {
|
||||
> .thumbnailImage:hover {
|
||||
> .thumbnailImageContainer:hover {
|
||||
cursor: grabbing;
|
||||
|
||||
&: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 {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
@ -567,14 +596,19 @@
|
||||
position: relative;
|
||||
scroll-margin-top: 20px;
|
||||
|
||||
&:not(.isDragging)::after {
|
||||
.thumbnailPasteButton {
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:not(.isDragging) > .thumbnailImageContainer::after {
|
||||
content: attr(page-number);
|
||||
border-radius: 8px;
|
||||
background-color: var(--image-page-number-bg);
|
||||
color: var(--image-page-number-fg);
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
inset-inline-end: calc(var(--thumbnail-width) / 2);
|
||||
inset-inline-end: 50%;
|
||||
min-width: 32px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
@ -590,7 +624,8 @@
|
||||
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);
|
||||
color: var(--image-current-page-number-fg);
|
||||
}
|
||||
@ -603,7 +638,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .thumbnailImage {
|
||||
> .thumbnailImageContainer {
|
||||
--thumbnail-dragging-scale: 1.4;
|
||||
|
||||
width: var(--thumbnail-width);
|
||||
@ -613,6 +648,17 @@
|
||||
box-sizing: content-box;
|
||||
outline: var(--image-outline);
|
||||
user-select: none;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: inherit;
|
||||
border-radius: inherit;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.missingThumbnailImage {
|
||||
content-visibility: hidden;
|
||||
@ -658,7 +704,7 @@
|
||||
&.multiple {
|
||||
box-shadow: var(--image-multiple-dragging-shadow);
|
||||
|
||||
> img {
|
||||
> .thumbnailImageContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user