@rightkit/document-core 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +31 -7
- package/dist/app/chrome.js +1 -1
- package/dist/app/documentPrinting.d.ts +0 -6
- package/dist/app/documentPrinting.js +0 -18
- package/dist/app/shell/nativeMenu.d.ts +3 -2
- package/dist/app/shell/nativeMenu.js +8 -5
- package/dist/documents/perfEventCounters.d.ts +0 -8
- package/dist/documents/perfEventCounters.js +2 -1
- package/dist/documents/preview/previewFrame.d.ts +2 -1
- package/dist/documents/preview/previewFrame.js +9 -7
- package/dist/documents/searchUtils.js +1 -1
- package/dist/host.d.ts +36 -0
- package/dist/host.js +54 -0
- package/dist/index.d.ts +1 -46
- package/dist/index.js +1 -46
- package/dist/settings/themes.js +1 -1
- package/dist/state/bridgeEvents.d.ts +1 -1
- package/dist/state/bridgeEvents.js +7 -4
- package/dist/state/recentUrls.js +4 -3
- package/dist/state/tauriBridge.d.ts +3 -47
- package/dist/state/tauriBridge.js +6 -162
- package/dist/styles.css +2 -2
- package/package.json +16 -87
- package/dist/app/shortcutChords.d.ts +0 -6
- package/dist/app/shortcutChords.js +0 -14
- package/dist/app/useCloseFindOnSurfaceChange.d.ts +0 -14
- package/dist/app/useCloseFindOnSurfaceChange.js +0 -23
- package/dist/app/useEditText.d.ts +0 -6
- package/dist/app/useEditText.js +0 -10
- package/dist/appName.d.ts +0 -2
- package/dist/appName.js +0 -8
- package/dist/documents/FullscreenIcon.d.ts +0 -4
- package/dist/documents/FullscreenIcon.js +0 -7
- package/dist/documents/mdx/mdxRuntimeScript.d.ts +0 -2
- package/dist/documents/mdx/mdxRuntimeScript.js +0 -3
- package/dist/documents/mdx/mdxRuntimeScript.txt +0 -584
- package/dist/documents/pdf/MacPdfReadPane.d.ts +0 -23
- package/dist/documents/pdf/MacPdfReadPane.js +0 -708
- package/dist/documents/pdf/NativePdfDocument.d.ts +0 -9
- package/dist/documents/pdf/NativePdfDocument.js +0 -41
- package/dist/documents/pdf/PdfInlineTextEditor.d.ts +0 -53
- package/dist/documents/pdf/PdfInlineTextEditor.js +0 -225
- package/dist/documents/pdf/PdfOcrLayer.d.ts +0 -4
- package/dist/documents/pdf/PdfOcrLayer.js +0 -30
- package/dist/documents/pdf/PdfOutline.d.ts +0 -6
- package/dist/documents/pdf/PdfOutline.js +0 -12
- package/dist/documents/pdf/PdfPasswordPrompt.d.ts +0 -5
- package/dist/documents/pdf/PdfPasswordPrompt.js +0 -24
- package/dist/documents/pdf/PdfThumbnails.d.ts +0 -8
- package/dist/documents/pdf/PdfThumbnails.js +0 -201
- package/dist/documents/pdf/PdfToolbar.d.ts +0 -47
- package/dist/documents/pdf/PdfToolbar.js +0 -76
- package/dist/documents/pdf/PdfViewer.d.ts +0 -36
- package/dist/documents/pdf/PdfViewer.js +0 -1522
- package/dist/documents/pdf/PdfViewerBody.d.ts +0 -22
- package/dist/documents/pdf/PdfViewerBody.js +0 -42
- package/dist/documents/pdf/macPdfNativeBridge.d.ts +0 -118
- package/dist/documents/pdf/macPdfNativeBridge.js +0 -134
- package/dist/documents/pdf/nearestVisiblePdfPage.d.ts +0 -2
- package/dist/documents/pdf/nearestVisiblePdfPage.js +0 -22
- package/dist/documents/pdf/pdfBench.d.ts +0 -88
- package/dist/documents/pdf/pdfBench.js +0 -535
- package/dist/documents/pdf/pdfBenchAutopilot.d.ts +0 -48
- package/dist/documents/pdf/pdfBenchAutopilot.js +0 -154
- package/dist/documents/pdf/pdfBenchTransport.d.ts +0 -21
- package/dist/documents/pdf/pdfBenchTransport.js +0 -233
- package/dist/documents/pdf/pdfEditModel.d.ts +0 -294
- package/dist/documents/pdf/pdfEditModel.js +0 -105
- package/dist/documents/pdf/pdfEditor.d.ts +0 -100
- package/dist/documents/pdf/pdfEditor.js +0 -68
- package/dist/documents/pdf/pdfOcr.d.ts +0 -36
- package/dist/documents/pdf/pdfOcr.js +0 -88
- package/dist/documents/pdf/pdfOcrEdit.d.ts +0 -32
- package/dist/documents/pdf/pdfOcrEdit.js +0 -138
- package/dist/documents/pdf/pdfPrinting.d.ts +0 -28
- package/dist/documents/pdf/pdfPrinting.js +0 -110
- package/dist/documents/pdf/pdfSave.d.ts +0 -87
- package/dist/documents/pdf/pdfSave.js +0 -250
- package/dist/documents/pdf/pdfShared.d.ts +0 -16
- package/dist/documents/pdf/pdfShared.js +0 -20
- package/dist/documents/pdf/pdfToMarkdown.d.ts +0 -11
- package/dist/documents/pdf/pdfToMarkdown.js +0 -67
- package/dist/documents/pdf/pdfjsLibGlobal.d.ts +0 -1
- package/dist/documents/pdf/pdfjsLibGlobal.js +0 -12
- package/dist/documents/pdf/toolbarIcons.d.ts +0 -18
- package/dist/documents/pdf/toolbarIcons.js +0 -31
- package/dist/documents/pdf/usePdfDocumentLoader.d.ts +0 -40
- package/dist/documents/pdf/usePdfDocumentLoader.js +0 -243
- package/dist/documents/pdf/usePdfEditSession.d.ts +0 -88
- package/dist/documents/pdf/usePdfEditSession.js +0 -341
- package/dist/documents/pdf/usePdfEditor.d.ts +0 -59
- package/dist/documents/pdf/usePdfEditor.js +0 -317
- package/dist/documents/pdf/usePdfFit.d.ts +0 -19
- package/dist/documents/pdf/usePdfFit.js +0 -55
- package/dist/documents/pdf/usePdfOcr.d.ts +0 -38
- package/dist/documents/pdf/usePdfOcr.js +0 -337
- package/dist/documents/pdf/usePdfPreviewRecompose.d.ts +0 -28
- package/dist/documents/pdf/usePdfPreviewRecompose.js +0 -256
- package/dist/documents/pdf/usePdfSavePlan.d.ts +0 -61
- package/dist/documents/pdf/usePdfSavePlan.js +0 -287
- package/dist/documents/pdf/usePdfSearch.d.ts +0 -31
- package/dist/documents/pdf/usePdfSearch.js +0 -367
- package/dist/documents/pdf/usePdfViewControls.d.ts +0 -72
- package/dist/documents/pdf/usePdfViewControls.js +0 -152
- package/dist/documents/scopedSelectAll.d.ts +0 -5
- package/dist/documents/scopedSelectAll.js +0 -77
- package/dist/documents/txt/cmScroll.d.ts +0 -11
- package/dist/documents/txt/cmScroll.js +0 -52
- package/dist/documents/useMirroredEditText.d.ts +0 -15
- package/dist/documents/useMirroredEditText.js +0 -29
- package/dist/documents/useSplitResize.d.ts +0 -7
- package/dist/documents/useSplitResize.js +0 -37
- package/dist/settings/rk-settings.css +0 -38
- package/dist/settings/sections/EditorSection.d.ts +0 -1
- package/dist/settings/sections/EditorSection.js +0 -18
- package/dist/settings/sections/FileAssociationsSection.d.ts +0 -1
- package/dist/settings/sections/FileAssociationsSection.js +0 -5
- package/dist/settings/sections/MacFileAssociationsBlock.d.ts +0 -33
- package/dist/settings/sections/MacFileAssociationsBlock.js +0 -151
- package/dist/settings/sections/ThemeSection.d.ts +0 -1
- package/dist/settings/sections/ThemeSection.js +0 -31
- package/dist/settings/sections/TypographySection.d.ts +0 -1
- package/dist/settings/sections/TypographySection.js +0 -19
- package/dist/settings/sections/onOff.d.ts +0 -11
- package/dist/settings/sections/onOff.js +0 -8
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { findLiteralMatches } from "../searchUtils.js";
|
|
3
|
-
import { MARK_ACTIVE_BG, MARK_BG, highlightSpan } from "./pdfShared.js";
|
|
4
|
-
const NO_RESULT = { count: 0, activeIndex: -1 };
|
|
5
|
-
export const PDF_SEARCH_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
6
|
-
function textBytes(text) {
|
|
7
|
-
// UTF-16 storage is a safe upper bound for this in-memory cache and avoids allocating a second
|
|
8
|
-
// encoded copy merely to account for it.
|
|
9
|
-
return text.length * 2;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Read page text through same streaming API used by rendered text layers. WKWebView's PDF.js worker
|
|
13
|
-
* can finish a stream-backed text layer while a parallel getTextContent() request remains empty;
|
|
14
|
-
* search must consume stream chunks so native search sees text already visible on page.
|
|
15
|
-
*/
|
|
16
|
-
export async function readPdfPageText(page, signal) {
|
|
17
|
-
const reader = page.streamTextContent().getReader();
|
|
18
|
-
const items = [];
|
|
19
|
-
let canceled = false;
|
|
20
|
-
const cancelRead = () => {
|
|
21
|
-
if (canceled)
|
|
22
|
-
return;
|
|
23
|
-
canceled = true;
|
|
24
|
-
void reader.cancel().catch(() => undefined);
|
|
25
|
-
};
|
|
26
|
-
signal?.addEventListener("abort", cancelRead, { once: true });
|
|
27
|
-
try {
|
|
28
|
-
while (true) {
|
|
29
|
-
if (signal?.aborted)
|
|
30
|
-
throw new DOMException("Search canceled", "AbortError");
|
|
31
|
-
const { done, value } = await reader.read();
|
|
32
|
-
if (done)
|
|
33
|
-
break;
|
|
34
|
-
for (const item of value.items) {
|
|
35
|
-
if ("str" in item)
|
|
36
|
-
items.push(item.str);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
finally {
|
|
41
|
-
signal?.removeEventListener("abort", cancelRead);
|
|
42
|
-
if (signal?.aborted)
|
|
43
|
-
cancelRead();
|
|
44
|
-
reader.releaseLock();
|
|
45
|
-
}
|
|
46
|
-
return items.join(" ");
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* PDF find, exposed as the app's shared `SearchProvider`. Owns the match model + committed query +
|
|
50
|
-
* options (read by the viewer's render path via the returned refs to draw <mark>s, and as effect
|
|
51
|
-
* deps to re-highlight already-rendered pages). Page text is extracted once per page and cached
|
|
52
|
-
* (original case — case folding happens at match time); rapid typing supersedes in-flight runs via a
|
|
53
|
-
* sequence guard. Supports match-case + whole-word; read-only (no replace).
|
|
54
|
-
*/
|
|
55
|
-
export function usePdfSearch({ url, pdfRef, slotRefs, scrollRef, goToPage, registerSearchProvider, }) {
|
|
56
|
-
const [committedQuery, setCommittedQuery] = useState("");
|
|
57
|
-
const [opts, setOpts] = useState({ matchCase: false, wholeWord: false });
|
|
58
|
-
const [matches, setMatches] = useState([]);
|
|
59
|
-
const [activeIdx, setActiveIdx] = useState(0);
|
|
60
|
-
const pageTextRef = useRef(new Map()); // page → original-case text (find cache)
|
|
61
|
-
const pageTextBytesRef = useRef(0);
|
|
62
|
-
const searchAbortRef = useRef(null);
|
|
63
|
-
const queryRef = useRef(""); // committed query, read by the viewer's renderPage to highlight
|
|
64
|
-
const optionsRef = useRef(opts); // current options, read by renderPage's highlight
|
|
65
|
-
const activeMarkRef = useRef(null);
|
|
66
|
-
const searchSeqRef = useRef(0); // bumped per search; guards stale overlapping runSearch results
|
|
67
|
-
const matchesLenRef = useRef(0); // live match count for stable next()/prev() provider methods
|
|
68
|
-
const activeIdxRef = useRef(0);
|
|
69
|
-
const searchResultListenersRef = useRef(new Set());
|
|
70
|
-
queryRef.current = committedQuery;
|
|
71
|
-
optionsRef.current = opts;
|
|
72
|
-
matchesLenRef.current = matches.length;
|
|
73
|
-
activeIdxRef.current = activeIdx;
|
|
74
|
-
const cachePageText = useCallback((page, text) => {
|
|
75
|
-
const previous = pageTextRef.current.get(page);
|
|
76
|
-
if (previous !== undefined)
|
|
77
|
-
pageTextBytesRef.current -= textBytes(previous);
|
|
78
|
-
pageTextRef.current.delete(page);
|
|
79
|
-
const bytes = textBytes(text);
|
|
80
|
-
if (bytes <= PDF_SEARCH_CACHE_MAX_BYTES) {
|
|
81
|
-
pageTextRef.current.set(page, text);
|
|
82
|
-
pageTextBytesRef.current += bytes;
|
|
83
|
-
}
|
|
84
|
-
while (pageTextBytesRef.current > PDF_SEARCH_CACHE_MAX_BYTES) {
|
|
85
|
-
const oldest = pageTextRef.current.keys().next().value;
|
|
86
|
-
if (oldest === undefined)
|
|
87
|
-
break;
|
|
88
|
-
const oldText = pageTextRef.current.get(oldest) ?? "";
|
|
89
|
-
pageTextRef.current.delete(oldest);
|
|
90
|
-
pageTextBytesRef.current -= textBytes(oldText);
|
|
91
|
-
}
|
|
92
|
-
}, []);
|
|
93
|
-
// --- Find: extract text, count matches, navigate -------------------------------
|
|
94
|
-
// NOTE (approximate by design): the match COUNT comes from each page's getTextContent() items
|
|
95
|
-
// joined with a space, while the highlight <mark>s are applied per rendered text-layer span. A
|
|
96
|
-
// query that straddles a text-item/span boundary can be counted but not visibly highlighted (and
|
|
97
|
-
// vice versa). Accepted as approximate PDF search; the page-nav fallback always lands right.
|
|
98
|
-
const runSearch = useCallback(async (raw, options, notifyListeners = false) => {
|
|
99
|
-
const pdf = pdfRef.current;
|
|
100
|
-
const q = raw.trim();
|
|
101
|
-
searchAbortRef.current?.abort();
|
|
102
|
-
const controller = new AbortController();
|
|
103
|
-
searchAbortRef.current = controller;
|
|
104
|
-
const seq = ++searchSeqRef.current; // supersede any in-flight search (rapid typing on big docs)
|
|
105
|
-
if (!pdf || !q) {
|
|
106
|
-
setMatches([]);
|
|
107
|
-
setActiveIdx(0);
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
// Extract uncached pages in parallel chunks instead of one page at a time — a first search on
|
|
111
|
-
// a big PDF used to freeze with zero feedback until every page had round-tripped through
|
|
112
|
-
// getTextContent(). Chunking keeps memory/IPC fan-out bounded while still overlapping the work.
|
|
113
|
-
const CHUNK = 8;
|
|
114
|
-
const pending = [];
|
|
115
|
-
for (let n = 1; n <= pdf.numPages; n++) {
|
|
116
|
-
if (!pageTextRef.current.has(n))
|
|
117
|
-
pending.push(n);
|
|
118
|
-
}
|
|
119
|
-
// Each page's text is scanned for matches exactly ONCE per search (counts keyed by page), and
|
|
120
|
-
// publishes just flatten the counts in page order. Rescanning every page's text on every
|
|
121
|
-
// chunk publish was quadratic on big PDFs (125 chunks × 1000 pages of regex work).
|
|
122
|
-
const counts = new Map();
|
|
123
|
-
const scanPage = (n, text = pageTextRef.current.get(n) ?? "") => {
|
|
124
|
-
counts.set(n, findLiteralMatches(text, q, options).length);
|
|
125
|
-
};
|
|
126
|
-
for (let n = 1; n <= pdf.numPages; n++) {
|
|
127
|
-
if (pageTextRef.current.has(n))
|
|
128
|
-
scanPage(n);
|
|
129
|
-
}
|
|
130
|
-
const flatten = () => {
|
|
131
|
-
const found = [];
|
|
132
|
-
for (let n = 1; n <= pdf.numPages; n++) {
|
|
133
|
-
const c = counts.get(n) ?? 0;
|
|
134
|
-
for (let occ = 0; occ < c; occ++)
|
|
135
|
-
found.push({ page: n, occ });
|
|
136
|
-
}
|
|
137
|
-
return found;
|
|
138
|
-
};
|
|
139
|
-
for (let i = 0; i < pending.length; i += CHUNK) {
|
|
140
|
-
if (searchSeqRef.current !== seq || controller.signal.aborted)
|
|
141
|
-
return [];
|
|
142
|
-
const chunk = pending.slice(i, i + CHUNK);
|
|
143
|
-
await Promise.all(chunk.map(async (n) => {
|
|
144
|
-
try {
|
|
145
|
-
const page = await pdf.getPage(n);
|
|
146
|
-
const text = await readPdfPageText(page, controller.signal);
|
|
147
|
-
if (controller.signal.aborted)
|
|
148
|
-
return;
|
|
149
|
-
cachePageText(n, text);
|
|
150
|
-
scanPage(n, text);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
if (!controller.signal.aborted) {
|
|
154
|
-
cachePageText(n, "");
|
|
155
|
-
scanPage(n, "");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}));
|
|
159
|
-
if (searchSeqRef.current !== seq || controller.signal.aborted)
|
|
160
|
-
return [];
|
|
161
|
-
// Publish a growing count as each chunk lands instead of staying blank until the whole
|
|
162
|
-
// document is scanned; the active index is only committed once, after the final chunk.
|
|
163
|
-
if (i + CHUNK < pending.length)
|
|
164
|
-
setMatches(flatten());
|
|
165
|
-
}
|
|
166
|
-
if (searchSeqRef.current !== seq || controller.signal.aborted)
|
|
167
|
-
return [];
|
|
168
|
-
const found = flatten();
|
|
169
|
-
const nextResult = {
|
|
170
|
-
count: found.length,
|
|
171
|
-
activeIndex: found.length ? 0 : -1,
|
|
172
|
-
};
|
|
173
|
-
const previousCount = matchesLenRef.current;
|
|
174
|
-
const previousResult = {
|
|
175
|
-
count: previousCount,
|
|
176
|
-
activeIndex: previousCount ? activeIdxRef.current : -1,
|
|
177
|
-
};
|
|
178
|
-
setMatches(found);
|
|
179
|
-
setActiveIdx(0);
|
|
180
|
-
if (notifyListeners &&
|
|
181
|
-
queryRef.current === q &&
|
|
182
|
-
(previousResult.count !== nextResult.count ||
|
|
183
|
-
previousResult.activeIndex !== nextResult.activeIndex)) {
|
|
184
|
-
for (const listener of searchResultListenersRef.current)
|
|
185
|
-
listener(q, nextResult);
|
|
186
|
-
}
|
|
187
|
-
return found;
|
|
188
|
-
}, [cachePageText, pdfRef]);
|
|
189
|
-
// A scanned page's empty text layer caches "" forever, leaving later OCR text invisible to find.
|
|
190
|
-
// Keep the cache private while letting recognition inject authoritative original-case text and
|
|
191
|
-
// recount an already-committed query through the same guarded search path.
|
|
192
|
-
const setPageText = useCallback((page, text) => {
|
|
193
|
-
cachePageText(page, text);
|
|
194
|
-
if (queryRef.current)
|
|
195
|
-
void runSearch(queryRef.current, optionsRef.current, true);
|
|
196
|
-
}, [cachePageText, runSearch]);
|
|
197
|
-
const invalidatePageText = useCallback((page) => {
|
|
198
|
-
const previous = pageTextRef.current.get(page);
|
|
199
|
-
if (previous !== undefined)
|
|
200
|
-
pageTextBytesRef.current -= textBytes(previous);
|
|
201
|
-
pageTextRef.current.delete(page);
|
|
202
|
-
}, []);
|
|
203
|
-
// SearchProvider for the shared FindBar. find() commits the query + options (drives the <mark>
|
|
204
|
-
// highlight render) and runs the search; next/prev step the active match (live count via a ref so
|
|
205
|
-
// the methods stay referentially stable). Returns the provider-owned {count, activeIndex}.
|
|
206
|
-
const providerFind = useCallback(async (raw, options) => {
|
|
207
|
-
setCommittedQuery(raw.trim());
|
|
208
|
-
setOpts(options);
|
|
209
|
-
// FindBar-initiated searches return their result directly; pushing it too would duplicate
|
|
210
|
-
// the update and can create a render loop.
|
|
211
|
-
const found = await runSearch(raw, options, false);
|
|
212
|
-
return { count: found.length, activeIndex: found.length ? 0 : -1 };
|
|
213
|
-
}, [runSearch]);
|
|
214
|
-
const providerNext = useCallback(() => {
|
|
215
|
-
const n = matchesLenRef.current;
|
|
216
|
-
if (!n)
|
|
217
|
-
return NO_RESULT;
|
|
218
|
-
let idx = 0;
|
|
219
|
-
setActiveIdx((i) => {
|
|
220
|
-
idx = (i + 1) % n;
|
|
221
|
-
return idx;
|
|
222
|
-
});
|
|
223
|
-
return { count: n, activeIndex: idx };
|
|
224
|
-
}, []);
|
|
225
|
-
const providerPrev = useCallback(() => {
|
|
226
|
-
const n = matchesLenRef.current;
|
|
227
|
-
if (!n)
|
|
228
|
-
return NO_RESULT;
|
|
229
|
-
let idx = 0;
|
|
230
|
-
setActiveIdx((i) => {
|
|
231
|
-
idx = (i - 1 + n) % n;
|
|
232
|
-
return idx;
|
|
233
|
-
});
|
|
234
|
-
return { count: n, activeIndex: idx };
|
|
235
|
-
}, []);
|
|
236
|
-
const providerSubscribe = useCallback((listener) => {
|
|
237
|
-
searchResultListenersRef.current.add(listener);
|
|
238
|
-
return () => searchResultListenersRef.current.delete(listener);
|
|
239
|
-
}, []);
|
|
240
|
-
const providerClose = useCallback(() => {
|
|
241
|
-
searchResultListenersRef.current.clear();
|
|
242
|
-
searchSeqRef.current++;
|
|
243
|
-
searchAbortRef.current?.abort();
|
|
244
|
-
searchAbortRef.current = null;
|
|
245
|
-
setCommittedQuery("");
|
|
246
|
-
setMatches([]);
|
|
247
|
-
setActiveIdx(0);
|
|
248
|
-
activeMarkRef.current = null;
|
|
249
|
-
}, []);
|
|
250
|
-
const searchProvider = useMemo(() => ({
|
|
251
|
-
kind: "pdf",
|
|
252
|
-
canReplace: false,
|
|
253
|
-
supportsMatchCase: true,
|
|
254
|
-
supportsWholeWord: true,
|
|
255
|
-
supportsRegex: true,
|
|
256
|
-
find: providerFind,
|
|
257
|
-
next: providerNext,
|
|
258
|
-
prev: providerPrev,
|
|
259
|
-
subscribe: providerSubscribe,
|
|
260
|
-
close: providerClose,
|
|
261
|
-
}), [providerFind, providerNext, providerPrev, providerSubscribe, providerClose]);
|
|
262
|
-
// Register on mount, unregister on unmount. DocumentRouter keys the viewer by doc.path, so a
|
|
263
|
-
// document switch remounts this hook — registration rides the normal lifecycle.
|
|
264
|
-
useEffect(() => {
|
|
265
|
-
if (!registerSearchProvider)
|
|
266
|
-
return;
|
|
267
|
-
registerSearchProvider(searchProvider);
|
|
268
|
-
return () => registerSearchProvider(null);
|
|
269
|
-
}, [registerSearchProvider, searchProvider]);
|
|
270
|
-
// Reset all search state when the document changes. `url` is deliberately a lifecycle trigger:
|
|
271
|
-
// the state belongs to the mounted PDF, rather than a value read by the reset body.
|
|
272
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: reset state when the document URL changes.
|
|
273
|
-
useEffect(() => {
|
|
274
|
-
pageTextRef.current.clear();
|
|
275
|
-
pageTextBytesRef.current = 0;
|
|
276
|
-
searchAbortRef.current?.abort();
|
|
277
|
-
searchAbortRef.current = null;
|
|
278
|
-
searchSeqRef.current++;
|
|
279
|
-
activeMarkRef.current = null;
|
|
280
|
-
setCommittedQuery("");
|
|
281
|
-
setMatches([]);
|
|
282
|
-
setActiveIdx(0);
|
|
283
|
-
return () => {
|
|
284
|
-
searchSeqRef.current++;
|
|
285
|
-
searchAbortRef.current?.abort();
|
|
286
|
-
searchAbortRef.current = null;
|
|
287
|
-
pageTextRef.current.clear();
|
|
288
|
-
pageTextBytesRef.current = 0;
|
|
289
|
-
};
|
|
290
|
-
}, [url]);
|
|
291
|
-
// Re-apply highlights to all *currently-rendered* text layers when the query OR options change —
|
|
292
|
-
// in place, without tearing pages down (that teardown was the typing jitter). Pages that scroll in
|
|
293
|
-
// afterward are highlighted by renderPage via queryRef/optionsRef. Empty query flattens marks away.
|
|
294
|
-
// The callback is also used after OCR mounts a recognized-text layer without changing the query.
|
|
295
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: slotRefs is a stable mutable render registry.
|
|
296
|
-
const rehighlight = useCallback(() => {
|
|
297
|
-
const q = queryRef.current;
|
|
298
|
-
const options = optionsRef.current;
|
|
299
|
-
slotRefs.current.forEach((slot) => {
|
|
300
|
-
for (const selector of [".textLayer", ".pdfOcrLayer"]) {
|
|
301
|
-
const layer = slot.querySelector(selector);
|
|
302
|
-
if (!layer)
|
|
303
|
-
continue;
|
|
304
|
-
layer.querySelectorAll("span").forEach((span) => {
|
|
305
|
-
if (q)
|
|
306
|
-
highlightSpan(span, q, options);
|
|
307
|
-
else if (span.childElementCount > 0)
|
|
308
|
-
span.replaceChildren(document.createTextNode(span.textContent ?? ""));
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
}, []);
|
|
313
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: query/options intentionally trigger ref-driven DOM repaint.
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
rehighlight();
|
|
316
|
-
}, [committedQuery, opts, rehighlight]);
|
|
317
|
-
// Emphasize the active match and bring it into view. Only scrolls when the match isn't already
|
|
318
|
-
// visible. `goToPage` is the fallback when the match's page hasn't rendered yet.
|
|
319
|
-
useEffect(() => {
|
|
320
|
-
if (matches.length === 0)
|
|
321
|
-
return;
|
|
322
|
-
const m = matches[activeIdx];
|
|
323
|
-
if (!m)
|
|
324
|
-
return;
|
|
325
|
-
let raf = 0;
|
|
326
|
-
let tries = 0;
|
|
327
|
-
let navigated = false;
|
|
328
|
-
const tick = () => {
|
|
329
|
-
const slot = slotRefs.current.get(m.page);
|
|
330
|
-
const marks = slot?.querySelectorAll("mark");
|
|
331
|
-
if (marks && marks.length > m.occ) {
|
|
332
|
-
if (activeMarkRef.current && activeMarkRef.current !== marks[m.occ]) {
|
|
333
|
-
activeMarkRef.current.style.background = MARK_BG;
|
|
334
|
-
}
|
|
335
|
-
const el = marks[m.occ];
|
|
336
|
-
el.style.background = MARK_ACTIVE_BG;
|
|
337
|
-
activeMarkRef.current = el;
|
|
338
|
-
const root = scrollRef.current;
|
|
339
|
-
if (root) {
|
|
340
|
-
const er = el.getBoundingClientRect();
|
|
341
|
-
const cr = root.getBoundingClientRect();
|
|
342
|
-
if (er.top < cr.top || er.bottom > cr.bottom)
|
|
343
|
-
el.scrollIntoView({ block: "center" });
|
|
344
|
-
}
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (!navigated) {
|
|
348
|
-
goToPage(m.page);
|
|
349
|
-
navigated = true;
|
|
350
|
-
}
|
|
351
|
-
if (tries++ < 60)
|
|
352
|
-
raf = requestAnimationFrame(tick);
|
|
353
|
-
};
|
|
354
|
-
raf = requestAnimationFrame(tick);
|
|
355
|
-
return () => cancelAnimationFrame(raf);
|
|
356
|
-
}, [activeIdx, matches, goToPage, scrollRef, slotRefs]);
|
|
357
|
-
// committedQuery drives the viewer's highlight re-render dependency; queryRef/optionsRef are read
|
|
358
|
-
// by its per-page render to apply <mark>s as pages render in.
|
|
359
|
-
return {
|
|
360
|
-
committedQuery,
|
|
361
|
-
queryRef,
|
|
362
|
-
optionsRef,
|
|
363
|
-
setPageText,
|
|
364
|
-
invalidatePageText,
|
|
365
|
-
rehighlight,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { type Dispatch, type MutableRefObject, type SetStateAction, type WheelEvent as ReactWheelEvent } from "react";
|
|
2
|
-
import type { PDFDocumentProxy } from "pdfjs-dist";
|
|
3
|
-
interface UsePdfViewControlsArgs {
|
|
4
|
-
active: boolean;
|
|
5
|
-
numPages: number;
|
|
6
|
-
pageDraft: string | null;
|
|
7
|
-
scale: number;
|
|
8
|
-
scaleDraft: string;
|
|
9
|
-
scrollRef: MutableRefObject<HTMLDivElement | null>;
|
|
10
|
-
pdfRef: MutableRefObject<PDFDocumentProxy | null>;
|
|
11
|
-
goToPage: (page: number) => void;
|
|
12
|
-
setPageDraft: Dispatch<SetStateAction<string | null>>;
|
|
13
|
-
setPageShake: Dispatch<SetStateAction<boolean>>;
|
|
14
|
-
setScale: Dispatch<SetStateAction<number>>;
|
|
15
|
-
setScaleDraft: Dispatch<SetStateAction<string>>;
|
|
16
|
-
setFitMode: Dispatch<SetStateAction<"width" | "page" | null>>;
|
|
17
|
-
setRotation: Dispatch<SetStateAction<number>>;
|
|
18
|
-
}
|
|
19
|
-
export declare function isPdfTypingTarget(target: EventTarget | null): boolean;
|
|
20
|
-
type PdfViewShortcutHandlers = {
|
|
21
|
-
/** True while Home/End should page-jump (the scroll pane itself has focus). */
|
|
22
|
-
homeEnd: () => boolean;
|
|
23
|
-
goHome: () => void;
|
|
24
|
-
goEnd: () => void;
|
|
25
|
-
zoomBy: (delta: number) => void;
|
|
26
|
-
fitWidth: () => void;
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* The PDF pane's view shortcuts as a registry: Home/End to the first/last page (scroll pane
|
|
30
|
-
* focused), Ctrl/Cmd `=`/`+` zoom in, `-` zoom out, `0` fit to width. A key that lands in a text
|
|
31
|
-
* field or editable region is left alone (`run` answers `false`).
|
|
32
|
-
*
|
|
33
|
-
* The raw handler these replace accepted every modifier combination around its named key (Shift,
|
|
34
|
-
* Alt, both Ctrl and Meta), ignored whether the event was already prevented, and did not look at
|
|
35
|
-
* IME composition. The registry options keep that: `ignoreExtraModifiers` (a named modifier must be
|
|
36
|
-
* held, any other may be), `ctrlormeta` (Ctrl or Meta, either or both), `allowDefaultPrevented`
|
|
37
|
-
* and `allowComposing`.
|
|
38
|
-
*/
|
|
39
|
-
export declare function createPdfViewShortcuts(handlers: PdfViewShortcutHandlers): {
|
|
40
|
-
platform: import("@rightkit/platform-ui").Platform;
|
|
41
|
-
register: (def: import("@rightkit/react-shell").ShortcutDef) => () => void;
|
|
42
|
-
handle: (event: KeyboardEvent) => boolean;
|
|
43
|
-
attach: (target?: Pick<EventTarget, "addEventListener" | "removeEventListener">) => () => void;
|
|
44
|
-
list: () => import("@rightkit/react-shell").ShortcutInfo[];
|
|
45
|
-
groups: () => {
|
|
46
|
-
title: string;
|
|
47
|
-
items: import("@rightkit/react-shell").ShortcutInfo[];
|
|
48
|
-
}[];
|
|
49
|
-
conflicts: () => {
|
|
50
|
-
chord: string;
|
|
51
|
-
ids: string[];
|
|
52
|
-
kind: "exact" | "prefix";
|
|
53
|
-
}[];
|
|
54
|
-
invoke(id: string): boolean;
|
|
55
|
-
chordFor: (id: string) => string | undefined;
|
|
56
|
-
setOverrides(next: Record<string, string | null>): void;
|
|
57
|
-
suspend(): void;
|
|
58
|
-
resume(): void;
|
|
59
|
-
isSuspended: () => boolean;
|
|
60
|
-
overrides: () => Record<string, string | null>;
|
|
61
|
-
subscribe(listener: () => void): () => void;
|
|
62
|
-
version: () => number;
|
|
63
|
-
};
|
|
64
|
-
export declare function usePdfViewControls({ active, numPages, pageDraft, scale, scaleDraft, scrollRef, pdfRef, goToPage, setPageDraft, setPageShake, setScale, setScaleDraft, setFitMode, setRotation, }: UsePdfViewControlsArgs): {
|
|
65
|
-
goToDest: (dest: string | unknown[] | null) => Promise<void>;
|
|
66
|
-
commitPageDraft: () => void;
|
|
67
|
-
zoom: (delta: number) => void;
|
|
68
|
-
commitScaleDraft: () => void;
|
|
69
|
-
onWheelZoom: (event: ReactWheelEvent<HTMLDivElement>) => void;
|
|
70
|
-
rotate: (delta: number) => void;
|
|
71
|
-
};
|
|
72
|
-
export {};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, } from "react";
|
|
2
|
-
import { createShortcutRegistry } from "@rightkit/react-shell";
|
|
3
|
-
import { clampScale, ZOOM_MAX, ZOOM_MIN } from "./pdfShared.js";
|
|
4
|
-
export function isPdfTypingTarget(target) {
|
|
5
|
-
if (!(target instanceof Element))
|
|
6
|
-
return false;
|
|
7
|
-
return (target.tagName === "INPUT" ||
|
|
8
|
-
target.tagName === "TEXTAREA" ||
|
|
9
|
-
(target instanceof HTMLElement && target.isContentEditable) ||
|
|
10
|
-
target.closest("[contenteditable='true']") !== null);
|
|
11
|
-
}
|
|
12
|
-
const LEGACY_KEY_MATCH = {
|
|
13
|
-
ignoreExtraModifiers: true,
|
|
14
|
-
allowDefaultPrevented: true,
|
|
15
|
-
allowComposing: true,
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* The PDF pane's view shortcuts as a registry: Home/End to the first/last page (scroll pane
|
|
19
|
-
* focused), Ctrl/Cmd `=`/`+` zoom in, `-` zoom out, `0` fit to width. A key that lands in a text
|
|
20
|
-
* field or editable region is left alone (`run` answers `false`).
|
|
21
|
-
*
|
|
22
|
-
* The raw handler these replace accepted every modifier combination around its named key (Shift,
|
|
23
|
-
* Alt, both Ctrl and Meta), ignored whether the event was already prevented, and did not look at
|
|
24
|
-
* IME composition. The registry options keep that: `ignoreExtraModifiers` (a named modifier must be
|
|
25
|
-
* held, any other may be), `ctrlormeta` (Ctrl or Meta, either or both), `allowDefaultPrevented`
|
|
26
|
-
* and `allowComposing`.
|
|
27
|
-
*/
|
|
28
|
-
export function createPdfViewShortcuts(handlers) {
|
|
29
|
-
const registry = createShortcutRegistry();
|
|
30
|
-
const guarded = (run) => (event) => {
|
|
31
|
-
if (isPdfTypingTarget(event.target))
|
|
32
|
-
return false;
|
|
33
|
-
run();
|
|
34
|
-
};
|
|
35
|
-
registry.register({
|
|
36
|
-
id: "pdf.first-page",
|
|
37
|
-
label: "First page",
|
|
38
|
-
chord: "home",
|
|
39
|
-
hidden: true,
|
|
40
|
-
...LEGACY_KEY_MATCH,
|
|
41
|
-
when: handlers.homeEnd,
|
|
42
|
-
run: guarded(handlers.goHome),
|
|
43
|
-
});
|
|
44
|
-
registry.register({
|
|
45
|
-
id: "pdf.last-page",
|
|
46
|
-
label: "Last page",
|
|
47
|
-
chord: "end",
|
|
48
|
-
hidden: true,
|
|
49
|
-
...LEGACY_KEY_MATCH,
|
|
50
|
-
when: handlers.homeEnd,
|
|
51
|
-
run: guarded(handlers.goEnd),
|
|
52
|
-
});
|
|
53
|
-
registry.register({
|
|
54
|
-
id: "pdf.zoom-in",
|
|
55
|
-
label: "Zoom in",
|
|
56
|
-
chord: ["ctrlormeta+=", "ctrlormeta+plus"],
|
|
57
|
-
hidden: true,
|
|
58
|
-
...LEGACY_KEY_MATCH,
|
|
59
|
-
run: guarded(() => handlers.zoomBy(0.15)),
|
|
60
|
-
});
|
|
61
|
-
registry.register({
|
|
62
|
-
id: "pdf.zoom-out",
|
|
63
|
-
label: "Zoom out",
|
|
64
|
-
chord: "ctrlormeta+-",
|
|
65
|
-
hidden: true,
|
|
66
|
-
...LEGACY_KEY_MATCH,
|
|
67
|
-
run: guarded(() => handlers.zoomBy(-0.15)),
|
|
68
|
-
});
|
|
69
|
-
registry.register({
|
|
70
|
-
id: "pdf.fit-width",
|
|
71
|
-
label: "Fit to width",
|
|
72
|
-
chord: "ctrlormeta+0",
|
|
73
|
-
hidden: true,
|
|
74
|
-
...LEGACY_KEY_MATCH,
|
|
75
|
-
run: guarded(handlers.fitWidth),
|
|
76
|
-
});
|
|
77
|
-
return registry;
|
|
78
|
-
}
|
|
79
|
-
export function usePdfViewControls({ active, numPages, pageDraft, scale, scaleDraft, scrollRef, pdfRef, goToPage, setPageDraft, setPageShake, setScale, setScaleDraft, setFitMode, setRotation, }) {
|
|
80
|
-
const goToDest = useCallback(async (dest) => {
|
|
81
|
-
const pdf = pdfRef.current;
|
|
82
|
-
if (!pdf || !dest)
|
|
83
|
-
return;
|
|
84
|
-
try {
|
|
85
|
-
const explicit = typeof dest === "string" ? await pdf.getDestination(dest) : dest;
|
|
86
|
-
const ref = Array.isArray(explicit) ? explicit[0] : null;
|
|
87
|
-
if (!ref)
|
|
88
|
-
return;
|
|
89
|
-
const pageIndex = await pdf.getPageIndex(ref);
|
|
90
|
-
goToPage(pageIndex + 1);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
/* malformed destination - ignore */
|
|
94
|
-
}
|
|
95
|
-
}, [goToPage, pdfRef]);
|
|
96
|
-
const commitPageDraft = useCallback(() => {
|
|
97
|
-
if (pageDraft == null)
|
|
98
|
-
return;
|
|
99
|
-
const n = Number(pageDraft);
|
|
100
|
-
if (Number.isFinite(n) && n >= 1 && (numPages === 0 || n <= numPages)) {
|
|
101
|
-
goToPage(n);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
setPageShake(true);
|
|
105
|
-
window.setTimeout(() => setPageShake(false), 360);
|
|
106
|
-
}
|
|
107
|
-
setPageDraft(null);
|
|
108
|
-
}, [goToPage, numPages, pageDraft, setPageDraft, setPageShake]);
|
|
109
|
-
const zoom = useCallback((delta) => {
|
|
110
|
-
setFitMode(null);
|
|
111
|
-
setScale((s) => clampScale(+(s + delta).toFixed(2)));
|
|
112
|
-
}, [setFitMode, setScale]);
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
setScaleDraft(String(Math.round(scale * 100)));
|
|
115
|
-
}, [scale, setScaleDraft]);
|
|
116
|
-
const commitScaleDraft = useCallback(() => {
|
|
117
|
-
const parsed = Number.parseInt(scaleDraft, 10);
|
|
118
|
-
if (Number.isNaN(parsed)) {
|
|
119
|
-
setScaleDraft(String(Math.round(scale * 100)));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const clamped = Math.max(Math.round(ZOOM_MIN * 100), Math.min(Math.round(ZOOM_MAX * 100), parsed));
|
|
123
|
-
setFitMode(null);
|
|
124
|
-
setScale(clamped / 100);
|
|
125
|
-
setScaleDraft(String(clamped));
|
|
126
|
-
}, [scale, scaleDraft, setFitMode, setScale, setScaleDraft]);
|
|
127
|
-
const onWheelZoom = useCallback((event) => {
|
|
128
|
-
if (!(event.ctrlKey || event.metaKey))
|
|
129
|
-
return;
|
|
130
|
-
event.preventDefault();
|
|
131
|
-
event.stopPropagation();
|
|
132
|
-
zoom(event.deltaY < 0 ? 0.1 : -0.1);
|
|
133
|
-
}, [zoom]);
|
|
134
|
-
const rotate = useCallback((delta) => setRotation((r) => (((r + delta) % 360) + 360) % 360), [setRotation]);
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
if (!active)
|
|
137
|
-
return;
|
|
138
|
-
const el = scrollRef.current;
|
|
139
|
-
const registry = createPdfViewShortcuts({
|
|
140
|
-
homeEnd: () => Boolean(el) && document.activeElement === el,
|
|
141
|
-
goHome: () => goToPage(1),
|
|
142
|
-
goEnd: () => goToPage(numPages),
|
|
143
|
-
zoomBy: (delta) => {
|
|
144
|
-
setFitMode(null);
|
|
145
|
-
setScale((s) => clampScale(+(s + delta).toFixed(2)));
|
|
146
|
-
},
|
|
147
|
-
fitWidth: () => setFitMode("width"),
|
|
148
|
-
});
|
|
149
|
-
return registry.attach(window);
|
|
150
|
-
}, [active, goToPage, numPages, scrollRef, setFitMode, setScale]);
|
|
151
|
-
return { goToDest, commitPageDraft, zoom, commitScaleDraft, onWheelZoom, rotate };
|
|
152
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export declare function isSelectAllShortcut(event: Pick<KeyboardEvent, "key" | "ctrlKey" | "metaKey" | "altKey">): boolean;
|
|
2
|
-
export declare function isEditableSelectionTarget(target: EventTarget | null): boolean;
|
|
3
|
-
export declare function selectionIntersectsElement(element: Element, win?: Window): boolean;
|
|
4
|
-
export declare function selectElementContents(element: Element, win?: Window): void;
|
|
5
|
-
export declare function selectVisualLineAtPoint(element: HTMLElement, clientY: number): boolean;
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
export function isSelectAllShortcut(event) {
|
|
2
|
-
return (event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a";
|
|
3
|
-
}
|
|
4
|
-
export function isEditableSelectionTarget(target) {
|
|
5
|
-
if (!(target instanceof Element))
|
|
6
|
-
return false;
|
|
7
|
-
const tag = target.tagName.toLowerCase();
|
|
8
|
-
if (tag === "input" || tag === "textarea" || tag === "select")
|
|
9
|
-
return true;
|
|
10
|
-
if (target instanceof HTMLElement && typeof target.isContentEditable === "boolean") {
|
|
11
|
-
return target.isContentEditable;
|
|
12
|
-
}
|
|
13
|
-
const editable = target.closest("[contenteditable]");
|
|
14
|
-
const value = editable?.getAttribute("contenteditable")?.trim().toLowerCase();
|
|
15
|
-
return value === "" || value === "true" || value === "plaintext-only";
|
|
16
|
-
}
|
|
17
|
-
export function selectionIntersectsElement(element, win = window) {
|
|
18
|
-
const selection = win.getSelection();
|
|
19
|
-
if (!selection || selection.rangeCount === 0)
|
|
20
|
-
return false;
|
|
21
|
-
for (let i = 0; i < selection.rangeCount; i += 1) {
|
|
22
|
-
const range = selection.getRangeAt(i);
|
|
23
|
-
if (range.intersectsNode(element))
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
export function selectElementContents(element, win = window) {
|
|
29
|
-
const doc = element.ownerDocument;
|
|
30
|
-
const range = doc.createRange();
|
|
31
|
-
range.selectNodeContents(element);
|
|
32
|
-
const selection = win.getSelection();
|
|
33
|
-
selection?.removeAllRanges();
|
|
34
|
-
selection?.addRange(range);
|
|
35
|
-
}
|
|
36
|
-
function caretRangeAtPoint(doc, x, y) {
|
|
37
|
-
const position = doc.caretPositionFromPoint?.(x, y);
|
|
38
|
-
if (position) {
|
|
39
|
-
const range = doc.createRange();
|
|
40
|
-
range.setStart(position.offsetNode, position.offset);
|
|
41
|
-
range.collapse(true);
|
|
42
|
-
return range;
|
|
43
|
-
}
|
|
44
|
-
return doc.caretRangeFromPoint?.(x, y) ?? null;
|
|
45
|
-
}
|
|
46
|
-
export function selectVisualLineAtPoint(element, clientY) {
|
|
47
|
-
const doc = element.ownerDocument;
|
|
48
|
-
const rect = element.getBoundingClientRect();
|
|
49
|
-
if (rect.width <= 0 || rect.height <= 0)
|
|
50
|
-
return false;
|
|
51
|
-
const xStart = Math.min(rect.right - 1, rect.left + 1);
|
|
52
|
-
const xEnd = Math.max(xStart, rect.right - 1);
|
|
53
|
-
const y = Math.max(rect.top + 1, Math.min(rect.bottom - 1, clientY));
|
|
54
|
-
const start = caretRangeAtPoint(doc, xStart, y);
|
|
55
|
-
const end = caretRangeAtPoint(doc, xEnd, y);
|
|
56
|
-
if (!start ||
|
|
57
|
-
!end ||
|
|
58
|
-
!element.contains(start.startContainer) ||
|
|
59
|
-
!element.contains(end.startContainer)) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
const range = doc.createRange();
|
|
63
|
-
if (start.compareBoundaryPoints(Range.START_TO_START, end) <= 0) {
|
|
64
|
-
range.setStart(start.startContainer, start.startOffset);
|
|
65
|
-
range.setEnd(end.startContainer, end.startOffset);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
range.setStart(end.startContainer, end.startOffset);
|
|
69
|
-
range.setEnd(start.startContainer, start.startOffset);
|
|
70
|
-
}
|
|
71
|
-
const selection = doc.defaultView?.getSelection();
|
|
72
|
-
if (!selection)
|
|
73
|
-
return false;
|
|
74
|
-
selection.removeAllRanges();
|
|
75
|
-
selection.addRange(range);
|
|
76
|
-
return true;
|
|
77
|
-
}
|