From 21372f6a58b13a4e3714da1f931cb0f1c7a66b40 Mon Sep 17 00:00:00 2001 From: Matthew Lawrence Date: Wed, 26 Nov 2025 16:29:13 +1100 Subject: [PATCH 1/3] fix: support text selection under search highlights --- web/text_layer_builder.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index cee7e0bab9cd8..424505ce970bd 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -318,7 +318,8 @@ class TextLayerBuilder { const parentTextLayer = anchor.parentElement?.closest(".textLayer"); const endDiv = this.#textLayers.get(parentTextLayer); - if (endDiv) { + const anchorHighlighted = anchor.classList?.contains("highlight"); + if (endDiv && !anchorHighlighted) { endDiv.style.width = parentTextLayer.style.width; endDiv.style.height = parentTextLayer.style.height; anchor.parentElement.insertBefore( From 10ecf64d0f62ca6629d25a630262d6dcc698ac40 Mon Sep 17 00:00:00 2001 From: Matthew Lawrence Date: Mon, 5 Jan 2026 10:18:08 +1100 Subject: [PATCH 2/3] review: normalise the anchor level early --- web/text_layer_builder.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 424505ce970bd..49a7d0310e16a 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -307,6 +307,9 @@ class TextLayerBuilder { if (anchor.nodeType === Node.TEXT_NODE) { anchor = anchor.parentNode; } + if (anchor.classList?.contains("highlight")) { + anchor = anchor.parentNode; + } if (!modifyStart && range.endOffset === 0) { do { while (!anchor.previousSibling) { @@ -318,8 +321,7 @@ class TextLayerBuilder { const parentTextLayer = anchor.parentElement?.closest(".textLayer"); const endDiv = this.#textLayers.get(parentTextLayer); - const anchorHighlighted = anchor.classList?.contains("highlight"); - if (endDiv && !anchorHighlighted) { + if (endDiv) { endDiv.style.width = parentTextLayer.style.width; endDiv.style.height = parentTextLayer.style.height; anchor.parentElement.insertBefore( From 54218596a27241755569fc97c9bc1a75e5828cef Mon Sep 17 00:00:00 2001 From: Matthew Lawrence Date: Wed, 14 Jan 2026 14:12:14 +1100 Subject: [PATCH 3/3] untested: add integration test --- test/integration/text_layer_spec.mjs | 74 ++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/integration/text_layer_spec.mjs b/test/integration/text_layer_spec.mjs index 47799fef4bd3f..f54f24289b5de 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -432,6 +432,80 @@ describe("Text layer", () => { ); }); }); + + describe("when selecting text with find highlights active", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("find_all.pdf", ".textLayer", 100); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("doesn't jump when selection anchor is inside a highlight element", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // Highlight all occurrences of the letter A (case insensitive). + await page.click("#viewFindButton"); + await page.waitForSelector("#findInput", { visible: true }); + await page.type("#findInput", "a"); + await page.click("#findHighlightAll + label"); + await page.waitForSelector(".textLayer .highlight"); + + // find_all.pdf contains 'AB BA' in a monospace font. These are + // the glyph metrics at 100% zoom, extracted from the PDF. + const glyphWidth = 15.98; + const expectedFirstAX = 30; + + // Compute the drag coordinates to select exactly "AB". The + // horizontal positions use the page origin and PDF glyph + // metrics; the vertical center comes from the highlight. + const pageDiv = await page.$(".page canvas"); + const pageBox = await pageDiv.boundingBox(); + const firstHighlight = await page.$(".textLayer .highlight"); + const highlightBox = await firstHighlight.boundingBox(); + + // Drag from beginning of first 'A' to end of second 'B' + const aStart = pageBox.x + expectedFirstAX; + const startY = Math.round( + highlightBox.y + highlightBox.height / 2 + ); + const bEnd = Math.round(aStart + glyphWidth * 2); + + await page.mouse.move(aStart, startY); + await page.mouse.down(); + await moveInSteps( + page, + { x: aStart, y: startY }, + { x: bEnd, y: startY }, + 20 + ); + await page.mouse.up(); + + const selection = await page.evaluate(() => + window.getSelection().toString() + ); + expect(selection).withContext(`In ${browserName}`).toEqual("AB"); + + // The selectionchange handler in TextLayerBuilder walks up + // from .highlight to its parent span before placing + // endOfContent (see text_layer_builder.js). Without that + // fix, endOfContent would be inserted inside the text span + // (as a sibling of the .highlight) instead of as a direct + // child of .textLayer. Verify the correct DOM structure. + const endOfContentIsDirectChild = await page.evaluate(() => { + const eoc = document.querySelector(".textLayer .endOfContent"); + return eoc?.parentElement?.classList.contains("textLayer"); + }); + expect(endOfContentIsDirectChild) + .withContext(`In ${browserName}`) + .toBeTrue(); + }) + ); + }); + }); }); describe("using selection carets", () => {