From 3a20ea75b90762f7b32fa2863486563bea64d990 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 16 Jan 2026 18:47:54 +0100 Subject: [PATCH] The 'find in page' feature must correctly work after the pages have been reorganized (bug 2010814) --- test/integration/reorganize_pages_spec.mjs | 153 +++++++++++++++++++++ web/pdf_find_controller.js | 36 ++++- web/pdf_viewer.js | 4 + web/text_highlighter.js | 13 +- 4 files changed, 196 insertions(+), 10 deletions(-) diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 2f06b40fa348a..72787c4b452a8 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -15,6 +15,7 @@ import { awaitPromise, + clearInput, closePages, createPromise, dragAndDrop, @@ -56,6 +57,46 @@ function waitForPagesEdited(page) { }); } +function getSearchResults(page) { + return page.evaluate(() => { + const pages = document.querySelectorAll(".page"); + const results = []; + for (let i = 0; i < pages.length; i++) { + const domPage = pages[i]; + const pageNumber = parseInt(domPage.getAttribute("data-page-number"), 10); + const highlights = domPage.querySelectorAll("span.highlight"); + if (highlights.length === 0) { + continue; + } + results.push([ + i + 1, + pageNumber, + Array.from(highlights).map(span => span.textContent), + ]); + } + return results; + }); +} + +function movePages(page, selectedPages, atIndex) { + return page.evaluate( + (selected, index) => { + const viewer = window.PDFViewerApplication.pdfViewer; + const pagesToMove = Array.from(selected).sort((a, b) => a - b); + viewer.pagesMapper.pagesNumber = + document.querySelectorAll(".page").length; + viewer.pagesMapper.movePages(new Set(pagesToMove), pagesToMove, index); + window.PDFViewerApplication.eventBus.dispatch("pagesedited", { + pagesMapper: viewer.pagesMapper, + index, + pagesToMove, + }); + }, + selectedPages, + atIndex + ); +} + describe("Reorganize Pages View", () => { describe("Drag & Drop", () => { let pages; @@ -262,4 +303,116 @@ describe("Reorganize Pages View", () => { ); }); }); + + describe("Search in pdf", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "page_with_number.pdf", + "#viewsManagerToggleButton", + "1", + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should check if the search is working after moving pages", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#viewFindButton"); + await page.waitForSelector(":has(> #findHighlightAll)", { + visible: true, + }); + await page.click(":has(> #findHighlightAll)"); + + await page.waitForSelector("#findInput", { visible: true }); + await page.type("#findInput", "1"); + await page.keyboard.press("Enter"); + + await page.waitForFunction( + () => document.querySelectorAll("span.highlight").length === 10 + ); + + let results = await getSearchResults(page); + expect(results) + .withContext(`In ${browserName}`) + .toEqual([ + // Page number, Id, [matches] + [1, 1, ["1"]], + [10, 10, ["1"]], + [11, 11, ["1", "1"]], + [12, 12, ["1"]], + [13, 13, ["1"]], + [14, 14, ["1"]], + [15, 15, ["1"]], + [16, 16, ["1"]], + [17, 17, ["1"]], + ]); + + await movePages(page, [11, 2], 3); + await page.waitForFunction( + () => document.querySelectorAll("span.highlight").length === 0 + ); + + await clearInput(page, "#findInput", true); + await page.type("#findInput", "1"); + await page.keyboard.press("Enter"); + + await page.waitForFunction( + () => document.querySelectorAll("span.highlight").length === 10 + ); + + results = await getSearchResults(page); + expect(results) + .withContext(`In ${browserName}`) + .toEqual([ + // Page number, Id, [matches] + [1, 1, ["1"]], + [4, 11, ["1", "1"]], + [11, 10, ["1"]], + [12, 12, ["1"]], + [13, 13, ["1"]], + [14, 14, ["1"]], + [15, 15, ["1"]], + [16, 16, ["1"]], + [17, 17, ["1"]], + ]); + + await movePages(page, [13], 0); + await page.waitForFunction( + () => document.querySelectorAll("span.highlight").length === 0 + ); + + await clearInput(page, "#findInput", true); + await page.type("#findInput", "1"); + await page.keyboard.press("Enter"); + + await page.waitForFunction( + () => document.querySelectorAll("span.highlight").length === 10 + ); + + results = await getSearchResults(page); + expect(results) + .withContext(`In ${browserName}`) + .toEqual([ + // Page number, Id, [matches] + [1, 13, ["1"]], + [2, 1, ["1"]], + [5, 11, ["1", "1"]], + [12, 10, ["1"]], + [13, 12, ["1"]], + [14, 14, ["1"]], + [15, 15, ["1"]], + [16, 16, ["1"]], + [17, 17, ["1"]], + ]); + }) + ); + }); + }); }); diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index 47a66946a3785..52b63b8a71612 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -17,7 +17,11 @@ /** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */ -import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js"; +import { + binarySearchFirstItem, + PagesMapper, + scrollIntoView, +} from "./ui_utils.js"; import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js"; const FindState = { @@ -422,6 +426,8 @@ class PDFFindController { #visitedPagesCount = 0; + #pagesMapper = PagesMapper.instance; + /** * @param {PDFFindControllerOptions} options */ @@ -439,6 +445,7 @@ class PDFFindController { this.#reset(); eventBus._on("find", this.#onFind.bind(this)); eventBus._on("findbarclose", this.#onFindBarClose.bind(this)); + eventBus._on("pagesedited", this.#onPagesEdited.bind(this)); } get highlightMatches() { @@ -794,12 +801,13 @@ class PDFFindController { if (query.length === 0) { return; // Do nothing: the matches should be wiped out already. } - const pageContent = this._pageContents[pageIndex]; + const pageId = this.getPageId(pageIndex); + const pageContent = this._pageContents[pageId]; const matcherResult = this.match(query, pageContent, pageIndex); const matches = (this._pageMatches[pageIndex] = []); const matchesLength = (this._pageMatchesLength[pageIndex] = []); - const diffs = this._pageDiffs[pageIndex]; + const diffs = this._pageDiffs[pageId]; matcherResult?.forEach(({ index, length }) => { const [matchPos, matchLen] = getOriginalIndex(diffs, index, length); @@ -848,7 +856,7 @@ class PDFFindController { * page. */ match(query, pageContent, pageIndex) { - const hasDiacritics = this._hasDiacritics[pageIndex]; + const hasDiacritics = this._hasDiacritics[this.getPageId(pageIndex)]; let isUnicode = false; if (typeof query === "string") { @@ -949,6 +957,14 @@ class PDFFindController { } } + getPageNumber(idx) { + return this.#pagesMapper.getPageNumber(idx + 1) - 1; + } + + getPageId(pageNumber) { + return this.#pagesMapper.getPageId(pageNumber + 1) - 1; + } + #updatePage(index) { if (this._scrollMatches && this._selected.pageIdx === index) { // If the page is selected, scroll the page into view, which triggers @@ -960,6 +976,7 @@ class PDFFindController { this._eventBus.dispatch("updatetextlayermatches", { source: this, pageIndex: index, + pageId: this.getPageId(index), }); } @@ -967,6 +984,7 @@ class PDFFindController { this._eventBus.dispatch("updatetextlayermatches", { source: this, pageIndex: -1, + pageId: -1, }); } @@ -998,7 +1016,7 @@ class PDFFindController { continue; } this._pendingFindMatches.add(i); - this._extractTextPromises[i].then(() => { + this._extractTextPromises[this.getPageId(i)].then(() => { this._pendingFindMatches.delete(i); this.#calculateMatch(i); }); @@ -1126,6 +1144,14 @@ class PDFFindController { } } + #onPagesEdited() { + if (this._extractTextPromises.length === 0) { + return; + } + this.#onFindBarClose(); + this._dirtyMatch = true; + } + #onFindBarClose(evt) { const pdfDocument = this._pdfDocument; // Since searching is asynchronous, ensure that the removal of highlighted diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 44dfba71afcb0..c6491d3f45e45 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -304,6 +304,10 @@ class PDFViewer { `The API version "${version}" does not match the Viewer version "${viewerVersion}".` ); } + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + this.pagesMapper = PagesMapper.instance; + } + this.container = options.container; this.viewer = options.viewer || options.container.firstElementChild; this.#viewerAlert = options.viewerAlert || null; diff --git a/web/text_highlighter.js b/web/text_highlighter.js index 72d883f4627af..9b8e3f5576219 100644 --- a/web/text_highlighter.js +++ b/web/text_highlighter.js @@ -77,7 +77,7 @@ class TextHighlighter { this.eventBus._on( "updatetextlayermatches", evt => { - if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) { + if (evt.pageId === this.pageIdx || evt.pageId === -1) { this._updateMatches(); } }, @@ -159,7 +159,8 @@ class TextHighlighter { const { findController, pageIdx } = this; const { textContentItemsStr, textDivs } = this; - const isSelectedPage = pageIdx === findController.selected.pageIdx; + const isSelectedPage = + findController.getPageNumber(pageIdx) === findController.selected.pageIdx; const selectedMatchIdx = findController.selected.matchIdx; const highlightAll = findController.state.highlightAll; let prevEnd = null; @@ -273,7 +274,7 @@ class TextHighlighter { findController.scrollMatchIntoView({ element: textDivs[begin.divIdx], selectedLeft, - pageIndex: pageIdx, + pageIndex: findController.getPageNumber(pageIdx), matchIndex: selectedMatchIdx, }); } @@ -308,8 +309,10 @@ class TextHighlighter { } // Convert the matches on the `findController` into the match format // used for the textLayer. - const pageMatches = findController.pageMatches[pageIdx] || null; - const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null; + const pageNumber = findController.getPageNumber(pageIdx); + const pageMatches = findController.pageMatches[pageNumber] || null; + const pageMatchesLength = + findController.pageMatchesLength[pageNumber] || null; this.matches = this._convertMatches(pageMatches, pageMatchesLength); this._renderMatches(this.matches);