@embedpdf/plugin-selection 1.5.0 → 2.0.0-next.1

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