feat(功能增强): 支持全文全选复制,完善选区取消
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-14 15:12:06 +08:00
1 parent a4ae065d43
commit 651e3cf5b5
4 files changed
+135 -8

No files matched your search

+90 -8
View File
@@ -57,6 +57,7 @@ const state = {
pageCache: new Map(), pageCache: new Map(),
svgFonts: new Map(), svgFonts: new Map(),
selectedPages: new Set(), selectedPages: new Set(),
documentSelection: null,
pageInFlight: new Map(), pageInFlight: new Map(),
pageRenderQueue: [], pageRenderQueue: [],
pageRenderRunning: false, pageRenderRunning: false,
@@ -207,11 +208,7 @@ el.panButton.addEventListener("click", togglePan);
el.continuousButton.addEventListener("click", toggleContinuous); el.continuousButton.addEventListener("click", toggleContinuous);
el.annotationButton.addEventListener("click", toggleAnnotations); el.annotationButton.addEventListener("click", toggleAnnotations);
document.addEventListener("copy", copySelection); document.addEventListener("copy", copySelection);
document.addEventListener("selectionchange", () => { document.addEventListener("selectionchange", syncSelection);
for (const index of state.selectedPages) {
unmountPage(index);
}
});
el.exportFormat.addEventListener("change", () => updateDPIControl()); el.exportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", () => exportFile(false)); el.exportPageButton.addEventListener("click", () => exportFile(false));
el.exportButton.addEventListener("click", openExportPanel); el.exportButton.addEventListener("click", openExportPanel);
@@ -289,10 +286,22 @@ updateSidebarState();
boot(); boot();
function handleKeyDown(event) { function handleKeyDown(event) {
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy") || el.exportPanel.open) { if (event.defaultPrevented || event.isComposing || event.altKey) {
return; return;
} }
const key = event.key; const key = event.key;
const target = event.target;
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
&& el.viewerPanel.contains(target) && !target.closest("input, textarea, select, [contenteditable]")) {
event.preventDefault();
if (!document.body.hasAttribute("aria-busy") && !el.exportPanel.open) {
selectDocumentText();
}
return;
}
if (document.body.hasAttribute("aria-busy") || el.exportPanel.open) {
return;
}
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
if (!event.shiftKey && key.toLowerCase() === "o") { if (!event.shiftKey && key.toLowerCase() === "o") {
event.preventDefault(); event.preventDefault();
@@ -306,7 +315,13 @@ function handleKeyDown(event) {
if (!state.doc) { if (!state.doc) {
return; return;
} }
const target = event.target; if (key === "Escape" && !event.shiftKey && state.documentSelection && el.viewerPanel.contains(target)
&& !target.closest("input, textarea, select, [contenteditable]")) {
event.preventDefault();
document.getSelection().removeAllRanges();
syncSelection();
return;
}
if (key === "F3") { if (key === "F3") {
event.preventDefault(); event.preventDefault();
if (state.searchMatches.length) { if (state.searchMatches.length) {
@@ -1684,6 +1699,7 @@ function resetPageFlow() {
state.svgFonts.clear(); state.svgFonts.clear();
state.pageCache.clear(); state.pageCache.clear();
state.selectedPages.clear(); state.selectedPages.clear();
state.documentSelection = null;
state.visiblePages.clear(); state.visiblePages.clear();
for (const task of state.pageInFlight.values()) { for (const task of state.pageInFlight.values()) {
task.resolve(null); task.resolve(null);
@@ -1740,7 +1756,8 @@ function flowPageObserver() {
function unmountPage(index) { function unmountPage(index) {
const shell = pageShell(index); const shell = pageShell(index);
const selection = document.getSelection(); const selection = document.getSelection();
if (selection.rangeCount && selection.getRangeAt(0).intersectsNode(shell)) { const range = selection.rangeCount ? selection.getRangeAt(0) : null;
if (range && (!state.documentSelection || !isDocumentSelection(range)) && range.intersectsNode(shell)) {
state.selectedPages.add(index); state.selectedPages.add(index);
return; return;
} }
@@ -1943,6 +1960,62 @@ function createTextLayer(text) {
return layer; return layer;
} }
function syncSelection() {
const selection = document.getSelection();
if (state.documentSelection && (!selection.rangeCount || !isDocumentSelection(selection.getRangeAt(0)))) {
state.documentSelection = null;
if (state.doc && !document.body.hasAttribute("aria-busy")) {
setStatus(pageStatus(state.pageIndex, state.doc.pageCount));
}
}
for (const index of state.selectedPages) {
unmountPage(index);
}
}
function isDocumentSelection(range) {
return range.startContainer === el.svgHost && range.startOffset === 0
&& range.endContainer === el.svgHost && range.endOffset === el.svgHost.childNodes.length;
}
async function selectDocumentText() {
const selection = document.getSelection();
if (state.documentSelection && selection.rangeCount && isDocumentSelection(selection.getRangeAt(0))) {
return;
}
const openSeq = state.openSeq;
const current = { text: null };
const active = () => state.documentSelection === current && openSeq === state.openSeq
&& selection.rangeCount > 0 && isDocumentSelection(selection.getRangeAt(0));
state.documentSelection = current;
const range = document.createRange();
range.selectNodeContents(el.svgHost);
selection.removeAllRanges();
selection.addRange(range);
const pages = [];
const count = state.doc.pageCount;
try {
for (let index = 0; index < count; index += 1) {
setStatus(`正在全选 ${index + 1} / ${count} 页`);
const text = await callWASM("ofdgoPageTextString", index);
if (!active()) {
return;
}
if (text) {
pages.push(text);
}
}
current.text = pages.join("\n");
setStatus(current.text ? "文字已全选" : "暂无文字");
} catch (err) {
if (active()) {
state.documentSelection = null;
selection.removeAllRanges();
showError(err, false);
}
}
}
function copySelection(event) { function copySelection(event) {
if (event.target.closest?.("input, textarea, [contenteditable]")) { if (event.target.closest?.("input, textarea, [contenteditable]")) {
return; return;
@@ -1952,6 +2025,15 @@ function copySelection(event) {
return; return;
} }
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
if (state.documentSelection && isDocumentSelection(range)) {
event.preventDefault();
if (state.documentSelection.text === null) {
setStatus("正在全选文字");
} else {
event.clipboardData.setData("text/plain", state.documentSelection.text);
}
return;
}
if (!el.svgHost.contains(range.startContainer) || !el.svgHost.contains(range.endContainer)) { if (!el.svgHost.contains(range.startContainer) || !el.svgHost.contains(range.endContainer)) {
return; return;
} }
+11
View File
@@ -85,6 +85,7 @@ func RunWASM() {
registerCallback("ofdgoRenderPage", renderPage) registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoSVGFontData", svgFontData) registerCallback("ofdgoSVGFontData", svgFontData)
registerCallback("ofdgoSearchPage", searchPage) registerCallback("ofdgoSearchPage", searchPage)
registerCallback("ofdgoPageTextString", pageTextString)
registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoParsePageRange", parsePageRange) registerCallback("ofdgoParsePageRange", parsePageRange)
registerExportCallback("ofdgoExportPage", exportPage) registerExportCallback("ofdgoExportPage", exportPage)
@@ -283,6 +284,16 @@ func renderPage(args []js.Value) (any, error) {
}), nil }), nil
} }
// pageTextString 提取OFD页面原文
// 入参: args 浏览器参数
// 返回: any 页面原文, error 错误信息
func pageTextString(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return currentSession.PageTextString(args[0].Int())
}
// searchPage 搜索OFD页面文字 // searchPage 搜索OFD页面文字
// 入参: args 浏览器参数 // 入参: args 浏览器参数
// 返回: any 搜索结果, error 错误信息 // 返回: any 搜索结果, error 错误信息
+18
View File
@@ -289,6 +289,24 @@ func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
return text, nil return text, nil
} }
// PageTextString 获取页面原文,不为全文复制保留额外的页面和文字索引
// 入参: index 页面索引
// 返回: string 页面原文, error 错误信息
func (s *Session) PageTextString(index int) (string, error) {
text := s.textCache[index]
if text == nil {
page, err := s.Reader.PageContentByIndex(index)
if err != nil {
return "", err
}
text, err = s.Renderer.PageText(page)
if err != nil {
return "", err
}
}
return text.String(), nil
}
// SearchPage 搜索指定页面,复用当前会话的文字索引 // SearchPage 搜索指定页面,复用当前会话的文字索引
// 入参: index 页面索引, query 搜索文字 // 入参: index 页面索引, query 搜索文字
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息 // 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
+16
View File
@@ -74,6 +74,22 @@ func (r *Renderer) PageText(page *PageContent) (*PageText, error) {
return renderer.pageText, nil return renderer.pageText, nil
} }
// String 按文本对象顺序合并原文,以换行分隔非空对象
// 返回: string 页面原文
func (p *PageText) String() string {
var text strings.Builder
for _, run := range p.Runs {
if run.Text == "" {
continue
}
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(run.Text)
}
return text.String()
}
// Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像 // Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像
// 入参: query 搜索文字 // 入参: query 搜索文字
// 返回: []TextMatch 匹配结果及上下文 // 返回: []TextMatch 匹配结果及上下文