@embedpdf/plugin-selection 1.4.1 → 2.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +445 -166
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/actions.d.ts +61 -24
  6. package/dist/lib/reducer.d.ts +2 -1
  7. package/dist/lib/selection-plugin.d.ts +25 -5
  8. package/dist/lib/selectors.d.ts +7 -7
  9. package/dist/lib/types.d.ts +62 -9
  10. package/dist/preact/index.cjs +1 -1
  11. package/dist/preact/index.cjs.map +1 -1
  12. package/dist/preact/index.js +88 -34
  13. package/dist/preact/index.js.map +1 -1
  14. package/dist/preact/utils.d.ts +1 -0
  15. package/dist/react/index.cjs +1 -1
  16. package/dist/react/index.cjs.map +1 -1
  17. package/dist/react/index.js +88 -34
  18. package/dist/react/index.js.map +1 -1
  19. package/dist/react/utils.d.ts +1 -0
  20. package/dist/shared/components/selection-layer.d.ts +7 -2
  21. package/dist/shared/index.d.ts +1 -0
  22. package/dist/shared/types.d.ts +7 -0
  23. package/dist/shared-preact/components/selection-layer.d.ts +7 -2
  24. package/dist/shared-preact/index.d.ts +1 -0
  25. package/dist/shared-preact/types.d.ts +7 -0
  26. package/dist/shared-react/components/selection-layer.d.ts +7 -2
  27. package/dist/shared-react/index.d.ts +1 -0
  28. package/dist/shared-react/types.d.ts +7 -0
  29. package/dist/svelte/components/SelectionLayer.svelte.d.ts +13 -2
  30. package/dist/svelte/index.cjs +1 -1
  31. package/dist/svelte/index.cjs.map +1 -1
  32. package/dist/svelte/index.d.ts +1 -0
  33. package/dist/svelte/index.js +162 -34
  34. package/dist/svelte/index.js.map +1 -1
  35. package/dist/svelte/types.d.ts +7 -0
  36. package/dist/vue/components/copy-to-clipboard.vue.d.ts +2 -1
  37. package/dist/vue/components/selection-layer.vue.d.ts +27 -3
  38. package/dist/vue/index.cjs +1 -1
  39. package/dist/vue/index.cjs.map +1 -1
  40. package/dist/vue/index.d.ts +1 -0
  41. package/dist/vue/index.js +137 -43
  42. package/dist/vue/index.js.map +1 -1
  43. package/dist/vue/types.d.ts +7 -0
  44. package/package.json +9 -8
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BasePlugin, createBehaviorEmitter, createEmitter, SET_DOCUMENT, REFRESH_PAGES } from "@embedpdf/core";
1
+ import { BasePlugin, createScopedEmitter, REFRESH_PAGES } from "@embedpdf/core";
2
2
  import { boundingRect, Task, ignore, PdfErrorCode, PdfTaskHelper } from "@embedpdf/models";
3
3
  const SELECTION_PLUGIN_ID = "selection";
4
4
  const manifest = {
@@ -7,36 +7,156 @@ const manifest = {
7
7
  version: "1.0.0",
8
8
  provides: ["selection"],
9
9
  requires: ["interaction-manager"],
10
- optional: [],
10
+ optional: ["viewport", "scroll"],
11
11
  defaultConfig: {
12
12
  enabled: true
13
13
  }
14
14
  };
15
- const CACHE_PAGE_GEOMETRY = "CACHE_PAGE_GEOMETRY";
16
- const SET_SELECTION = "SET_SELECTION";
17
- const START_SELECTION = "START_SELECTION";
18
- const END_SELECTION = "END_SELECTION";
19
- const CLEAR_SELECTION = "CLEAR_SELECTION";
20
- const SET_RECTS = "SET_RECTS";
21
- const SET_SLICES = "SET_SLICES";
22
- const RESET = "RESET";
23
- const cachePageGeometry = (page, geo) => ({
15
+ const INIT_SELECTION_STATE = "SELECTION/INIT_STATE";
16
+ const CLEANUP_SELECTION_STATE = "SELECTION/CLEANUP_STATE";
17
+ const CACHE_PAGE_GEOMETRY = "SELECTION/CACHE_PAGE_GEOMETRY";
18
+ const SET_SELECTION = "SELECTION/SET_SELECTION";
19
+ const START_SELECTION = "SELECTION/START_SELECTION";
20
+ const END_SELECTION = "SELECTION/END_SELECTION";
21
+ const CLEAR_SELECTION = "SELECTION/CLEAR_SELECTION";
22
+ const SET_RECTS = "SELECTION/SET_RECTS";
23
+ const SET_SLICES = "SELECTION/SET_SLICES";
24
+ const RESET = "SELECTION/RESET";
25
+ const initSelectionState = (documentId, state) => ({
26
+ type: INIT_SELECTION_STATE,
27
+ payload: { documentId, state }
28
+ });
29
+ const cleanupSelectionState = (documentId) => ({
30
+ type: CLEANUP_SELECTION_STATE,
31
+ payload: documentId
32
+ });
33
+ const cachePageGeometry = (documentId, page, geo) => ({
24
34
  type: CACHE_PAGE_GEOMETRY,
25
- payload: { page, geo }
35
+ payload: { documentId, page, geo }
26
36
  });
27
- const setSelection = (sel) => ({
37
+ const setSelection = (documentId, sel) => ({
28
38
  type: SET_SELECTION,
29
- payload: sel
39
+ payload: { documentId, selection: sel }
40
+ });
41
+ const startSelection = (documentId) => ({
42
+ type: START_SELECTION,
43
+ payload: { documentId }
30
44
  });
31
- const startSelection = () => ({ type: START_SELECTION });
32
- const endSelection = () => ({ type: END_SELECTION });
33
- const clearSelection = () => ({ type: CLEAR_SELECTION });
34
- const setRects = (allRects) => ({
45
+ const endSelection = (documentId) => ({
46
+ type: END_SELECTION,
47
+ payload: { documentId }
48
+ });
49
+ const clearSelection = (documentId) => ({
50
+ type: CLEAR_SELECTION,
51
+ payload: { documentId }
52
+ });
53
+ const setRects = (documentId, allRects) => ({
35
54
  type: SET_RECTS,
36
- payload: allRects
55
+ payload: { documentId, rects: allRects }
56
+ });
57
+ const setSlices = (documentId, slices) => ({ type: SET_SLICES, payload: { documentId, slices } });
58
+ const initialSelectionDocumentState = {
59
+ geometry: {},
60
+ rects: {},
61
+ slices: {},
62
+ selection: null,
63
+ active: false,
64
+ selecting: false
65
+ };
66
+ const initialState = {
67
+ documents: {}
68
+ };
69
+ const updateDocState = (state, documentId, newDocState) => ({
70
+ ...state,
71
+ documents: {
72
+ ...state.documents,
73
+ [documentId]: newDocState
74
+ }
37
75
  });
38
- const setSlices = (slices) => ({ type: SET_SLICES, payload: slices });
39
- const reset = () => ({ type: RESET });
76
+ const selectionReducer = (state = initialState, action) => {
77
+ switch (action.type) {
78
+ case INIT_SELECTION_STATE: {
79
+ const { documentId, state: docState } = action.payload;
80
+ return updateDocState(state, documentId, docState);
81
+ }
82
+ case CLEANUP_SELECTION_STATE: {
83
+ const documentId = action.payload;
84
+ const { [documentId]: removed, ...remaining } = state.documents;
85
+ return {
86
+ ...state,
87
+ documents: remaining
88
+ };
89
+ }
90
+ case CACHE_PAGE_GEOMETRY: {
91
+ const { documentId, page, geo } = action.payload;
92
+ const docState = state.documents[documentId];
93
+ if (!docState) return state;
94
+ return updateDocState(state, documentId, {
95
+ ...docState,
96
+ geometry: { ...docState.geometry, [page]: geo }
97
+ });
98
+ }
99
+ case SET_SELECTION: {
100
+ const { documentId, selection } = action.payload;
101
+ const docState = state.documents[documentId];
102
+ if (!docState) return state;
103
+ return updateDocState(state, documentId, {
104
+ ...docState,
105
+ selection,
106
+ active: true
107
+ });
108
+ }
109
+ case START_SELECTION: {
110
+ const { documentId } = action.payload;
111
+ const docState = state.documents[documentId];
112
+ if (!docState) return state;
113
+ return updateDocState(state, documentId, {
114
+ ...docState,
115
+ selecting: true,
116
+ selection: null,
117
+ rects: {}
118
+ });
119
+ }
120
+ case END_SELECTION: {
121
+ const { documentId } = action.payload;
122
+ const docState = state.documents[documentId];
123
+ if (!docState) return state;
124
+ return updateDocState(state, documentId, { ...docState, selecting: false });
125
+ }
126
+ case CLEAR_SELECTION: {
127
+ const { documentId } = action.payload;
128
+ const docState = state.documents[documentId];
129
+ if (!docState) return state;
130
+ return updateDocState(state, documentId, {
131
+ ...docState,
132
+ selecting: false,
133
+ selection: null,
134
+ rects: {},
135
+ active: false
136
+ });
137
+ }
138
+ case SET_RECTS: {
139
+ const { documentId, rects } = action.payload;
140
+ const docState = state.documents[documentId];
141
+ if (!docState) return state;
142
+ return updateDocState(state, documentId, { ...docState, rects });
143
+ }
144
+ case SET_SLICES: {
145
+ const { documentId, slices } = action.payload;
146
+ const docState = state.documents[documentId];
147
+ if (!docState) return state;
148
+ return updateDocState(state, documentId, { ...docState, slices });
149
+ }
150
+ case RESET: {
151
+ const { documentId } = action.payload;
152
+ const docState = state.documents[documentId];
153
+ if (!docState) return state;
154
+ return updateDocState(state, documentId, initialSelectionDocumentState);
155
+ }
156
+ default:
157
+ return state;
158
+ }
159
+ };
40
160
  function selectRectsForPage(state, page) {
41
161
  return state.rects[page] ?? [];
42
162
  }
@@ -208,266 +328,425 @@ function mergeAdjacentRects(textRuns) {
208
328
  }
209
329
  const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
210
330
  constructor(id, registry) {
211
- var _a;
331
+ var _a, _b, _c;
212
332
  super(id, registry);
213
- this.enabledModes = /* @__PURE__ */ new Set(["pointerMode"]);
214
- this.selecting = false;
333
+ this.enabledModesPerDoc = /* @__PURE__ */ new Map();
334
+ this.selecting = /* @__PURE__ */ new Map();
335
+ this.anchor = /* @__PURE__ */ new Map();
215
336
  this.pageCallbacks = /* @__PURE__ */ new Map();
216
- this.selChange$ = createBehaviorEmitter();
217
- this.textRetrieved$ = createBehaviorEmitter();
218
- this.copyToClipboard$ = createEmitter();
219
- this.beginSelection$ = createEmitter();
220
- this.endSelection$ = createEmitter();
221
- this.interactionManagerCapability = (_a = this.registry.getPlugin("interaction-manager")) == null ? void 0 : _a.provides();
222
- this.coreStore.onAction(SET_DOCUMENT, (_action) => {
223
- this.dispatch(reset());
224
- this.notifyAllPages();
225
- });
337
+ this.menuPlacement$ = createScopedEmitter((documentId, placement) => ({ documentId, placement }));
338
+ this.selChange$ = createScopedEmitter((documentId, selection) => ({ documentId, selection }));
339
+ this.textRetrieved$ = createScopedEmitter(
340
+ (documentId, text) => ({ documentId, text })
341
+ );
342
+ this.copyToClipboard$ = createScopedEmitter(
343
+ (documentId, text) => ({ documentId, text }),
344
+ { cache: false }
345
+ );
346
+ this.beginSelection$ = createScopedEmitter((documentId, data) => ({ documentId, page: data.page, index: data.index }), { cache: false });
347
+ this.endSelection$ = createScopedEmitter(
348
+ (documentId) => ({ documentId }),
349
+ { cache: false }
350
+ );
351
+ this.viewportCapability = null;
352
+ this.scrollCapability = null;
353
+ const imPlugin = registry.getPlugin("interaction-manager");
354
+ if (!imPlugin) {
355
+ throw new Error("SelectionPlugin: InteractionManagerPlugin is required.");
356
+ }
357
+ this.interactionManagerCapability = imPlugin.provides();
358
+ this.viewportCapability = ((_a = registry.getPlugin("viewport")) == null ? void 0 : _a.provides()) ?? null;
359
+ this.scrollCapability = ((_b = registry.getPlugin("scroll")) == null ? void 0 : _b.provides()) ?? null;
226
360
  this.coreStore.onAction(REFRESH_PAGES, (action) => {
227
- const tasks = action.payload.map((pageIdx) => this.getNewPageGeometryAndCache(pageIdx));
361
+ const { documentId, pageIndexes } = action.payload;
362
+ const tasks = pageIndexes.map(
363
+ (pageIndex) => this.getNewPageGeometryAndCache(documentId, pageIndex)
364
+ );
228
365
  Task.all(tasks).wait(() => {
229
- action.payload.forEach((pageIdx) => {
230
- this.notifyPage(pageIdx);
366
+ pageIndexes.forEach((pageIndex) => {
367
+ this.notifyPage(documentId, pageIndex);
231
368
  });
232
369
  }, ignore);
233
370
  });
371
+ (_c = this.viewportCapability) == null ? void 0 : _c.onViewportChange(
372
+ (event) => {
373
+ this.recalculateMenuPlacement(event.documentId);
374
+ },
375
+ { mode: "throttle", wait: 100 }
376
+ );
234
377
  }
235
378
  /* ── life-cycle ────────────────────────────────────────── */
379
+ onDocumentLoadingStarted(documentId) {
380
+ this.dispatch(initSelectionState(documentId, initialSelectionDocumentState));
381
+ this.enabledModesPerDoc.set(documentId, /* @__PURE__ */ new Set(["pointerMode"]));
382
+ this.pageCallbacks.set(documentId, /* @__PURE__ */ new Map());
383
+ this.selecting.set(documentId, false);
384
+ this.anchor.set(documentId, void 0);
385
+ }
386
+ onDocumentClosed(documentId) {
387
+ this.dispatch(cleanupSelectionState(documentId));
388
+ this.enabledModesPerDoc.delete(documentId);
389
+ this.pageCallbacks.delete(documentId);
390
+ this.selecting.delete(documentId);
391
+ this.anchor.delete(documentId);
392
+ this.selChange$.clearScope(documentId);
393
+ this.textRetrieved$.clearScope(documentId);
394
+ this.copyToClipboard$.clearScope(documentId);
395
+ this.beginSelection$.clearScope(documentId);
396
+ this.endSelection$.clearScope(documentId);
397
+ this.menuPlacement$.clearScope(documentId);
398
+ }
236
399
  async initialize() {
237
400
  }
238
401
  async destroy() {
239
402
  this.selChange$.clear();
403
+ this.textRetrieved$.clear();
404
+ this.copyToClipboard$.clear();
405
+ this.beginSelection$.clear();
406
+ this.endSelection$.clear();
407
+ this.menuPlacement$.clear();
408
+ super.destroy();
240
409
  }
241
410
  /* ── capability exposed to UI / other plugins ─────────── */
242
411
  buildCapability() {
412
+ const getDocId = (documentId) => documentId ?? this.getActiveDocumentId();
413
+ return {
414
+ // Active document operations
415
+ getFormattedSelection: (docId) => getFormattedSelection(this.getDocumentState(getDocId(docId))),
416
+ getFormattedSelectionForPage: (p, docId) => getFormattedSelectionForPage(this.getDocumentState(getDocId(docId)), p),
417
+ getHighlightRectsForPage: (p, docId) => selectRectsForPage(this.getDocumentState(getDocId(docId)), p),
418
+ getHighlightRects: (docId) => this.getDocumentState(getDocId(docId)).rects,
419
+ getBoundingRectForPage: (p, docId) => selectBoundingRectForPage(this.getDocumentState(getDocId(docId)), p),
420
+ getBoundingRects: (docId) => selectBoundingRectsForAllPages(this.getDocumentState(getDocId(docId))),
421
+ getSelectedText: (docId) => this.getSelectedText(getDocId(docId)),
422
+ clear: (docId) => this.clearSelection(getDocId(docId)),
423
+ copyToClipboard: (docId) => this.copyToClipboard(getDocId(docId)),
424
+ getState: (docId) => this.getDocumentState(getDocId(docId)),
425
+ enableForMode: (modeId, docId) => {
426
+ var _a;
427
+ return (_a = this.enabledModesPerDoc.get(getDocId(docId))) == null ? void 0 : _a.add(modeId);
428
+ },
429
+ isEnabledForMode: (modeId, docId) => {
430
+ var _a;
431
+ return ((_a = this.enabledModesPerDoc.get(getDocId(docId))) == null ? void 0 : _a.has(modeId)) ?? false;
432
+ },
433
+ // Document-scoped operations
434
+ forDocument: this.createSelectionScope.bind(this),
435
+ // Global events
436
+ onCopyToClipboard: this.copyToClipboard$.onGlobal,
437
+ onSelectionChange: this.selChange$.onGlobal,
438
+ onTextRetrieved: this.textRetrieved$.onGlobal,
439
+ onBeginSelection: this.beginSelection$.onGlobal,
440
+ onEndSelection: this.endSelection$.onGlobal
441
+ };
442
+ }
443
+ createSelectionScope(documentId) {
243
444
  return {
244
- getFormattedSelection: () => getFormattedSelection(this.state),
245
- getFormattedSelectionForPage: (p) => getFormattedSelectionForPage(this.state, p),
246
- getHighlightRectsForPage: (p) => selectRectsForPage(this.state, p),
247
- getHighlightRects: () => this.state.rects,
248
- getBoundingRectForPage: (p) => selectBoundingRectForPage(this.state, p),
249
- getBoundingRects: () => selectBoundingRectsForAllPages(this.state),
250
- onCopyToClipboard: this.copyToClipboard$.on,
251
- onSelectionChange: this.selChange$.on,
252
- onTextRetrieved: this.textRetrieved$.on,
253
- onBeginSelection: this.beginSelection$.on,
254
- onEndSelection: this.endSelection$.on,
255
- clear: () => this.clearSelection(),
256
- getSelectedText: () => this.getSelectedText(),
257
- copyToClipboard: () => this.copyToClipboard(),
258
- enableForMode: (id) => this.enabledModes.add(id),
259
- isEnabledForMode: (id) => this.enabledModes.has(id),
260
- getState: () => this.state
445
+ getFormattedSelection: () => getFormattedSelection(this.getDocumentState(documentId)),
446
+ getFormattedSelectionForPage: (p) => getFormattedSelectionForPage(this.getDocumentState(documentId), p),
447
+ getHighlightRectsForPage: (p) => selectRectsForPage(this.getDocumentState(documentId), p),
448
+ getHighlightRects: () => this.getDocumentState(documentId).rects,
449
+ getBoundingRectForPage: (p) => selectBoundingRectForPage(this.getDocumentState(documentId), p),
450
+ getBoundingRects: () => selectBoundingRectsForAllPages(this.getDocumentState(documentId)),
451
+ getSelectedText: () => this.getSelectedText(documentId),
452
+ clear: () => this.clearSelection(documentId),
453
+ copyToClipboard: () => this.copyToClipboard(documentId),
454
+ getState: () => this.getDocumentState(documentId),
455
+ onSelectionChange: this.selChange$.forScope(documentId),
456
+ onTextRetrieved: this.textRetrieved$.forScope(documentId),
457
+ onCopyToClipboard: this.copyToClipboard$.forScope(documentId),
458
+ onBeginSelection: this.beginSelection$.forScope(documentId),
459
+ onEndSelection: this.endSelection$.forScope(documentId)
261
460
  };
262
461
  }
462
+ getDocumentState(documentId) {
463
+ const state = this.state.documents[documentId];
464
+ if (!state) {
465
+ throw new Error(`Selection state not found for document: ${documentId}`);
466
+ }
467
+ return state;
468
+ }
469
+ /**
470
+ * Subscribe to menu placement changes for a specific document
471
+ * @param documentId - The document ID to subscribe to
472
+ * @param listener - Callback function that receives placement updates
473
+ * @returns Unsubscribe function
474
+ */
475
+ onMenuPlacement(documentId, listener) {
476
+ return this.menuPlacement$.forScope(documentId)(listener);
477
+ }
263
478
  registerSelectionOnPage(opts) {
264
- if (!this.interactionManagerCapability) {
479
+ var _a;
480
+ const { documentId, pageIndex, onRectsChange } = opts;
481
+ const docState = this.state.documents[documentId];
482
+ if (!docState) {
265
483
  this.logger.warn(
266
484
  "SelectionPlugin",
267
- "MissingDependency",
268
- "Interaction manager plugin not loaded, text selection disabled"
485
+ "RegisterFailed",
486
+ `Cannot register selection on page ${pageIndex} for document ${documentId}: document state not initialized.`
269
487
  );
270
488
  return () => {
271
489
  };
272
490
  }
273
- const { pageIndex, onRectsChange } = opts;
274
- this.pageCallbacks.set(pageIndex, onRectsChange);
275
- const geoTask = this.getOrLoadGeometry(pageIndex);
491
+ (_a = this.pageCallbacks.get(documentId)) == null ? void 0 : _a.set(pageIndex, onRectsChange);
492
+ const geoTask = this.getOrLoadGeometry(documentId, pageIndex);
493
+ const interactionScope = this.interactionManagerCapability.forDocument(documentId);
494
+ const enabledModes = this.enabledModesPerDoc.get(documentId);
276
495
  onRectsChange({
277
- rects: selectRectsForPage(this.state, pageIndex),
278
- boundingRect: selectBoundingRectForPage(this.state, pageIndex)
496
+ rects: selectRectsForPage(docState, pageIndex),
497
+ boundingRect: selectBoundingRectForPage(docState, pageIndex)
279
498
  });
280
499
  const handlers = {
281
500
  onPointerDown: (point, _evt, modeId) => {
282
- if (!this.enabledModes.has(modeId)) return;
283
- this.clearSelection();
284
- const cached = this.state.geometry[pageIndex];
501
+ if (!(enabledModes == null ? void 0 : enabledModes.has(modeId))) return;
502
+ this.clearSelection(documentId);
503
+ const cached = this.getDocumentState(documentId).geometry[pageIndex];
285
504
  if (cached) {
286
505
  const g = glyphAt(cached, point);
287
506
  if (g !== -1) {
288
- this.beginSelection(pageIndex, g);
507
+ this.beginSelection(documentId, pageIndex, g);
289
508
  }
290
509
  }
291
510
  },
292
511
  onPointerMove: (point, _evt, modeId) => {
293
- var _a, _b;
294
- if (!this.enabledModes.has(modeId)) return;
295
- const cached = this.state.geometry[pageIndex];
512
+ if (!(enabledModes == null ? void 0 : enabledModes.has(modeId))) return;
513
+ const cached = this.getDocumentState(documentId).geometry[pageIndex];
296
514
  if (cached) {
297
515
  const g = glyphAt(cached, point);
298
516
  if (g !== -1) {
299
- (_a = this.interactionManagerCapability) == null ? void 0 : _a.setCursor("selection-text", "text", 10);
517
+ interactionScope.setCursor("selection-text", "text", 10);
300
518
  } else {
301
- (_b = this.interactionManagerCapability) == null ? void 0 : _b.removeCursor("selection-text");
519
+ interactionScope.removeCursor("selection-text");
302
520
  }
303
- if (this.selecting && g !== -1) {
304
- this.updateSelection(pageIndex, g);
521
+ if (this.selecting.get(documentId) && g !== -1) {
522
+ this.updateSelection(documentId, pageIndex, g);
305
523
  }
306
524
  }
307
525
  },
308
526
  onPointerUp: (_point, _evt, modeId) => {
309
- if (!this.enabledModes.has(modeId)) return;
310
- this.endSelection();
527
+ if (!(enabledModes == null ? void 0 : enabledModes.has(modeId))) return;
528
+ this.endSelection(documentId);
311
529
  },
312
530
  onHandlerActiveEnd: (modeId) => {
313
- if (!this.enabledModes.has(modeId)) return;
314
- this.clearSelection();
531
+ if (!(enabledModes == null ? void 0 : enabledModes.has(modeId))) return;
532
+ this.clearSelection(documentId);
315
533
  }
316
534
  };
317
535
  const unregisterHandlers = this.interactionManagerCapability.registerAlways({
318
- scope: { type: "page", pageIndex },
536
+ scope: { type: "page", documentId, pageIndex },
319
537
  handlers
320
538
  });
321
539
  return () => {
540
+ var _a2;
322
541
  unregisterHandlers();
323
- this.pageCallbacks.delete(pageIndex);
542
+ (_a2 = this.pageCallbacks.get(documentId)) == null ? void 0 : _a2.delete(pageIndex);
324
543
  geoTask.abort({ code: PdfErrorCode.Cancelled, message: "Cleanup" });
325
544
  };
326
545
  }
327
- notifyPage(pageIndex) {
546
+ /**
547
+ * Helper to calculate viewport relative metrics for a page rect.
548
+ * Returns null if the rect cannot be converted to viewport space.
549
+ */
550
+ getPlacementMetrics(documentId, pageIndex, rect, vpMetrics) {
328
551
  var _a;
329
- const callback = this.pageCallbacks.get(pageIndex);
552
+ const scrollScope = (_a = this.scrollCapability) == null ? void 0 : _a.forDocument(documentId);
553
+ const viewportRect = scrollScope == null ? void 0 : scrollScope.getRectPositionForPage(pageIndex, rect);
554
+ if (!viewportRect) return null;
555
+ const rectTopInView = viewportRect.origin.y - vpMetrics.scrollTop;
556
+ const rectBottomInView = viewportRect.origin.y + viewportRect.size.height - vpMetrics.scrollTop;
557
+ return {
558
+ pageIndex,
559
+ rect,
560
+ // Original Page Rect
561
+ spaceAbove: rectTopInView,
562
+ spaceBelow: vpMetrics.clientHeight - rectBottomInView,
563
+ isBottomVisible: rectBottomInView > 0 && rectBottomInView <= vpMetrics.clientHeight,
564
+ isTopVisible: rectTopInView >= 0 && rectTopInView < vpMetrics.clientHeight
565
+ };
566
+ }
567
+ recalculateMenuPlacement(documentId) {
568
+ const docState = this.state.documents[documentId];
569
+ if (!docState) return;
570
+ if (docState.selecting || docState.selection === null) {
571
+ this.menuPlacement$.emit(documentId, null);
572
+ return;
573
+ }
574
+ const bounds = selectBoundingRectsForAllPages(docState);
575
+ if (bounds.length === 0) {
576
+ this.menuPlacement$.emit(documentId, null);
577
+ return;
578
+ }
579
+ const tail = bounds[bounds.length - 1];
580
+ if (!this.viewportCapability || !this.scrollCapability) {
581
+ this.menuPlacement$.emit(documentId, {
582
+ pageIndex: tail.page,
583
+ rect: tail.rect,
584
+ spaceAbove: 0,
585
+ spaceBelow: Infinity,
586
+ // Pretend we have infinite space below to prevent auto-flipping
587
+ suggestTop: false,
588
+ isVisible: true
589
+ // Assume visible
590
+ });
591
+ return;
592
+ }
593
+ const viewportScope = this.viewportCapability.forDocument(documentId);
594
+ const vpMetrics = viewportScope.getMetrics();
595
+ const MENU_HEIGHT_GUESS = 40;
596
+ const head = bounds[0];
597
+ const tailMetrics = this.getPlacementMetrics(documentId, tail.page, tail.rect, vpMetrics);
598
+ const headMetrics = this.getPlacementMetrics(documentId, head.page, head.rect, vpMetrics);
599
+ if (tailMetrics) {
600
+ if (tailMetrics.isBottomVisible && tailMetrics.spaceBelow > MENU_HEIGHT_GUESS) {
601
+ this.menuPlacement$.emit(documentId, {
602
+ ...tailMetrics,
603
+ suggestTop: false,
604
+ isVisible: true
605
+ });
606
+ return;
607
+ }
608
+ }
609
+ if (headMetrics) {
610
+ if (headMetrics.isTopVisible) {
611
+ this.menuPlacement$.emit(documentId, {
612
+ ...headMetrics,
613
+ suggestTop: true,
614
+ isVisible: true
615
+ });
616
+ return;
617
+ }
618
+ }
619
+ if (tailMetrics && tailMetrics.isBottomVisible) {
620
+ this.menuPlacement$.emit(documentId, {
621
+ ...tailMetrics,
622
+ suggestTop: false,
623
+ isVisible: true
624
+ });
625
+ return;
626
+ }
627
+ this.menuPlacement$.emit(documentId, null);
628
+ }
629
+ notifyPage(documentId, pageIndex) {
630
+ var _a;
631
+ const callback = (_a = this.pageCallbacks.get(documentId)) == null ? void 0 : _a.get(pageIndex);
330
632
  if (callback) {
331
- const mode = (_a = this.interactionManagerCapability) == null ? void 0 : _a.getActiveMode();
633
+ const docState = this.getDocumentState(documentId);
634
+ const mode = this.interactionManagerCapability.forDocument(documentId).getActiveMode();
332
635
  if (mode === "pointerMode") {
333
636
  callback({
334
- rects: selectRectsForPage(this.state, pageIndex),
335
- boundingRect: selectBoundingRectForPage(this.state, pageIndex)
637
+ rects: selectRectsForPage(docState, pageIndex),
638
+ boundingRect: selectBoundingRectForPage(docState, pageIndex)
336
639
  });
337
640
  } else {
338
641
  callback({ rects: [], boundingRect: null });
339
642
  }
340
643
  }
341
644
  }
342
- notifyAllPages() {
343
- this.pageCallbacks.forEach((_, pageIndex) => {
344
- this.notifyPage(pageIndex);
645
+ notifyAllPages(documentId) {
646
+ var _a;
647
+ (_a = this.pageCallbacks.get(documentId)) == null ? void 0 : _a.forEach((_, pageIndex) => {
648
+ this.notifyPage(documentId, pageIndex);
345
649
  });
346
650
  }
347
- getNewPageGeometryAndCache(pageIdx) {
348
- if (!this.coreState.core.document)
651
+ getNewPageGeometryAndCache(documentId, pageIdx) {
652
+ const coreDoc = this.getCoreDocument(documentId);
653
+ if (!coreDoc || !coreDoc.document)
349
654
  return PdfTaskHelper.reject({ code: PdfErrorCode.NotFound, message: "Doc Not Found" });
350
- const page = this.coreState.core.document.pages.find((p) => p.index === pageIdx);
351
- const task = this.engine.getPageGeometry(this.coreState.core.document, page);
655
+ const page = coreDoc.document.pages.find((p) => p.index === pageIdx);
656
+ const task = this.engine.getPageGeometry(coreDoc.document, page);
352
657
  task.wait((geo) => {
353
- this.dispatch(cachePageGeometry(pageIdx, geo));
658
+ this.dispatch(cachePageGeometry(documentId, pageIdx, geo));
354
659
  }, ignore);
355
660
  return task;
356
661
  }
357
662
  /* ── geometry cache ───────────────────────────────────── */
358
- getOrLoadGeometry(pageIdx) {
359
- const cached = this.state.geometry[pageIdx];
663
+ getOrLoadGeometry(documentId, pageIdx) {
664
+ const cached = this.getDocumentState(documentId).geometry[pageIdx];
360
665
  if (cached) return PdfTaskHelper.resolve(cached);
361
- return this.getNewPageGeometryAndCache(pageIdx);
666
+ return this.getNewPageGeometryAndCache(documentId, pageIdx);
362
667
  }
363
668
  /* ── selection state updates ───────────────────────────── */
364
- beginSelection(page, index) {
365
- this.selecting = true;
366
- this.anchor = { page, index };
367
- this.dispatch(startSelection());
368
- this.beginSelection$.emit({ page, index });
369
- }
370
- endSelection() {
371
- this.selecting = false;
372
- this.anchor = void 0;
373
- this.dispatch(endSelection());
374
- this.endSelection$.emit();
375
- }
376
- clearSelection() {
377
- this.selecting = false;
378
- this.anchor = void 0;
379
- this.dispatch(clearSelection());
380
- this.selChange$.emit(null);
381
- this.notifyAllPages();
382
- }
383
- updateSelection(page, index) {
384
- if (!this.selecting || !this.anchor) return;
385
- const a = this.anchor;
669
+ beginSelection(documentId, page, index) {
670
+ this.selecting.set(documentId, true);
671
+ this.anchor.set(documentId, { page, index });
672
+ this.dispatch(startSelection(documentId));
673
+ this.beginSelection$.emit(documentId, { page, index });
674
+ this.recalculateMenuPlacement(documentId);
675
+ }
676
+ endSelection(documentId) {
677
+ this.selecting.set(documentId, false);
678
+ this.anchor.set(documentId, void 0);
679
+ this.dispatch(endSelection(documentId));
680
+ this.endSelection$.emit(documentId);
681
+ this.recalculateMenuPlacement(documentId);
682
+ }
683
+ clearSelection(documentId) {
684
+ this.selecting.set(documentId, false);
685
+ this.anchor.set(documentId, void 0);
686
+ this.dispatch(clearSelection(documentId));
687
+ this.selChange$.emit(documentId, null);
688
+ this.menuPlacement$.emit(documentId, null);
689
+ this.notifyAllPages(documentId);
690
+ }
691
+ updateSelection(documentId, page, index) {
692
+ if (!this.selecting.get(documentId) || !this.anchor.get(documentId)) return;
693
+ const a = this.anchor.get(documentId);
386
694
  const forward = page > a.page || page === a.page && index >= a.index;
387
695
  const start = forward ? a : { page, index };
388
696
  const end = forward ? { page, index } : a;
389
697
  const range = { start, end };
390
- this.dispatch(setSelection(range));
391
- this.updateRectsAndSlices(range);
392
- this.selChange$.emit(range);
698
+ this.dispatch(setSelection(documentId, range));
699
+ this.updateRectsAndSlices(documentId, range);
700
+ this.selChange$.emit(documentId, range);
393
701
  for (let p = range.start.page; p <= range.end.page; p++) {
394
- this.notifyPage(p);
702
+ this.notifyPage(documentId, p);
395
703
  }
396
704
  }
397
- updateRectsAndSlices(range) {
705
+ updateRectsAndSlices(documentId, range) {
706
+ const docState = this.getDocumentState(documentId);
398
707
  const allRects = {};
399
708
  const allSlices = {};
400
709
  for (let p = range.start.page; p <= range.end.page; p++) {
401
- const geo = this.state.geometry[p];
710
+ const geo = docState.geometry[p];
402
711
  const sb = sliceBounds(range, geo, p);
403
712
  if (!sb) continue;
404
713
  allRects[p] = rectsWithinSlice(geo, sb.from, sb.to);
405
714
  allSlices[p] = { start: sb.from, count: sb.to - sb.from + 1 };
406
715
  }
407
- this.dispatch(setRects(allRects));
408
- this.dispatch(setSlices(allSlices));
716
+ this.dispatch(setRects(documentId, allRects));
717
+ this.dispatch(setSlices(documentId, allSlices));
409
718
  }
410
- getSelectedText() {
411
- if (!this.coreState.core.document || !this.state.selection) {
719
+ getSelectedText(documentId) {
720
+ const coreDoc = this.getCoreDocument(documentId);
721
+ const docState = this.getDocumentState(documentId);
722
+ if (!(coreDoc == null ? void 0 : coreDoc.document) || !docState.selection) {
412
723
  return PdfTaskHelper.reject({
413
724
  code: PdfErrorCode.NotFound,
414
725
  message: "Doc Not Found or No Selection"
415
726
  });
416
727
  }
417
- const sel = this.state.selection;
728
+ const sel = docState.selection;
418
729
  const req = [];
419
730
  for (let p = sel.start.page; p <= sel.end.page; p++) {
420
- const s = this.state.slices[p];
731
+ const s = docState.slices[p];
421
732
  if (s) req.push({ pageIndex: p, charIndex: s.start, charCount: s.count });
422
733
  }
423
734
  if (req.length === 0) return PdfTaskHelper.resolve([]);
424
- const task = this.engine.getTextSlices(this.coreState.core.document, req);
735
+ const task = this.engine.getTextSlices(coreDoc.document, req);
425
736
  task.wait((text) => {
426
- this.textRetrieved$.emit(text);
737
+ this.textRetrieved$.emit(documentId, text);
427
738
  }, ignore);
428
739
  return task;
429
740
  }
430
- copyToClipboard() {
431
- const text = this.getSelectedText();
741
+ copyToClipboard(documentId) {
742
+ const text = this.getSelectedText(documentId);
432
743
  text.wait((text2) => {
433
- this.copyToClipboard$.emit(text2.join("\n"));
744
+ this.copyToClipboard$.emit(documentId, text2.join("\n"));
434
745
  }, ignore);
435
746
  }
436
747
  };
437
748
  _SelectionPlugin.id = "selection";
438
749
  let SelectionPlugin = _SelectionPlugin;
439
- const initialState = {
440
- geometry: {},
441
- rects: {},
442
- slices: {},
443
- selection: null,
444
- active: false,
445
- selecting: false
446
- };
447
- const selectionReducer = (state = initialState, action) => {
448
- switch (action.type) {
449
- case CACHE_PAGE_GEOMETRY: {
450
- const { page, geo } = action.payload;
451
- return { ...state, geometry: { ...state.geometry, [page]: geo } };
452
- }
453
- case SET_SELECTION:
454
- return { ...state, selection: action.payload, active: true };
455
- case START_SELECTION:
456
- return { ...state, selecting: true, selection: null, rects: {} };
457
- case END_SELECTION:
458
- return { ...state, selecting: false };
459
- case CLEAR_SELECTION:
460
- return { ...state, selecting: false, selection: null, rects: {}, active: false };
461
- case SET_RECTS:
462
- return { ...state, rects: action.payload };
463
- case SET_SLICES:
464
- return { ...state, slices: action.payload };
465
- case RESET:
466
- return initialState;
467
- default:
468
- return state;
469
- }
470
- };
471
750
  const SelectionPluginPackage = {
472
751
  manifest,
473
752
  create: (registry) => new SelectionPlugin(SELECTION_PLUGIN_ID, registry),