@embedpdf/plugin-selection 2.15.0 → 3.0.0-next.1
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/LICENSE +192 -21
- package/dist/index.cjs +575 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -0
- package/dist/index.d.ts +167 -1
- package/dist/index.js +549 -1508
- package/dist/index.js.map +1 -1
- package/package.json +21 -62
- package/dist/lib/actions.d.ts +0 -103
- package/dist/lib/handlers/index.d.ts +0 -2
- package/dist/lib/handlers/marquee-selection.handler.d.ts +0 -30
- package/dist/lib/handlers/text-selection.handler.d.ts +0 -52
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/reducer.d.ts +0 -5
- package/dist/lib/selection-plugin.d.ts +0 -121
- package/dist/lib/selectors.d.ts +0 -14
- package/dist/lib/types.d.ts +0 -268
- package/dist/lib/utils.d.ts +0 -108
- package/dist/preact/adapter.d.ts +0 -4
- package/dist/preact/core.d.ts +0 -1
- package/dist/preact/index.cjs +0 -2
- package/dist/preact/index.cjs.map +0 -1
- package/dist/preact/index.d.ts +0 -1
- package/dist/preact/index.js +0 -218
- package/dist/preact/index.js.map +0 -1
- package/dist/preact/interaction-manager.d.ts +0 -1
- package/dist/preact/utils.d.ts +0 -1
- package/dist/react/adapter.d.ts +0 -2
- package/dist/react/core.d.ts +0 -1
- package/dist/react/index.cjs +0 -2
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -217
- package/dist/react/index.js.map +0 -1
- package/dist/react/interaction-manager.d.ts +0 -1
- package/dist/react/utils.d.ts +0 -1
- package/dist/shared/components/copy-to-clipboard.d.ts +0 -1
- package/dist/shared/components/index.d.ts +0 -4
- package/dist/shared/components/marquee-selection.d.ts +0 -36
- package/dist/shared/components/selection-layer.d.ts +0 -29
- package/dist/shared/components/text-selection.d.ts +0 -21
- package/dist/shared/hooks/index.d.ts +0 -1
- package/dist/shared/hooks/use-selection.d.ts +0 -11
- package/dist/shared/index.d.ts +0 -5
- package/dist/shared/types.d.ts +0 -7
- package/dist/shared-preact/components/copy-to-clipboard.d.ts +0 -1
- package/dist/shared-preact/components/index.d.ts +0 -4
- package/dist/shared-preact/components/marquee-selection.d.ts +0 -36
- package/dist/shared-preact/components/selection-layer.d.ts +0 -29
- package/dist/shared-preact/components/text-selection.d.ts +0 -21
- package/dist/shared-preact/hooks/index.d.ts +0 -1
- package/dist/shared-preact/hooks/use-selection.d.ts +0 -11
- package/dist/shared-preact/index.d.ts +0 -5
- package/dist/shared-preact/types.d.ts +0 -7
- package/dist/shared-react/components/copy-to-clipboard.d.ts +0 -1
- package/dist/shared-react/components/index.d.ts +0 -4
- package/dist/shared-react/components/marquee-selection.d.ts +0 -36
- package/dist/shared-react/components/selection-layer.d.ts +0 -29
- package/dist/shared-react/components/text-selection.d.ts +0 -21
- package/dist/shared-react/hooks/index.d.ts +0 -1
- package/dist/shared-react/hooks/use-selection.d.ts +0 -11
- package/dist/shared-react/index.d.ts +0 -5
- package/dist/shared-react/types.d.ts +0 -7
- package/dist/svelte/components/CopyToClipboard.svelte.d.ts +0 -3
- package/dist/svelte/components/MarqueeSelection.svelte.d.ts +0 -27
- package/dist/svelte/components/SelectionLayer.svelte.d.ts +0 -32
- package/dist/svelte/components/TextSelection.svelte.d.ts +0 -22
- package/dist/svelte/components/index.d.ts +0 -4
- package/dist/svelte/hooks/index.d.ts +0 -1
- package/dist/svelte/hooks/use-selection.svelte.d.ts +0 -19
- package/dist/svelte/index.cjs +0 -2
- package/dist/svelte/index.cjs.map +0 -1
- package/dist/svelte/index.d.ts +0 -5
- package/dist/svelte/index.js +0 -359
- package/dist/svelte/index.js.map +0 -1
- package/dist/svelte/types.d.ts +0 -7
- package/dist/vue/components/copy-to-clipboard.vue.d.ts +0 -3
- package/dist/vue/components/index.d.ts +0 -4
- package/dist/vue/components/marquee-selection.vue.d.ts +0 -29
- package/dist/vue/components/selection-layer.vue.d.ts +0 -60
- package/dist/vue/components/text-selection.vue.d.ts +0 -51
- package/dist/vue/hooks/index.d.ts +0 -1
- package/dist/vue/hooks/use-selection.d.ts +0 -11
- package/dist/vue/index.cjs +0 -2
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.ts +0 -5
- package/dist/vue/index.js +0 -311
- package/dist/vue/index.js.map +0 -1
- package/dist/vue/types.d.ts +0 -7
package/dist/index.js
CHANGED
|
@@ -1,1523 +1,564 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const initSelectionState = (documentId, state) => ({
|
|
27
|
-
type: INIT_SELECTION_STATE,
|
|
28
|
-
payload: { documentId, state }
|
|
29
|
-
});
|
|
30
|
-
const cleanupSelectionState = (documentId) => ({
|
|
31
|
-
type: CLEANUP_SELECTION_STATE,
|
|
32
|
-
payload: documentId
|
|
33
|
-
});
|
|
34
|
-
const cachePageGeometry = (documentId, page, geo) => ({
|
|
35
|
-
type: CACHE_PAGE_GEOMETRY,
|
|
36
|
-
payload: { documentId, page, geo }
|
|
37
|
-
});
|
|
38
|
-
const setSelection = (documentId, sel) => ({
|
|
39
|
-
type: SET_SELECTION,
|
|
40
|
-
payload: { documentId, selection: sel }
|
|
41
|
-
});
|
|
42
|
-
const startSelection = (documentId) => ({
|
|
43
|
-
type: START_SELECTION,
|
|
44
|
-
payload: { documentId }
|
|
45
|
-
});
|
|
46
|
-
const endSelection = (documentId) => ({
|
|
47
|
-
type: END_SELECTION,
|
|
48
|
-
payload: { documentId }
|
|
49
|
-
});
|
|
50
|
-
const clearSelection = (documentId) => ({
|
|
51
|
-
type: CLEAR_SELECTION,
|
|
52
|
-
payload: { documentId }
|
|
1
|
+
import { createCapabilityToken, definePlugin } from "@embedpdf/core";
|
|
2
|
+
import { InteractionToken } from "@embedpdf/plugin-interaction";
|
|
3
|
+
import { applyRect, boundsOfRects, pageGeometry } from "@embedpdf/core-geometry";
|
|
4
|
+
//#region src/geometry.ts
|
|
5
|
+
/**
|
|
6
|
+
* The pure selection geometry: engine text geometry (PDF user space, y-up) →
|
|
7
|
+
* viewer content space (y-down), glyph hit-testing, word/line expansion, and the
|
|
8
|
+
* line-merge. No DOM, no engine, no React.
|
|
9
|
+
*
|
|
10
|
+
* The non-trivial bits are ports of battle-tested algorithms, adapted to v3's
|
|
11
|
+
* content-space `Rect {x,y,width,height}`:
|
|
12
|
+
* - `glyphAt` — PDFium `GetIndexAtPos` (exact tight box, then a tolerance pass).
|
|
13
|
+
* - `rectsForRange` — Chromium `pdfium_range.cc` `MergeAdjacentRects` (one rect per line).
|
|
14
|
+
* - `expandToWord/Line` — Chromium `OnMultipleClick` (double = word, triple = line).
|
|
15
|
+
* The PDF↔content y-flip is the geometry package's `pageGeometry` (the one place
|
|
16
|
+
* that math lives), never hand-rolled here.
|
|
17
|
+
*/
|
|
18
|
+
const FLAG_EMPTY = 2;
|
|
19
|
+
const isBoundary = (flags) => (flags & 3) !== 0;
|
|
20
|
+
const isEmpty = (flags) => (flags & FLAG_EMPTY) !== 0;
|
|
21
|
+
const toContent = (m, b) => applyRect(m, {
|
|
22
|
+
x: b.left,
|
|
23
|
+
y: b.bottom,
|
|
24
|
+
width: b.right - b.left,
|
|
25
|
+
height: b.top - b.bottom
|
|
53
26
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return updateDocState(state, documentId, docState);
|
|
86
|
-
}
|
|
87
|
-
case CLEANUP_SELECTION_STATE: {
|
|
88
|
-
const documentId = action.payload;
|
|
89
|
-
const { [documentId]: removed, ...remaining } = state.documents;
|
|
90
|
-
return {
|
|
91
|
-
...state,
|
|
92
|
-
documents: remaining
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
case CACHE_PAGE_GEOMETRY: {
|
|
96
|
-
const { documentId, page, geo } = action.payload;
|
|
97
|
-
const docState = state.documents[documentId];
|
|
98
|
-
if (!docState) return state;
|
|
99
|
-
return updateDocState(state, documentId, {
|
|
100
|
-
...docState,
|
|
101
|
-
geometry: { ...docState.geometry, [page]: geo }
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
case SET_SELECTION: {
|
|
105
|
-
const { documentId, selection } = action.payload;
|
|
106
|
-
const docState = state.documents[documentId];
|
|
107
|
-
if (!docState) return state;
|
|
108
|
-
return updateDocState(state, documentId, {
|
|
109
|
-
...docState,
|
|
110
|
-
selection,
|
|
111
|
-
active: true
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
case START_SELECTION: {
|
|
115
|
-
const { documentId } = action.payload;
|
|
116
|
-
const docState = state.documents[documentId];
|
|
117
|
-
if (!docState) return state;
|
|
118
|
-
return updateDocState(state, documentId, {
|
|
119
|
-
...docState,
|
|
120
|
-
selecting: true,
|
|
121
|
-
selection: null,
|
|
122
|
-
rects: {}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
case END_SELECTION: {
|
|
126
|
-
const { documentId } = action.payload;
|
|
127
|
-
const docState = state.documents[documentId];
|
|
128
|
-
if (!docState) return state;
|
|
129
|
-
return updateDocState(state, documentId, { ...docState, selecting: false });
|
|
130
|
-
}
|
|
131
|
-
case CLEAR_SELECTION: {
|
|
132
|
-
const { documentId } = action.payload;
|
|
133
|
-
const docState = state.documents[documentId];
|
|
134
|
-
if (!docState) return state;
|
|
135
|
-
return updateDocState(state, documentId, {
|
|
136
|
-
...docState,
|
|
137
|
-
selecting: false,
|
|
138
|
-
selection: null,
|
|
139
|
-
rects: {},
|
|
140
|
-
active: false
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
case SET_RECTS: {
|
|
144
|
-
const { documentId, rects } = action.payload;
|
|
145
|
-
const docState = state.documents[documentId];
|
|
146
|
-
if (!docState) return state;
|
|
147
|
-
return updateDocState(state, documentId, { ...docState, rects });
|
|
148
|
-
}
|
|
149
|
-
case SET_SLICES: {
|
|
150
|
-
const { documentId, slices } = action.payload;
|
|
151
|
-
const docState = state.documents[documentId];
|
|
152
|
-
if (!docState) return state;
|
|
153
|
-
return updateDocState(state, documentId, { ...docState, slices });
|
|
154
|
-
}
|
|
155
|
-
case EVICT_PAGE_GEOMETRY: {
|
|
156
|
-
const { documentId, pages } = action.payload;
|
|
157
|
-
const docState = state.documents[documentId];
|
|
158
|
-
if (!docState) return state;
|
|
159
|
-
const geometry = { ...docState.geometry };
|
|
160
|
-
const rects = { ...docState.rects };
|
|
161
|
-
const slices = { ...docState.slices };
|
|
162
|
-
for (const p of pages) {
|
|
163
|
-
delete geometry[p];
|
|
164
|
-
delete rects[p];
|
|
165
|
-
delete slices[p];
|
|
166
|
-
}
|
|
167
|
-
return updateDocState(state, documentId, { ...docState, geometry, rects, slices });
|
|
168
|
-
}
|
|
169
|
-
case RESET: {
|
|
170
|
-
const { documentId } = action.payload;
|
|
171
|
-
const docState = state.documents[documentId];
|
|
172
|
-
if (!docState) return state;
|
|
173
|
-
return updateDocState(state, documentId, initialSelectionDocumentState);
|
|
174
|
-
}
|
|
175
|
-
default:
|
|
176
|
-
return state;
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
function selectRectsForPage(state, page) {
|
|
180
|
-
return state.rects[page] ?? [];
|
|
181
|
-
}
|
|
182
|
-
function selectBoundingRectForPage(state, page) {
|
|
183
|
-
return boundingRect(selectRectsForPage(state, page));
|
|
27
|
+
/**
|
|
28
|
+
* Flatten a page's text geometry into content-space glyphs + runs. Rotation does
|
|
29
|
+
* NOT enter here — the overlay rides the page's CSS rotation, so glyphs stay in
|
|
30
|
+
* un-rotated content space (the crop-relative y-flip is the only conversion).
|
|
31
|
+
*/
|
|
32
|
+
function buildPageText(snapshot, crop, rotation, userUnit) {
|
|
33
|
+
const { pdfToContent } = pageGeometry({
|
|
34
|
+
crop,
|
|
35
|
+
rotation,
|
|
36
|
+
userUnit
|
|
37
|
+
}, 1);
|
|
38
|
+
const glyphs = [];
|
|
39
|
+
const runs = [];
|
|
40
|
+
for (const run of snapshot.runs) {
|
|
41
|
+
const start = glyphs.length;
|
|
42
|
+
for (const g of run.glyphs) glyphs.push({
|
|
43
|
+
loose: toContent(pdfToContent, g.looseBox),
|
|
44
|
+
tight: g.tightBox ? toContent(pdfToContent, g.tightBox) : void 0,
|
|
45
|
+
flags: g.flags
|
|
46
|
+
});
|
|
47
|
+
runs.push({
|
|
48
|
+
start,
|
|
49
|
+
count: run.glyphs.length,
|
|
50
|
+
rect: toContent(pdfToContent, run.rect),
|
|
51
|
+
fontSize: run.fontSize
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
glyphs,
|
|
56
|
+
runs
|
|
57
|
+
};
|
|
184
58
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
59
|
+
function avgGlyphHeight(text) {
|
|
60
|
+
let total = 0;
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const g of text.glyphs) {
|
|
63
|
+
if (isEmpty(g.flags)) continue;
|
|
64
|
+
total += g.loose.height;
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
return count === 0 ? 0 : total / count;
|
|
194
68
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The glyph index at a content-space point, or null when nothing is near (so the
|
|
71
|
+
* caller can show the pointer cursor off-text). PDFium `GetIndexAtPos`: exact
|
|
72
|
+
* tight-box containment first, then a tolerance pass (closest by Manhattan
|
|
73
|
+
* distance within `toleranceFactor × average glyph height`).
|
|
74
|
+
*/
|
|
75
|
+
function glyphAt(text, p, toleranceFactor = 1.5) {
|
|
76
|
+
for (const run of text.runs) {
|
|
77
|
+
const r = run.rect;
|
|
78
|
+
if (p.x < r.x || p.x > r.x + r.width || p.y < r.y || p.y > r.y + r.height) continue;
|
|
79
|
+
for (let i = 0; i < run.count; i++) {
|
|
80
|
+
const b = text.glyphs[run.start + i].tight ?? text.glyphs[run.start + i].loose;
|
|
81
|
+
if (p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height) return run.start + i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (toleranceFactor <= 0) return null;
|
|
85
|
+
const half = avgGlyphHeight(text) * toleranceFactor / 2;
|
|
86
|
+
let best = -1;
|
|
87
|
+
let bestDist = Infinity;
|
|
88
|
+
for (const run of text.runs) {
|
|
89
|
+
const r = run.rect;
|
|
90
|
+
if (p.y < r.y - half || p.y > r.y + r.height + half || p.x < r.x - half || p.x > r.x + r.width + half) continue;
|
|
91
|
+
for (let i = 0; i < run.count; i++) {
|
|
92
|
+
const g = text.glyphs[run.start + i];
|
|
93
|
+
if (isEmpty(g.flags)) continue;
|
|
94
|
+
const b = g.tight ?? g.loose;
|
|
95
|
+
if (p.x < b.x - half || p.x > b.x + b.width + half || p.y < b.y - half || p.y > b.y + b.height + half) continue;
|
|
96
|
+
const dx = Math.min(Math.abs(p.x - b.x), Math.abs(p.x - (b.x + b.width)));
|
|
97
|
+
const dy = Math.min(Math.abs(p.y - b.y), Math.abs(p.y - (b.y + b.height)));
|
|
98
|
+
if (dx + dy < bestDist) {
|
|
99
|
+
bestDist = dx + dy;
|
|
100
|
+
best = run.start + i;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return best >= 0 ? best : null;
|
|
201
105
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
pageIndex,
|
|
212
|
-
rect: boundingRect2,
|
|
213
|
-
segmentRects
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return result;
|
|
106
|
+
/** Double-click: the word around `i` (walk to space/empty glyphs both ways). */
|
|
107
|
+
function expandToWord(text, i) {
|
|
108
|
+
const n = text.glyphs.length;
|
|
109
|
+
if (i < 0 || i >= n) return [i, i];
|
|
110
|
+
let from = i;
|
|
111
|
+
while (from > 0 && !isBoundary(text.glyphs[from - 1].flags)) from--;
|
|
112
|
+
let to = i;
|
|
113
|
+
while (to < n - 1 && !isBoundary(text.glyphs[to + 1].flags)) to++;
|
|
114
|
+
return [from, to];
|
|
218
115
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
for (let i = 0; i < run.glyphs.length; i++) {
|
|
244
|
-
const g = run.glyphs[i];
|
|
245
|
-
if (g.flags === 2) continue;
|
|
246
|
-
const gx = g.tightX ?? g.x;
|
|
247
|
-
const gy = g.tightY ?? g.y;
|
|
248
|
-
const gw = g.tightWidth ?? g.width;
|
|
249
|
-
const gh = g.tightHeight ?? g.height;
|
|
250
|
-
const expandedLeft = gx - halfTol;
|
|
251
|
-
const expandedRight = gx + gw + halfTol;
|
|
252
|
-
const expandedTop = gy - halfTol;
|
|
253
|
-
const expandedBottom = gy + gh + halfTol;
|
|
254
|
-
if (pt.x < expandedLeft || pt.x > expandedRight || pt.y < expandedTop || pt.y > expandedBottom) {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const curXdif = Math.min(Math.abs(pt.x - gx), Math.abs(pt.x - (gx + gw)));
|
|
258
|
-
const curYdif = Math.min(Math.abs(pt.y - gy), Math.abs(pt.y - (gy + gh)));
|
|
259
|
-
const dist = curXdif + curYdif;
|
|
260
|
-
if (dist < bestDist) {
|
|
261
|
-
bestDist = dist;
|
|
262
|
-
bestIndex = run.charStart + i;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return bestIndex;
|
|
116
|
+
/** Triple-click: the full visual line — runs whose vertical extent overlaps the anchor run's. */
|
|
117
|
+
function expandToLine(text, i) {
|
|
118
|
+
const ri = text.runs.findIndex((r) => i >= r.start && i < r.start + r.count);
|
|
119
|
+
if (ri < 0) return [i, i];
|
|
120
|
+
const anchor = text.runs[ri];
|
|
121
|
+
const top = anchor.rect.y;
|
|
122
|
+
const bottom = anchor.rect.y + anchor.rect.height;
|
|
123
|
+
let from = anchor.start;
|
|
124
|
+
let to = anchor.start + anchor.count - 1;
|
|
125
|
+
for (let r = ri - 1; r >= 0; r--) {
|
|
126
|
+
const run = text.runs[r];
|
|
127
|
+
if (isZero(run.rect)) continue;
|
|
128
|
+
if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;
|
|
129
|
+
from = run.start;
|
|
130
|
+
}
|
|
131
|
+
for (let r = ri + 1; r < text.runs.length; r++) {
|
|
132
|
+
const run = text.runs[r];
|
|
133
|
+
if (isZero(run.rect)) continue;
|
|
134
|
+
if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;
|
|
135
|
+
to = run.start + run.count - 1;
|
|
136
|
+
}
|
|
137
|
+
return [from, to];
|
|
267
138
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
139
|
+
const CHAR_DISTANCE_FACTOR = 2.5;
|
|
140
|
+
const FONT_SIZE_RATIO_THRESHOLD = 1.5;
|
|
141
|
+
const VERTICAL_OVERLAP_THRESHOLD = .8;
|
|
142
|
+
const LINE_OVERLAP_THRESHOLD = .5;
|
|
143
|
+
function rectsForRange(text, from, to) {
|
|
144
|
+
const lo = Math.max(0, Math.min(from, to));
|
|
145
|
+
const hi = Math.min(text.glyphs.length - 1, Math.max(from, to));
|
|
146
|
+
const subRuns = [];
|
|
147
|
+
for (const run of text.runs) {
|
|
148
|
+
const runEnd = run.start + run.count - 1;
|
|
149
|
+
if (runEnd < lo || run.start > hi) continue;
|
|
150
|
+
const s = Math.max(lo, run.start);
|
|
151
|
+
const e = Math.min(hi, runEnd);
|
|
152
|
+
let minX = Infinity;
|
|
153
|
+
let maxX = -Infinity;
|
|
154
|
+
let minY = Infinity;
|
|
155
|
+
let maxY = -Infinity;
|
|
156
|
+
let charCount = 0;
|
|
157
|
+
let widthSum = 0;
|
|
158
|
+
let prevRight = -Infinity;
|
|
159
|
+
const flush = () => {
|
|
160
|
+
if (minX !== Infinity && charCount > 0) subRuns.push({
|
|
161
|
+
rect: {
|
|
162
|
+
x: minX,
|
|
163
|
+
y: minY,
|
|
164
|
+
width: maxX - minX,
|
|
165
|
+
height: maxY - minY
|
|
166
|
+
},
|
|
167
|
+
charCount,
|
|
168
|
+
fontSize: run.fontSize
|
|
169
|
+
});
|
|
170
|
+
minX = Infinity;
|
|
171
|
+
maxX = -Infinity;
|
|
172
|
+
minY = Infinity;
|
|
173
|
+
maxY = -Infinity;
|
|
174
|
+
charCount = 0;
|
|
175
|
+
widthSum = 0;
|
|
176
|
+
prevRight = -Infinity;
|
|
177
|
+
};
|
|
178
|
+
for (let gi = s; gi <= e; gi++) {
|
|
179
|
+
const g = text.glyphs[gi];
|
|
180
|
+
if (isEmpty(g.flags)) continue;
|
|
181
|
+
const b = g.loose;
|
|
182
|
+
if (charCount > 0 && prevRight > -Infinity) {
|
|
183
|
+
const avg = widthSum / charCount;
|
|
184
|
+
if (avg > 0 && Math.abs(b.x - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
|
|
185
|
+
}
|
|
186
|
+
minX = Math.min(minX, b.x);
|
|
187
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
188
|
+
minY = Math.min(minY, b.y);
|
|
189
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
190
|
+
charCount++;
|
|
191
|
+
widthSum += b.width;
|
|
192
|
+
prevRight = b.x + b.width;
|
|
193
|
+
}
|
|
194
|
+
flush();
|
|
195
|
+
}
|
|
196
|
+
return mergeAdjacentRects(subRuns);
|
|
280
197
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
198
|
+
function mergeAdjacentRects(runs) {
|
|
199
|
+
const out = [];
|
|
200
|
+
let prev = null;
|
|
201
|
+
let cur = null;
|
|
202
|
+
for (const run of runs) {
|
|
203
|
+
if (prev && cur && shouldMerge(prev, run)) cur = boundsOfRects([cur, run.rect]);
|
|
204
|
+
else {
|
|
205
|
+
if (cur) out.push(cur);
|
|
206
|
+
cur = run.rect;
|
|
207
|
+
}
|
|
208
|
+
prev = run;
|
|
209
|
+
}
|
|
210
|
+
if (cur && cur.width > 0 && cur.height > 0) out.push(cur);
|
|
211
|
+
return out;
|
|
289
212
|
}
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
let charCount = 0;
|
|
302
|
-
let widthSum = 0;
|
|
303
|
-
let prevRight = -Infinity;
|
|
304
|
-
const flushSubRun = () => {
|
|
305
|
-
if (minX !== Infinity && charCount > 0) {
|
|
306
|
-
textRuns.push({
|
|
307
|
-
rect: {
|
|
308
|
-
origin: { x: minX, y: minY },
|
|
309
|
-
size: { width: maxX - minX, height: maxY - minY }
|
|
310
|
-
},
|
|
311
|
-
charCount,
|
|
312
|
-
fontSize: run.fontSize
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
minX = Infinity;
|
|
316
|
-
maxX = -Infinity;
|
|
317
|
-
minY = Infinity;
|
|
318
|
-
maxY = -Infinity;
|
|
319
|
-
charCount = 0;
|
|
320
|
-
widthSum = 0;
|
|
321
|
-
prevRight = -Infinity;
|
|
322
|
-
};
|
|
323
|
-
for (let i = sIdx; i <= eIdx; i++) {
|
|
324
|
-
const g = run.glyphs[i];
|
|
325
|
-
if (g.flags === 2) continue;
|
|
326
|
-
if (charCount > 0 && prevRight > -Infinity) {
|
|
327
|
-
const gap = Math.abs(g.x - prevRight);
|
|
328
|
-
const avgWidth = widthSum / charCount;
|
|
329
|
-
if (avgWidth > 0 && gap > CHAR_DISTANCE_FACTOR * avgWidth) {
|
|
330
|
-
flushSubRun();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
minX = Math.min(minX, g.x);
|
|
334
|
-
maxX = Math.max(maxX, g.x + g.width);
|
|
335
|
-
minY = Math.min(minY, g.y);
|
|
336
|
-
maxY = Math.max(maxY, g.y + g.height);
|
|
337
|
-
charCount++;
|
|
338
|
-
widthSum += g.width;
|
|
339
|
-
prevRight = g.x + g.width;
|
|
340
|
-
}
|
|
341
|
-
flushSubRun();
|
|
342
|
-
}
|
|
343
|
-
if (!merge) {
|
|
344
|
-
return textRuns.map((run) => run.rect);
|
|
345
|
-
}
|
|
346
|
-
return mergeAdjacentRects(textRuns);
|
|
213
|
+
function shouldMerge(a, b) {
|
|
214
|
+
if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
|
|
215
|
+
if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
|
|
216
|
+
}
|
|
217
|
+
if (verticalOverlap(a.rect, b.rect) < VERTICAL_OVERLAP_THRESHOLD) return false;
|
|
218
|
+
const aw = a.rect.width / a.charCount;
|
|
219
|
+
const bw = b.rect.width / b.charCount;
|
|
220
|
+
const aL = a.rect.x - aw;
|
|
221
|
+
const aR = a.rect.x + a.rect.width + aw;
|
|
222
|
+
const bL = b.rect.x - bw;
|
|
223
|
+
return aL < b.rect.x + b.rect.width + bw && aR > bL;
|
|
347
224
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
origin: { x: left, y: top },
|
|
355
|
-
size: { width: right - left, height: bottom - top }
|
|
356
|
-
};
|
|
225
|
+
function verticalOverlap(a, b) {
|
|
226
|
+
if (a.height <= 0 || b.height <= 0) return 0;
|
|
227
|
+
const u = Math.max(a.y + a.height, b.y + b.height) - Math.min(a.y, b.y);
|
|
228
|
+
if (u === a.height || u === b.height) return 1;
|
|
229
|
+
return Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y)) / u;
|
|
357
230
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const width = Math.max(0, right - left);
|
|
364
|
-
const height = Math.max(0, bottom - top);
|
|
365
|
-
return {
|
|
366
|
-
origin: { x: left, y: top },
|
|
367
|
-
size: { width, height }
|
|
368
|
-
};
|
|
231
|
+
const isZero = (r) => r.width === 0 && r.height === 0;
|
|
232
|
+
function overlapV(top1, bottom1, top2, bottom2) {
|
|
233
|
+
const u = Math.max(bottom1, bottom2) - Math.min(top1, top2);
|
|
234
|
+
if (u === 0) return false;
|
|
235
|
+
return Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2)) / u >= LINE_OVERLAP_THRESHOLD;
|
|
369
236
|
}
|
|
370
|
-
|
|
371
|
-
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/capability.ts
|
|
239
|
+
const EMPTY = [];
|
|
240
|
+
/**
|
|
241
|
+
* The selection capability. The reducer state holds the selection range, the
|
|
242
|
+
* derived content-space rects (per page), and per-page loaded flags; the (large,
|
|
243
|
+
* non-serializable) per-page text geometry is cached HERE in the closure.
|
|
244
|
+
*
|
|
245
|
+
* Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph), so
|
|
246
|
+
* a drag from page 2 into page 4 selects the tail of 2, all of 3, and the head of
|
|
247
|
+
* 4. `recompute` rebuilds the merged line rects for every loaded page in the span
|
|
248
|
+
* and re-runs whenever a mid-span page finishes loading.
|
|
249
|
+
*/
|
|
250
|
+
function createSelectionCapability(ctx) {
|
|
251
|
+
const cache = /* @__PURE__ */ new Map();
|
|
252
|
+
const pending = /* @__PURE__ */ new Set();
|
|
253
|
+
const changeCbs = /* @__PURE__ */ new Set();
|
|
254
|
+
const commitCbs = /* @__PURE__ */ new Set();
|
|
255
|
+
const fireChange = () => changeCbs.forEach((cb) => cb());
|
|
256
|
+
const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;
|
|
257
|
+
const ponAtIndex = (i) => ctx.document()?.pages[i]?.pageObjectNumber;
|
|
258
|
+
function orderedEnds(sel) {
|
|
259
|
+
const ai = pageIndexOf(sel.anchor.pon);
|
|
260
|
+
const fi = pageIndexOf(sel.focus.pon);
|
|
261
|
+
return ai < fi || ai === fi && sel.anchor.glyph <= sel.focus.glyph ? {
|
|
262
|
+
start: sel.anchor,
|
|
263
|
+
end: sel.focus,
|
|
264
|
+
direction: "forward"
|
|
265
|
+
} : {
|
|
266
|
+
start: sel.focus,
|
|
267
|
+
end: sel.anchor,
|
|
268
|
+
direction: "backward"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function endpointFor(ptr, which) {
|
|
272
|
+
const rects = ctx.getState().rects[ptr.pon] ?? EMPTY;
|
|
273
|
+
if (!rects.length) return null;
|
|
274
|
+
return {
|
|
275
|
+
pon: ptr.pon,
|
|
276
|
+
rect: which === "start" ? rects[0] : rects[rects.length - 1]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function snapshot() {
|
|
280
|
+
const { selection, rects } = ctx.getState();
|
|
281
|
+
const pages = Object.keys(rects).map(Number).filter((pon) => (rects[pon]?.length ?? 0) > 0).map((pon) => ({
|
|
282
|
+
pon,
|
|
283
|
+
rects: rects[pon]
|
|
284
|
+
}));
|
|
285
|
+
if (!selection) return {
|
|
286
|
+
pages,
|
|
287
|
+
start: null,
|
|
288
|
+
end: null,
|
|
289
|
+
direction: "forward"
|
|
290
|
+
};
|
|
291
|
+
const { start, end, direction } = orderedEnds(selection);
|
|
292
|
+
return {
|
|
293
|
+
pages,
|
|
294
|
+
start: endpointFor(start, "start"),
|
|
295
|
+
end: endpointFor(end, "end"),
|
|
296
|
+
direction
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function ensurePage(pon) {
|
|
300
|
+
if (cache.has(pon) || pending.has(pon)) return;
|
|
301
|
+
const doc = ctx.doc;
|
|
302
|
+
const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
|
|
303
|
+
if (!doc || !layout) return;
|
|
304
|
+
pending.add(pon);
|
|
305
|
+
doc.page(pon).geometry.read().then((snapshot) => {
|
|
306
|
+
pending.delete(pon);
|
|
307
|
+
cache.set(pon, buildPageText(snapshot, layout.boxes.crop, layout.rotation, layout.userUnit));
|
|
308
|
+
ctx.dispatch({
|
|
309
|
+
type: "PAGE_LOADED",
|
|
310
|
+
pon
|
|
311
|
+
});
|
|
312
|
+
if (ctx.getState().selection) recompute();
|
|
313
|
+
}, () => {
|
|
314
|
+
pending.delete(pon);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function recompute(sel = ctx.getState().selection) {
|
|
318
|
+
if (!sel) return;
|
|
319
|
+
const { start, end } = orderedEnds(sel);
|
|
320
|
+
const si = pageIndexOf(start.pon);
|
|
321
|
+
const ei = pageIndexOf(end.pon);
|
|
322
|
+
if (si < 0 || ei < 0) return;
|
|
323
|
+
const rects = {};
|
|
324
|
+
for (let i = si; i <= ei; i++) {
|
|
325
|
+
const pon = ponAtIndex(i);
|
|
326
|
+
if (pon == null) continue;
|
|
327
|
+
const text = cache.get(pon);
|
|
328
|
+
if (!text) {
|
|
329
|
+
ensurePage(pon);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
rects[pon] = rectsForRange(text, i === si ? start.glyph : 0, i === ei ? end.glyph : text.glyphs.length - 1);
|
|
333
|
+
}
|
|
334
|
+
ctx.dispatch({
|
|
335
|
+
type: "SET",
|
|
336
|
+
selection: sel,
|
|
337
|
+
rects
|
|
338
|
+
});
|
|
339
|
+
fireChange();
|
|
340
|
+
}
|
|
341
|
+
function selectSpan(pon, point, expand) {
|
|
342
|
+
const text = cache.get(pon);
|
|
343
|
+
if (!text) return;
|
|
344
|
+
const i = glyphAt(text, point);
|
|
345
|
+
if (i == null) return;
|
|
346
|
+
const [from, to] = expand === "word" ? expandToWord(text, i) : expandToLine(text, i);
|
|
347
|
+
recompute({
|
|
348
|
+
anchor: {
|
|
349
|
+
pon,
|
|
350
|
+
glyph: from
|
|
351
|
+
},
|
|
352
|
+
focus: {
|
|
353
|
+
pon,
|
|
354
|
+
glyph: to
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
ensurePage,
|
|
360
|
+
isLoaded: (pon) => !!ctx.getState().loaded[pon],
|
|
361
|
+
isOverText: (pon, point) => {
|
|
362
|
+
const text = cache.get(pon);
|
|
363
|
+
return text ? glyphAt(text, point) != null : false;
|
|
364
|
+
},
|
|
365
|
+
beginAt: (pon, point) => {
|
|
366
|
+
const text = cache.get(pon);
|
|
367
|
+
if (!text) return false;
|
|
368
|
+
const i = glyphAt(text, point);
|
|
369
|
+
if (i == null) return false;
|
|
370
|
+
recompute({
|
|
371
|
+
anchor: {
|
|
372
|
+
pon,
|
|
373
|
+
glyph: i
|
|
374
|
+
},
|
|
375
|
+
focus: {
|
|
376
|
+
pon,
|
|
377
|
+
glyph: i
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return true;
|
|
381
|
+
},
|
|
382
|
+
selectWord: (pon, point) => selectSpan(pon, point, "word"),
|
|
383
|
+
selectLine: (pon, point) => selectSpan(pon, point, "line"),
|
|
384
|
+
extendTo: (pon, point) => {
|
|
385
|
+
const cur = ctx.getState().selection;
|
|
386
|
+
if (!cur) return;
|
|
387
|
+
const text = cache.get(pon);
|
|
388
|
+
if (!text) {
|
|
389
|
+
ensurePage(pon);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const i = glyphAt(text, point);
|
|
393
|
+
if (i == null) return;
|
|
394
|
+
recompute({
|
|
395
|
+
anchor: cur.anchor,
|
|
396
|
+
focus: {
|
|
397
|
+
pon,
|
|
398
|
+
glyph: i
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
end: () => commitCbs.forEach((cb) => cb()),
|
|
403
|
+
clear: () => {
|
|
404
|
+
ctx.dispatch({ type: "CLEAR" });
|
|
405
|
+
fireChange();
|
|
406
|
+
},
|
|
407
|
+
snapshot,
|
|
408
|
+
rectsForPage: (pon) => ctx.getState().rects[pon] ?? EMPTY,
|
|
409
|
+
hasSelection: () => ctx.getState().selection != null,
|
|
410
|
+
selectedPages: () => {
|
|
411
|
+
const { rects } = ctx.getState();
|
|
412
|
+
return Object.keys(rects).map(Number).filter((pon) => (rects[pon]?.length ?? 0) > 0);
|
|
413
|
+
},
|
|
414
|
+
onChange: (cb) => {
|
|
415
|
+
changeCbs.add(cb);
|
|
416
|
+
return () => changeCbs.delete(cb);
|
|
417
|
+
},
|
|
418
|
+
onCommit: (cb) => {
|
|
419
|
+
commitCbs.add(cb);
|
|
420
|
+
return () => commitCbs.delete(cb);
|
|
421
|
+
},
|
|
422
|
+
setHighlightVisible: (visible) => ctx.dispatch({
|
|
423
|
+
type: "SET_HIGHLIGHT_HIDDEN",
|
|
424
|
+
hidden: !visible
|
|
425
|
+
}),
|
|
426
|
+
highlightVisible: () => !ctx.getState().highlightHidden
|
|
427
|
+
};
|
|
372
428
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/handler.ts
|
|
431
|
+
const CURSOR_TOKEN = "selection-text";
|
|
432
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
433
|
+
/**
|
|
434
|
+
* The text-selection pointer handler. It is live only under tools that enable the
|
|
435
|
+
* `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
|
|
436
|
+
* switching to pan disables text selection with zero special-casing here).
|
|
437
|
+
*
|
|
438
|
+
* - single click → deselect; records an anchor but selects NOTHING until
|
|
439
|
+
* the pointer moves past the drag threshold (Chrome/Acrobat feel)
|
|
440
|
+
* - single-click + drag → caret selection from the anchor (extends across pages)
|
|
441
|
+
* - double-click → select the word
|
|
442
|
+
* - triple-click → select the visual line
|
|
443
|
+
* - down off-text → deselect, and DON'T capture (so it can't block)
|
|
444
|
+
* - hover → I-beam only when over text, else the pointer cursor
|
|
445
|
+
*/
|
|
446
|
+
function createTextSelectHandler(selection, interaction) {
|
|
447
|
+
let anchor = null;
|
|
448
|
+
let dragging = false;
|
|
449
|
+
return {
|
|
450
|
+
id: "text-select",
|
|
451
|
+
priority: 60,
|
|
452
|
+
enabledFor: (tool) => tool.enables.has("text-select"),
|
|
453
|
+
onDown: (s) => {
|
|
454
|
+
anchor = null;
|
|
455
|
+
dragging = false;
|
|
456
|
+
if (!s.page) return false;
|
|
457
|
+
const { pon, point } = s.page;
|
|
458
|
+
const clicks = s.clickCount ?? 1;
|
|
459
|
+
if (clicks >= 3) {
|
|
460
|
+
selection.selectLine(pon, point);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (clicks === 2) {
|
|
464
|
+
selection.selectWord(pon, point);
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
selection.clear();
|
|
468
|
+
if (!selection.isOverText(pon, point)) return false;
|
|
469
|
+
anchor = {
|
|
470
|
+
pon,
|
|
471
|
+
point,
|
|
472
|
+
vx: s.viewport.x,
|
|
473
|
+
vy: s.viewport.y
|
|
474
|
+
};
|
|
475
|
+
return true;
|
|
476
|
+
},
|
|
477
|
+
onMove: (s) => {
|
|
478
|
+
if (!s.page || !anchor) return;
|
|
479
|
+
if (!dragging) {
|
|
480
|
+
if (Math.hypot(s.viewport.x - anchor.vx, s.viewport.y - anchor.vy) < DRAG_THRESHOLD_PX) return;
|
|
481
|
+
dragging = true;
|
|
482
|
+
selection.beginAt(anchor.pon, anchor.point);
|
|
483
|
+
}
|
|
484
|
+
selection.extendTo(s.page.pon, s.page.point);
|
|
485
|
+
},
|
|
486
|
+
onUp: () => {
|
|
487
|
+
anchor = null;
|
|
488
|
+
dragging = false;
|
|
489
|
+
selection.end();
|
|
490
|
+
},
|
|
491
|
+
onHover: (s) => {
|
|
492
|
+
if (!s.page) {
|
|
493
|
+
interaction.setCursor(CURSOR_TOKEN, null);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
selection.ensurePage(s.page.pon);
|
|
497
|
+
const overText = selection.isOverText(s.page.pon, s.page.point);
|
|
498
|
+
interaction.setCursor(CURSOR_TOKEN, overText ? "text" : null, 10);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
381
501
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
const VERTICAL_OVERLAP_THRESHOLD = 0.8;
|
|
391
|
-
const rect1 = textRun1.rect;
|
|
392
|
-
const rect2 = textRun2.rect;
|
|
393
|
-
if (getVerticalOverlap(rect1, rect2) < VERTICAL_OVERLAP_THRESHOLD) {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
const HORIZONTAL_WIDTH_FACTOR = 1;
|
|
397
|
-
const averageWidth1 = HORIZONTAL_WIDTH_FACTOR * rect1.size.width / textRun1.charCount;
|
|
398
|
-
const averageWidth2 = HORIZONTAL_WIDTH_FACTOR * rect2.size.width / textRun2.charCount;
|
|
399
|
-
const rect1Left = rect1.origin.x - averageWidth1;
|
|
400
|
-
const rect1Right = rect1.origin.x + rect1.size.width + averageWidth1;
|
|
401
|
-
const rect2Left = rect2.origin.x - averageWidth2;
|
|
402
|
-
const rect2Right = rect2.origin.x + rect2.size.width + averageWidth2;
|
|
403
|
-
return rect1Left < rect2Right && rect1Right > rect2Left;
|
|
404
|
-
}
|
|
405
|
-
function mergeAdjacentRects(textRuns) {
|
|
406
|
-
const results = [];
|
|
407
|
-
let previousTextRun = null;
|
|
408
|
-
let currentRect = null;
|
|
409
|
-
for (const textRun of textRuns) {
|
|
410
|
-
if (previousTextRun && currentRect) {
|
|
411
|
-
if (shouldMergeHorizontalRects(previousTextRun, textRun)) {
|
|
412
|
-
currentRect = rectUnion(currentRect, textRun.rect);
|
|
413
|
-
} else {
|
|
414
|
-
results.push(currentRect);
|
|
415
|
-
currentRect = textRun.rect;
|
|
416
|
-
}
|
|
417
|
-
} else {
|
|
418
|
-
currentRect = textRun.rect;
|
|
419
|
-
}
|
|
420
|
-
previousTextRun = textRun;
|
|
421
|
-
}
|
|
422
|
-
if (currentRect && !rectIsEmpty(currentRect)) {
|
|
423
|
-
results.push(currentRect);
|
|
424
|
-
}
|
|
425
|
-
return results;
|
|
426
|
-
}
|
|
427
|
-
const VERTICAL_OVERLAP_THRESHOLD_LINE = 0.5;
|
|
428
|
-
function resolveCharIndex(geo, charIndex) {
|
|
429
|
-
for (let r = 0; r < geo.runs.length; r++) {
|
|
430
|
-
const run = geo.runs[r];
|
|
431
|
-
const localIdx = charIndex - run.charStart;
|
|
432
|
-
if (localIdx >= 0 && localIdx < run.glyphs.length) {
|
|
433
|
-
return { runIdx: r, localIdx };
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
function isGlyphWordBoundary(flags) {
|
|
439
|
-
return flags === 1 || flags === 2;
|
|
440
|
-
}
|
|
441
|
-
function expandToWordBoundary(geo, charIndex) {
|
|
442
|
-
const resolved = resolveCharIndex(geo, charIndex);
|
|
443
|
-
if (!resolved) return null;
|
|
444
|
-
const totalChars = getTotalCharCount(geo);
|
|
445
|
-
if (totalChars === 0) return null;
|
|
446
|
-
let from = charIndex;
|
|
447
|
-
while (from > 0) {
|
|
448
|
-
const prev = resolveCharIndex(geo, from - 1);
|
|
449
|
-
if (!prev) break;
|
|
450
|
-
if (isGlyphWordBoundary(geo.runs[prev.runIdx].glyphs[prev.localIdx].flags)) break;
|
|
451
|
-
from--;
|
|
452
|
-
}
|
|
453
|
-
let to = charIndex;
|
|
454
|
-
while (to < totalChars - 1) {
|
|
455
|
-
const next = resolveCharIndex(geo, to + 1);
|
|
456
|
-
if (!next) break;
|
|
457
|
-
if (isGlyphWordBoundary(geo.runs[next.runIdx].glyphs[next.localIdx].flags)) break;
|
|
458
|
-
to++;
|
|
459
|
-
}
|
|
460
|
-
return { from, to };
|
|
461
|
-
}
|
|
462
|
-
function expandToLineBoundary(geo, charIndex) {
|
|
463
|
-
const resolved = resolveCharIndex(geo, charIndex);
|
|
464
|
-
if (!resolved) return null;
|
|
465
|
-
const anchorRun = geo.runs[resolved.runIdx];
|
|
466
|
-
const anchorTop = anchorRun.rect.y;
|
|
467
|
-
const anchorBottom = anchorRun.rect.y + anchorRun.rect.height;
|
|
468
|
-
let from = anchorRun.charStart;
|
|
469
|
-
let to = anchorRun.charStart + anchorRun.glyphs.length - 1;
|
|
470
|
-
for (let r = resolved.runIdx - 1; r >= 0; r--) {
|
|
471
|
-
const run = geo.runs[r];
|
|
472
|
-
if (isZeroSizeRun(run)) continue;
|
|
473
|
-
if (!runsOverlapVertically(run.rect.y, run.rect.y + run.rect.height, anchorTop, anchorBottom)) {
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
from = run.charStart;
|
|
477
|
-
}
|
|
478
|
-
for (let r = resolved.runIdx + 1; r < geo.runs.length; r++) {
|
|
479
|
-
const run = geo.runs[r];
|
|
480
|
-
if (isZeroSizeRun(run)) continue;
|
|
481
|
-
if (!runsOverlapVertically(run.rect.y, run.rect.y + run.rect.height, anchorTop, anchorBottom)) {
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
to = run.charStart + run.glyphs.length - 1;
|
|
485
|
-
}
|
|
486
|
-
return { from, to };
|
|
487
|
-
}
|
|
488
|
-
function isZeroSizeRun(run) {
|
|
489
|
-
return run.rect.width === 0 && run.rect.height === 0;
|
|
490
|
-
}
|
|
491
|
-
function runsOverlapVertically(top1, bottom1, top2, bottom2) {
|
|
492
|
-
const unionHeight = Math.max(bottom1, bottom2) - Math.min(top1, top2);
|
|
493
|
-
const intersectHeight = Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2));
|
|
494
|
-
if (unionHeight === 0) return false;
|
|
495
|
-
return intersectHeight / unionHeight >= VERTICAL_OVERLAP_THRESHOLD_LINE;
|
|
496
|
-
}
|
|
497
|
-
function getTotalCharCount(geo) {
|
|
498
|
-
if (geo.runs.length === 0) return 0;
|
|
499
|
-
const lastRun = geo.runs[geo.runs.length - 1];
|
|
500
|
-
return lastRun.charStart + lastRun.glyphs.length;
|
|
501
|
-
}
|
|
502
|
-
const TRIPLE_CLICK_INTERVAL_MS = 500;
|
|
503
|
-
function createTextSelectionHandler(opts) {
|
|
504
|
-
const minDrag = opts.minDragDistance ?? 3;
|
|
505
|
-
const tolFactor = opts.toleranceFactor ?? 1.5;
|
|
506
|
-
let anchorGlyph = null;
|
|
507
|
-
let anchorPos = null;
|
|
508
|
-
let dragStarted = false;
|
|
509
|
-
let lastDblClickTime = 0;
|
|
510
|
-
function reset() {
|
|
511
|
-
var _a;
|
|
512
|
-
anchorGlyph = null;
|
|
513
|
-
anchorPos = null;
|
|
514
|
-
dragStarted = false;
|
|
515
|
-
(_a = opts.setHasTextAnchor) == null ? void 0 : _a.call(opts, false);
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
onPointerDown: (point, evt, modeId) => {
|
|
519
|
-
var _a, _b;
|
|
520
|
-
if (evt.target === evt.currentTarget) {
|
|
521
|
-
(_a = opts.onEmptySpaceClick) == null ? void 0 : _a.call(opts, modeId);
|
|
522
|
-
}
|
|
523
|
-
if (!opts.isEnabled(modeId)) return;
|
|
524
|
-
const now = Date.now();
|
|
525
|
-
if (lastDblClickTime === 0 || now - lastDblClickTime >= TRIPLE_CLICK_INTERVAL_MS) {
|
|
526
|
-
opts.onClear(modeId);
|
|
527
|
-
}
|
|
528
|
-
const geo = opts.getGeometry();
|
|
529
|
-
if (!geo) return;
|
|
530
|
-
const g = glyphAt(geo, point, tolFactor);
|
|
531
|
-
if (g !== -1) {
|
|
532
|
-
anchorGlyph = g;
|
|
533
|
-
anchorPos = point;
|
|
534
|
-
dragStarted = false;
|
|
535
|
-
(_b = opts.setHasTextAnchor) == null ? void 0 : _b.call(opts, true);
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
onPointerMove: (point, _evt, modeId) => {
|
|
539
|
-
if (!opts.isEnabled(modeId)) return;
|
|
540
|
-
const geo = opts.getGeometry();
|
|
541
|
-
if (!geo) return;
|
|
542
|
-
const g = glyphAt(geo, point, tolFactor);
|
|
543
|
-
opts.setCursor(g !== -1 ? "text" : null);
|
|
544
|
-
if (anchorGlyph !== null && anchorPos && !dragStarted) {
|
|
545
|
-
const dx = point.x - anchorPos.x;
|
|
546
|
-
const dy = point.y - anchorPos.y;
|
|
547
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
548
|
-
if (dist >= minDrag) {
|
|
549
|
-
dragStarted = true;
|
|
550
|
-
opts.onBegin(anchorGlyph, modeId);
|
|
551
|
-
if (g !== -1) {
|
|
552
|
-
opts.onUpdate(g, modeId);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (opts.isSelecting() && g !== -1) {
|
|
558
|
-
opts.onUpdate(g, modeId);
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
onPointerUp: (_point, _evt, modeId) => {
|
|
562
|
-
if (!opts.isEnabled(modeId)) {
|
|
563
|
-
reset();
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
if (dragStarted) {
|
|
567
|
-
opts.onEnd(modeId);
|
|
568
|
-
}
|
|
569
|
-
reset();
|
|
570
|
-
},
|
|
571
|
-
onDoubleClick: (point, _evt, modeId) => {
|
|
572
|
-
var _a;
|
|
573
|
-
if (!opts.isEnabled(modeId)) return;
|
|
574
|
-
const geo = opts.getGeometry();
|
|
575
|
-
if (!geo) return;
|
|
576
|
-
const g = glyphAt(geo, point, tolFactor);
|
|
577
|
-
if (g === -1) return;
|
|
578
|
-
lastDblClickTime = Date.now();
|
|
579
|
-
(_a = opts.onWordSelect) == null ? void 0 : _a.call(opts, g, modeId);
|
|
580
|
-
},
|
|
581
|
-
onClick: (point, _evt, modeId) => {
|
|
582
|
-
var _a;
|
|
583
|
-
if (!opts.isEnabled(modeId)) return;
|
|
584
|
-
if (lastDblClickTime === 0) return;
|
|
585
|
-
const now = Date.now();
|
|
586
|
-
if (now - lastDblClickTime > TRIPLE_CLICK_INTERVAL_MS) {
|
|
587
|
-
lastDblClickTime = 0;
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
lastDblClickTime = 0;
|
|
591
|
-
const geo = opts.getGeometry();
|
|
592
|
-
if (!geo) return;
|
|
593
|
-
const g = glyphAt(geo, point, tolFactor);
|
|
594
|
-
if (g === -1) return;
|
|
595
|
-
(_a = opts.onLineSelect) == null ? void 0 : _a.call(opts, g, modeId);
|
|
596
|
-
},
|
|
597
|
-
onHandlerActiveEnd: (modeId) => {
|
|
598
|
-
reset();
|
|
599
|
-
if (!opts.isEnabled(modeId)) return;
|
|
600
|
-
opts.onClear(modeId);
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
function createMarqueeSelectionHandler(opts) {
|
|
605
|
-
const { pageSize, scale, minDragPx = 5 } = opts;
|
|
606
|
-
let start = null;
|
|
607
|
-
let last = null;
|
|
608
|
-
return {
|
|
609
|
-
onPointerDown: (pos, evt, modeId) => {
|
|
610
|
-
var _a, _b;
|
|
611
|
-
if (!opts.isEnabled(modeId)) return;
|
|
612
|
-
if ((_a = opts.isTextSelecting) == null ? void 0 : _a.call(opts)) return;
|
|
613
|
-
start = pos;
|
|
614
|
-
last = { origin: { x: pos.x, y: pos.y }, size: { width: 0, height: 0 } };
|
|
615
|
-
opts.onBegin(pos, modeId);
|
|
616
|
-
(_b = evt.setPointerCapture) == null ? void 0 : _b.call(evt);
|
|
617
|
-
},
|
|
618
|
-
onPointerMove: (pos, _evt, modeId) => {
|
|
619
|
-
if (!start || !opts.isEnabled(modeId)) return;
|
|
620
|
-
const x = clamp(pos.x, 0, pageSize.width);
|
|
621
|
-
const y = clamp(pos.y, 0, pageSize.height);
|
|
622
|
-
last = {
|
|
623
|
-
origin: { x: Math.min(start.x, x), y: Math.min(start.y, y) },
|
|
624
|
-
size: { width: Math.abs(x - start.x), height: Math.abs(y - start.y) }
|
|
625
|
-
};
|
|
626
|
-
opts.onChange(last, modeId);
|
|
627
|
-
},
|
|
628
|
-
onPointerUp: (_pos, evt, modeId) => {
|
|
629
|
-
var _a;
|
|
630
|
-
if (!opts.isEnabled(modeId)) return;
|
|
631
|
-
if (last && start) {
|
|
632
|
-
const dragPx = Math.max(last.size.width, last.size.height) * scale;
|
|
633
|
-
if (dragPx > minDragPx) {
|
|
634
|
-
opts.onEnd(last, modeId);
|
|
635
|
-
} else {
|
|
636
|
-
opts.onCancel(modeId);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
start = null;
|
|
640
|
-
last = null;
|
|
641
|
-
(_a = evt.releasePointerCapture) == null ? void 0 : _a.call(evt);
|
|
642
|
-
},
|
|
643
|
-
onPointerCancel: (_pos, evt, modeId) => {
|
|
644
|
-
var _a;
|
|
645
|
-
if (!opts.isEnabled(modeId)) return;
|
|
646
|
-
start = null;
|
|
647
|
-
last = null;
|
|
648
|
-
opts.onCancel(modeId);
|
|
649
|
-
(_a = evt.releasePointerCapture) == null ? void 0 : _a.call(evt);
|
|
650
|
-
}
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
654
|
-
constructor(id, registry, config) {
|
|
655
|
-
var _a, _b, _c;
|
|
656
|
-
super(id, registry);
|
|
657
|
-
this.enabledModesPerDoc = /* @__PURE__ */ new Map();
|
|
658
|
-
this.selecting = /* @__PURE__ */ new Map();
|
|
659
|
-
this.anchor = /* @__PURE__ */ new Map();
|
|
660
|
-
this.hasTextAnchor = /* @__PURE__ */ new Map();
|
|
661
|
-
this.marqueePage = /* @__PURE__ */ new Map();
|
|
662
|
-
this.pageCallbacks = /* @__PURE__ */ new Map();
|
|
663
|
-
this.geoAccessOrder = /* @__PURE__ */ new Map();
|
|
664
|
-
this.menuPlacement$ = createScopedEmitter((documentId, placement) => ({ documentId, placement }));
|
|
665
|
-
this.selChange$ = createScopedEmitter((documentId, selection) => ({
|
|
666
|
-
documentId,
|
|
667
|
-
selection,
|
|
668
|
-
modeId: this.interactionManagerCapability.forDocument(documentId).getActiveMode()
|
|
669
|
-
}));
|
|
670
|
-
this.textRetrieved$ = createScopedEmitter(
|
|
671
|
-
(documentId, text) => ({ documentId, text })
|
|
672
|
-
);
|
|
673
|
-
this.copyToClipboard$ = createScopedEmitter(
|
|
674
|
-
(documentId, text) => ({ documentId, text }),
|
|
675
|
-
{ cache: false }
|
|
676
|
-
);
|
|
677
|
-
this.beginSelection$ = createScopedEmitter(
|
|
678
|
-
(documentId, data) => ({
|
|
679
|
-
documentId,
|
|
680
|
-
page: data.page,
|
|
681
|
-
index: data.index,
|
|
682
|
-
modeId: data.modeId
|
|
683
|
-
}),
|
|
684
|
-
{ cache: false }
|
|
685
|
-
);
|
|
686
|
-
this.endSelection$ = createScopedEmitter((documentId, data) => ({ documentId, modeId: data.modeId }), { cache: false });
|
|
687
|
-
this.marqueeChange$ = createScopedEmitter(
|
|
688
|
-
(documentId, data) => ({
|
|
689
|
-
documentId,
|
|
690
|
-
pageIndex: data.pageIndex,
|
|
691
|
-
rect: data.rect,
|
|
692
|
-
modeId: data.modeId
|
|
693
|
-
}),
|
|
694
|
-
{ cache: false }
|
|
695
|
-
);
|
|
696
|
-
this.marqueeEnd$ = createScopedEmitter(
|
|
697
|
-
(documentId, data) => ({
|
|
698
|
-
documentId,
|
|
699
|
-
pageIndex: data.pageIndex,
|
|
700
|
-
rect: data.rect,
|
|
701
|
-
modeId: data.modeId
|
|
702
|
-
}),
|
|
703
|
-
{ cache: false }
|
|
704
|
-
);
|
|
705
|
-
this.emptySpaceClick$ = createScopedEmitter(
|
|
706
|
-
(documentId, data) => ({
|
|
707
|
-
documentId,
|
|
708
|
-
pageIndex: data.pageIndex,
|
|
709
|
-
modeId: data.modeId
|
|
710
|
-
}),
|
|
711
|
-
{ cache: false }
|
|
712
|
-
);
|
|
713
|
-
this.viewportCapability = null;
|
|
714
|
-
this.scrollCapability = null;
|
|
715
|
-
this.config = config;
|
|
716
|
-
this.menuHeight = config.menuHeight ?? 40;
|
|
717
|
-
const imPlugin = registry.getPlugin("interaction-manager");
|
|
718
|
-
if (!imPlugin) {
|
|
719
|
-
throw new Error("SelectionPlugin: InteractionManagerPlugin is required.");
|
|
720
|
-
}
|
|
721
|
-
this.interactionManagerCapability = imPlugin.provides();
|
|
722
|
-
this.viewportCapability = ((_a = registry.getPlugin("viewport")) == null ? void 0 : _a.provides()) ?? null;
|
|
723
|
-
this.scrollCapability = ((_b = registry.getPlugin("scroll")) == null ? void 0 : _b.provides()) ?? null;
|
|
724
|
-
this.coreStore.onAction(REFRESH_PAGES, (action) => {
|
|
725
|
-
const { documentId, pageIndexes } = action.payload;
|
|
726
|
-
const tasks = pageIndexes.map(
|
|
727
|
-
(pageIndex) => this.getNewPageGeometryAndCache(documentId, pageIndex)
|
|
728
|
-
);
|
|
729
|
-
Task.all(tasks).wait(() => {
|
|
730
|
-
pageIndexes.forEach((pageIndex) => {
|
|
731
|
-
this.notifyPage(documentId, pageIndex);
|
|
732
|
-
});
|
|
733
|
-
}, ignore);
|
|
734
|
-
});
|
|
735
|
-
(_c = this.viewportCapability) == null ? void 0 : _c.onViewportChange(
|
|
736
|
-
(event) => {
|
|
737
|
-
this.recalculateMenuPlacement(event.documentId);
|
|
738
|
-
},
|
|
739
|
-
{ mode: "throttle", wait: 100 }
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
/* ── life-cycle ────────────────────────────────────────── */
|
|
743
|
-
onDocumentLoadingStarted(documentId) {
|
|
744
|
-
var _a;
|
|
745
|
-
this.dispatch(initSelectionState(documentId, initialSelectionDocumentState));
|
|
746
|
-
const marqueeEnabled = ((_a = this.config.marquee) == null ? void 0 : _a.enabled) !== false;
|
|
747
|
-
this.enabledModesPerDoc.set(
|
|
748
|
-
documentId,
|
|
749
|
-
/* @__PURE__ */ new Map([
|
|
750
|
-
[
|
|
751
|
-
"pointerMode",
|
|
752
|
-
{
|
|
753
|
-
enableSelection: true,
|
|
754
|
-
showSelectionRects: true,
|
|
755
|
-
enableMarquee: marqueeEnabled,
|
|
756
|
-
showMarqueeRects: true
|
|
757
|
-
}
|
|
758
|
-
]
|
|
759
|
-
])
|
|
760
|
-
);
|
|
761
|
-
this.pageCallbacks.set(documentId, /* @__PURE__ */ new Map());
|
|
762
|
-
this.geoAccessOrder.set(documentId, []);
|
|
763
|
-
this.selecting.set(documentId, false);
|
|
764
|
-
this.anchor.set(documentId, void 0);
|
|
765
|
-
this.hasTextAnchor.set(documentId, false);
|
|
766
|
-
}
|
|
767
|
-
onDocumentClosed(documentId) {
|
|
768
|
-
this.dispatch(cleanupSelectionState(documentId));
|
|
769
|
-
this.enabledModesPerDoc.delete(documentId);
|
|
770
|
-
this.pageCallbacks.delete(documentId);
|
|
771
|
-
this.geoAccessOrder.delete(documentId);
|
|
772
|
-
this.selecting.delete(documentId);
|
|
773
|
-
this.hasTextAnchor.delete(documentId);
|
|
774
|
-
this.anchor.delete(documentId);
|
|
775
|
-
this.marqueePage.delete(documentId);
|
|
776
|
-
this.selChange$.clearScope(documentId);
|
|
777
|
-
this.textRetrieved$.clearScope(documentId);
|
|
778
|
-
this.copyToClipboard$.clearScope(documentId);
|
|
779
|
-
this.beginSelection$.clearScope(documentId);
|
|
780
|
-
this.endSelection$.clearScope(documentId);
|
|
781
|
-
this.menuPlacement$.clearScope(documentId);
|
|
782
|
-
this.marqueeChange$.clearScope(documentId);
|
|
783
|
-
this.marqueeEnd$.clearScope(documentId);
|
|
784
|
-
this.emptySpaceClick$.clearScope(documentId);
|
|
785
|
-
}
|
|
786
|
-
async initialize() {
|
|
787
|
-
}
|
|
788
|
-
async destroy() {
|
|
789
|
-
this.selChange$.clear();
|
|
790
|
-
this.textRetrieved$.clear();
|
|
791
|
-
this.copyToClipboard$.clear();
|
|
792
|
-
this.beginSelection$.clear();
|
|
793
|
-
this.endSelection$.clear();
|
|
794
|
-
this.menuPlacement$.clear();
|
|
795
|
-
this.marqueeChange$.clear();
|
|
796
|
-
this.marqueeEnd$.clear();
|
|
797
|
-
this.emptySpaceClick$.clear();
|
|
798
|
-
super.destroy();
|
|
799
|
-
}
|
|
800
|
-
/* ── capability exposed to UI / other plugins ─────────── */
|
|
801
|
-
buildCapability() {
|
|
802
|
-
const getDocId = (documentId) => documentId ?? this.getActiveDocumentId();
|
|
803
|
-
return {
|
|
804
|
-
// Active document operations
|
|
805
|
-
getFormattedSelection: (docId) => getFormattedSelection(this.getDocumentState(getDocId(docId))),
|
|
806
|
-
getFormattedSelectionForPage: (p, docId) => getFormattedSelectionForPage(this.getDocumentState(getDocId(docId)), p),
|
|
807
|
-
getHighlightRectsForPage: (p, docId) => selectRectsForPage(this.getDocumentState(getDocId(docId)), p),
|
|
808
|
-
getHighlightRects: (docId) => this.getDocumentState(getDocId(docId)).rects,
|
|
809
|
-
getBoundingRectForPage: (p, docId) => selectBoundingRectForPage(this.getDocumentState(getDocId(docId)), p),
|
|
810
|
-
getBoundingRects: (docId) => selectBoundingRectsForAllPages(this.getDocumentState(getDocId(docId))),
|
|
811
|
-
getSelectedText: (docId) => this.getSelectedText(getDocId(docId)),
|
|
812
|
-
setSelection: (range, docId) => this.applySelection(getDocId(docId), range),
|
|
813
|
-
clear: (docId) => this.clearSelection(getDocId(docId)),
|
|
814
|
-
copyToClipboard: (docId) => this.copyToClipboard(getDocId(docId)),
|
|
815
|
-
getState: (docId) => this.getDocumentState(getDocId(docId)),
|
|
816
|
-
enableForMode: (modeId, options, docId) => {
|
|
817
|
-
var _a;
|
|
818
|
-
return (_a = this.enabledModesPerDoc.get(getDocId(docId))) == null ? void 0 : _a.set(modeId, { ...options });
|
|
819
|
-
},
|
|
820
|
-
isEnabledForMode: (modeId, docId) => {
|
|
821
|
-
var _a;
|
|
822
|
-
return ((_a = this.enabledModesPerDoc.get(getDocId(docId))) == null ? void 0 : _a.has(modeId)) ?? false;
|
|
823
|
-
},
|
|
824
|
-
setMarqueeEnabled: (enabled, docId) => this.setMarqueeEnabled(getDocId(docId), enabled),
|
|
825
|
-
isMarqueeEnabled: (docId) => this.isMarqueeEnabled(getDocId(docId)),
|
|
826
|
-
// Document-scoped operations
|
|
827
|
-
forDocument: this.createSelectionScope.bind(this),
|
|
828
|
-
// Global events
|
|
829
|
-
onCopyToClipboard: this.copyToClipboard$.onGlobal,
|
|
830
|
-
onSelectionChange: this.selChange$.onGlobal,
|
|
831
|
-
onTextRetrieved: this.textRetrieved$.onGlobal,
|
|
832
|
-
onBeginSelection: this.beginSelection$.onGlobal,
|
|
833
|
-
onEndSelection: this.endSelection$.onGlobal,
|
|
834
|
-
// Marquee selection events
|
|
835
|
-
onMarqueeChange: this.marqueeChange$.onGlobal,
|
|
836
|
-
onMarqueeEnd: this.marqueeEnd$.onGlobal,
|
|
837
|
-
// Empty space click event
|
|
838
|
-
onEmptySpaceClick: this.emptySpaceClick$.onGlobal
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
createSelectionScope(documentId) {
|
|
842
|
-
return {
|
|
843
|
-
getFormattedSelection: () => getFormattedSelection(this.getDocumentState(documentId)),
|
|
844
|
-
getFormattedSelectionForPage: (p) => getFormattedSelectionForPage(this.getDocumentState(documentId), p),
|
|
845
|
-
getHighlightRectsForPage: (p) => selectRectsForPage(this.getDocumentState(documentId), p),
|
|
846
|
-
getHighlightRects: () => this.getDocumentState(documentId).rects,
|
|
847
|
-
getBoundingRectForPage: (p) => selectBoundingRectForPage(this.getDocumentState(documentId), p),
|
|
848
|
-
getBoundingRects: () => selectBoundingRectsForAllPages(this.getDocumentState(documentId)),
|
|
849
|
-
getSelectedText: () => this.getSelectedText(documentId),
|
|
850
|
-
setSelection: (range) => this.applySelection(documentId, range),
|
|
851
|
-
clear: () => this.clearSelection(documentId),
|
|
852
|
-
copyToClipboard: () => this.copyToClipboard(documentId),
|
|
853
|
-
getState: () => this.getDocumentState(documentId),
|
|
854
|
-
setMarqueeEnabled: (enabled) => this.setMarqueeEnabled(documentId, enabled),
|
|
855
|
-
isMarqueeEnabled: () => this.isMarqueeEnabled(documentId),
|
|
856
|
-
onSelectionChange: this.selChange$.forScope(documentId),
|
|
857
|
-
onTextRetrieved: this.textRetrieved$.forScope(documentId),
|
|
858
|
-
onCopyToClipboard: this.copyToClipboard$.forScope(documentId),
|
|
859
|
-
onBeginSelection: this.beginSelection$.forScope(documentId),
|
|
860
|
-
onEndSelection: this.endSelection$.forScope(documentId),
|
|
861
|
-
onMarqueeChange: this.marqueeChange$.forScope(documentId),
|
|
862
|
-
onMarqueeEnd: this.marqueeEnd$.forScope(documentId),
|
|
863
|
-
onEmptySpaceClick: this.emptySpaceClick$.forScope(documentId)
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
getDocumentState(documentId) {
|
|
867
|
-
const state = this.state.documents[documentId];
|
|
868
|
-
if (!state) {
|
|
869
|
-
throw new Error(`Selection state not found for document: ${documentId}`);
|
|
870
|
-
}
|
|
871
|
-
return state;
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* Subscribe to menu placement changes for a specific document
|
|
875
|
-
* @param documentId - The document ID to subscribe to
|
|
876
|
-
* @param listener - Callback function that receives placement updates
|
|
877
|
-
* @returns Unsubscribe function
|
|
878
|
-
*/
|
|
879
|
-
onMenuPlacement(documentId, listener) {
|
|
880
|
-
return this.menuPlacement$.forScope(documentId)(listener);
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Register text selection on a page. Uses `registerAlways` so any plugin
|
|
884
|
-
* can enable text selection for their mode via `enableForMode()`.
|
|
885
|
-
*/
|
|
886
|
-
registerSelectionOnPage(opts) {
|
|
887
|
-
var _a;
|
|
888
|
-
const { documentId, pageIndex, onRectsChange } = opts;
|
|
889
|
-
const docState = this.state.documents[documentId];
|
|
890
|
-
if (!docState) {
|
|
891
|
-
this.logger.warn(
|
|
892
|
-
"SelectionPlugin",
|
|
893
|
-
"RegisterFailed",
|
|
894
|
-
`Cannot register selection on page ${pageIndex} for document ${documentId}: document state not initialized.`
|
|
895
|
-
);
|
|
896
|
-
return () => {
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
(_a = this.pageCallbacks.get(documentId)) == null ? void 0 : _a.set(pageIndex, onRectsChange);
|
|
900
|
-
const geoTask = this.getOrLoadGeometry(documentId, pageIndex);
|
|
901
|
-
const interactionScope = this.interactionManagerCapability.forDocument(documentId);
|
|
902
|
-
const enabledModes = this.enabledModesPerDoc.get(documentId);
|
|
903
|
-
onRectsChange({
|
|
904
|
-
rects: selectRectsForPage(docState, pageIndex),
|
|
905
|
-
boundingRect: selectBoundingRectForPage(docState, pageIndex)
|
|
906
|
-
});
|
|
907
|
-
geoTask.wait((geo) => {
|
|
908
|
-
const currentState = this.getDocumentState(documentId);
|
|
909
|
-
const sel = currentState.selection;
|
|
910
|
-
if (!sel || pageIndex < sel.start.page || pageIndex > sel.end.page) return;
|
|
911
|
-
const sb = sliceBounds(sel, geo, pageIndex);
|
|
912
|
-
if (!sb) return;
|
|
913
|
-
const pageRects = rectsWithinSlice(geo, sb.from, sb.to);
|
|
914
|
-
this.dispatch(setRects(documentId, { ...currentState.rects, [pageIndex]: pageRects }));
|
|
915
|
-
this.dispatch(
|
|
916
|
-
setSlices(documentId, {
|
|
917
|
-
...currentState.slices,
|
|
918
|
-
[pageIndex]: { start: sb.from, count: sb.to - sb.from + 1 }
|
|
919
|
-
})
|
|
920
|
-
);
|
|
921
|
-
this.notifyPage(documentId, pageIndex);
|
|
922
|
-
}, ignore);
|
|
923
|
-
const textHandler = createTextSelectionHandler({
|
|
924
|
-
getGeometry: () => this.getDocumentState(documentId).geometry[pageIndex],
|
|
925
|
-
isEnabled: (modeId) => {
|
|
926
|
-
const config = enabledModes == null ? void 0 : enabledModes.get(modeId);
|
|
927
|
-
if (!config) return false;
|
|
928
|
-
return config.enableSelection !== false;
|
|
929
|
-
},
|
|
930
|
-
onBegin: (g, modeId) => this.beginSelection(documentId, pageIndex, g, modeId),
|
|
931
|
-
onUpdate: (g, modeId) => this.updateSelection(documentId, pageIndex, g, modeId),
|
|
932
|
-
onEnd: (modeId) => this.endSelection(documentId, modeId),
|
|
933
|
-
onClear: (modeId) => this.clearSelection(documentId, modeId),
|
|
934
|
-
isSelecting: () => this.selecting.get(documentId) ?? false,
|
|
935
|
-
setCursor: (cursor) => cursor ? interactionScope.setCursor("selection-text", cursor, 10) : interactionScope.removeCursor("selection-text"),
|
|
936
|
-
onEmptySpaceClick: (modeId) => this.emptySpaceClick$.emit(documentId, { pageIndex, modeId }),
|
|
937
|
-
onWordSelect: (g, modeId) => this.selectWord(documentId, pageIndex, g, modeId),
|
|
938
|
-
onLineSelect: (g, modeId) => this.selectLine(documentId, pageIndex, g, modeId),
|
|
939
|
-
setHasTextAnchor: (active) => this.hasTextAnchor.set(documentId, active),
|
|
940
|
-
minDragDistance: this.config.minSelectionDragDistance,
|
|
941
|
-
toleranceFactor: this.config.toleranceFactor
|
|
942
|
-
});
|
|
943
|
-
const unregisterHandlers = this.interactionManagerCapability.registerAlways({
|
|
944
|
-
scope: { type: "page", documentId, pageIndex },
|
|
945
|
-
handlers: textHandler
|
|
946
|
-
});
|
|
947
|
-
return () => {
|
|
948
|
-
var _a2;
|
|
949
|
-
unregisterHandlers();
|
|
950
|
-
(_a2 = this.pageCallbacks.get(documentId)) == null ? void 0 : _a2.delete(pageIndex);
|
|
951
|
-
geoTask.abort({ code: PdfErrorCode.Cancelled, message: "Cleanup" });
|
|
952
|
-
};
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Register marquee selection on a page. Uses `registerAlways` so any plugin
|
|
956
|
-
* can enable marquee selection for their mode via `enableForMode({ enableMarquee: true })`.
|
|
957
|
-
*/
|
|
958
|
-
registerMarqueeOnPage(opts) {
|
|
959
|
-
var _a;
|
|
960
|
-
const { documentId, pageIndex, scale, onRectChange } = opts;
|
|
961
|
-
const docState = this.state.documents[documentId];
|
|
962
|
-
if (!docState) {
|
|
963
|
-
this.logger.warn(
|
|
964
|
-
"SelectionPlugin",
|
|
965
|
-
"RegisterMarqueeFailed",
|
|
966
|
-
`Cannot register marquee on page ${pageIndex} for document ${documentId}: document state not initialized.`
|
|
967
|
-
);
|
|
968
|
-
return () => {
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
const coreDoc = this.coreState.core.documents[documentId];
|
|
972
|
-
if (!coreDoc || !coreDoc.document) {
|
|
973
|
-
this.logger.warn(
|
|
974
|
-
"SelectionPlugin",
|
|
975
|
-
"DocumentNotFound",
|
|
976
|
-
`Cannot register marquee on page ${pageIndex}: document not found`
|
|
977
|
-
);
|
|
978
|
-
return () => {
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
const page = coreDoc.document.pages[pageIndex];
|
|
982
|
-
if (!page) {
|
|
983
|
-
this.logger.warn(
|
|
984
|
-
"SelectionPlugin",
|
|
985
|
-
"PageNotFound",
|
|
986
|
-
`Cannot register marquee on page ${pageIndex}: page not found`
|
|
987
|
-
);
|
|
988
|
-
return () => {
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
const pageSize = page.size;
|
|
992
|
-
const minDragPx = ((_a = this.config.marquee) == null ? void 0 : _a.minDragPx) ?? 5;
|
|
993
|
-
const shouldShowRect = () => {
|
|
994
|
-
var _a2;
|
|
995
|
-
const mode = this.interactionManagerCapability.forDocument(documentId).getActiveMode();
|
|
996
|
-
const config = (_a2 = this.enabledModesPerDoc.get(documentId)) == null ? void 0 : _a2.get(mode);
|
|
997
|
-
return (config == null ? void 0 : config.showMarqueeRects) !== false;
|
|
998
|
-
};
|
|
999
|
-
const marqueeHandler = createMarqueeSelectionHandler({
|
|
1000
|
-
pageSize,
|
|
1001
|
-
scale,
|
|
1002
|
-
minDragPx,
|
|
1003
|
-
isEnabled: (modeId) => {
|
|
1004
|
-
var _a2;
|
|
1005
|
-
const config = (_a2 = this.enabledModesPerDoc.get(documentId)) == null ? void 0 : _a2.get(modeId);
|
|
1006
|
-
return (config == null ? void 0 : config.enableMarquee) === true;
|
|
1007
|
-
},
|
|
1008
|
-
isTextSelecting: () => (this.selecting.get(documentId) ?? false) || (this.hasTextAnchor.get(documentId) ?? false),
|
|
1009
|
-
onBegin: (pos, modeId) => this.beginMarquee(documentId, pageIndex, pos, modeId),
|
|
1010
|
-
onChange: (rect, modeId) => {
|
|
1011
|
-
this.updateMarquee(documentId, pageIndex, rect, modeId);
|
|
1012
|
-
onRectChange(shouldShowRect() ? rect : null);
|
|
1013
|
-
},
|
|
1014
|
-
onEnd: (rect, modeId) => {
|
|
1015
|
-
this.endMarquee(documentId, pageIndex, rect, modeId);
|
|
1016
|
-
onRectChange(null);
|
|
1017
|
-
},
|
|
1018
|
-
onCancel: (modeId) => {
|
|
1019
|
-
this.cancelMarquee(documentId, modeId);
|
|
1020
|
-
onRectChange(null);
|
|
1021
|
-
}
|
|
1022
|
-
});
|
|
1023
|
-
const unregisterHandlers = this.interactionManagerCapability.registerAlways({
|
|
1024
|
-
scope: { type: "page", documentId, pageIndex },
|
|
1025
|
-
handlers: marqueeHandler
|
|
1026
|
-
});
|
|
1027
|
-
return unregisterHandlers;
|
|
1028
|
-
}
|
|
1029
|
-
/**
|
|
1030
|
-
* Helper to calculate viewport relative metrics for a page rect.
|
|
1031
|
-
* Returns null if the rect cannot be converted to viewport space.
|
|
1032
|
-
*/
|
|
1033
|
-
getPlacementMetrics(documentId, pageIndex, rect, vpMetrics) {
|
|
1034
|
-
var _a;
|
|
1035
|
-
const scrollScope = (_a = this.scrollCapability) == null ? void 0 : _a.forDocument(documentId);
|
|
1036
|
-
const viewportRect = scrollScope == null ? void 0 : scrollScope.getRectPositionForPage(pageIndex, rect);
|
|
1037
|
-
if (!viewportRect) return null;
|
|
1038
|
-
const rectTopInView = viewportRect.origin.y - vpMetrics.scrollTop;
|
|
1039
|
-
const rectBottomInView = viewportRect.origin.y + viewportRect.size.height - vpMetrics.scrollTop;
|
|
1040
|
-
return {
|
|
1041
|
-
pageIndex,
|
|
1042
|
-
rect,
|
|
1043
|
-
// Original Page Rect
|
|
1044
|
-
spaceAbove: rectTopInView,
|
|
1045
|
-
spaceBelow: vpMetrics.clientHeight - rectBottomInView,
|
|
1046
|
-
isBottomVisible: rectBottomInView > 0 && rectBottomInView <= vpMetrics.clientHeight,
|
|
1047
|
-
isTopVisible: rectTopInView >= 0 && rectTopInView < vpMetrics.clientHeight
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
emitMenuPlacement(documentId, placement) {
|
|
1051
|
-
this.menuPlacement$.emit(documentId, placement);
|
|
1052
|
-
if (placement) {
|
|
1053
|
-
this.interactionManagerCapability.claimPageActivity(
|
|
1054
|
-
documentId,
|
|
1055
|
-
"selection-menu",
|
|
1056
|
-
placement.pageIndex
|
|
1057
|
-
);
|
|
1058
|
-
} else {
|
|
1059
|
-
this.interactionManagerCapability.releasePageActivity(documentId, "selection-menu");
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
recalculateMenuPlacement(documentId) {
|
|
1063
|
-
const docState = this.state.documents[documentId];
|
|
1064
|
-
if (!docState) return;
|
|
1065
|
-
if (docState.selecting || docState.selection === null) {
|
|
1066
|
-
this.emitMenuPlacement(documentId, null);
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
const bounds = selectBoundingRectsForAllPages(docState);
|
|
1070
|
-
if (bounds.length === 0) {
|
|
1071
|
-
this.emitMenuPlacement(documentId, null);
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
const tail = bounds[bounds.length - 1];
|
|
1075
|
-
if (!this.viewportCapability || !this.scrollCapability) {
|
|
1076
|
-
this.emitMenuPlacement(documentId, {
|
|
1077
|
-
pageIndex: tail.page,
|
|
1078
|
-
rect: tail.rect,
|
|
1079
|
-
spaceAbove: 0,
|
|
1080
|
-
spaceBelow: Infinity,
|
|
1081
|
-
// Pretend we have infinite space below to prevent auto-flipping
|
|
1082
|
-
suggestTop: false,
|
|
1083
|
-
isVisible: true
|
|
1084
|
-
// Assume visible
|
|
1085
|
-
});
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
const viewportScope = this.viewportCapability.forDocument(documentId);
|
|
1089
|
-
const vpMetrics = viewportScope.getMetrics();
|
|
1090
|
-
const head = bounds[0];
|
|
1091
|
-
const tailMetrics = this.getPlacementMetrics(documentId, tail.page, tail.rect, vpMetrics);
|
|
1092
|
-
const headMetrics = this.getPlacementMetrics(documentId, head.page, head.rect, vpMetrics);
|
|
1093
|
-
if (tailMetrics) {
|
|
1094
|
-
if (tailMetrics.isBottomVisible && tailMetrics.spaceBelow > this.menuHeight) {
|
|
1095
|
-
this.emitMenuPlacement(documentId, {
|
|
1096
|
-
...tailMetrics,
|
|
1097
|
-
suggestTop: false,
|
|
1098
|
-
isVisible: true
|
|
1099
|
-
});
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
if (headMetrics) {
|
|
1104
|
-
if (headMetrics.isTopVisible) {
|
|
1105
|
-
this.emitMenuPlacement(documentId, {
|
|
1106
|
-
...headMetrics,
|
|
1107
|
-
suggestTop: true,
|
|
1108
|
-
isVisible: true
|
|
1109
|
-
});
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
if (tailMetrics && tailMetrics.isBottomVisible) {
|
|
1114
|
-
this.emitMenuPlacement(documentId, {
|
|
1115
|
-
...tailMetrics,
|
|
1116
|
-
suggestTop: false,
|
|
1117
|
-
isVisible: true
|
|
1118
|
-
});
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
this.emitMenuPlacement(documentId, null);
|
|
1122
|
-
}
|
|
1123
|
-
notifyPage(documentId, pageIndex) {
|
|
1124
|
-
var _a, _b;
|
|
1125
|
-
const callback = (_a = this.pageCallbacks.get(documentId)) == null ? void 0 : _a.get(pageIndex);
|
|
1126
|
-
if (callback) {
|
|
1127
|
-
const docState = this.getDocumentState(documentId);
|
|
1128
|
-
const mode = this.interactionManagerCapability.forDocument(documentId).getActiveMode();
|
|
1129
|
-
const modeConfig = (_b = this.enabledModesPerDoc.get(documentId)) == null ? void 0 : _b.get(mode);
|
|
1130
|
-
const shouldShowRects = modeConfig && (modeConfig.showSelectionRects ?? modeConfig.showRects) !== false;
|
|
1131
|
-
if (shouldShowRects) {
|
|
1132
|
-
callback({
|
|
1133
|
-
rects: selectRectsForPage(docState, pageIndex),
|
|
1134
|
-
boundingRect: selectBoundingRectForPage(docState, pageIndex)
|
|
1135
|
-
});
|
|
1136
|
-
} else {
|
|
1137
|
-
callback({ rects: [], boundingRect: null });
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
notifyAllPages(documentId) {
|
|
1142
|
-
var _a;
|
|
1143
|
-
(_a = this.pageCallbacks.get(documentId)) == null ? void 0 : _a.forEach((_, pageIndex) => {
|
|
1144
|
-
this.notifyPage(documentId, pageIndex);
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
getNewPageGeometryAndCache(documentId, pageIdx) {
|
|
1148
|
-
const coreDoc = this.getCoreDocument(documentId);
|
|
1149
|
-
if (!coreDoc || !coreDoc.document)
|
|
1150
|
-
return PdfTaskHelper.reject({ code: PdfErrorCode.NotFound, message: "Doc Not Found" });
|
|
1151
|
-
const page = coreDoc.document.pages.find((p) => p.index === pageIdx);
|
|
1152
|
-
const task = this.engine.getPageGeometry(coreDoc.document, page);
|
|
1153
|
-
task.wait((geo) => {
|
|
1154
|
-
this.dispatch(cachePageGeometry(documentId, pageIdx, geo));
|
|
1155
|
-
this.touchGeometry(documentId, pageIdx);
|
|
1156
|
-
}, ignore);
|
|
1157
|
-
return task;
|
|
1158
|
-
}
|
|
1159
|
-
/* ── geometry cache ───────────────────────────────────── */
|
|
1160
|
-
getOrLoadGeometry(documentId, pageIdx) {
|
|
1161
|
-
const cached = this.getDocumentState(documentId).geometry[pageIdx];
|
|
1162
|
-
if (cached) {
|
|
1163
|
-
this.touchGeometry(documentId, pageIdx);
|
|
1164
|
-
return PdfTaskHelper.resolve(cached);
|
|
1165
|
-
}
|
|
1166
|
-
return this.getNewPageGeometryAndCache(documentId, pageIdx);
|
|
1167
|
-
}
|
|
1168
|
-
/* ── geometry LRU eviction ──────────────────────────────── */
|
|
1169
|
-
touchGeometry(documentId, pageIdx) {
|
|
1170
|
-
const order = this.geoAccessOrder.get(documentId);
|
|
1171
|
-
if (!order) return;
|
|
1172
|
-
const idx = order.indexOf(pageIdx);
|
|
1173
|
-
if (idx > -1) order.splice(idx, 1);
|
|
1174
|
-
order.push(pageIdx);
|
|
1175
|
-
this.evictGeometryIfNeeded(documentId);
|
|
1176
|
-
}
|
|
1177
|
-
evictGeometryIfNeeded(documentId) {
|
|
1178
|
-
const max = this.config.maxCachedGeometries ?? 50;
|
|
1179
|
-
const order = this.geoAccessOrder.get(documentId);
|
|
1180
|
-
if (!order || order.length <= max) return;
|
|
1181
|
-
const pinned = this.pageCallbacks.get(documentId);
|
|
1182
|
-
const toEvict = [];
|
|
1183
|
-
while (order.length - toEvict.length > max) {
|
|
1184
|
-
const candidate = order.find((p) => !toEvict.includes(p) && !(pinned == null ? void 0 : pinned.has(p)));
|
|
1185
|
-
if (candidate === void 0) break;
|
|
1186
|
-
toEvict.push(candidate);
|
|
1187
|
-
}
|
|
1188
|
-
if (toEvict.length === 0) return;
|
|
1189
|
-
for (const p of toEvict) {
|
|
1190
|
-
const idx = order.indexOf(p);
|
|
1191
|
-
if (idx > -1) order.splice(idx, 1);
|
|
1192
|
-
}
|
|
1193
|
-
this.dispatch(evictPageGeometry(documentId, toEvict));
|
|
1194
|
-
}
|
|
1195
|
-
/* ── selection state updates ───────────────────────────── */
|
|
1196
|
-
beginSelection(documentId, page, index, modeId) {
|
|
1197
|
-
this.selecting.set(documentId, true);
|
|
1198
|
-
this.anchor.set(documentId, { page, index });
|
|
1199
|
-
this.dispatch(startSelection(documentId));
|
|
1200
|
-
this.beginSelection$.emit(documentId, { page, index, modeId });
|
|
1201
|
-
this.recalculateMenuPlacement(documentId);
|
|
1202
|
-
}
|
|
1203
|
-
endSelection(documentId, modeId) {
|
|
1204
|
-
this.selecting.set(documentId, false);
|
|
1205
|
-
this.anchor.set(documentId, void 0);
|
|
1206
|
-
this.dispatch(endSelection(documentId));
|
|
1207
|
-
this.endSelection$.emit(documentId, { modeId });
|
|
1208
|
-
this.recalculateMenuPlacement(documentId);
|
|
1209
|
-
}
|
|
1210
|
-
clearSelection(documentId, _modeId) {
|
|
1211
|
-
this.selecting.set(documentId, false);
|
|
1212
|
-
this.anchor.set(documentId, void 0);
|
|
1213
|
-
this.dispatch(clearSelection(documentId));
|
|
1214
|
-
this.selChange$.emit(documentId, null);
|
|
1215
|
-
this.emitMenuPlacement(documentId, null);
|
|
1216
|
-
this.notifyAllPages(documentId);
|
|
1217
|
-
}
|
|
1218
|
-
/**
|
|
1219
|
-
* Programmatically apply (or restore) a text selection without going through
|
|
1220
|
-
* the pointer drag flow. Useful for restoring a previously persisted selection
|
|
1221
|
-
* or driving selection from code (e.g. search results).
|
|
1222
|
-
*
|
|
1223
|
-
* Geometry for every page spanned by the range is loaded on demand (the data
|
|
1224
|
-
* comes from the WASM engine), so the returned task settles only once the
|
|
1225
|
-
* selection rects have actually been computed.
|
|
1226
|
-
*
|
|
1227
|
-
* Passing `null` is equivalent to calling {@link clearSelection}.
|
|
1228
|
-
*
|
|
1229
|
-
* @returns a task that resolves once the selection has been applied.
|
|
1230
|
-
*/
|
|
1231
|
-
applySelection(documentId, range) {
|
|
1232
|
-
if (!this.state.documents[documentId]) {
|
|
1233
|
-
return PdfTaskHelper.reject({
|
|
1234
|
-
code: PdfErrorCode.NotFound,
|
|
1235
|
-
message: `Selection state not found for document: ${documentId}`
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
if (range === null) {
|
|
1239
|
-
this.clearSelection(documentId);
|
|
1240
|
-
return PdfTaskHelper.resolve(void 0);
|
|
1241
|
-
}
|
|
1242
|
-
const validationError = this.validateRange(documentId, range);
|
|
1243
|
-
if (validationError) {
|
|
1244
|
-
return PdfTaskHelper.reject(validationError);
|
|
1245
|
-
}
|
|
1246
|
-
const normalized = this.normalizeRange(range);
|
|
1247
|
-
const pages = [];
|
|
1248
|
-
for (let p = normalized.start.page; p <= normalized.end.page; p++) {
|
|
1249
|
-
pages.push(p);
|
|
1250
|
-
}
|
|
1251
|
-
const geoTasks = pages.map((p) => this.getOrLoadGeometry(documentId, p));
|
|
1252
|
-
const result = PdfTaskHelper.create();
|
|
1253
|
-
Task.all(geoTasks).wait(() => {
|
|
1254
|
-
if (!this.state.documents[documentId]) {
|
|
1255
|
-
result.reject({
|
|
1256
|
-
code: PdfErrorCode.NotFound,
|
|
1257
|
-
message: `Selection state not found for document: ${documentId}`
|
|
1258
|
-
});
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
const clamped = this.clampRangeToGeometry(documentId, normalized);
|
|
1262
|
-
if (!clamped) {
|
|
1263
|
-
result.reject({
|
|
1264
|
-
code: PdfErrorCode.NotFound,
|
|
1265
|
-
message: `Cannot apply selection: page(s) ${normalized.start.page}-${normalized.end.page} have no text geometry`
|
|
1266
|
-
});
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
this.selecting.set(documentId, false);
|
|
1270
|
-
this.anchor.set(documentId, void 0);
|
|
1271
|
-
const previouslyHighlighted = Object.keys(this.getDocumentState(documentId).rects).map(
|
|
1272
|
-
Number
|
|
1273
|
-
);
|
|
1274
|
-
this.dispatch(startSelection(documentId));
|
|
1275
|
-
this.dispatch(setSelection(documentId, clamped));
|
|
1276
|
-
this.updateRectsAndSlices(documentId, clamped);
|
|
1277
|
-
this.dispatch(endSelection(documentId));
|
|
1278
|
-
this.selChange$.emit(documentId, clamped);
|
|
1279
|
-
const pagesToNotify = new Set(previouslyHighlighted);
|
|
1280
|
-
for (let p = clamped.start.page; p <= clamped.end.page; p++) {
|
|
1281
|
-
pagesToNotify.add(p);
|
|
1282
|
-
}
|
|
1283
|
-
pagesToNotify.forEach((p) => this.notifyPage(documentId, p));
|
|
1284
|
-
this.recalculateMenuPlacement(documentId);
|
|
1285
|
-
result.resolve(void 0);
|
|
1286
|
-
}, (error) => result.reject(error.reason));
|
|
1287
|
-
return result;
|
|
1288
|
-
}
|
|
1289
|
-
/**
|
|
1290
|
-
* Validate the structure and page bounds of a programmatic selection range.
|
|
1291
|
-
* Returns a rejection reason when the input is invalid, or null when it is
|
|
1292
|
-
* structurally sound. Glyph-index bounds are enforced separately, after the
|
|
1293
|
-
* page geometry has loaded (see {@link clampRangeToGeometry}).
|
|
1294
|
-
*/
|
|
1295
|
-
validateRange(documentId, range) {
|
|
1296
|
-
var _a, _b;
|
|
1297
|
-
const isValidPointer = (g) => !!g && typeof g === "object" && Number.isInteger(g.page) && Number.isInteger(g.index);
|
|
1298
|
-
if (!range || typeof range !== "object" || !range.start || !range.end) {
|
|
1299
|
-
return { code: PdfErrorCode.Unknown, message: "Invalid selection range: expected { start, end }" };
|
|
1300
|
-
}
|
|
1301
|
-
if (!isValidPointer(range.start) || !isValidPointer(range.end)) {
|
|
1302
|
-
return {
|
|
1303
|
-
code: PdfErrorCode.Unknown,
|
|
1304
|
-
message: "Invalid selection range: start/end must have integer page and index"
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
if (range.start.index < 0 || range.end.index < 0) {
|
|
1308
|
-
return { code: PdfErrorCode.Unknown, message: "Invalid selection range: glyph index cannot be negative" };
|
|
1309
|
-
}
|
|
1310
|
-
const pageCount = ((_b = (_a = this.getCoreDocument(documentId)) == null ? void 0 : _a.document) == null ? void 0 : _b.pageCount) ?? 0;
|
|
1311
|
-
const minPage = Math.min(range.start.page, range.end.page);
|
|
1312
|
-
const maxPage = Math.max(range.start.page, range.end.page);
|
|
1313
|
-
if (minPage < 0 || maxPage >= pageCount) {
|
|
1314
|
-
return {
|
|
1315
|
-
code: PdfErrorCode.NotFound,
|
|
1316
|
-
message: `Invalid selection range: page ${minPage < 0 ? minPage : maxPage} out of bounds [0, ${pageCount})`
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
return null;
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Clamp the glyph indices of a (normalized) range to the number of glyphs
|
|
1323
|
-
* available on the corresponding pages. Returns null when none of the
|
|
1324
|
-
* spanned pages have any text geometry to select.
|
|
1325
|
-
*/
|
|
1326
|
-
clampRangeToGeometry(documentId, range) {
|
|
1327
|
-
const docState = this.getDocumentState(documentId);
|
|
1328
|
-
const lastGlyphIndex = (page) => {
|
|
1329
|
-
const geo = docState.geometry[page];
|
|
1330
|
-
if (!geo || geo.runs.length === 0) return -1;
|
|
1331
|
-
const lastRun = geo.runs[geo.runs.length - 1];
|
|
1332
|
-
return lastRun.charStart + lastRun.glyphs.length - 1;
|
|
1333
|
-
};
|
|
1334
|
-
const startMax = lastGlyphIndex(range.start.page);
|
|
1335
|
-
const endMax = lastGlyphIndex(range.end.page);
|
|
1336
|
-
if (startMax < 0 && endMax < 0) return null;
|
|
1337
|
-
const clampIndex = (index, max) => max < 0 ? 0 : Math.min(Math.max(index, 0), max);
|
|
1338
|
-
return {
|
|
1339
|
-
start: { page: range.start.page, index: clampIndex(range.start.index, startMax) },
|
|
1340
|
-
end: { page: range.end.page, index: clampIndex(range.end.index, endMax) }
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
/** Order a range so that `start` never comes after `end`. */
|
|
1344
|
-
normalizeRange(range) {
|
|
1345
|
-
const { start, end } = range;
|
|
1346
|
-
const forward = end.page > start.page || end.page === start.page && end.index >= start.index;
|
|
1347
|
-
return forward ? { start, end } : { start: end, end: start };
|
|
1348
|
-
}
|
|
1349
|
-
selectWord(documentId, page, charIndex, modeId) {
|
|
1350
|
-
const geo = this.getDocumentState(documentId).geometry[page];
|
|
1351
|
-
if (!geo) return;
|
|
1352
|
-
const bounds = expandToWordBoundary(geo, charIndex);
|
|
1353
|
-
if (!bounds) return;
|
|
1354
|
-
this.applyInstantSelection(documentId, page, bounds.from, bounds.to, modeId);
|
|
1355
|
-
}
|
|
1356
|
-
selectLine(documentId, page, charIndex, modeId) {
|
|
1357
|
-
const geo = this.getDocumentState(documentId).geometry[page];
|
|
1358
|
-
if (!geo) return;
|
|
1359
|
-
const bounds = expandToLineBoundary(geo, charIndex);
|
|
1360
|
-
if (!bounds) return;
|
|
1361
|
-
this.applyInstantSelection(documentId, page, bounds.from, bounds.to, modeId);
|
|
1362
|
-
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Set a selection range without going through the drag begin/update/end flow.
|
|
1365
|
-
* Used by double-click (word) and triple-click (line) selection.
|
|
1366
|
-
*/
|
|
1367
|
-
applyInstantSelection(documentId, page, from, to, modeId) {
|
|
1368
|
-
const range = {
|
|
1369
|
-
start: { page, index: from },
|
|
1370
|
-
end: { page, index: to }
|
|
1371
|
-
};
|
|
1372
|
-
this.selecting.set(documentId, false);
|
|
1373
|
-
this.anchor.set(documentId, void 0);
|
|
1374
|
-
this.dispatch(startSelection(documentId));
|
|
1375
|
-
this.dispatch(setSelection(documentId, range));
|
|
1376
|
-
this.updateRectsAndSlices(documentId, range);
|
|
1377
|
-
this.dispatch(endSelection(documentId));
|
|
1378
|
-
this.selChange$.emit(documentId, range);
|
|
1379
|
-
this.beginSelection$.emit(documentId, { page, index: from, modeId });
|
|
1380
|
-
this.endSelection$.emit(documentId, { modeId });
|
|
1381
|
-
for (let p = range.start.page; p <= range.end.page; p++) {
|
|
1382
|
-
this.notifyPage(documentId, p);
|
|
1383
|
-
}
|
|
1384
|
-
this.recalculateMenuPlacement(documentId);
|
|
1385
|
-
}
|
|
1386
|
-
updateSelection(documentId, page, index, modeId) {
|
|
1387
|
-
if (!this.selecting.get(documentId) || !this.anchor.get(documentId)) return;
|
|
1388
|
-
const a = this.anchor.get(documentId);
|
|
1389
|
-
const forward = page > a.page || page === a.page && index >= a.index;
|
|
1390
|
-
const start = forward ? a : { page, index };
|
|
1391
|
-
const end = forward ? { page, index } : a;
|
|
1392
|
-
const range = { start, end };
|
|
1393
|
-
this.dispatch(setSelection(documentId, range));
|
|
1394
|
-
this.updateRectsAndSlices(documentId, range);
|
|
1395
|
-
this.selChange$.emit(documentId, range);
|
|
1396
|
-
for (let p = range.start.page; p <= range.end.page; p++) {
|
|
1397
|
-
this.notifyPage(documentId, p);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
updateRectsAndSlices(documentId, range) {
|
|
1401
|
-
const docState = this.getDocumentState(documentId);
|
|
1402
|
-
const allRects = {};
|
|
1403
|
-
const allSlices = {};
|
|
1404
|
-
for (let p = range.start.page; p <= range.end.page; p++) {
|
|
1405
|
-
const geo = docState.geometry[p];
|
|
1406
|
-
const sb = sliceBounds(range, geo, p);
|
|
1407
|
-
if (!sb) continue;
|
|
1408
|
-
allRects[p] = rectsWithinSlice(geo, sb.from, sb.to);
|
|
1409
|
-
allSlices[p] = { start: sb.from, count: sb.to - sb.from + 1 };
|
|
1410
|
-
}
|
|
1411
|
-
this.dispatch(setRects(documentId, allRects));
|
|
1412
|
-
this.dispatch(setSlices(documentId, allSlices));
|
|
1413
|
-
}
|
|
1414
|
-
getSelectedText(documentId) {
|
|
1415
|
-
if (!this.checkPermission(documentId, PdfPermissionFlag.CopyContents)) {
|
|
1416
|
-
this.logger.debug(
|
|
1417
|
-
"SelectionPlugin",
|
|
1418
|
-
"GetSelectedText",
|
|
1419
|
-
`Cannot get selected text: document ${documentId} lacks CopyContents permission`
|
|
1420
|
-
);
|
|
1421
|
-
return PdfTaskHelper.reject({
|
|
1422
|
-
code: PdfErrorCode.Security,
|
|
1423
|
-
message: "Document lacks CopyContents permission"
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
const coreDoc = this.getCoreDocument(documentId);
|
|
1427
|
-
const docState = this.getDocumentState(documentId);
|
|
1428
|
-
if (!(coreDoc == null ? void 0 : coreDoc.document) || !docState.selection) {
|
|
1429
|
-
return PdfTaskHelper.reject({
|
|
1430
|
-
code: PdfErrorCode.NotFound,
|
|
1431
|
-
message: "Doc Not Found or No Selection"
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
const sel = docState.selection;
|
|
1435
|
-
const req = [];
|
|
1436
|
-
for (let p = sel.start.page; p <= sel.end.page; p++) {
|
|
1437
|
-
const s = docState.slices[p];
|
|
1438
|
-
if (s) req.push({ pageIndex: p, charIndex: s.start, charCount: s.count });
|
|
1439
|
-
}
|
|
1440
|
-
if (req.length === 0) return PdfTaskHelper.resolve([]);
|
|
1441
|
-
const task = this.engine.getTextSlices(coreDoc.document, req);
|
|
1442
|
-
task.wait((text) => {
|
|
1443
|
-
this.textRetrieved$.emit(documentId, text);
|
|
1444
|
-
}, ignore);
|
|
1445
|
-
return task;
|
|
1446
|
-
}
|
|
1447
|
-
copyToClipboard(documentId) {
|
|
1448
|
-
if (!this.checkPermission(documentId, PdfPermissionFlag.CopyContents)) {
|
|
1449
|
-
this.logger.debug(
|
|
1450
|
-
"SelectionPlugin",
|
|
1451
|
-
"CopyToClipboard",
|
|
1452
|
-
`Cannot copy to clipboard: document ${documentId} lacks CopyContents permission`
|
|
1453
|
-
);
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
const text = this.getSelectedText(documentId);
|
|
1457
|
-
text.wait((text2) => {
|
|
1458
|
-
this.copyToClipboard$.emit(documentId, text2.join("\n"));
|
|
1459
|
-
}, ignore);
|
|
1460
|
-
}
|
|
1461
|
-
/* ── marquee selection state updates ─────────────────────── */
|
|
1462
|
-
beginMarquee(documentId, pageIndex, _startPos, _modeId) {
|
|
1463
|
-
this.marqueePage.set(documentId, pageIndex);
|
|
1464
|
-
}
|
|
1465
|
-
updateMarquee(documentId, pageIndex, rect, modeId) {
|
|
1466
|
-
this.marqueeChange$.emit(documentId, { pageIndex, rect, modeId });
|
|
1467
|
-
}
|
|
1468
|
-
endMarquee(documentId, pageIndex, rect, modeId) {
|
|
1469
|
-
this.marqueeEnd$.emit(documentId, { pageIndex, rect, modeId });
|
|
1470
|
-
this.marqueeChange$.emit(documentId, { pageIndex, rect: null, modeId });
|
|
1471
|
-
this.marqueePage.delete(documentId);
|
|
1472
|
-
}
|
|
1473
|
-
cancelMarquee(documentId, modeId) {
|
|
1474
|
-
const pageIndex = this.marqueePage.get(documentId);
|
|
1475
|
-
if (pageIndex !== void 0) {
|
|
1476
|
-
this.marqueeChange$.emit(documentId, { pageIndex, rect: null, modeId });
|
|
1477
|
-
this.marqueePage.delete(documentId);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
/** @deprecated — shim for backward compat; delegates to pointerMode config */
|
|
1481
|
-
setMarqueeEnabled(documentId, enabled) {
|
|
1482
|
-
const modes = this.enabledModesPerDoc.get(documentId);
|
|
1483
|
-
if (!modes) return;
|
|
1484
|
-
const current = modes.get("pointerMode");
|
|
1485
|
-
if (current) {
|
|
1486
|
-
current.enableMarquee = enabled;
|
|
1487
|
-
} else if (enabled) {
|
|
1488
|
-
modes.set("pointerMode", { enableMarquee: true });
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
/** @deprecated — shim for backward compat; reads pointerMode config */
|
|
1492
|
-
isMarqueeEnabled(documentId) {
|
|
1493
|
-
var _a;
|
|
1494
|
-
const config = (_a = this.enabledModesPerDoc.get(documentId)) == null ? void 0 : _a.get("pointerMode");
|
|
1495
|
-
return (config == null ? void 0 : config.enableMarquee) !== false;
|
|
1496
|
-
}
|
|
1497
|
-
};
|
|
1498
|
-
_SelectionPlugin.id = "selection";
|
|
1499
|
-
let SelectionPlugin = _SelectionPlugin;
|
|
1500
|
-
const SelectionPluginPackage = {
|
|
1501
|
-
manifest,
|
|
1502
|
-
create: (registry, config) => new SelectionPlugin(SELECTION_PLUGIN_ID, registry, config),
|
|
1503
|
-
reducer: selectionReducer,
|
|
1504
|
-
initialState
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/reducer.ts
|
|
504
|
+
const initialSelectionState = {
|
|
505
|
+
selection: null,
|
|
506
|
+
rects: {},
|
|
507
|
+
loaded: {},
|
|
508
|
+
highlightHidden: false
|
|
1505
509
|
};
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
510
|
+
const selectionReducer = (state, action) => {
|
|
511
|
+
switch (action.type) {
|
|
512
|
+
case "PAGE_LOADED": return state.loaded[action.pon] ? state : {
|
|
513
|
+
...state,
|
|
514
|
+
loaded: {
|
|
515
|
+
...state.loaded,
|
|
516
|
+
[action.pon]: true
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
case "SET": return {
|
|
520
|
+
...state,
|
|
521
|
+
selection: action.selection,
|
|
522
|
+
rects: action.rects
|
|
523
|
+
};
|
|
524
|
+
case "CLEAR": return state.selection === null && Object.keys(state.rects).length === 0 ? state : {
|
|
525
|
+
...state,
|
|
526
|
+
selection: null,
|
|
527
|
+
rects: {}
|
|
528
|
+
};
|
|
529
|
+
case "SET_HIGHLIGHT_HIDDEN": return state.highlightHidden === action.hidden ? state : {
|
|
530
|
+
...state,
|
|
531
|
+
highlightHidden: action.hidden
|
|
532
|
+
};
|
|
533
|
+
default: return state;
|
|
534
|
+
}
|
|
1522
535
|
};
|
|
1523
|
-
//#
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/types.ts
|
|
538
|
+
const SelectionToken = createCapabilityToken("selection");
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/selection.plugin.ts
|
|
541
|
+
/**
|
|
542
|
+
* Text selection — document-scoped, requires the interaction hub. In `init` it
|
|
543
|
+
* registers ITS pointer handler with the hub; the hub owns the pointer stream
|
|
544
|
+
* and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection
|
|
545
|
+
* only needs the page coordinate context + the engine's text geometry.
|
|
546
|
+
*/
|
|
547
|
+
const selectionPlugin = () => definePlugin({
|
|
548
|
+
id: "selection",
|
|
549
|
+
token: SelectionToken,
|
|
550
|
+
scope: "document",
|
|
551
|
+
requires: [InteractionToken],
|
|
552
|
+
initialState: initialSelectionState,
|
|
553
|
+
reduce: selectionReducer,
|
|
554
|
+
capability: createSelectionCapability,
|
|
555
|
+
init: (ctx) => {
|
|
556
|
+
const interaction = ctx.get(InteractionToken);
|
|
557
|
+
const selection = ctx.get(SelectionToken);
|
|
558
|
+
interaction.registerHandler(createTextSelectHandler(selection, interaction));
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
//#endregion
|
|
562
|
+
export { SelectionToken, buildPageText, createSelectionCapability, createTextSelectHandler, expandToLine, expandToWord, glyphAt, initialSelectionState, rectsForRange, selectionPlugin, selectionReducer };
|
|
563
|
+
|
|
564
|
+
//# sourceMappingURL=index.js.map
|