Merge pull request #20577 from calixteman/update_search

The 'find in page' feature must correctly work after the pages have been reorganized (bug 2010814)
This commit is contained in:
calixteman 2026-01-18 20:57:32 +01:00 committed by GitHub
commit f04deeeddf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 196 additions and 10 deletions

View File

@ -15,6 +15,7 @@
import { import {
awaitPromise, awaitPromise,
clearInput,
closePages, closePages,
createPromise, createPromise,
dragAndDrop, 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("Reorganize Pages View", () => {
describe("Drag & Drop", () => { describe("Drag & Drop", () => {
let pages; 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"]],
]);
})
);
});
});
}); });

View File

@ -17,7 +17,11 @@
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */ /** @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"; import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js";
const FindState = { const FindState = {
@ -422,6 +426,8 @@ class PDFFindController {
#visitedPagesCount = 0; #visitedPagesCount = 0;
#pagesMapper = PagesMapper.instance;
/** /**
* @param {PDFFindControllerOptions} options * @param {PDFFindControllerOptions} options
*/ */
@ -439,6 +445,7 @@ class PDFFindController {
this.#reset(); this.#reset();
eventBus._on("find", this.#onFind.bind(this)); eventBus._on("find", this.#onFind.bind(this));
eventBus._on("findbarclose", this.#onFindBarClose.bind(this)); eventBus._on("findbarclose", this.#onFindBarClose.bind(this));
eventBus._on("pagesedited", this.#onPagesEdited.bind(this));
} }
get highlightMatches() { get highlightMatches() {
@ -794,12 +801,13 @@ class PDFFindController {
if (query.length === 0) { if (query.length === 0) {
return; // Do nothing: the matches should be wiped out already. 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 matcherResult = this.match(query, pageContent, pageIndex);
const matches = (this._pageMatches[pageIndex] = []); const matches = (this._pageMatches[pageIndex] = []);
const matchesLength = (this._pageMatchesLength[pageIndex] = []); const matchesLength = (this._pageMatchesLength[pageIndex] = []);
const diffs = this._pageDiffs[pageIndex]; const diffs = this._pageDiffs[pageId];
matcherResult?.forEach(({ index, length }) => { matcherResult?.forEach(({ index, length }) => {
const [matchPos, matchLen] = getOriginalIndex(diffs, index, length); const [matchPos, matchLen] = getOriginalIndex(diffs, index, length);
@ -848,7 +856,7 @@ class PDFFindController {
* page. * page.
*/ */
match(query, pageContent, pageIndex) { match(query, pageContent, pageIndex) {
const hasDiacritics = this._hasDiacritics[pageIndex]; const hasDiacritics = this._hasDiacritics[this.getPageId(pageIndex)];
let isUnicode = false; let isUnicode = false;
if (typeof query === "string") { 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) { #updatePage(index) {
if (this._scrollMatches && this._selected.pageIdx === index) { if (this._scrollMatches && this._selected.pageIdx === index) {
// If the page is selected, scroll the page into view, which triggers // If the page is selected, scroll the page into view, which triggers
@ -960,6 +976,7 @@ class PDFFindController {
this._eventBus.dispatch("updatetextlayermatches", { this._eventBus.dispatch("updatetextlayermatches", {
source: this, source: this,
pageIndex: index, pageIndex: index,
pageId: this.getPageId(index),
}); });
} }
@ -967,6 +984,7 @@ class PDFFindController {
this._eventBus.dispatch("updatetextlayermatches", { this._eventBus.dispatch("updatetextlayermatches", {
source: this, source: this,
pageIndex: -1, pageIndex: -1,
pageId: -1,
}); });
} }
@ -998,7 +1016,7 @@ class PDFFindController {
continue; continue;
} }
this._pendingFindMatches.add(i); this._pendingFindMatches.add(i);
this._extractTextPromises[i].then(() => { this._extractTextPromises[this.getPageId(i)].then(() => {
this._pendingFindMatches.delete(i); this._pendingFindMatches.delete(i);
this.#calculateMatch(i); this.#calculateMatch(i);
}); });
@ -1126,6 +1144,14 @@ class PDFFindController {
} }
} }
#onPagesEdited() {
if (this._extractTextPromises.length === 0) {
return;
}
this.#onFindBarClose();
this._dirtyMatch = true;
}
#onFindBarClose(evt) { #onFindBarClose(evt) {
const pdfDocument = this._pdfDocument; const pdfDocument = this._pdfDocument;
// Since searching is asynchronous, ensure that the removal of highlighted // Since searching is asynchronous, ensure that the removal of highlighted

View File

@ -304,6 +304,10 @@ class PDFViewer {
`The API version "${version}" does not match the Viewer version "${viewerVersion}".` `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.container = options.container;
this.viewer = options.viewer || options.container.firstElementChild; this.viewer = options.viewer || options.container.firstElementChild;
this.#viewerAlert = options.viewerAlert || null; this.#viewerAlert = options.viewerAlert || null;

View File

@ -77,7 +77,7 @@ class TextHighlighter {
this.eventBus._on( this.eventBus._on(
"updatetextlayermatches", "updatetextlayermatches",
evt => { evt => {
if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) { if (evt.pageId === this.pageIdx || evt.pageId === -1) {
this._updateMatches(); this._updateMatches();
} }
}, },
@ -159,7 +159,8 @@ class TextHighlighter {
const { findController, pageIdx } = this; const { findController, pageIdx } = this;
const { textContentItemsStr, textDivs } = 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 selectedMatchIdx = findController.selected.matchIdx;
const highlightAll = findController.state.highlightAll; const highlightAll = findController.state.highlightAll;
let prevEnd = null; let prevEnd = null;
@ -273,7 +274,7 @@ class TextHighlighter {
findController.scrollMatchIntoView({ findController.scrollMatchIntoView({
element: textDivs[begin.divIdx], element: textDivs[begin.divIdx],
selectedLeft, selectedLeft,
pageIndex: pageIdx, pageIndex: findController.getPageNumber(pageIdx),
matchIndex: selectedMatchIdx, matchIndex: selectedMatchIdx,
}); });
} }
@ -308,8 +309,10 @@ class TextHighlighter {
} }
// Convert the matches on the `findController` into the match format // Convert the matches on the `findController` into the match format
// used for the textLayer. // used for the textLayer.
const pageMatches = findController.pageMatches[pageIdx] || null; const pageNumber = findController.getPageNumber(pageIdx);
const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null; const pageMatches = findController.pageMatches[pageNumber] || null;
const pageMatchesLength =
findController.pageMatchesLength[pageNumber] || null;
this.matches = this._convertMatches(pageMatches, pageMatchesLength); this.matches = this._convertMatches(pageMatches, pageMatchesLength);
this._renderMatches(this.matches); this._renderMatches(this.matches);