mirror of
https://github.com/zen-browser/pdf.js.git
synced 2025-07-08 09:20:06 +02:00
Merge pull request #18481 from nicolo-ribaudo/fix-selection-with-links
Disable link annotations during text selection
This commit is contained in:
commit
6f9fc70926
5 changed files with 289 additions and 101 deletions
|
@ -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();
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
awaitPromise,
|
||||||
closePages,
|
closePages,
|
||||||
closeSinglePage,
|
closeSinglePage,
|
||||||
getSpanRectFromText,
|
getSpanRectFromText,
|
||||||
|
@ -98,111 +99,268 @@ describe("Text layer", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("using mouse", () => {
|
describe("using mouse", () => {
|
||||||
let pages;
|
describe("doesn't jump when hovering on an empty area", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
pages = await loadAndWait(
|
pages = await loadAndWait(
|
||||||
"tracemonkey.pdf",
|
"tracemonkey.pdf",
|
||||||
`.page[data-page-number = "1"] .endOfContent`
|
`.page[data-page-number = "1"] .endOfContent`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await closePages(pages);
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("in a single page", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, 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();
|
||||||
|
|
||||||
|
await expectAsync(page)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toHaveRoughlySelected(
|
||||||
|
"code sequences, records\n" +
|
||||||
|
"them, and compiles them to fast native code. We call suc"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("across multiple pages", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const scrollTarget = await getSpanRectFromText(
|
||||||
|
page,
|
||||||
|
1,
|
||||||
|
"Unlike method-based dynamic compilers, our dynamic com-"
|
||||||
|
);
|
||||||
|
await page.evaluate(top => {
|
||||||
|
document.getElementById("viewerContainer").scrollTop = top;
|
||||||
|
}, scrollTarget.y - 50);
|
||||||
|
|
||||||
|
const [
|
||||||
|
positionStartPage1,
|
||||||
|
positionEndPage1,
|
||||||
|
positionStartPage2,
|
||||||
|
positionEndPage2,
|
||||||
|
] = await Promise.all([
|
||||||
|
getSpanRectFromText(
|
||||||
|
page,
|
||||||
|
1,
|
||||||
|
"Each compiled trace covers one path through the program with"
|
||||||
|
).then(middlePosition),
|
||||||
|
getSpanRectFromText(
|
||||||
|
page,
|
||||||
|
1,
|
||||||
|
"or that the same types will occur in subsequent loop iterations."
|
||||||
|
).then(middlePosition),
|
||||||
|
getSpanRectFromText(
|
||||||
|
page,
|
||||||
|
2,
|
||||||
|
"Hence, recording and compiling a trace"
|
||||||
|
).then(middlePosition),
|
||||||
|
getSpanRectFromText(
|
||||||
|
page,
|
||||||
|
2,
|
||||||
|
"cache. Alternatively, the VM could simply stop tracing, and give up"
|
||||||
|
).then(belowEndPosition),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.mouse.move(positionStartPage1.x, positionStartPage1.y);
|
||||||
|
await page.mouse.down();
|
||||||
|
|
||||||
|
await moveInSteps(page, positionStartPage1, positionEndPage1, 20);
|
||||||
|
await moveInSteps(page, positionEndPage1, positionStartPage2, 20);
|
||||||
|
|
||||||
|
await expectAsync(page)
|
||||||
|
.withContext(`In ${browserName}, first selection`)
|
||||||
|
.toHaveRoughlySelected(
|
||||||
|
/path through the program .*Hence, recording a/s
|
||||||
|
);
|
||||||
|
|
||||||
|
await moveInSteps(page, positionStartPage2, positionEndPage2, 20);
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expectAsync(page)
|
||||||
|
.withContext(`In ${browserName}, second selection`)
|
||||||
|
.toHaveRoughlySelected(
|
||||||
|
/path through.*Hence, recording and .* tracing, and give/s
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't jump when hovering on an empty area", async () => {
|
describe("when selecting over a link", () => {
|
||||||
await Promise.all(
|
let pages;
|
||||||
pages.map(async ([browserName, 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);
|
beforeAll(async () => {
|
||||||
await page.mouse.down();
|
pages = await loadAndWait(
|
||||||
await moveInSteps(page, positionStart, positionEnd, 20);
|
"annotation-link-text-popup.pdf",
|
||||||
await page.mouse.up();
|
`.page[data-page-number = "1"] .endOfContent`
|
||||||
|
);
|
||||||
await expectAsync(page)
|
});
|
||||||
.withContext(`In ${browserName}`)
|
afterAll(async () => {
|
||||||
.toHaveRoughlySelected(
|
await closePages(pages);
|
||||||
"code sequences, records\n" +
|
});
|
||||||
"them, and compiles them to fast native code. We call suc"
|
afterEach(() =>
|
||||||
);
|
Promise.all(
|
||||||
})
|
pages.map(([_, page]) =>
|
||||||
|
page.evaluate(() => window.getSelection().removeAllRanges())
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't jump when hovering on an empty area (multi-page)", async () => {
|
function waitForClick(page, selector, timeout) {
|
||||||
await Promise.all(
|
return page.evaluateHandle(
|
||||||
pages.map(async ([browserName, page]) => {
|
(sel, timeoutDelay) => {
|
||||||
const scrollTarget = await getSpanRectFromText(
|
const element = document.querySelector(sel);
|
||||||
page,
|
const timeoutSignal = AbortSignal.timeout(timeoutDelay);
|
||||||
1,
|
return [
|
||||||
"Unlike method-based dynamic compilers, our dynamic com-"
|
new Promise(resolve => {
|
||||||
);
|
timeoutSignal.addEventListener(
|
||||||
await page.evaluate(top => {
|
"abort",
|
||||||
document.getElementById("viewerContainer").scrollTop = top;
|
() => resolve(false),
|
||||||
}, scrollTarget.y - 50);
|
{ once: true }
|
||||||
|
);
|
||||||
|
element.addEventListener(
|
||||||
|
"click",
|
||||||
|
e => {
|
||||||
|
e.preventDefault();
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
{ once: true, signal: timeoutSignal }
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
selector,
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [
|
it("allows selecting within the link", async () => {
|
||||||
positionStartPage1,
|
await Promise.all(
|
||||||
positionEndPage1,
|
pages.map(async ([browserName, page]) => {
|
||||||
positionStartPage2,
|
const [positionStart, positionEnd] = await Promise.all([
|
||||||
positionEndPage2,
|
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
|
||||||
] = await Promise.all([
|
getSpanRectFromText(page, 1, "mozilla.org").then(
|
||||||
getSpanRectFromText(
|
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,
|
page,
|
||||||
1,
|
"#pdfjs_internal_id_8R",
|
||||||
"Each compiled trace covers one path through the program with"
|
1000
|
||||||
).then(middlePosition),
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
1,
|
|
||||||
"or that the same types will occur in subsequent loop iterations."
|
|
||||||
).then(middlePosition),
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
2,
|
|
||||||
"Hence, recording and compiling a trace"
|
|
||||||
).then(middlePosition),
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
2,
|
|
||||||
"cache. Alternatively, the VM could simply stop tracing, and give up"
|
|
||||||
).then(belowEndPosition),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await page.mouse.move(positionStartPage1.x, positionStartPage1.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
|
|
||||||
await moveInSteps(page, positionStartPage1, positionEndPage1, 20);
|
|
||||||
await moveInSteps(page, positionEndPage1, positionStartPage2, 20);
|
|
||||||
|
|
||||||
await expectAsync(page)
|
|
||||||
.withContext(`In ${browserName}, first selection`)
|
|
||||||
.toHaveRoughlySelected(
|
|
||||||
/path through the program .*Hence, recording a/s
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await moveInSteps(page, positionStartPage2, positionEndPage2, 20);
|
await page.mouse.click(positionEnd.x, positionEnd.y);
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
await expectAsync(page)
|
const clicked = await awaitPromise(clickPromiseHandle);
|
||||||
.withContext(`In ${browserName}, second selection`)
|
expect(clicked).toBeTrue();
|
||||||
.toHaveRoughlySelected(
|
})
|
||||||
/path through.*Hence, recording and .* tracing, and give/s
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue