feat(功能强化): 支持页面文字选择复制,优化选区显示
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-12 23:18:49 +08:00
1 parent 2ea937b7be
commit 1c7f3c03e9
6 files changed
+189 -11

No files matched your search

+30
View File
@@ -785,6 +785,34 @@ button.outline-link:hover {
transform-origin: 0 0;
}
.text-layer {
position: absolute;
inset: 0;
overflow: hidden;
color: transparent;
opacity: 0.24;
pointer-events: none;
-webkit-user-select: text;
user-select: text;
text-size-adjust: none;
}
.text-run > span {
position: absolute;
left: 0;
top: 0;
font: 100px/1 sans-serif;
white-space: pre;
transform-origin: 0 0;
cursor: text;
pointer-events: auto;
}
.text-run > span::selection {
color: transparent;
background: var(--accent);
}
.page-link {
position: absolute;
}
@@ -833,6 +861,8 @@ button.outline-link:hover {
display: block;
max-width: none;
transform-origin: 0 0;
-webkit-user-select: none;
user-select: none;
}
.meta-list {
+81 -4
View File
@@ -50,6 +50,7 @@ const state = {
continuous: false,
renderAnnotations: true,
pageCache: new Map(),
selectedPages: new Set(),
pageInFlight: new Map(),
pageRenderQueue: [],
pageRenderRunning: false,
@@ -194,6 +195,12 @@ el.fitButton.addEventListener("click", fitWidth);
el.fitHeightButton.addEventListener("click", fitHeight);
el.continuousButton.addEventListener("click", toggleContinuous);
el.annotationButton.addEventListener("click", toggleAnnotations);
document.addEventListener("copy", copySelection);
document.addEventListener("selectionchange", () => {
for (const index of state.selectedPages) {
unmountPage(index);
}
});
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", exportCurrentPage);
el.exportButton.addEventListener("click", exportPDF);
@@ -1471,6 +1478,7 @@ function renderPageFlow() {
function resetPageFlow() {
state.pageCache.clear();
state.selectedPages.clear();
state.visiblePages.clear();
for (const task of state.pageInFlight.values()) {
task.resolve(null);
@@ -1509,13 +1517,11 @@ function flowPageObserver() {
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
if (!entry.isIntersecting) {
state.visiblePages.delete(index);
if (entry.target.classList.contains("rendered")) {
entry.target.querySelector(".page-surface").replaceChildren();
entry.target.classList.remove("rendered");
}
unmountPage(index);
continue;
}
state.visiblePages.add(index);
state.selectedPages.delete(index);
renderFlowPage(index, { openSeq, priority: 4 });
}
}, {
@@ -1526,6 +1532,18 @@ function flowPageObserver() {
return state.pageObserver;
}
function unmountPage(index) {
const shell = pageShell(index);
const selection = document.getSelection();
if (selection.rangeCount && selection.getRangeAt(0).intersectsNode(shell)) {
state.selectedPages.add(index);
return;
}
shell.querySelector(".page-surface").replaceChildren();
shell.classList.remove("rendered");
state.selectedPages.delete(index);
}
async function renderFlowPage(index, options = {}) {
const openSeq = options.openSeq || state.openSeq;
if (openSeq !== state.openSeq) {
@@ -1638,6 +1656,7 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
const svg = parseSVG(page.svg, `p${openSeq}-${index}`);
svg.classList.add("ofd-svg");
surface.replaceChildren(svg);
surface.append(createTextLayer(page.text));
for (const link of page.links) {
let url;
try {
@@ -1668,6 +1687,64 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
}
}
function createTextLayer(text) {
const layer = document.createElement("div");
layer.className = "text-layer";
const measure = document.createElement("canvas").getContext("2d");
measure.font = "100px sans-serif";
for (const run of text.runs || []) {
const line = document.createElement("span");
line.className = "text-run";
const chars = Array.from(run.text);
for (const item of run.spans || []) {
const span = document.createElement("span");
span.textContent = chars.slice(item.start, item.end).join("");
const width = measure.measureText(span.textContent).width || 1;
const [a, b, c, d, e, f] = item.matrix.map((value) => value * MM_TO_PX);
span.style.transform = `matrix(${a / width}, ${b / width}, ${c / 100}, ${d / 100}, ${e}, ${f})`;
line.append(span);
}
layer.append(line);
}
return layer;
}
function copySelection(event) {
if (event.target.closest?.("input, textarea, [contenteditable]")) {
return;
}
const selection = document.getSelection();
if (selection.isCollapsed || !selection.rangeCount) {
return;
}
const range = selection.getRangeAt(0);
if (!el.svgHost.contains(range.startContainer) || !el.svgHost.contains(range.endContainer)) {
return;
}
const lines = [];
for (const run of el.svgHost.querySelectorAll(".text-run")) {
if (!range.intersectsNode(run)) {
continue;
}
const part = range.cloneRange();
if (!run.contains(part.startContainer)) {
part.setStart(run, 0);
}
if (!run.contains(part.endContainer)) {
part.setEnd(run, run.childNodes.length);
}
const text = part.cloneContents().textContent;
if (text) {
lines.push(text);
}
}
if (!lines.length) {
return;
}
event.clipboardData.setData("text/plain", lines.join("\n"));
event.preventDefault();
}
function markFlowPageError(index, err) {
const shell = pageShell(index);
if (!shell) {