mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-18 14:18:23 +02:00
[chrome] Fix text selection with .markedContent
The current text layer approach based on absolutely positioned `<span>` elements by default causes flickering with text selection, and we have browser-specific workarounds to solve that. In Chrome, the workaround involves moving the `.endOfContent` element to right after the last element that contains some selected content. This works well in simple PDFs, but breaks when we have `span.markedContent` elements. Given a text layer structure like the following, rendered as four consecutive lines: ```html <span class="markedContent"> <br> <span>development enter the construction phase (estimated at around</span> </span> <span class="markedContent"> <br> <span>300 MEUR).</span> </span> <span class="markedContent"> <br> <span>Kreate's EBITA increased to 2.8 MEUR (Q4'23: 2.7 MEUR) and the</span> </span> <span class="markedContent"> <br> <span>margin rose to 3.7% (Q4'23: 3.4%). However, profitability was</span> </span> ``` when starting to select from inside the first line and dragging down to the empty space after the second line, Chrome will anchor the selection at the beginning of either the `<br>` or the `<span>` inside the last `.markedContent`, depending on whether the selection is in "per-character mode" (i.e. click and drag) or "per-word mode" (i.e. double click and drag). This causes us to insert the `.endOfContent` element in the wrong place (one element too far), which causes one more line to be selected, which triggers another `"selecctionchange"` event, which causes us to move `.endOfContent` again, and so on, looping until when the whole page is selected. This commit fixes the issue by making sure that when the end of the selection range points to the _begining_ of an element, we walk back the dom finding the first non-empty element, and attatch `.endOfContent` to the end of that.
This commit is contained in:
parent
72feb4c256
commit
da5b681b16
4 changed files with 107 additions and 0 deletions
|
@ -217,6 +217,104 @@ describe("Text layer", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("doesn't jump when hovering on an empty area, with .markedContent", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait(
|
||||
"chrome-text-selection-markedContent.pdf",
|
||||
`.page[data-page-number = "1"] .endOfContent`
|
||||
);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("in per-character selection mode", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const [positionStart, positionEnd] = await Promise.all([
|
||||
getSpanRectFromText(
|
||||
page,
|
||||
1,
|
||||
"strengthen in the coming quarters as the railway projects under"
|
||||
).then(middlePosition),
|
||||
getSpanRectFromText(
|
||||
page,
|
||||
1,
|
||||
"development enter the construction phase (estimated at around"
|
||||
).then(belowEndPosition),
|
||||
]);
|
||||
|
||||
await page.mouse.move(positionStart.x, positionStart.y);
|
||||
await page.mouse.down();
|
||||
await moveInSteps(page, positionStart, positionEnd, 20);
|
||||
await page.mouse.up();
|
||||
|
||||
await expectAsync(page)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toHaveRoughlySelected(
|
||||
"rs as the railway projects under\n" +
|
||||
"development enter the construction phase (estimated at "
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("in per-word selection mode", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const [positionStart, positionEnd] = await Promise.all([
|
||||
getSpanRectFromText(
|
||||
page,
|
||||
1,
|
||||
"strengthen in the coming quarters as the railway projects under"
|
||||
).then(middlePosition),
|
||||
getSpanRectFromText(
|
||||
page,
|
||||
1,
|
||||
"development enter the construction phase (estimated at around"
|
||||
).then(belowEndPosition),
|
||||
]);
|
||||
|
||||
if (browserName !== "firefox") {
|
||||
await page.mouse.move(positionStart.x, positionStart.y);
|
||||
await page.mouse.down({ clickCount: 1 });
|
||||
await page.mouse.up({ clickCount: 1 });
|
||||
await page.mouse.down({ clickCount: 2 });
|
||||
} else {
|
||||
// When running tests with Firefox we use WebDriver BiDi, for
|
||||
// which puppeteer doesn't support emulating "double click and
|
||||
// hold". We need to manually dispatch an action through the
|
||||
// protocol.
|
||||
// See https://github.com/puppeteer/puppeteer/issues/13745.
|
||||
await page.mainFrame().browsingContext.performActions([
|
||||
{
|
||||
type: "pointer",
|
||||
id: "__puppeteer_mouse",
|
||||
actions: [
|
||||
{ type: "pointerMove", ...positionStart },
|
||||
{ type: "pointerDown", button: 0 },
|
||||
{ type: "pointerUp", button: 0 },
|
||||
{ type: "pointerDown", button: 0 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
await moveInSteps(page, positionStart, positionEnd, 20);
|
||||
await page.mouse.up();
|
||||
|
||||
await expectAsync(page)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toHaveRoughlySelected(
|
||||
"quarters as the railway projects under\n" +
|
||||
"development enter the construction phase (estimated at around"
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when selecting over a link", () => {
|
||||
let pages;
|
||||
|
||||
|
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
|
@ -718,3 +718,4 @@
|
|||
!issue19424.pdf
|
||||
!issue18529.pdf
|
||||
!issue16742.pdf
|
||||
!chrome-text-selection-markedContent.pdf
|
||||
|
|
BIN
test/pdfs/chrome-text-selection-markedContent.pdf
Normal file
BIN
test/pdfs/chrome-text-selection-markedContent.pdf
Normal file
Binary file not shown.
|
@ -307,6 +307,14 @@ class TextLayerBuilder {
|
|||
if (anchor.nodeType === Node.TEXT_NODE) {
|
||||
anchor = anchor.parentNode;
|
||||
}
|
||||
if (!modifyStart && range.endOffset === 0) {
|
||||
do {
|
||||
while (!anchor.previousSibling) {
|
||||
anchor = anchor.parentNode;
|
||||
}
|
||||
anchor = anchor.previousSibling;
|
||||
} while (!anchor.childNodes.length);
|
||||
}
|
||||
|
||||
const parentTextLayer = anchor.parentElement?.closest(".textLayer");
|
||||
const endDiv = this.#textLayers.get(parentTextLayer);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue