diff --git a/src/display/api.js b/src/display/api.js index e89807ac0e6eb..2c09555563879 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1502,12 +1502,9 @@ class PDFPageProxy { optionalContentConfigPromise ||= this._transport.getOptionalContentConfig(renderingIntent); - let intentState = this._intentStates.get(cacheKey); - if (!intentState) { - intentState = Object.create(null); - this._intentStates.set(cacheKey, intentState); - } - + const intentState = this._intentStates.getOrInsertComputed(cacheKey, () => + Object.create(null) + ); // Ensure that a pending `streamReader` cancel timeout is always aborted. if (intentState.streamReaderCancelTimeout) { clearTimeout(intentState.streamReaderCancelTimeout); @@ -1676,11 +1673,10 @@ class PDFPageProxy { isEditing, /* isOpList = */ true ); - let intentState = this._intentStates.get(intentArgs.cacheKey); - if (!intentState) { - intentState = Object.create(null); - this._intentStates.set(intentArgs.cacheKey, intentState); - } + const intentState = this._intentStates.getOrInsertComputed( + intentArgs.cacheKey, + () => Object.create(null) + ); let opListTask; if (!intentState.opListReadCapability) { @@ -2516,14 +2512,9 @@ class WorkerTransport { } #cacheSimpleMethod(name, data = null) { - const cachedPromise = this.#methodPromises.get(name); - if (cachedPromise) { - return cachedPromise; - } - const promise = this.messageHandler.sendWithPromise(name, data); - - this.#methodPromises.set(name, promise); - return promise; + return this.#methodPromises.getOrInsertComputed(name, () => + this.messageHandler.sendWithPromise(name, data) + ); } #onProgress({ loaded, total }) { @@ -3135,22 +3126,17 @@ class WorkerTransport { } getMetadata() { - const name = "GetMetadata", - cachedPromise = this.#methodPromises.get(name); - if (cachedPromise) { - return cachedPromise; - } - const promise = this.messageHandler - .sendWithPromise(name, null) - .then(results => ({ + const name = "GetMetadata"; + + return this.#methodPromises.getOrInsertComputed(name, () => + this.messageHandler.sendWithPromise(name, null).then(results => ({ info: results[0], metadata: results[1] ? new Metadata(results[1]) : null, contentDispositionFilename: this.#fullReader?.filename ?? null, contentLength: this.#fullReader?.contentLength ?? null, hasStructTree: results[2], - })); - this.#methodPromises.set(name, promise); - return promise; + })) + ); } getMarkInfo() { diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 3b4be61ff8e66..018a92d309cc1 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -23,6 +23,9 @@ import { getAnnotationSelector, getRect, getThumbnailSelector, + kbCopy, + kbCut, + kbDelete, loadAndWait, scrollIntoView, waitAndClick, @@ -841,4 +844,144 @@ describe("Reorganize Pages View", () => { ); }); }); + + describe("Keyboard shortcuts for cut and copy (bug 2018139)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "page_with_number.pdf", + "#viewsManagerToggleButton", + "1", + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should cut pages with Ctrl+X and paste them", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(1)}) input` + ); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(3)}) input` + ); + + let handlePagesEdited = await waitForPagesEdited(page, "cut"); + await kbCut(page); + + 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); + }) + ); + }); + + it("should copy pages with Ctrl+C and paste them", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(1)}) input` + ); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(3)}) input` + ); + + let handlePagesEdited = await waitForPagesEdited(page, "copy"); + await kbCopy(page); + + 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); + }) + ); + }); + }); + + describe("Keyboard shortcuts for delete (bug 2010831)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "page_with_number.pdf", + "#viewsManagerToggleButton", + "1", + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should delete pages with the Delete key", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(1)}) input` + ); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(3)}) input` + ); + + const handlePagesEdited = await waitForPagesEdited(page); + await kbDelete(page); + + 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); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index e571d2f63059d..6e561ac1aa333 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -702,6 +702,14 @@ async function kbCopy(page) { await page.keyboard.press("c", { commands: ["Copy"] }); await page.keyboard.up(modifier); } +async function kbCut(page) { + await page.keyboard.down(modifier); + await page.keyboard.press("x", { commands: ["Cut"] }); + await page.keyboard.up(modifier); +} +async function kbDelete(page) { + await page.keyboard.press("Delete"); +} async function kbPaste(page) { await page.keyboard.down(modifier); await page.keyboard.press("v", { commands: ["Paste"] }); @@ -997,6 +1005,9 @@ export { kbBigMoveLeft, kbBigMoveRight, kbBigMoveUp, + kbCopy, + kbCut, + kbDelete, kbDeleteLastWord, kbFocusNext, kbFocusPrevious, diff --git a/web/pdf_find_utils.js b/web/pdf_find_utils.js index dc32e073d2172..bb52a46231037 100644 --- a/web/pdf_find_utils.js +++ b/web/pdf_find_utils.js @@ -120,11 +120,21 @@ function getNormalizeWithNFKC() { (typeof PDFJSDev === "undefined" && FeatureTest.platform.isFirefox) || (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ) { + // The `NormalizeWithNFKC` string is generated with the for loop below. + // Because of a small difference between Chrome and Firefox, the string is + // only hardcoded for Firefox, and Chrome (or others) will generate it at + // runtime. + // In order to detect if the string is up to date, a check is performed in + // the loop below, and if a difference is detected, an error is thrown. /* eslint-disable no-irregular-whitespace */ NormalizeWithNFKC ||= `\xA0¨ª¯²-µ¸-º¼-¾IJ-ijĿ-ŀʼnſDŽ-njDZ-dzʰ-ʸ˘-˝ˠ-ˤʹͺ;΄-΅·ϐ-ϖϰ-ϲϴ-ϵϹևٵ-ٸक़-य़ড়-ঢ়য়ਲ਼ਸ਼ਖ਼-ਜ਼ਫ਼ଡ଼-ଢ଼ำຳໜ-ໝ༌གྷཌྷདྷབྷཛྷཀྵჼᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵪᵸᶛ-ᶿẚ-ẛάέήίόύώΆ᾽-῁ΈΉ῍-῏ΐΊ῝-῟ΰΎ῭-`ΌΏ´-῾ - ‑‗․-… ″-‴‶-‷‼‾⁇-⁉⁗ ⁰-ⁱ⁴-₎ₐ-ₜ₨℀-℃℅-ℇ℉-ℓℕ-№ℙ-ℝ℠-™ℤΩℨK-ℭℯ-ℱℳ-ℹ℻-⅀ⅅ-ⅉ⅐-ⅿ↉∬-∭∯-∰〈-〉①-⓪⨌⩴-⩶⫝̸ⱼ-ⱽⵯ⺟⻳⼀-⿕ 〶〸-〺゛-゜ゟヿㄱ-ㆎ㆒-㆟㈀-㈞㈠-㉇㉐-㉾㊀-㏿ꚜ-ꚝꝰ꟱-ꟴꟸ-ꟹꭜ-ꭟꭩ豈-嗀塚晴凞-羽蘒諸逸-都飯-舘並-龎ff-stﬓ-ﬗיִײַ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-﷼︐-︙︰-﹄﹇-﹒﹔-﹦﹨-﹫ﹰ-ﹲﹴﹶ-ﻼ!-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ¢-₩`; } - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + if ( + typeof PDFJSDev === "undefined" || + PDFJSDev.test("TESTING") || + (!PDFJSDev.test("MOZCENTRAL") && !NormalizeWithNFKC) + ) { const ranges = []; const range = []; const diacriticsRegex = /^\p{M}$/u; diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index 5619a0eef1852..e823979cb6222 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -884,6 +884,33 @@ class PDFThumbnailViewer { } // For checkboxes, let the default behavior handle toggling break; + case "c": + if ( + this.#enableSplitMerge && + (e.ctrlKey || e.metaKey) && + this.#selectedPages?.size + ) { + this.#copyPages(); + stopEvent(e); + } + break; + case "x": + if ( + this.#enableSplitMerge && + (e.ctrlKey || e.metaKey) && + this.#selectedPages?.size + ) { + this.#cutPages(); + stopEvent(e); + } + break; + case "Delete": + case "Backspace": + if (this.#enableSplitMerge && this.#selectedPages?.size) { + this.#deletePages(); + stopEvent(e); + } + break; } }); this.container.addEventListener("click", e => {