@embedpdf/plugin-selection 3.0.0-next.4 → 3.0.0-next.6
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/dist/index.cjs +20 -399
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -172
- package/dist/index.d.ts +13 -172
- package/dist/index.js +18 -389
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +11 -0
- package/dist/internal.d.cts +62 -0
- package/dist/internal.d.ts +62 -0
- package/dist/internal.js +2 -0
- package/dist/reducer-CZG2TZgo.js +688 -0
- package/dist/reducer-CZG2TZgo.js.map +1 -0
- package/dist/reducer-Dyf8p5h-.cjs +741 -0
- package/dist/reducer-Dyf8p5h-.cjs.map +1 -0
- package/dist/types-DtWaZIsm.d.cts +286 -0
- package/dist/types-DtWaZIsm.d.ts +286 -0
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -1,400 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as createSelectionCapability, l as SelectionToken$1, n as selectionReducer, r as createTextSelectHandler, t as initialSelectionState } from "./reducer-CZG2TZgo.js";
|
|
2
|
+
import { definePlugin } from "@embedpdf/core";
|
|
2
3
|
import { InteractionToken } from "@embedpdf/plugin-interaction";
|
|
3
|
-
import { applyPoint, invert, pageGeometry, textQuadBounds } from "@embedpdf/core-geometry";
|
|
4
|
-
import { buildPageTextLayout, expandTextRangeToLine, expandTextRangeToWord, textGlyphAt, textGlyphQuad, textSegmentsForRange } from "@embedpdf/engine-core/runtime";
|
|
5
|
-
//#region src/geometry.ts
|
|
6
|
-
/**
|
|
7
|
-
* The selection plugin's COORDINATE SEAM — and nothing else.
|
|
8
|
-
*
|
|
9
|
-
* Segmentation, hit-testing, and word/line expansion live in the canonical
|
|
10
|
-
* engine-core text layout (`@embedpdf/engine-core` `text/layout`): a text
|
|
11
|
-
* range has exactly one segmentation whether it came from a drag or a
|
|
12
|
-
* search. This module only converts between the viewer's content space
|
|
13
|
-
* (y-down, crop-relative) and the engine's PDF space (y-up) — pointer
|
|
14
|
-
* points inward, canonical segments outward. The PDF↔content y-flip is the
|
|
15
|
-
* geometry package's `pageGeometry` (the one place that math lives), never
|
|
16
|
-
* hand-rolled here.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Build a page's selection geometry. Page display rotation does NOT enter
|
|
20
|
-
* here — the overlay rides the page's CSS rotation, so content space stays
|
|
21
|
-
* un-rotated (the crop-relative y-flip is the only conversion; zoom = 1,
|
|
22
|
-
* the viewer's zoom is applied later by `PageTransform.pageToContent`).
|
|
23
|
-
*/
|
|
24
|
-
function buildSelectionPageGeometry(snapshot, crop, rotation, userUnit) {
|
|
25
|
-
const { pdfToContent } = pageGeometry({
|
|
26
|
-
crop,
|
|
27
|
-
rotation,
|
|
28
|
-
userUnit
|
|
29
|
-
}, 1);
|
|
30
|
-
return {
|
|
31
|
-
layout: buildPageTextLayout(snapshot),
|
|
32
|
-
toContent: pdfToContent,
|
|
33
|
-
fromContent: invert(pdfToContent)
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/** A content-space pointer point in the layout's PDF space. */
|
|
37
|
-
function contentPointToPdf(geom, p) {
|
|
38
|
-
return applyPoint(geom.fromContent, p);
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* A canonical PDF quad in content space. Engine quads carry frame-geometric
|
|
42
|
-
* slot semantics (`p1..p4` = upper-start, upper-end, lower-start,
|
|
43
|
-
* lower-end) — a trusted producer, so the y-flip maps corners straight onto
|
|
44
|
-
* their names; imported-annotation normalization does NOT apply here.
|
|
45
|
-
*/
|
|
46
|
-
function toContentTextQuad(geom, quad) {
|
|
47
|
-
const pt = (p) => applyPoint(geom.toContent, p);
|
|
48
|
-
return {
|
|
49
|
-
upperStart: pt(quad.p1),
|
|
50
|
-
upperEnd: pt(quad.p2),
|
|
51
|
-
lowerStart: pt(quad.p3),
|
|
52
|
-
lowerEnd: pt(quad.p4)
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/** A canonical segment in content space (`rect` recomputed from the mapped
|
|
56
|
-
* quad so the pair can never drift). */
|
|
57
|
-
function toContentSegment(geom, segment) {
|
|
58
|
-
const quad = toContentTextQuad(geom, segment.quad);
|
|
59
|
-
return {
|
|
60
|
-
quad,
|
|
61
|
-
rect: textQuadBounds(quad),
|
|
62
|
-
advance: segment.advance
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
//#endregion
|
|
66
|
-
//#region src/capability.ts
|
|
67
|
-
const EMPTY_SEGMENTS = [];
|
|
68
|
-
/**
|
|
69
|
-
* The selection capability. The reducer state holds the selection range, the
|
|
70
|
-
* derived content-space segments (per page), and per-page loaded flags; the
|
|
71
|
-
* (large, non-serializable) per-page canonical layout is cached HERE in the
|
|
72
|
-
* closure. Segmentation itself is the engine-core text layout — selection
|
|
73
|
-
* owns gestures and state, never how glyphs become lines.
|
|
74
|
-
*
|
|
75
|
-
* Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph),
|
|
76
|
-
* so a drag from page 2 into page 4 selects the tail of 2, all of 3, and the
|
|
77
|
-
* head of 4. `recompute` rebuilds the merged line segments for every loaded
|
|
78
|
-
* page in the span and re-runs whenever a mid-span page finishes loading.
|
|
79
|
-
*/
|
|
80
|
-
function createSelectionCapability(ctx) {
|
|
81
|
-
const cache = /* @__PURE__ */ new Map();
|
|
82
|
-
const pending = /* @__PURE__ */ new Set();
|
|
83
|
-
const changeCbs = /* @__PURE__ */ new Set();
|
|
84
|
-
const commitCbs = /* @__PURE__ */ new Set();
|
|
85
|
-
const fireChange = () => changeCbs.forEach((cb) => cb());
|
|
86
|
-
const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;
|
|
87
|
-
const ponAtIndex = (i) => ctx.document()?.pages[i]?.pageObjectNumber;
|
|
88
|
-
const glyphAt = (geom, point) => textGlyphAt(geom.layout, contentPointToPdf(geom, point));
|
|
89
|
-
function orderedEnds(sel) {
|
|
90
|
-
const ai = pageIndexOf(sel.anchor.pon);
|
|
91
|
-
const fi = pageIndexOf(sel.focus.pon);
|
|
92
|
-
return ai < fi || ai === fi && sel.anchor.glyph <= sel.focus.glyph ? {
|
|
93
|
-
start: sel.anchor,
|
|
94
|
-
end: sel.focus,
|
|
95
|
-
direction: "forward"
|
|
96
|
-
} : {
|
|
97
|
-
start: sel.focus,
|
|
98
|
-
end: sel.anchor,
|
|
99
|
-
direction: "backward"
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
function endpointFor(ptr, which) {
|
|
103
|
-
const segments = ctx.getState().segments[ptr.pon] ?? EMPTY_SEGMENTS;
|
|
104
|
-
if (!segments.length) return null;
|
|
105
|
-
const segment = which === "start" ? segments[0] : segments[segments.length - 1];
|
|
106
|
-
const geom = cache.get(ptr.pon);
|
|
107
|
-
const cell = geom ? textGlyphQuad(geom.layout, ptr.glyph) : null;
|
|
108
|
-
if (geom && cell) {
|
|
109
|
-
const glyphQuad = toContentTextQuad(geom, cell);
|
|
110
|
-
return {
|
|
111
|
-
pon: ptr.pon,
|
|
112
|
-
glyphQuad,
|
|
113
|
-
advance: segment.advance,
|
|
114
|
-
rect: textQuadBounds(glyphQuad)
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
pon: ptr.pon,
|
|
119
|
-
glyphQuad: segment.quad,
|
|
120
|
-
advance: segment.advance,
|
|
121
|
-
rect: segment.rect
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function snapshot() {
|
|
125
|
-
const { selection, segments } = ctx.getState();
|
|
126
|
-
const pages = Object.keys(segments).map(Number).filter((pon) => (segments[pon]?.length ?? 0) > 0).map((pon) => ({
|
|
127
|
-
pon,
|
|
128
|
-
segments: segments[pon]
|
|
129
|
-
}));
|
|
130
|
-
if (!selection) return {
|
|
131
|
-
pages,
|
|
132
|
-
start: null,
|
|
133
|
-
end: null,
|
|
134
|
-
direction: "forward"
|
|
135
|
-
};
|
|
136
|
-
const { start, end, direction } = orderedEnds(selection);
|
|
137
|
-
return {
|
|
138
|
-
pages,
|
|
139
|
-
start: endpointFor(start, "start"),
|
|
140
|
-
end: endpointFor(end, "end"),
|
|
141
|
-
direction
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function ensurePage(pon) {
|
|
145
|
-
if (cache.has(pon) || pending.has(pon)) return;
|
|
146
|
-
const doc = ctx.doc;
|
|
147
|
-
const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
|
|
148
|
-
if (!doc || !layout) return;
|
|
149
|
-
pending.add(pon);
|
|
150
|
-
doc.page(pon).geometry.read().then((snapshot) => {
|
|
151
|
-
pending.delete(pon);
|
|
152
|
-
cache.set(pon, buildSelectionPageGeometry(snapshot, layout.boxes.crop, layout.rotation, layout.userUnit));
|
|
153
|
-
ctx.dispatch({
|
|
154
|
-
type: "PAGE_LOADED",
|
|
155
|
-
pon
|
|
156
|
-
});
|
|
157
|
-
if (ctx.getState().selection) recompute();
|
|
158
|
-
}, () => {
|
|
159
|
-
pending.delete(pon);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
function recompute(sel = ctx.getState().selection) {
|
|
163
|
-
if (!sel) return;
|
|
164
|
-
const { start, end } = orderedEnds(sel);
|
|
165
|
-
const si = pageIndexOf(start.pon);
|
|
166
|
-
const ei = pageIndexOf(end.pon);
|
|
167
|
-
if (si < 0 || ei < 0) return;
|
|
168
|
-
const segments = {};
|
|
169
|
-
for (let i = si; i <= ei; i++) {
|
|
170
|
-
const pon = ponAtIndex(i);
|
|
171
|
-
if (pon == null) continue;
|
|
172
|
-
const geom = cache.get(pon);
|
|
173
|
-
if (!geom) {
|
|
174
|
-
ensurePage(pon);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const from = i === si ? start.glyph : 0;
|
|
178
|
-
const to = i === ei ? end.glyph : geom.layout.glyphs.length - 1;
|
|
179
|
-
segments[pon] = textSegmentsForRange(geom.layout, from, to - from + 1).map((s) => toContentSegment(geom, s));
|
|
180
|
-
}
|
|
181
|
-
ctx.dispatch({
|
|
182
|
-
type: "SET",
|
|
183
|
-
selection: sel,
|
|
184
|
-
segments
|
|
185
|
-
});
|
|
186
|
-
fireChange();
|
|
187
|
-
}
|
|
188
|
-
function selectSpan(pon, point, expand) {
|
|
189
|
-
const geom = cache.get(pon);
|
|
190
|
-
if (!geom) return;
|
|
191
|
-
const i = glyphAt(geom, point);
|
|
192
|
-
if (i == null) return;
|
|
193
|
-
const [from, to] = expand === "word" ? expandTextRangeToWord(geom.layout, i) : expandTextRangeToLine(geom.layout, i);
|
|
194
|
-
recompute({
|
|
195
|
-
anchor: {
|
|
196
|
-
pon,
|
|
197
|
-
glyph: from
|
|
198
|
-
},
|
|
199
|
-
focus: {
|
|
200
|
-
pon,
|
|
201
|
-
glyph: to
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
ensurePage,
|
|
207
|
-
isLoaded: (pon) => !!ctx.getState().loaded[pon],
|
|
208
|
-
isOverText: (pon, point) => {
|
|
209
|
-
const geom = cache.get(pon);
|
|
210
|
-
return geom ? glyphAt(geom, point) != null : false;
|
|
211
|
-
},
|
|
212
|
-
beginAt: (pon, point) => {
|
|
213
|
-
const geom = cache.get(pon);
|
|
214
|
-
if (!geom) return false;
|
|
215
|
-
const i = glyphAt(geom, point);
|
|
216
|
-
if (i == null) return false;
|
|
217
|
-
recompute({
|
|
218
|
-
anchor: {
|
|
219
|
-
pon,
|
|
220
|
-
glyph: i
|
|
221
|
-
},
|
|
222
|
-
focus: {
|
|
223
|
-
pon,
|
|
224
|
-
glyph: i
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
return true;
|
|
228
|
-
},
|
|
229
|
-
selectWord: (pon, point) => selectSpan(pon, point, "word"),
|
|
230
|
-
selectLine: (pon, point) => selectSpan(pon, point, "line"),
|
|
231
|
-
extendTo: (pon, point) => {
|
|
232
|
-
const cur = ctx.getState().selection;
|
|
233
|
-
if (!cur) return;
|
|
234
|
-
const geom = cache.get(pon);
|
|
235
|
-
if (!geom) {
|
|
236
|
-
ensurePage(pon);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const i = glyphAt(geom, point);
|
|
240
|
-
if (i == null) return;
|
|
241
|
-
recompute({
|
|
242
|
-
anchor: cur.anchor,
|
|
243
|
-
focus: {
|
|
244
|
-
pon,
|
|
245
|
-
glyph: i
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
},
|
|
249
|
-
end: () => commitCbs.forEach((cb) => cb()),
|
|
250
|
-
clear: () => {
|
|
251
|
-
ctx.dispatch({ type: "CLEAR" });
|
|
252
|
-
fireChange();
|
|
253
|
-
},
|
|
254
|
-
snapshot,
|
|
255
|
-
segmentsForPage: (pon) => ctx.getState().segments[pon] ?? EMPTY_SEGMENTS,
|
|
256
|
-
rectsForPage: (pon) => (ctx.getState().segments[pon] ?? EMPTY_SEGMENTS).map((s) => s.rect),
|
|
257
|
-
hasSelection: () => ctx.getState().selection != null,
|
|
258
|
-
selectedPages: () => {
|
|
259
|
-
const { segments } = ctx.getState();
|
|
260
|
-
return Object.keys(segments).map(Number).filter((pon) => (segments[pon]?.length ?? 0) > 0);
|
|
261
|
-
},
|
|
262
|
-
onChange: (cb) => {
|
|
263
|
-
changeCbs.add(cb);
|
|
264
|
-
return () => changeCbs.delete(cb);
|
|
265
|
-
},
|
|
266
|
-
onCommit: (cb) => {
|
|
267
|
-
commitCbs.add(cb);
|
|
268
|
-
return () => commitCbs.delete(cb);
|
|
269
|
-
},
|
|
270
|
-
setHighlightVisible: (visible) => ctx.dispatch({
|
|
271
|
-
type: "SET_HIGHLIGHT_HIDDEN",
|
|
272
|
-
hidden: !visible
|
|
273
|
-
}),
|
|
274
|
-
highlightVisible: () => !ctx.getState().highlightHidden
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
//#endregion
|
|
278
|
-
//#region src/handler.ts
|
|
279
|
-
const CURSOR_TOKEN = "selection-text";
|
|
280
|
-
const DRAG_THRESHOLD_PX = 4;
|
|
281
|
-
/**
|
|
282
|
-
* The text-selection pointer handler. It is live only under tools that enable the
|
|
283
|
-
* `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
|
|
284
|
-
* switching to pan disables text selection with zero special-casing here).
|
|
285
|
-
*
|
|
286
|
-
* - single click → deselect; records an anchor but selects NOTHING until
|
|
287
|
-
* the pointer moves past the drag threshold (Chrome/Acrobat feel)
|
|
288
|
-
* - single-click + drag → caret selection from the anchor (extends across pages)
|
|
289
|
-
* - double-click → select the word
|
|
290
|
-
* - triple-click → select the visual line
|
|
291
|
-
* - down off-text → deselect, and DON'T capture (so it can't block)
|
|
292
|
-
* - hover → I-beam only when over text, else the pointer cursor
|
|
293
|
-
*/
|
|
294
|
-
function createTextSelectHandler(selection, interaction) {
|
|
295
|
-
let anchor = null;
|
|
296
|
-
let dragging = false;
|
|
297
|
-
return {
|
|
298
|
-
id: "text-select",
|
|
299
|
-
priority: 60,
|
|
300
|
-
enabledFor: (tool) => tool.enables.has("text-select"),
|
|
301
|
-
onDown: (s) => {
|
|
302
|
-
anchor = null;
|
|
303
|
-
dragging = false;
|
|
304
|
-
if (!s.page) return false;
|
|
305
|
-
const { pon, point } = s.page;
|
|
306
|
-
const clicks = s.clickCount ?? 1;
|
|
307
|
-
if (clicks >= 3) {
|
|
308
|
-
selection.selectLine(pon, point);
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
if (clicks === 2) {
|
|
312
|
-
selection.selectWord(pon, point);
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
selection.clear();
|
|
316
|
-
if (!selection.isOverText(pon, point)) return false;
|
|
317
|
-
anchor = {
|
|
318
|
-
pon,
|
|
319
|
-
point,
|
|
320
|
-
vx: s.viewport.x,
|
|
321
|
-
vy: s.viewport.y
|
|
322
|
-
};
|
|
323
|
-
return true;
|
|
324
|
-
},
|
|
325
|
-
onMove: (s) => {
|
|
326
|
-
if (!s.page || !anchor) return;
|
|
327
|
-
if (!dragging) {
|
|
328
|
-
if (Math.hypot(s.viewport.x - anchor.vx, s.viewport.y - anchor.vy) < DRAG_THRESHOLD_PX) return;
|
|
329
|
-
dragging = true;
|
|
330
|
-
selection.beginAt(anchor.pon, anchor.point);
|
|
331
|
-
}
|
|
332
|
-
selection.extendTo(s.page.pon, s.page.point);
|
|
333
|
-
},
|
|
334
|
-
onUp: () => {
|
|
335
|
-
anchor = null;
|
|
336
|
-
dragging = false;
|
|
337
|
-
selection.end();
|
|
338
|
-
},
|
|
339
|
-
onHover: (s) => {
|
|
340
|
-
if (!s.page) {
|
|
341
|
-
interaction.setCursor(CURSOR_TOKEN, null);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
selection.ensurePage(s.page.pon);
|
|
345
|
-
const overText = selection.isOverText(s.page.pon, s.page.point);
|
|
346
|
-
interaction.setCursor(CURSOR_TOKEN, overText ? "text" : null, 10);
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/reducer.ts
|
|
352
|
-
const initialSelectionState = {
|
|
353
|
-
selection: null,
|
|
354
|
-
segments: {},
|
|
355
|
-
loaded: {},
|
|
356
|
-
highlightHidden: false
|
|
357
|
-
};
|
|
358
|
-
const selectionReducer = (state, action) => {
|
|
359
|
-
switch (action.type) {
|
|
360
|
-
case "PAGE_LOADED": return state.loaded[action.pon] ? state : {
|
|
361
|
-
...state,
|
|
362
|
-
loaded: {
|
|
363
|
-
...state.loaded,
|
|
364
|
-
[action.pon]: true
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
case "SET": return {
|
|
368
|
-
...state,
|
|
369
|
-
selection: action.selection,
|
|
370
|
-
segments: action.segments
|
|
371
|
-
};
|
|
372
|
-
case "CLEAR": return state.selection === null && Object.keys(state.segments).length === 0 ? state : {
|
|
373
|
-
...state,
|
|
374
|
-
selection: null,
|
|
375
|
-
segments: {}
|
|
376
|
-
};
|
|
377
|
-
case "SET_HIGHLIGHT_HIDDEN": return state.highlightHidden === action.hidden ? state : {
|
|
378
|
-
...state,
|
|
379
|
-
highlightHidden: action.hidden
|
|
380
|
-
};
|
|
381
|
-
default: return state;
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/types.ts
|
|
386
|
-
const SelectionToken = createCapabilityToken("selection");
|
|
387
|
-
//#endregion
|
|
388
4
|
//#region src/selection.plugin.ts
|
|
389
5
|
/**
|
|
390
6
|
* Text selection — document-scoped, requires the interaction hub. In `init` it
|
|
391
7
|
* registers ITS pointer handler with the hub; the hub owns the pointer stream
|
|
392
8
|
* and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection
|
|
393
9
|
* only needs the page coordinate context + the engine's text geometry.
|
|
10
|
+
*
|
|
11
|
+
* Registry/content invalidation is wired inside the capability itself
|
|
12
|
+
* (revision watch + document event subscription), so headless/programmatic
|
|
13
|
+
* use gets the same reconciliation as the full viewer.
|
|
394
14
|
*/
|
|
395
15
|
const selectionPlugin = () => definePlugin({
|
|
396
16
|
id: "selection",
|
|
397
|
-
token: SelectionToken,
|
|
17
|
+
token: SelectionToken$1,
|
|
398
18
|
scope: "document",
|
|
399
19
|
requires: [InteractionToken],
|
|
400
20
|
initialState: initialSelectionState,
|
|
@@ -402,11 +22,20 @@ const selectionPlugin = () => definePlugin({
|
|
|
402
22
|
capability: createSelectionCapability,
|
|
403
23
|
init: (ctx) => {
|
|
404
24
|
const interaction = ctx.get(InteractionToken);
|
|
405
|
-
const selection = ctx.get(SelectionToken);
|
|
25
|
+
const selection = ctx.get(SelectionToken$1);
|
|
406
26
|
interaction.registerHandler(createTextSelectHandler(selection, interaction));
|
|
407
27
|
}
|
|
408
28
|
});
|
|
409
29
|
//#endregion
|
|
410
|
-
|
|
30
|
+
//#region src/index.ts
|
|
31
|
+
/**
|
|
32
|
+
* The selection capability token, narrowed to the public
|
|
33
|
+
* {@link SelectionCapability} lens. It is the SAME runtime token the plugin
|
|
34
|
+
* provides — the host-only surface (pointer gestures, geometry warming) is
|
|
35
|
+
* reachable only via `@embedpdf/plugin-selection/internal`.
|
|
36
|
+
*/
|
|
37
|
+
const SelectionToken = SelectionToken$1;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { SelectionToken, selectionPlugin };
|
|
411
40
|
|
|
412
41
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/geometry.ts","../src/capability.ts","../src/handler.ts","../src/reducer.ts","../src/types.ts","../src/selection.plugin.ts"],"sourcesContent":["/**\n * The selection plugin's COORDINATE SEAM — and nothing else.\n *\n * Segmentation, hit-testing, and word/line expansion live in the canonical\n * engine-core text layout (`@embedpdf/engine-core` `text/layout`): a text\n * range has exactly one segmentation whether it came from a drag or a\n * search. This module only converts between the viewer's content space\n * (y-down, crop-relative) and the engine's PDF space (y-up) — pointer\n * points inward, canonical segments outward. The PDF↔content y-flip is the\n * geometry package's `pageGeometry` (the one place that math lives), never\n * hand-rolled here.\n */\nimport {\n applyPoint,\n invert,\n pageGeometry,\n textQuadBounds,\n type Mat2D,\n type Point,\n type PointIn,\n type Rect,\n type TextQuad,\n} from '@embedpdf/core-geometry';\nimport {\n buildPageTextLayout,\n type PageGeometrySnapshot,\n type PageTextLayout,\n type PdfPoint,\n type PdfQuad,\n type PdfRect,\n type PdfTextSegment,\n} from '@embedpdf/engine-core/runtime';\n\n/**\n * One merged visual line of a selection, in content space — the structural\n * twin of the engine's `PdfTextSegment`. `quad` carries frame-geometric\n * corner semantics (upper = ascent side, start = the frame's −x); `advance`\n * is the READING direction along the baseline (+1 = toward `end`), derived\n * from the glyph sequence — geometry and bidi stay separate concerns.\n * `rect` is the content-space AABB.\n */\nexport interface SelectionSegment {\n quad: TextQuad;\n rect: Rect;\n advance: 1 | -1;\n}\n\n/** A page's cached selection geometry: the canonical layout + the seam. */\nexport interface SelectionPageGeometry {\n layout: PageTextLayout;\n toContent: Mat2D<'pdf', 'content'>;\n fromContent: Mat2D<'content', 'pdf'>;\n}\n\n/**\n * Build a page's selection geometry. Page display rotation does NOT enter\n * here — the overlay rides the page's CSS rotation, so content space stays\n * un-rotated (the crop-relative y-flip is the only conversion; zoom = 1,\n * the viewer's zoom is applied later by `PageTransform.pageToContent`).\n */\nexport function buildSelectionPageGeometry(\n snapshot: PageGeometrySnapshot,\n crop: PdfRect,\n rotation: 0 | 90 | 180 | 270,\n userUnit: number,\n): SelectionPageGeometry {\n const { pdfToContent } = pageGeometry({ crop, rotation, userUnit }, 1);\n return {\n layout: buildPageTextLayout(snapshot),\n toContent: pdfToContent,\n fromContent: invert(pdfToContent),\n };\n}\n\n/** A content-space pointer point in the layout's PDF space. */\nexport function contentPointToPdf(geom: SelectionPageGeometry, p: Point): PdfPoint {\n return applyPoint(geom.fromContent, p as PointIn<'content'>);\n}\n\n/**\n * A canonical PDF quad in content space. Engine quads carry frame-geometric\n * slot semantics (`p1..p4` = upper-start, upper-end, lower-start,\n * lower-end) — a trusted producer, so the y-flip maps corners straight onto\n * their names; imported-annotation normalization does NOT apply here.\n */\nexport function toContentTextQuad(geom: SelectionPageGeometry, quad: PdfQuad): TextQuad {\n const pt = (p: PdfPoint): Point => applyPoint(geom.toContent, p as PointIn<'pdf'>);\n return {\n upperStart: pt(quad.p1),\n upperEnd: pt(quad.p2),\n lowerStart: pt(quad.p3),\n lowerEnd: pt(quad.p4),\n };\n}\n\n/** A canonical segment in content space (`rect` recomputed from the mapped\n * quad so the pair can never drift). */\nexport function toContentSegment(\n geom: SelectionPageGeometry,\n segment: PdfTextSegment,\n): SelectionSegment {\n const quad = toContentTextQuad(geom, segment.quad);\n return { quad, rect: textQuadBounds(quad), advance: segment.advance };\n}\n","import type { PageObjectNumber, PluginContext } from '@embedpdf/core';\nimport { textQuadBounds, type Point } from '@embedpdf/core-geometry';\nimport {\n expandTextRangeToLine,\n expandTextRangeToWord,\n textGlyphAt,\n textGlyphQuad,\n textSegmentsForRange,\n} from '@embedpdf/engine-core/runtime';\nimport {\n buildSelectionPageGeometry,\n contentPointToPdf,\n toContentSegment,\n toContentTextQuad,\n type SelectionPageGeometry,\n type SelectionSegment,\n} from './geometry';\nimport type {\n GlyphPointer,\n SelectionAction,\n SelectionCapability,\n SelectionEndpoint,\n SelectionRange,\n SelectionSnapshot,\n SelectionState,\n} from './types';\n\nconst EMPTY_SEGMENTS: SelectionSegment[] = [];\n\n/**\n * The selection capability. The reducer state holds the selection range, the\n * derived content-space segments (per page), and per-page loaded flags; the\n * (large, non-serializable) per-page canonical layout is cached HERE in the\n * closure. Segmentation itself is the engine-core text layout — selection\n * owns gestures and state, never how glyphs become lines.\n *\n * Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph),\n * so a drag from page 2 into page 4 selects the tail of 2, all of 3, and the\n * head of 4. `recompute` rebuilds the merged line segments for every loaded\n * page in the span and re-runs whenever a mid-span page finishes loading.\n */\nexport function createSelectionCapability(\n ctx: PluginContext<SelectionState, SelectionAction>,\n): SelectionCapability {\n const cache = new Map<number, SelectionPageGeometry>();\n const pending = new Set<number>();\n // Consumers (e.g. text-markup) observe the selection without selection knowing\n // about them: `change` fires whenever the segments change, `commit` when a\n // gesture ends (pointer-up). One typed callback each — not an event bus.\n const changeCbs = new Set<() => void>();\n const commitCbs = new Set<() => void>();\n const fireChange = (): void => changeCbs.forEach((cb) => cb());\n\n const pageIndexOf = (pon: PageObjectNumber): number =>\n ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;\n const ponAtIndex = (i: number): PageObjectNumber | undefined =>\n ctx.document()?.pages[i]?.pageObjectNumber;\n\n const glyphAt = (geom: SelectionPageGeometry, point: Point): number | null =>\n textGlyphAt(geom.layout, contentPointToPdf(geom, point));\n\n // Order the two ends of a selection by document position (page, then glyph).\n function orderedEnds(sel: SelectionRange): {\n start: GlyphPointer;\n end: GlyphPointer;\n direction: 'forward' | 'backward';\n } {\n const ai = pageIndexOf(sel.anchor.pon);\n const fi = pageIndexOf(sel.focus.pon);\n const anchorFirst = ai < fi || (ai === fi && sel.anchor.glyph <= sel.focus.glyph);\n return anchorFirst\n ? { start: sel.anchor, end: sel.focus, direction: 'forward' }\n : { start: sel.focus, end: sel.anchor, direction: 'backward' };\n }\n\n function endpointFor(ptr: GlyphPointer, which: 'start' | 'end'): SelectionEndpoint | null {\n const segments = ctx.getState().segments[ptr.pon] ?? EMPTY_SEGMENTS;\n if (!segments.length) return null;\n const segment = which === 'start' ? segments[0] : segments[segments.length - 1];\n\n // Anchor the endpoint to the boundary GLYPH's own oriented cell so caret\n // placement lands on the exact character edge; fall back to the segment\n // when the glyph is degenerate (e.g. a generated space).\n const geom = cache.get(ptr.pon);\n const cell = geom ? textGlyphQuad(geom.layout, ptr.glyph) : null;\n if (geom && cell) {\n const glyphQuad = toContentTextQuad(geom, cell);\n return {\n pon: ptr.pon,\n glyphQuad,\n advance: segment.advance,\n rect: textQuadBounds(glyphQuad),\n };\n }\n return { pon: ptr.pon, glyphQuad: segment.quad, advance: segment.advance, rect: segment.rect };\n }\n\n function snapshot(): SelectionSnapshot {\n const { selection, segments } = ctx.getState();\n const pages = Object.keys(segments)\n .map(Number)\n .filter((pon) => (segments[pon]?.length ?? 0) > 0)\n .map((pon) => ({ pon: pon as PageObjectNumber, segments: segments[pon] }));\n if (!selection) return { pages, start: null, end: null, direction: 'forward' };\n const { start, end, direction } = orderedEnds(selection);\n return {\n pages,\n start: endpointFor(start, 'start'),\n end: endpointFor(end, 'end'),\n direction,\n };\n }\n\n function ensurePage(pon: PageObjectNumber): void {\n if (cache.has(pon) || pending.has(pon)) return;\n const doc = ctx.doc;\n const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!doc || !layout) return;\n pending.add(pon);\n doc\n .page(pon)\n .geometry.read()\n .then(\n (snapshot) => {\n pending.delete(pon);\n cache.set(\n pon,\n buildSelectionPageGeometry(\n snapshot,\n layout.boxes.crop,\n layout.rotation,\n layout.userUnit,\n ),\n );\n ctx.dispatch({ type: 'PAGE_LOADED', pon });\n if (ctx.getState().selection) recompute(); // a mid-span page arrived → fill its segments\n },\n () => {\n pending.delete(pon); // doc closed / read aborted — ignore\n },\n );\n }\n\n // Rebuild merged line segments for every loaded page in the span; ensure the rest.\n function recompute(sel: SelectionRange | null = ctx.getState().selection): void {\n if (!sel) return;\n const { start, end } = orderedEnds(sel);\n const si = pageIndexOf(start.pon);\n const ei = pageIndexOf(end.pon);\n if (si < 0 || ei < 0) return;\n const segments: Record<number, SelectionSegment[]> = {};\n for (let i = si; i <= ei; i++) {\n const pon = ponAtIndex(i);\n if (pon == null) continue;\n const geom = cache.get(pon);\n if (!geom) {\n ensurePage(pon); // not loaded yet — it'll recompute when ready\n continue;\n }\n const from = i === si ? start.glyph : 0;\n const to = i === ei ? end.glyph : geom.layout.glyphs.length - 1;\n segments[pon] = textSegmentsForRange(geom.layout, from, to - from + 1).map((s) =>\n toContentSegment(geom, s),\n );\n }\n ctx.dispatch({ type: 'SET', selection: sel, segments });\n fireChange();\n }\n\n // Set the selection to a flat [from,to] glyph span on one page (word/line).\n function selectSpan(pon: PageObjectNumber, point: Point, expand: 'word' | 'line'): void {\n const geom = cache.get(pon);\n if (!geom) return;\n const i = glyphAt(geom, point);\n if (i == null) return;\n const [from, to] =\n expand === 'word'\n ? expandTextRangeToWord(geom.layout, i)\n : expandTextRangeToLine(geom.layout, i);\n recompute({ anchor: { pon, glyph: from }, focus: { pon, glyph: to } });\n }\n\n return {\n ensurePage,\n\n isLoaded: (pon) => !!ctx.getState().loaded[pon],\n\n isOverText: (pon, point: Point) => {\n const geom = cache.get(pon);\n return geom ? glyphAt(geom, point) != null : false;\n },\n\n beginAt: (pon, point: Point) => {\n const geom = cache.get(pon);\n if (!geom) return false;\n const i = glyphAt(geom, point);\n if (i == null) return false; // not near text — caller deselects, doesn't capture\n recompute({ anchor: { pon, glyph: i }, focus: { pon, glyph: i } });\n return true;\n },\n\n selectWord: (pon, point: Point) => selectSpan(pon, point, 'word'),\n selectLine: (pon, point: Point) => selectSpan(pon, point, 'line'),\n\n extendTo: (pon, point: Point) => {\n const cur = ctx.getState().selection;\n if (!cur) return;\n const geom = cache.get(pon);\n if (!geom) {\n ensurePage(pon); // dragged onto a not-yet-loaded page — warm it, recompute on load\n return;\n }\n const i = glyphAt(geom, point);\n if (i == null) return; // off-text — keep the last focus\n recompute({ anchor: cur.anchor, focus: { pon, glyph: i } });\n },\n\n end: () => commitCbs.forEach((cb) => cb()), // the gesture ended (pointer-up) → notify consumers\n\n clear: () => {\n ctx.dispatch({ type: 'CLEAR' });\n fireChange();\n },\n\n snapshot,\n\n segmentsForPage: (pon) => ctx.getState().segments[pon] ?? EMPTY_SEGMENTS,\n\n rectsForPage: (pon) =>\n (ctx.getState().segments[pon] ?? EMPTY_SEGMENTS).map((s) => s.rect),\n\n hasSelection: () => ctx.getState().selection != null,\n\n selectedPages: () => {\n const { segments } = ctx.getState();\n return Object.keys(segments)\n .map(Number)\n .filter((pon) => (segments[pon]?.length ?? 0) > 0) as PageObjectNumber[];\n },\n\n onChange: (cb) => {\n changeCbs.add(cb);\n return () => changeCbs.delete(cb);\n },\n onCommit: (cb) => {\n commitCbs.add(cb);\n return () => commitCbs.delete(cb);\n },\n\n setHighlightVisible: (visible) =>\n ctx.dispatch({ type: 'SET_HIGHLIGHT_HIDDEN', hidden: !visible }),\n highlightVisible: () => !ctx.getState().highlightHidden,\n };\n}\n","import type { InteractionCapability, InteractionHandler } from '@embedpdf/plugin-interaction';\nimport type { SelectionCapability } from './types';\n\nconst CURSOR_TOKEN = 'selection-text';\nconst DRAG_THRESHOLD_PX = 4; // viewport px the pointer must move before a drag-select begins\n\n/**\n * The text-selection pointer handler. It is live only under tools that enable the\n * `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so\n * switching to pan disables text selection with zero special-casing here).\n *\n * - single click → deselect; records an anchor but selects NOTHING until\n * the pointer moves past the drag threshold (Chrome/Acrobat feel)\n * - single-click + drag → caret selection from the anchor (extends across pages)\n * - double-click → select the word\n * - triple-click → select the visual line\n * - down off-text → deselect, and DON'T capture (so it can't block)\n * - hover → I-beam only when over text, else the pointer cursor\n */\nexport function createTextSelectHandler(\n selection: SelectionCapability,\n interaction: InteractionCapability,\n): InteractionHandler {\n // Per-gesture drag-threshold state (one active gesture at a time — the hub owner).\n let anchor: { pon: number; point: { x: number; y: number }; vx: number; vy: number } | null =\n null;\n let dragging = false;\n\n return {\n id: 'text-select',\n priority: 60,\n enabledFor: (tool) => tool.enables.has('text-select'),\n onDown: (s) => {\n anchor = null;\n dragging = false;\n if (!s.page) return false; // over a gap — let a lower handler (e.g. scroll) try\n const { pon, point } = s.page;\n const clicks = s.clickCount ?? 1;\n if (clicks >= 3) {\n selection.selectLine(pon, point);\n return true;\n }\n if (clicks === 2) {\n selection.selectWord(pon, point);\n return true;\n }\n // Single click: clear immediately (clicking deselects), then record an anchor\n // ONLY if over text — but begin no selection until the drag threshold is met.\n selection.clear();\n if (!selection.isOverText(pon, point)) return false; // empty space → don't capture\n anchor = { pon, point, vx: s.viewport.x, vy: s.viewport.y };\n return true;\n },\n onMove: (s) => {\n if (!s.page || !anchor) return;\n if (!dragging) {\n if (Math.hypot(s.viewport.x - anchor.vx, s.viewport.y - anchor.vy) < DRAG_THRESHOLD_PX) {\n return; // still a click, not a drag — select nothing yet\n }\n dragging = true;\n selection.beginAt(anchor.pon, anchor.point); // open the selection at the anchor\n }\n selection.extendTo(s.page.pon, s.page.point);\n },\n onUp: () => {\n anchor = null;\n dragging = false;\n selection.end();\n },\n onHover: (s) => {\n if (!s.page) {\n interaction.setCursor(CURSOR_TOKEN, null); // off the page → pointer\n return;\n }\n // Warm geometry on first hover; show the I-beam ONLY when actually over text.\n selection.ensurePage(s.page.pon);\n const overText = selection.isOverText(s.page.pon, s.page.point);\n interaction.setCursor(CURSOR_TOKEN, overText ? 'text' : null, 10);\n },\n };\n}\n","import type { SelectionAction, SelectionState } from './types';\n\nexport const initialSelectionState: SelectionState = {\n selection: null,\n segments: {},\n loaded: {},\n highlightHidden: false,\n};\n\nexport const selectionReducer = (\n state: SelectionState,\n action: SelectionAction,\n): SelectionState => {\n switch (action.type) {\n case 'PAGE_LOADED':\n return state.loaded[action.pon]\n ? state\n : { ...state, loaded: { ...state.loaded, [action.pon]: true } };\n case 'SET':\n return { ...state, selection: action.selection, segments: action.segments };\n case 'CLEAR':\n return state.selection === null && Object.keys(state.segments).length === 0\n ? state\n : { ...state, selection: null, segments: {} };\n case 'SET_HIGHLIGHT_HIDDEN':\n return state.highlightHidden === action.hidden\n ? state\n : { ...state, highlightHidden: action.hidden };\n default:\n return state;\n }\n};\n","import { createCapabilityToken, type PageObjectNumber } from '@embedpdf/core';\nimport type { Point, Rect, TextQuad } from '@embedpdf/core-geometry';\nimport type { SelectionSegment } from './geometry';\n\nexport type { SelectionSegment } from './geometry';\n\n/** A glyph address: a page + a flat glyph index within that page's geometry. */\nexport interface GlyphPointer {\n pon: PageObjectNumber;\n glyph: number;\n}\n\n/** Anchor = where the drag began, focus = the current end. Inclusive. */\nexport interface SelectionRange {\n anchor: GlyphPointer;\n focus: GlyphPointer;\n}\n\n/**\n * A selection boundary, anchored to the boundary GLYPH's own oriented cell.\n * `advance` is the reading direction of the segment it belongs to (+1 = the\n * frame's +x), so caret consumers place at the trailing edge without\n * re-deriving bidi from geometry. `rect` is the AABB (scroll targets).\n */\nexport interface SelectionEndpoint {\n pon: PageObjectNumber;\n glyphQuad: TextQuad;\n advance: 1 | -1;\n rect: Rect;\n}\n\nexport interface SelectionSnapshot {\n /** Per-page canonical segments — the ONE geometry consumers act on.\n * Boxes are derived views (`segment.rect`, or `rectsForPage()`). */\n pages: Array<{ pon: PageObjectNumber; segments: SelectionSegment[] }>;\n start: SelectionEndpoint | null;\n end: SelectionEndpoint | null;\n direction: 'forward' | 'backward';\n}\n\nexport interface SelectionState {\n selection: SelectionRange | null;\n /** Derived per-line segments per page, in CONTENT space (y-down, PDF units). */\n segments: Record<number, SelectionSegment[]>;\n /** Pages whose text geometry has loaded (so the layer re-renders when ready). */\n loaded: Record<number, boolean>;\n /** When a consumer owns the selection visual (e.g. a markup tool draws its own\n * preview), the default highlight rects are suppressed. */\n highlightHidden: boolean;\n}\n\nexport type SelectionAction =\n | { type: 'PAGE_LOADED'; pon: PageObjectNumber }\n | { type: 'SET'; selection: SelectionRange; segments: Record<number, SelectionSegment[]> }\n | { type: 'CLEAR' }\n | { type: 'SET_HIGHLIGHT_HIDDEN'; hidden: boolean };\n\nexport interface SelectionCapability {\n /** Warm a page's text geometry (idempotent). Layers call this when a page mounts. */\n ensurePage(pon: PageObjectNumber): void;\n isLoaded(pon: PageObjectNumber): boolean;\n /** Is a content-space point on (or near) text? Drives the I-beam vs pointer cursor. */\n isOverText(pon: PageObjectNumber, point: Point): boolean;\n /** Begin a caret selection at a page point. Returns false if not near any text. */\n beginAt(pon: PageObjectNumber, point: Point): boolean;\n /** Double-click: select the word around the point. */\n selectWord(pon: PageObjectNumber, point: Point): void;\n /** Triple-click: select the whole visual line around the point. */\n selectLine(pon: PageObjectNumber, point: Point): void;\n /** Extend the current selection to a page point (drag). */\n extendTo(pon: PageObjectNumber, point: Point): void;\n end(): void;\n clear(): void;\n /** Coherent read-model for consumers that create annotations or selection UI. */\n snapshot(): SelectionSnapshot;\n /** Per-line oriented segments for a page, in content space — the layer's input. */\n segmentsForPage(pon: PageObjectNumber): SelectionSegment[];\n /** The segments' AABBs — for consumers that genuinely want boxes (scroll,\n * conservative regions). Never a substitute for the oriented quads in\n * geometry that gets drawn or persisted. */\n rectsForPage(pon: PageObjectNumber): Rect[];\n hasSelection(): boolean;\n /** The pages the current selection covers (those with at least one rect) — so a\n * cross-page action (e.g. text-markup creation) can fan out per page. */\n selectedPages(): PageObjectNumber[];\n /** Fires whenever the selection rects change (e.g. drag-extend) — for live preview. */\n onChange(cb: () => void): () => void;\n /** Fires when a selection gesture ends (pointer-up) — the commit point. */\n onCommit(cb: () => void): () => void;\n /** Suppress / restore the default highlight visual (a consumer drawing its own). */\n setHighlightVisible(visible: boolean): void;\n highlightVisible(): boolean;\n}\n\nexport const SelectionToken = createCapabilityToken<SelectionCapability>('selection');\n","import { definePlugin } from '@embedpdf/core';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n interaction.registerHandler(createTextSelectHandler(selection, interaction));\n },\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["SelectionToken","SelectionHostToken"],"sources":["../src/selection.plugin.ts","../src/index.ts"],"sourcesContent":["import { definePlugin } from '@embedpdf/core';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionHostCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n *\n * Registry/content invalidation is wired inside the capability itself\n * (revision watch + document event subscription), so headless/programmatic\n * use gets the same reconciliation as the full viewer.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionHostCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n interaction.registerHandler(createTextSelectHandler(selection, interaction));\n },\n });\n","/**\n * @embedpdf/plugin-selection — text selection over the engine's text geometry.\n *\n * The PUBLIC surface: the plugin factory, the capability token (narrowed to\n * the public lens), and the range/read vocabulary. Selection ranges live in\n * CHARACTER space (half-open `TextRange` — the same space search hits\n * address), geometry needs `doc.text.select`, text extraction needs\n * `doc.text.copy`, and neither permission implies the other.\n *\n * Framework/host plumbing (gesture intents, geometry warming, the reducer,\n * the coordinate seam) lives behind `@embedpdf/plugin-selection/internal`.\n * Clipboard writes live in `@embedpdf/web` — this package is DOM-free.\n */\nimport type { CapabilityToken } from '@embedpdf/core';\nimport { SelectionToken as SelectionHostToken } from './types';\nimport type { SelectionCapability } from './types';\n\nexport { selectionPlugin } from './selection.plugin';\nexport type {\n SelectionCapability,\n SelectionEndpoint,\n SelectionMenuAnchor,\n SelectionRangeInput,\n SelectionSnapshot,\n TextPosition,\n TextRange,\n} from './types';\nexport type { SelectionSegment } from './geometry';\n\n/**\n * The selection capability token, narrowed to the public\n * {@link SelectionCapability} lens. It is the SAME runtime token the plugin\n * provides — the host-only surface (pointer gestures, geometry warming) is\n * reachable only via `@embedpdf/plugin-selection/internal`.\n */\nexport const SelectionToken =\n SelectionHostToken as unknown as CapabilityToken<SelectionCapability>;\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAa,wBACX,aAAuE;CACrE,IAAI;CACJ,OAAOA;CACP,OAAO;CACP,UAAU,CAAC,gBAAgB;CAC3B,cAAc;CACd,QAAQ;CACR,YAAY;CACZ,OAAO,QAAQ;EACb,MAAM,cAAc,IAAI,IAAI,gBAAgB;EAC5C,MAAM,YAAY,IAAI,IAAIA,gBAAc;EACxC,YAAY,gBAAgB,wBAAwB,WAAW,WAAW,CAAC;CAC7E;AACF,CAAC;;;;;;;;;ACGH,MAAa,iBACXC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_reducer = require("./reducer-Dyf8p5h-.cjs");
|
|
3
|
+
exports.SelectionToken = require_reducer.SelectionToken;
|
|
4
|
+
exports.buildSelectionPageGeometry = require_reducer.buildSelectionPageGeometry;
|
|
5
|
+
exports.contentPointToPdf = require_reducer.contentPointToPdf;
|
|
6
|
+
exports.createSelectionCapability = require_reducer.createSelectionCapability;
|
|
7
|
+
exports.createTextSelectHandler = require_reducer.createTextSelectHandler;
|
|
8
|
+
exports.initialSelectionState = require_reducer.initialSelectionState;
|
|
9
|
+
exports.selectionReducer = require_reducer.selectionReducer;
|
|
10
|
+
exports.toContentSegment = require_reducer.toContentSegment;
|
|
11
|
+
exports.toContentTextQuad = require_reducer.toContentTextQuad;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { _ as contentPointToPdf, a as SelectionHostCapability, d as SelectionToken, g as buildSelectionPageGeometry, m as SelectionPageGeometry, n as SelectionAction, s as SelectionRange, t as GlyphPointer, u as SelectionState, v as toContentSegment, y as toContentTextQuad } from "./types-DtWaZIsm.cjs";
|
|
2
|
+
import { PluginContext } from "@embedpdf/core";
|
|
3
|
+
import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
|
|
4
|
+
//#region src/capability.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The selection capability — ONE closure returning the host lens (the
|
|
7
|
+
* public lens is the same object, narrowed at the package root).
|
|
8
|
+
*
|
|
9
|
+
* State model: the reducer holds the selection range and the derived
|
|
10
|
+
* content-space segments; the large, non-serializable per-page caches live
|
|
11
|
+
* HERE in the closure, split by what invalidates them:
|
|
12
|
+
*
|
|
13
|
+
* - `rawGeometry` — PDF-space geometry snapshots, rotation-independent
|
|
14
|
+
* (pages load normalized; see `PageRotateResult`). Dropped only when a
|
|
15
|
+
* page leaves the registry or page CONTENT changes (redaction apply /
|
|
16
|
+
* flatten).
|
|
17
|
+
* - `derived` — the content-space transform + layout, keyed by the
|
|
18
|
+
* layout params (crop/rotation/userUnit) so a view-rotate re-derives
|
|
19
|
+
* from the cached raw snapshot with NO refetch.
|
|
20
|
+
* - `textSnapshots` — per-page text (for `readText`), same content-only
|
|
21
|
+
* invalidation as `rawGeometry`.
|
|
22
|
+
*
|
|
23
|
+
* Permission model: `doc.text.select` gates geometry (selection exists),
|
|
24
|
+
* `doc.text.copy` gates text (extraction) — neither implies the other.
|
|
25
|
+
* Host gesture functions are INERT when select is denied (`ensurePage`
|
|
26
|
+
* no-ops, so nothing warms and nothing hit-tests: the pointer handler
|
|
27
|
+
* needs no special-casing); the public `select()` THROWS `PermissionDenied`
|
|
28
|
+
* so programmatic misuse is loud; `clear()` is always allowed. The engine
|
|
29
|
+
* independently enforces both scopes — these gates exist so the plugin
|
|
30
|
+
* never issues a request that is guaranteed to fail.
|
|
31
|
+
*
|
|
32
|
+
* Selection is cross-page: glyphs are ordered globally by (pageIndex,
|
|
33
|
+
* glyph), so a drag from page 2 into page 4 selects the tail of 2, all of
|
|
34
|
+
* 3, and the head of 4. `recompute` rebuilds the merged line segments for
|
|
35
|
+
* every loaded page in the span, re-runs whenever a mid-span page finishes
|
|
36
|
+
* loading, and CLAMPS stored endpoints once their page's geometry is known
|
|
37
|
+
* (so `selectAll`'s open-ended focus settles to the real last character).
|
|
38
|
+
*/
|
|
39
|
+
declare function createSelectionCapability(ctx: PluginContext<SelectionState, SelectionAction>): SelectionHostCapability;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/handler.d.ts
|
|
42
|
+
/**
|
|
43
|
+
* The text-selection pointer handler. It is live only under tools that enable the
|
|
44
|
+
* `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
|
|
45
|
+
* switching to pan disables text selection with zero special-casing here).
|
|
46
|
+
*
|
|
47
|
+
* - single click → deselect; records an anchor but selects NOTHING until
|
|
48
|
+
* the pointer moves past the drag threshold (Chrome/Acrobat feel)
|
|
49
|
+
* - single-click + drag → caret selection from the anchor (extends across pages)
|
|
50
|
+
* - double-click → select the word
|
|
51
|
+
* - triple-click → select the visual line
|
|
52
|
+
* - down off-text → deselect, and DON'T capture (so it can't block)
|
|
53
|
+
* - hover → I-beam only when over text, else the pointer cursor
|
|
54
|
+
*/
|
|
55
|
+
declare function createTextSelectHandler(selection: SelectionHostCapability, interaction: InteractionCapability): InteractionHandler;
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/reducer.d.ts
|
|
58
|
+
declare const initialSelectionState: SelectionState;
|
|
59
|
+
declare const selectionReducer: (state: SelectionState, action: SelectionAction) => SelectionState;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { type GlyphPointer, type SelectionAction, type SelectionHostCapability, type SelectionPageGeometry, type SelectionRange, type SelectionState, SelectionToken, buildSelectionPageGeometry, contentPointToPdf, createSelectionCapability, createTextSelectHandler, initialSelectionState, selectionReducer, toContentSegment, toContentTextQuad };
|
|
62
|
+
//# sourceMappingURL=internal.d.cts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { _ as contentPointToPdf, a as SelectionHostCapability, d as SelectionToken, g as buildSelectionPageGeometry, m as SelectionPageGeometry, n as SelectionAction, s as SelectionRange, t as GlyphPointer, u as SelectionState, v as toContentSegment, y as toContentTextQuad } from "./types-DtWaZIsm.js";
|
|
2
|
+
import { PluginContext } from "@embedpdf/core";
|
|
3
|
+
import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
|
|
4
|
+
//#region src/capability.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The selection capability — ONE closure returning the host lens (the
|
|
7
|
+
* public lens is the same object, narrowed at the package root).
|
|
8
|
+
*
|
|
9
|
+
* State model: the reducer holds the selection range and the derived
|
|
10
|
+
* content-space segments; the large, non-serializable per-page caches live
|
|
11
|
+
* HERE in the closure, split by what invalidates them:
|
|
12
|
+
*
|
|
13
|
+
* - `rawGeometry` — PDF-space geometry snapshots, rotation-independent
|
|
14
|
+
* (pages load normalized; see `PageRotateResult`). Dropped only when a
|
|
15
|
+
* page leaves the registry or page CONTENT changes (redaction apply /
|
|
16
|
+
* flatten).
|
|
17
|
+
* - `derived` — the content-space transform + layout, keyed by the
|
|
18
|
+
* layout params (crop/rotation/userUnit) so a view-rotate re-derives
|
|
19
|
+
* from the cached raw snapshot with NO refetch.
|
|
20
|
+
* - `textSnapshots` — per-page text (for `readText`), same content-only
|
|
21
|
+
* invalidation as `rawGeometry`.
|
|
22
|
+
*
|
|
23
|
+
* Permission model: `doc.text.select` gates geometry (selection exists),
|
|
24
|
+
* `doc.text.copy` gates text (extraction) — neither implies the other.
|
|
25
|
+
* Host gesture functions are INERT when select is denied (`ensurePage`
|
|
26
|
+
* no-ops, so nothing warms and nothing hit-tests: the pointer handler
|
|
27
|
+
* needs no special-casing); the public `select()` THROWS `PermissionDenied`
|
|
28
|
+
* so programmatic misuse is loud; `clear()` is always allowed. The engine
|
|
29
|
+
* independently enforces both scopes — these gates exist so the plugin
|
|
30
|
+
* never issues a request that is guaranteed to fail.
|
|
31
|
+
*
|
|
32
|
+
* Selection is cross-page: glyphs are ordered globally by (pageIndex,
|
|
33
|
+
* glyph), so a drag from page 2 into page 4 selects the tail of 2, all of
|
|
34
|
+
* 3, and the head of 4. `recompute` rebuilds the merged line segments for
|
|
35
|
+
* every loaded page in the span, re-runs whenever a mid-span page finishes
|
|
36
|
+
* loading, and CLAMPS stored endpoints once their page's geometry is known
|
|
37
|
+
* (so `selectAll`'s open-ended focus settles to the real last character).
|
|
38
|
+
*/
|
|
39
|
+
declare function createSelectionCapability(ctx: PluginContext<SelectionState, SelectionAction>): SelectionHostCapability;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/handler.d.ts
|
|
42
|
+
/**
|
|
43
|
+
* The text-selection pointer handler. It is live only under tools that enable the
|
|
44
|
+
* `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
|
|
45
|
+
* switching to pan disables text selection with zero special-casing here).
|
|
46
|
+
*
|
|
47
|
+
* - single click → deselect; records an anchor but selects NOTHING until
|
|
48
|
+
* the pointer moves past the drag threshold (Chrome/Acrobat feel)
|
|
49
|
+
* - single-click + drag → caret selection from the anchor (extends across pages)
|
|
50
|
+
* - double-click → select the word
|
|
51
|
+
* - triple-click → select the visual line
|
|
52
|
+
* - down off-text → deselect, and DON'T capture (so it can't block)
|
|
53
|
+
* - hover → I-beam only when over text, else the pointer cursor
|
|
54
|
+
*/
|
|
55
|
+
declare function createTextSelectHandler(selection: SelectionHostCapability, interaction: InteractionCapability): InteractionHandler;
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/reducer.d.ts
|
|
58
|
+
declare const initialSelectionState: SelectionState;
|
|
59
|
+
declare const selectionReducer: (state: SelectionState, action: SelectionAction) => SelectionState;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { type GlyphPointer, type SelectionAction, type SelectionHostCapability, type SelectionPageGeometry, type SelectionRange, type SelectionState, SelectionToken, buildSelectionPageGeometry, contentPointToPdf, createSelectionCapability, createTextSelectHandler, initialSelectionState, selectionReducer, toContentSegment, toContentTextQuad };
|
|
62
|
+
//# sourceMappingURL=internal.d.ts.map
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as buildSelectionPageGeometry, c as toContentTextQuad, i as createSelectionCapability, l as SelectionToken, n as selectionReducer, o as contentPointToPdf, r as createTextSelectHandler, s as toContentSegment, t as initialSelectionState } from "./reducer-CZG2TZgo.js";
|
|
2
|
+
export { SelectionToken, buildSelectionPageGeometry, contentPointToPdf, createSelectionCapability, createTextSelectHandler, initialSelectionState, selectionReducer, toContentSegment, toContentTextQuad };
|