Merge pull request #18481 from nicolo-ribaudo/fix-selection-with-links

Disable link annotations during text selection
This commit is contained in:
Tim van der Meij 2024-07-28 12:08:38 +02:00 committed by GitHub
commit 6f9fc70926
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 289 additions and 101 deletions

View file

@ -173,7 +173,7 @@ async function getSpanRectFromText(page, pageNumber, text) {
return page.evaluate( return page.evaluate(
(number, content) => { (number, content) => {
for (const el of document.querySelectorAll( for (const el of document.querySelectorAll(
`.page[data-page-number="${number}"] > .textLayer > span` `.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
)) { )) {
if (el.textContent === content) { if (el.textContent === content) {
const { x, y, width, height } = el.getBoundingClientRect(); const { x, y, width, height } = el.getBoundingClientRect();

View file

@ -14,6 +14,7 @@
*/ */
import { import {
awaitPromise,
closePages, closePages,
closeSinglePage, closeSinglePage,
getSpanRectFromText, getSpanRectFromText,
@ -98,6 +99,7 @@ describe("Text layer", () => {
}); });
describe("using mouse", () => { describe("using mouse", () => {
describe("doesn't jump when hovering on an empty area", () => {
let pages; let pages;
beforeAll(async () => { beforeAll(async () => {
@ -110,7 +112,7 @@ describe("Text layer", () => {
await closePages(pages); await closePages(pages);
}); });
it("doesn't jump when hovering on an empty area", async () => { it("in a single page", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([ const [positionStart, positionEnd] = await Promise.all([
@ -141,7 +143,7 @@ describe("Text layer", () => {
); );
}); });
it("doesn't jump when hovering on an empty area (multi-page)", async () => { it("across multiple pages", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
const scrollTarget = await getSpanRectFromText( const scrollTarget = await getSpanRectFromText(
@ -206,6 +208,162 @@ describe("Text layer", () => {
}); });
}); });
describe("when selecting over a link", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"annotation-link-text-popup.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterAll(async () => {
await closePages(pages);
});
afterEach(() =>
Promise.all(
pages.map(([_, page]) =>
page.evaluate(() => window.getSelection().removeAllRanges())
)
)
);
function waitForClick(page, selector, timeout) {
return page.evaluateHandle(
(sel, timeoutDelay) => {
const element = document.querySelector(sel);
const timeoutSignal = AbortSignal.timeout(timeoutDelay);
return [
new Promise(resolve => {
timeoutSignal.addEventListener(
"abort",
() => resolve(false),
{ once: true }
);
element.addEventListener(
"click",
e => {
e.preventDefault();
resolve(true);
},
{ once: true, signal: timeoutSignal }
);
}),
];
},
selector,
timeout
);
}
it("allows selecting within the link", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
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("Link\nmozil");
})
);
});
it("allows selecting within the link when going backwards", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Text").then(middlePosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
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("a.org\nTe");
})
);
});
it("allows clicking the link after selecting", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
const clickPromiseHandle = await waitForClick(
page,
"#pdfjs_internal_id_8R",
1000
);
await page.mouse.click(positionEnd.x, positionEnd.y);
const clicked = await awaitPromise(clickPromiseHandle);
expect(clicked).toBeTrue();
})
);
});
it("allows clicking the link after changing selection with the keyboard", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Shift");
const clickPromiseHandle = await waitForClick(
page,
"#pdfjs_internal_id_8R",
1000
);
await page.mouse.click(positionEnd.x, positionEnd.y);
const clicked = await awaitPromise(clickPromiseHandle);
expect(clicked).toBeTrue();
})
);
});
});
});
describe("using selection carets", () => { describe("using selection carets", () => {
let browser; let browser;
let page; let page;

View file

@ -126,6 +126,10 @@
} }
} }
.textLayer.selecting ~ & section {
pointer-events: none;
}
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a { :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
position: absolute; position: absolute;
font-size: 1em; font-size: 1em;

View file

@ -118,9 +118,9 @@
z-index: 0; z-index: 0;
cursor: default; cursor: default;
user-select: none; user-select: none;
}
&.active { &.selecting .endOfContent {
top: 0; top: 0;
} }
}
} }

View file

@ -150,8 +150,8 @@ class TextLayerBuilder {
#bindMouse(end) { #bindMouse(end) {
const { div } = this; const { div } = this;
div.addEventListener("mousedown", evt => { div.addEventListener("mousedown", () => {
end.classList.add("active"); div.classList.add("selecting");
}); });
div.addEventListener("copy", event => { div.addEventListener("copy", event => {
@ -193,16 +193,42 @@ class TextLayerBuilder {
end.style.width = ""; end.style.width = "";
end.style.height = ""; end.style.height = "";
} }
end.classList.remove("active"); textLayer.classList.remove("selecting");
}; };
let isPointerDown = false;
document.addEventListener(
"pointerdown",
() => {
isPointerDown = true;
},
{ signal }
);
document.addEventListener( document.addEventListener(
"pointerup", "pointerup",
() => { () => {
isPointerDown = false;
this.#textLayers.forEach(reset); this.#textLayers.forEach(reset);
}, },
{ signal } { signal }
); );
window.addEventListener(
"blur",
() => {
isPointerDown = false;
this.#textLayers.forEach(reset);
},
{ signal }
);
document.addEventListener(
"keyup",
() => {
if (!isPointerDown) {
this.#textLayers.forEach(reset);
}
},
{ signal }
);
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -237,7 +263,7 @@ class TextLayerBuilder {
for (const [textLayerDiv, endDiv] of this.#textLayers) { for (const [textLayerDiv, endDiv] of this.#textLayers) {
if (activeTextLayers.has(textLayerDiv)) { if (activeTextLayers.has(textLayerDiv)) {
endDiv.classList.add("active"); textLayerDiv.classList.add("selecting");
} else { } else {
reset(endDiv, textLayerDiv); reset(endDiv, textLayerDiv);
} }