Fix the selection rendering when a page has been destroyed and rendered again (bug 2054348)

The draw layer keeps a reference on the text layer div in order to render the
selection, but it was destroyed only along with the annotation editor layer.
Hence, when the editor is disabled, like in Firefox for Android, scrolling far
enough to destroy a page view left the draw layer with a reference on a removed
text layer: the new one was never registered, consequently no selection was
rendered anymore on that page.
This commit is contained in:
Calixte Denizet 2026-07-30 16:59:28 +02:00
parent 73707b62e9
commit 9e02ceed09
4 changed files with 103 additions and 2 deletions

View File

@ -641,7 +641,8 @@ class DrawLayer {
textLayerData.selectionDiv = div;
}
if (!div.parentNode && drawLayer.#parent) {
if (drawLayer.#parent && div.parentNode !== drawLayer.#parent) {
// The div can still be in a canvas wrapper which has been removed.
drawLayer.#parent.append(div);
this.#selections.add(div);
}

View File

@ -20,9 +20,11 @@
import {
closePages,
closeSinglePage,
firstPageOnTop,
getSpanRectFromText,
kbSelectAll,
loadAndWait,
scrollIntoView,
waitForEvent,
} from "./test_utils.mjs";
import { MathClamp } from "../../src/shared/math_clamp.js";
@ -1148,6 +1150,94 @@ describe("Text layer", () => {
);
});
});
describe("when the page has been destroyed and rendered again", () => {
const selectionSelector = ".canvasWrapper .selection svg path[d]";
const timeout = 5000;
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
undefined,
{ annotationEditorMode: -1 }
);
});
afterEach(async () => {
await closePages(pages);
});
async function selectSomeText(page) {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"them, and compiles them to fast native code. We call such a se-"
).then(belowEndPosition),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
}
it("must draw the selection", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await selectSomeText(page);
await page.waitForSelector(selectionSelector, { timeout });
await page.evaluate(() => {
document.getSelection().removeAllRanges();
});
await page.waitForSelector(selectionSelector, {
hidden: true,
timeout,
});
// Scroll down, page by page, until the first page view is
// evicted from the buffer: its canvas wrapper is then removed.
const pagesCount = await page.evaluate(
() => window.PDFViewerApplication.pagesCount
);
let isDestroyed = false;
for (let i = 2; i <= pagesCount && !isDestroyed; i++) {
const selector = `.page[data-page-number = "${i}"]`;
await scrollIntoView(page, selector);
await page.waitForSelector(
`${selector} .canvasWrapper canvas`,
{ timeout: 0 }
);
isDestroyed = !(await page.$(
`.page[data-page-number = "1"] .canvasWrapper`
));
}
expect(isDestroyed)
.withContext(`In ${browserName}, first page destroyed`)
.toBeTrue();
await firstPageOnTop(page);
await page.waitForSelector(
`.page[data-page-number = "1"] .canvasWrapper canvas`,
{ timeout: 0 }
);
await selectSomeText(page);
await page.waitForSelector(selectionSelector, { timeout });
})
);
});
});
});
describe("using selection carets", () => {

View File

@ -370,6 +370,7 @@ const PDFViewerApplication = {
// Set some specific preferences for tests.
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
Object.assign(opts, {
annotationEditorMode: x => parseInt(x, 10),
capCanvasAreaFactor: x => parseInt(x, 10),
docBaseUrl: x => x,
enableAltText: x => x === "true",

View File

@ -936,9 +936,14 @@ class PDFPageView extends BasePDFPageView {
if (this.structTreeLayer && !this.textLayer) {
this.structTreeLayer = null;
}
// The annotation editor layer and the draw layer keep references on the
// text layer (the latter uses its div in order to render the selection),
// hence they must be recreated too.
if (
this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
(!keepAnnotationEditorLayer ||
!this.annotationEditorLayer.div ||
!this.textLayer)
) {
if (this.drawLayer) {
this.drawLayer.cancel();
@ -947,6 +952,10 @@ class PDFPageView extends BasePDFPageView {
this.annotationEditorLayer.cancel();
this.annotationEditorLayer = null;
}
if (this.drawLayer && !this.textLayer) {
this.drawLayer.cancel();
this.drawLayer = null;
}
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
this.xfaLayer.cancel();
this.xfaLayer = null;