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 {
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"]],
]);
})
);
});
});
});

View File

@ -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

View File

@ -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;

View File

@ -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);