@embedpdf/plugin-selection 3.0.0-next.14 → 3.0.0-next.16

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.
Files changed (41) hide show
  1. package/dist/contract/host.cjs +16 -2
  2. package/dist/contract/host.cjs.map +1 -0
  3. package/dist/contract/host.d.cts +3 -2
  4. package/dist/contract/host.d.ts +3 -2
  5. package/dist/contract/host.js +16 -2
  6. package/dist/contract/host.js.map +1 -0
  7. package/dist/contract-BUiw1d8F.cjs +36 -0
  8. package/dist/contract-BUiw1d8F.cjs.map +1 -0
  9. package/dist/contract-DBXA64ep.js +26 -0
  10. package/dist/contract-DBXA64ep.js.map +1 -0
  11. package/dist/contract-DLs4vXVt.d.cts +252 -0
  12. package/dist/contract-DLs4vXVt.d.ts +252 -0
  13. package/dist/contract.cjs +3 -6
  14. package/dist/contract.d.cts +2 -7
  15. package/dist/contract.d.ts +2 -7
  16. package/dist/contract.js +2 -7
  17. package/dist/host-contract-DO2aLU2e.d.ts +34 -0
  18. package/dist/host-contract-Ddo4PcJJ.d.cts +34 -0
  19. package/dist/index.cjs +777 -51
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +97 -29
  22. package/dist/index.d.ts +97 -29
  23. package/dist/index.js +772 -51
  24. package/dist/index.js.map +1 -1
  25. package/package.json +5 -9
  26. package/dist/contract.cjs.map +0 -1
  27. package/dist/contract.js.map +0 -1
  28. package/dist/internal.cjs +0 -13
  29. package/dist/internal.d.cts +0 -63
  30. package/dist/internal.d.ts +0 -63
  31. package/dist/internal.js +0 -4
  32. package/dist/reducer-BAOQ2CJ8.cjs +0 -732
  33. package/dist/reducer-BAOQ2CJ8.cjs.map +0 -1
  34. package/dist/reducer-DbKtk9Xl.js +0 -685
  35. package/dist/reducer-DbKtk9Xl.js.map +0 -1
  36. package/dist/types-BVUcOsCh.js +0 -14
  37. package/dist/types-BVUcOsCh.js.map +0 -1
  38. package/dist/types-CUrUVl6K.d.cts +0 -292
  39. package/dist/types-CUrUVl6K.d.ts +0 -292
  40. package/dist/types-D9kEBMFj.cjs +0 -18
  41. package/dist/types-D9kEBMFj.cjs.map +0 -1
@@ -1,685 +0,0 @@
1
- import { applyPoint, invert, pageGeometry, textQuadBounds } from "@embedpdf/core-geometry";
2
- import { PermissionDenied, buildPageTextLayout, expandTextRangeToLine, expandTextRangeToWord, sliceTextByChars, textGlyphAt, textGlyphQuad, textSegmentsForRange } from "@embedpdf/engine-core/runtime";
3
- //#region src/geometry.ts
4
- /**
5
- * The selection plugin's COORDINATE SEAM — and nothing else.
6
- *
7
- * Segmentation, hit-testing, and word/line expansion live in the canonical
8
- * engine-core text layout (`@embedpdf/engine-core` `text/layout`): a text
9
- * range has exactly one segmentation whether it came from a drag or a
10
- * search. This module only converts between the viewer's content space
11
- * (y-down, crop-relative) and the engine's PDF space (y-up) — pointer
12
- * points inward, canonical segments outward. The PDF↔content y-flip is the
13
- * geometry package's `pageGeometry` (the one place that math lives), never
14
- * hand-rolled here.
15
- */
16
- /**
17
- * Build a page's selection geometry. Page display rotation does NOT enter
18
- * here — the overlay rides the page's CSS rotation, so content space stays
19
- * un-rotated (the crop-relative y-flip is the only conversion; zoom = 1,
20
- * the viewer's zoom is applied later by `PageTransform.toPixels`).
21
- */
22
- function buildSelectionPageGeometry(snapshot, crop, rotation, userUnit) {
23
- const { pdfToContent } = pageGeometry({
24
- crop,
25
- rotation,
26
- userUnit
27
- }, 1);
28
- return {
29
- layout: buildPageTextLayout(snapshot),
30
- toContent: pdfToContent,
31
- fromContent: invert(pdfToContent)
32
- };
33
- }
34
- /** A content-space pointer point in the layout's PDF space. */
35
- function contentPointToPdf(geom, p) {
36
- return applyPoint(geom.fromContent, p);
37
- }
38
- /**
39
- * A canonical PDF quad in content space. Engine quads carry frame-geometric
40
- * slot semantics (`p1..p4` = upper-start, upper-end, lower-start,
41
- * lower-end) — a trusted producer, so the y-flip maps corners straight onto
42
- * their names; imported-annotation normalization does NOT apply here.
43
- */
44
- function toContentTextQuad(geom, quad) {
45
- const pt = (p) => applyPoint(geom.toContent, p);
46
- return {
47
- upperStart: pt(quad.p1),
48
- upperEnd: pt(quad.p2),
49
- lowerStart: pt(quad.p3),
50
- lowerEnd: pt(quad.p4)
51
- };
52
- }
53
- /** A canonical segment in content space (`rect` recomputed from the mapped
54
- * quad so the pair can never drift). */
55
- function toContentSegment(geom, segment) {
56
- const quad = toContentTextQuad(geom, segment.quad);
57
- return {
58
- quad,
59
- rect: textQuadBounds(quad),
60
- advance: segment.advance
61
- };
62
- }
63
- //#endregion
64
- //#region src/capability.ts
65
- const EMPTY_SEGMENTS = [];
66
- /** How many page-text reads `readText()` keeps in flight at once. */
67
- const TEXT_READ_CONCURRENCY = 8;
68
- /**
69
- * The selection capability — ONE closure returning the host lens (the
70
- * public lens is the same object, narrowed at the package root).
71
- *
72
- * State model: the reducer holds the selection range and the derived
73
- * content-space segments; the large, non-serializable per-page caches live
74
- * HERE in the closure, split by what invalidates them:
75
- *
76
- * - `rawGeometry` — PDF-space geometry snapshots, rotation-independent
77
- * (pages load normalized; see `PageRotateResult`). Dropped only when a
78
- * page leaves the registry or page CONTENT changes (redaction apply /
79
- * flatten).
80
- * - `derived` — the content-space transform + layout, keyed by the
81
- * layout params (crop/rotation/userUnit) so a view-rotate re-derives
82
- * from the cached raw snapshot with NO refetch.
83
- * - `textSnapshots` — per-page text (for `readText`), same content-only
84
- * invalidation as `rawGeometry`.
85
- *
86
- * Permission model: `doc.text.select` gates geometry (selection exists),
87
- * `doc.text.copy` gates text (extraction) — neither implies the other.
88
- * Host gesture functions are INERT when select is denied (`ensurePage`
89
- * no-ops, so nothing warms and nothing hit-tests: the pointer handler
90
- * needs no special-casing); the public `select()` THROWS `PermissionDenied`
91
- * so programmatic misuse is loud; `clear()` is always allowed. The engine
92
- * independently enforces both scopes — these gates exist so the plugin
93
- * never issues a request that is guaranteed to fail.
94
- *
95
- * Selection is cross-page: glyphs are ordered globally by (pageIndex,
96
- * glyph), so a drag from page 2 into page 4 selects the tail of 2, all of
97
- * 3, and the head of 4. `recompute` rebuilds the merged line segments for
98
- * every loaded page in the span, re-runs whenever a mid-span page finishes
99
- * loading, and CLAMPS stored endpoints once their page's geometry is known
100
- * (so `selectAll`'s open-ended focus settles to the real last character).
101
- */
102
- function createSelectionCapability(ctx) {
103
- const rawGeometry = /* @__PURE__ */ new Map();
104
- const derived = /* @__PURE__ */ new Map();
105
- const pending = /* @__PURE__ */ new Set();
106
- const textSnapshots = /* @__PURE__ */ new Map();
107
- /** Bumped on content invalidation; in-flight reads from before are discarded. */
108
- let epoch = 0;
109
- const changeCbs = /* @__PURE__ */ new Set();
110
- const commitCbs = /* @__PURE__ */ new Set();
111
- const fireChange = () => changeCbs.forEach((cb) => cb());
112
- const canSelect = () => ctx.doc?.security.allows("doc.text.select") ?? false;
113
- const canCopy = () => ctx.doc?.security.allows("doc.text.copy") ?? false;
114
- const layoutOf = (pon) => ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
115
- const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;
116
- const ponAtIndex = (i) => ctx.document()?.pages[i]?.pageObjectNumber;
117
- /**
118
- * The content-space geometry for a page, derived on demand from the raw
119
- * snapshot + the CURRENT layout params. A view-rotate (or crop change)
120
- * changes the key and re-derives — raw caches and the engine are never
121
- * touched (rotation is presentation metadata).
122
- */
123
- function geometryFor(pon) {
124
- const raw = rawGeometry.get(pon);
125
- const layout = layoutOf(pon);
126
- if (!raw || !layout) return null;
127
- const crop = layout.boxes.crop;
128
- const key = `${layout.rotation}|${layout.userUnit}|${crop.left},${crop.bottom},${crop.right},${crop.top}`;
129
- const hit = derived.get(pon);
130
- if (hit && hit.key === key) return hit.geom;
131
- const geom = buildSelectionPageGeometry(raw, crop, layout.rotation, layout.userUnit);
132
- derived.set(pon, {
133
- key,
134
- geom
135
- });
136
- return geom;
137
- }
138
- const glyphAt = (geom, point) => textGlyphAt(geom.layout, contentPointToPdf(geom, point));
139
- function orderedEnds(sel) {
140
- const ai = pageIndexOf(sel.anchor.pon);
141
- const fi = pageIndexOf(sel.focus.pon);
142
- return ai < fi || ai === fi && sel.anchor.glyph <= sel.focus.glyph ? {
143
- start: sel.anchor,
144
- end: sel.focus,
145
- direction: "forward"
146
- } : {
147
- start: sel.focus,
148
- end: sel.anchor,
149
- direction: "backward"
150
- };
151
- }
152
- function endpointFor(ptr, which) {
153
- const segments = ctx.getState().segments[ptr.pon] ?? EMPTY_SEGMENTS;
154
- if (!segments.length) return null;
155
- const segment = which === "start" ? segments[0] : segments[segments.length - 1];
156
- const geom = geometryFor(ptr.pon);
157
- const cell = geom ? textGlyphQuad(geom.layout, ptr.glyph) : null;
158
- if (geom && cell) {
159
- const glyphQuad = toContentTextQuad(geom, cell);
160
- return {
161
- pon: ptr.pon,
162
- glyphQuad,
163
- advance: segment.advance,
164
- rect: textQuadBounds(glyphQuad)
165
- };
166
- }
167
- return {
168
- pon: ptr.pon,
169
- glyphQuad: segment.quad,
170
- advance: segment.advance,
171
- rect: segment.rect
172
- };
173
- }
174
- /** Union box of a page's segments, or null when none have materialized. */
175
- function pageBounds(pon) {
176
- const segs = ctx.getState().segments[pon];
177
- if (!segs || segs.length === 0) return null;
178
- let x1 = Infinity;
179
- let y1 = Infinity;
180
- let x2 = -Infinity;
181
- let y2 = -Infinity;
182
- for (const s of segs) {
183
- x1 = Math.min(x1, s.rect.x);
184
- y1 = Math.min(y1, s.rect.y);
185
- x2 = Math.max(x2, s.rect.x + s.rect.width);
186
- y2 = Math.max(y2, s.rect.y + s.rect.height);
187
- }
188
- return {
189
- x: x1,
190
- y: y1,
191
- width: x2 - x1,
192
- height: y2 - y1
193
- };
194
- }
195
- function menuAnchor() {
196
- const sel = ctx.getState().selection;
197
- if (!sel) return null;
198
- const { end } = orderedEnds(sel);
199
- let bounds = pageBounds(end.pon);
200
- if (bounds) return {
201
- pon: end.pon,
202
- bounds
203
- };
204
- const pages = ctx.document()?.pages ?? [];
205
- for (let i = pages.length - 1; i >= 0; i--) {
206
- const pon = pages[i].pageObjectNumber;
207
- bounds = pageBounds(pon);
208
- if (bounds) return {
209
- pon,
210
- bounds
211
- };
212
- }
213
- return null;
214
- }
215
- function snapshot() {
216
- const { selection, segments } = ctx.getState();
217
- const pages = Object.keys(segments).map(Number).filter((pon) => (segments[pon]?.length ?? 0) > 0).map((pon) => ({
218
- pon,
219
- segments: segments[pon]
220
- }));
221
- if (!selection) return {
222
- pages,
223
- start: null,
224
- end: null,
225
- direction: "forward",
226
- range: null
227
- };
228
- const { start, end, direction } = orderedEnds(selection);
229
- return {
230
- pages,
231
- start: endpointFor(start, "start"),
232
- end: endpointFor(end, "end"),
233
- direction,
234
- range: {
235
- start: {
236
- pon: start.pon,
237
- index: start.glyph
238
- },
239
- end: {
240
- pon: end.pon,
241
- index: end.glyph + 1
242
- }
243
- }
244
- };
245
- }
246
- function ensurePage(pon) {
247
- if (rawGeometry.has(pon) || pending.has(pon)) return;
248
- if (!canSelect()) return;
249
- const doc = ctx.doc;
250
- if (!doc || !layoutOf(pon)) return;
251
- pending.add(pon);
252
- const at = epoch;
253
- doc.page(pon).geometry.read().then((snapshot) => {
254
- pending.delete(pon);
255
- if (at !== epoch) return;
256
- rawGeometry.set(pon, snapshot);
257
- ctx.dispatch({
258
- type: "PAGE_LOADED",
259
- pon
260
- });
261
- if (ctx.getState().selection) recompute();
262
- }, () => {
263
- pending.delete(pon);
264
- });
265
- }
266
- /** Clamp a pointer into its page's real character range once geometry is
267
- * known — how `selectAll`'s open-ended focus settles. */
268
- function clampPointer(ptr) {
269
- const geom = geometryFor(ptr.pon);
270
- if (!geom) return ptr;
271
- const max = Math.max(geom.layout.glyphs.length - 1, 0);
272
- const glyph = Math.max(0, Math.min(ptr.glyph, max));
273
- return glyph === ptr.glyph ? ptr : {
274
- pon: ptr.pon,
275
- glyph
276
- };
277
- }
278
- function recompute(sel = ctx.getState().selection) {
279
- if (!sel) return;
280
- const clamped = {
281
- anchor: clampPointer(sel.anchor),
282
- focus: clampPointer(sel.focus)
283
- };
284
- const { start, end } = orderedEnds(clamped);
285
- const si = pageIndexOf(start.pon);
286
- const ei = pageIndexOf(end.pon);
287
- if (si < 0 || ei < 0) {
288
- clearSelection();
289
- return;
290
- }
291
- const segments = {};
292
- for (let i = si; i <= ei; i++) {
293
- const pon = ponAtIndex(i);
294
- if (pon == null) continue;
295
- const geom = geometryFor(pon);
296
- if (!geom) {
297
- ensurePage(pon);
298
- continue;
299
- }
300
- const from = i === si ? start.glyph : 0;
301
- const to = i === ei ? end.glyph : geom.layout.glyphs.length - 1;
302
- segments[pon] = textSegmentsForRange(geom.layout, from, to - from + 1).map((s) => toContentSegment(geom, s));
303
- }
304
- ctx.dispatch({
305
- type: "SET",
306
- selection: clamped,
307
- segments
308
- });
309
- fireChange();
310
- }
311
- /** Flip the gesture-in-flight FACT (see `SelectionState.selecting`). Set
312
- * BEFORE the recompute that fires `onChange`, so change listeners always
313
- * observe a coherent (selecting, segments) pair. */
314
- function setSelecting(selecting) {
315
- ctx.dispatch({
316
- type: "SET_SELECTING",
317
- selecting
318
- });
319
- }
320
- function clearSelection() {
321
- setSelecting(false);
322
- ctx.dispatch({ type: "CLEAR" });
323
- fireChange();
324
- }
325
- function selectSpanAt(pon, point, expand) {
326
- const geom = geometryFor(pon);
327
- if (!geom) return false;
328
- const i = glyphAt(geom, point);
329
- if (i == null) return false;
330
- const [from, to] = expand === "word" ? expandTextRangeToWord(geom.layout, i) : expandTextRangeToLine(geom.layout, i);
331
- setSelecting(true);
332
- recompute({
333
- anchor: {
334
- pon,
335
- glyph: from
336
- },
337
- focus: {
338
- pon,
339
- glyph: to
340
- }
341
- });
342
- return true;
343
- }
344
- function assertCanSelect(context) {
345
- if (!canSelect()) throw new PermissionDenied("doc.text.select", context);
346
- }
347
- /** The public programmatic entry: a half-open character range in. */
348
- function select(input) {
349
- assertCanSelect("selection.select");
350
- const range = "pon" in input ? {
351
- start: {
352
- pon: input.pon,
353
- index: input.start
354
- },
355
- end: {
356
- pon: input.pon,
357
- index: input.start + input.count
358
- }
359
- } : input;
360
- const si = pageIndexOf(range.start.pon);
361
- let ei = pageIndexOf(range.end.pon);
362
- if (si < 0 || ei < 0) throw new Error(`select: unknown page object ${si < 0 ? range.start.pon : range.end.pon}`);
363
- let endIndex = range.end.index;
364
- if (ei < si || ei === si && endIndex <= range.start.index) {
365
- clearSelection();
366
- return;
367
- }
368
- if (endIndex === 0) {
369
- ei -= 1;
370
- if (ei < si) {
371
- clearSelection();
372
- return;
373
- }
374
- endIndex = Number.MAX_SAFE_INTEGER;
375
- }
376
- const focusPon = ponAtIndex(ei);
377
- if (focusPon == null) throw new Error(`select: unknown page at index ${ei}`);
378
- setSelecting(false);
379
- recompute({
380
- anchor: {
381
- pon: range.start.pon,
382
- glyph: Math.max(0, range.start.index)
383
- },
384
- focus: {
385
- pon: focusPon,
386
- glyph: endIndex - 1
387
- }
388
- });
389
- }
390
- function selectAll() {
391
- assertCanSelect("selection.selectAll");
392
- const pages = ctx.document()?.pages ?? [];
393
- if (pages.length === 0) return;
394
- setSelecting(false);
395
- recompute({
396
- anchor: {
397
- pon: pages[0].pageObjectNumber,
398
- glyph: 0
399
- },
400
- focus: {
401
- pon: pages[pages.length - 1].pageObjectNumber,
402
- glyph: Number.MAX_SAFE_INTEGER
403
- }
404
- });
405
- }
406
- /** Per-page text snapshot, cached as a PROMISE so concurrent readers
407
- * share one fetch. Rejections are evicted (a denied or failed read must
408
- * not poison the cache for a later authorized call). */
409
- function pageTextSnapshot(pon) {
410
- let p = textSnapshots.get(pon);
411
- if (!p) {
412
- const doc = ctx.doc;
413
- if (!doc) return Promise.reject(/* @__PURE__ */ new Error("readText: document is not open"));
414
- p = Promise.resolve(doc.page(pon).text.read());
415
- p.catch(() => textSnapshots.delete(pon));
416
- textSnapshots.set(pon, p);
417
- }
418
- return p;
419
- }
420
- async function readText() {
421
- const sel = ctx.getState().selection;
422
- if (!sel) return "";
423
- if (!canCopy()) throw new PermissionDenied("doc.text.copy", "selection.readText");
424
- const { start, end } = orderedEnds(sel);
425
- const si = pageIndexOf(start.pon);
426
- const ei = pageIndexOf(end.pon);
427
- if (si < 0 || ei < 0) return "";
428
- const spans = [];
429
- for (let i = si; i <= ei; i++) {
430
- const pon = ponAtIndex(i);
431
- if (pon == null) continue;
432
- spans.push({
433
- pon,
434
- from: i === si ? start.glyph : 0,
435
- to: i === ei ? end.glyph + 1 : Number.MAX_SAFE_INTEGER
436
- });
437
- }
438
- const parts = new Array(spans.length);
439
- for (let base = 0; base < spans.length; base += TEXT_READ_CONCURRENCY) {
440
- const batch = spans.slice(base, base + TEXT_READ_CONCURRENCY);
441
- (await Promise.all(batch.map((s) => pageTextSnapshot(s.pon)))).forEach((snap, j) => {
442
- parts[base + j] = sliceTextByChars(snap, batch[j].from, batch[j].to);
443
- });
444
- }
445
- return parts.join("\n");
446
- }
447
- /** Structural registry change (rotate/move/delete/insert — `revision`
448
- * bumped): drop caches for pages that left, then recompute. Rotation
449
- * re-derives transforms via the layout key; a deleted endpoint page
450
- * clears via recompute's registry check. Raw snapshots are NEVER
451
- * refetched here — they are rotation-independent. */
452
- function onPagesUpdated() {
453
- const alive = new Set((ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber));
454
- for (const pon of [...rawGeometry.keys()]) if (!alive.has(pon)) {
455
- rawGeometry.delete(pon);
456
- derived.delete(pon);
457
- textSnapshots.delete(pon);
458
- }
459
- if (ctx.getState().selection) recompute();
460
- }
461
- /** Page CONTENT changed (redaction apply / flatten): every cached
462
- * snapshot — geometry AND text — is stale, and any live range points
463
- * into a character space that no longer exists. Drop everything, clear. */
464
- function invalidateContent() {
465
- epoch++;
466
- rawGeometry.clear();
467
- derived.clear();
468
- pending.clear();
469
- textSnapshots.clear();
470
- if (ctx.getState().selection) clearSelection();
471
- }
472
- let lastRevision = ctx.document()?.revision ?? -1;
473
- const offStore = ctx.subscribe(() => {
474
- const revision = ctx.document()?.revision;
475
- if (revision === void 0 || revision === lastRevision) return;
476
- lastRevision = revision;
477
- onPagesUpdated();
478
- });
479
- ctx.cleanup(offStore);
480
- const offEvents = ctx.doc?.events.subscribe((event) => {
481
- if (event.type === "redaction.applied" || event.type === "pages.flattened") invalidateContent();
482
- });
483
- if (offEvents) ctx.cleanup(offEvents);
484
- return {
485
- canSelect,
486
- canCopy,
487
- select,
488
- selectAll,
489
- clear: clearSelection,
490
- snapshot,
491
- menuAnchor,
492
- isSelecting: () => ctx.getState().selecting,
493
- hasSelection: () => ctx.getState().selection != null,
494
- selectedPages: () => {
495
- const { segments } = ctx.getState();
496
- return Object.keys(segments).map(Number).filter((pon) => (segments[pon]?.length ?? 0) > 0);
497
- },
498
- segmentsForPage: (pon) => ctx.getState().segments[pon] ?? EMPTY_SEGMENTS,
499
- rectsForPage: (pon) => (ctx.getState().segments[pon] ?? EMPTY_SEGMENTS).map((s) => s.rect),
500
- readText,
501
- onChange: (cb) => {
502
- changeCbs.add(cb);
503
- return () => changeCbs.delete(cb);
504
- },
505
- onCommit: (cb) => {
506
- commitCbs.add(cb);
507
- return () => commitCbs.delete(cb);
508
- },
509
- ensurePage,
510
- isLoaded: (pon) => !!ctx.getState().loaded[pon],
511
- isOverText: (pon, point) => {
512
- const geom = geometryFor(pon);
513
- return geom ? glyphAt(geom, point) != null : false;
514
- },
515
- beginAt: (pon, point) => {
516
- const geom = geometryFor(pon);
517
- if (!geom) return false;
518
- const i = glyphAt(geom, point);
519
- if (i == null) return false;
520
- setSelecting(true);
521
- recompute({
522
- anchor: {
523
- pon,
524
- glyph: i
525
- },
526
- focus: {
527
- pon,
528
- glyph: i
529
- }
530
- });
531
- return true;
532
- },
533
- selectWordAt: (pon, point) => selectSpanAt(pon, point, "word"),
534
- selectLineAt: (pon, point) => selectSpanAt(pon, point, "line"),
535
- extendTo: (pon, point) => {
536
- const cur = ctx.getState().selection;
537
- if (!cur) return;
538
- const geom = geometryFor(pon);
539
- if (!geom) {
540
- ensurePage(pon);
541
- return;
542
- }
543
- const i = glyphAt(geom, point);
544
- if (i == null) return;
545
- recompute({
546
- anchor: cur.anchor,
547
- focus: {
548
- pon,
549
- glyph: i
550
- }
551
- });
552
- },
553
- end: () => {
554
- setSelecting(false);
555
- commitCbs.forEach((cb) => cb());
556
- },
557
- setHighlightVisible: (visible) => ctx.dispatch({
558
- type: "SET_HIGHLIGHT_HIDDEN",
559
- hidden: !visible
560
- }),
561
- highlightVisible: () => !ctx.getState().highlightHidden
562
- };
563
- }
564
- //#endregion
565
- //#region src/handler.ts
566
- const CURSOR_TOKEN = "selection-text";
567
- const DRAG_THRESHOLD_PX = 4;
568
- /**
569
- * The text-selection pointer handler. It is live only under tools that enable the
570
- * `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
571
- * switching to pan disables text selection with zero special-casing here).
572
- *
573
- * - single click → deselect; records an anchor but selects NOTHING until
574
- * the pointer moves past the drag threshold (Chrome/Acrobat feel)
575
- * - single-click + drag → caret selection from the anchor (extends across pages)
576
- * - double-click → select the word
577
- * - triple-click → select the visual line
578
- * - down off-text → deselect, and DON'T capture (so it can't block)
579
- * - hover → I-beam only when over text, else the pointer cursor
580
- */
581
- function createTextSelectHandler(selection, interaction, feedback) {
582
- let anchor = null;
583
- let dragging = false;
584
- return {
585
- id: "text-select",
586
- priority: 60,
587
- enabledFor: (tool) => tool.enables.has("text-select"),
588
- onDown: (s) => {
589
- anchor = null;
590
- dragging = false;
591
- if (!s.page) return false;
592
- const { pon, point } = s.page;
593
- const clicks = s.clickCount ?? 1;
594
- if (clicks >= 3) {
595
- selection.selectLineAt(pon, point);
596
- return true;
597
- }
598
- if (clicks === 2) {
599
- if (selection.selectWordAt(pon, point) && s.gesture === "long-press") feedback?.selection();
600
- return true;
601
- }
602
- selection.clear();
603
- if (!selection.isOverText(pon, point)) return false;
604
- anchor = {
605
- pon,
606
- point,
607
- vx: s.viewport.x,
608
- vy: s.viewport.y
609
- };
610
- return true;
611
- },
612
- onMove: (s) => {
613
- if (!s.page || !anchor) return;
614
- if (!dragging) {
615
- if (Math.hypot(s.viewport.x - anchor.vx, s.viewport.y - anchor.vy) < DRAG_THRESHOLD_PX) return;
616
- dragging = true;
617
- selection.beginAt(anchor.pon, anchor.point);
618
- }
619
- selection.extendTo(s.page.pon, s.page.point);
620
- },
621
- onUp: () => {
622
- anchor = null;
623
- dragging = false;
624
- selection.end();
625
- },
626
- onCancel: () => {
627
- anchor = null;
628
- dragging = false;
629
- selection.clear();
630
- selection.end();
631
- },
632
- onHover: (s) => {
633
- if (!s.page) {
634
- interaction.setCursor(CURSOR_TOKEN, null);
635
- return;
636
- }
637
- selection.ensurePage(s.page.pon);
638
- const overText = selection.isOverText(s.page.pon, s.page.point);
639
- interaction.setCursor(CURSOR_TOKEN, overText ? "text" : null, 10);
640
- }
641
- };
642
- }
643
- //#endregion
644
- //#region src/reducer.ts
645
- const initialSelectionState = {
646
- selection: null,
647
- segments: {},
648
- loaded: {},
649
- highlightHidden: false,
650
- selecting: false
651
- };
652
- const selectionReducer = (state, action) => {
653
- switch (action.type) {
654
- case "PAGE_LOADED": return state.loaded[action.pon] ? state : {
655
- ...state,
656
- loaded: {
657
- ...state.loaded,
658
- [action.pon]: true
659
- }
660
- };
661
- case "SET": return {
662
- ...state,
663
- selection: action.selection,
664
- segments: action.segments
665
- };
666
- case "CLEAR": return state.selection === null && Object.keys(state.segments).length === 0 ? state : {
667
- ...state,
668
- selection: null,
669
- segments: {}
670
- };
671
- case "SET_HIGHLIGHT_HIDDEN": return state.highlightHidden === action.hidden ? state : {
672
- ...state,
673
- highlightHidden: action.hidden
674
- };
675
- case "SET_SELECTING": return state.selecting === action.selecting ? state : {
676
- ...state,
677
- selecting: action.selecting
678
- };
679
- default: return state;
680
- }
681
- };
682
- //#endregion
683
- export { buildSelectionPageGeometry as a, toContentTextQuad as c, createSelectionCapability as i, selectionReducer as n, contentPointToPdf as o, createTextSelectHandler as r, toContentSegment as s, initialSelectionState as t };
684
-
685
- //# sourceMappingURL=reducer-DbKtk9Xl.js.map