@embedpdf/plugin-viewport 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 (39) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +499 -111
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/actions.d.ts +77 -9
  6. package/dist/lib/types.d.ts +55 -12
  7. package/dist/lib/viewport-plugin.d.ts +24 -9
  8. package/dist/preact/index.cjs +1 -1
  9. package/dist/preact/index.cjs.map +1 -1
  10. package/dist/preact/index.js +40 -15
  11. package/dist/preact/index.js.map +1 -1
  12. package/dist/react/index.cjs +1 -1
  13. package/dist/react/index.cjs.map +1 -1
  14. package/dist/react/index.js +40 -15
  15. package/dist/react/index.js.map +1 -1
  16. package/dist/shared/components/viewport.d.ts +5 -1
  17. package/dist/shared/hooks/use-viewport-ref.d.ts +1 -1
  18. package/dist/shared/hooks/use-viewport.d.ts +11 -1
  19. package/dist/shared-preact/components/viewport.d.ts +5 -1
  20. package/dist/shared-preact/hooks/use-viewport-ref.d.ts +1 -1
  21. package/dist/shared-preact/hooks/use-viewport.d.ts +11 -1
  22. package/dist/shared-react/components/viewport.d.ts +5 -1
  23. package/dist/shared-react/hooks/use-viewport-ref.d.ts +1 -1
  24. package/dist/shared-react/hooks/use-viewport.d.ts +11 -1
  25. package/dist/svelte/components/Viewport.svelte.d.ts +4 -0
  26. package/dist/svelte/hooks/use-viewport-ref.svelte.d.ts +5 -1
  27. package/dist/svelte/hooks/use-viewport.svelte.d.ts +15 -4
  28. package/dist/svelte/index.cjs +1 -1
  29. package/dist/svelte/index.cjs.map +1 -1
  30. package/dist/svelte/index.js +86 -21
  31. package/dist/svelte/index.js.map +1 -1
  32. package/dist/vue/components/viewport.vue.d.ts +9 -2
  33. package/dist/vue/hooks/use-viewport-ref.d.ts +6 -1
  34. package/dist/vue/hooks/use-viewport.d.ts +12 -1
  35. package/dist/vue/index.cjs +1 -1
  36. package/dist/vue/index.cjs.map +1 -1
  37. package/dist/vue/index.js +66 -38
  38. package/dist/vue/index.js.map +1 -1
  39. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -13,37 +13,52 @@ const manifest = {
13
13
  scrollEndDelay: 300
14
14
  }
15
15
  };
16
+ const INIT_VIEWPORT_STATE = "INIT_VIEWPORT_STATE";
17
+ const CLEANUP_VIEWPORT_STATE = "CLEANUP_VIEWPORT_STATE";
18
+ const REGISTER_VIEWPORT = "REGISTER_VIEWPORT";
19
+ const UNREGISTER_VIEWPORT = "UNREGISTER_VIEWPORT";
16
20
  const SET_VIEWPORT_METRICS = "SET_VIEWPORT_METRICS";
17
21
  const SET_VIEWPORT_SCROLL_METRICS = "SET_VIEWPORT_SCROLL_METRICS";
18
22
  const SET_VIEWPORT_GAP = "SET_VIEWPORT_GAP";
19
23
  const SET_SCROLL_ACTIVITY = "SET_SCROLL_ACTIVITY";
20
24
  const SET_SMOOTH_SCROLL_ACTIVITY = "SET_SMOOTH_SCROLL_ACTIVITY";
25
+ const SET_ACTIVE_VIEWPORT_DOCUMENT = "SET_ACTIVE_VIEWPORT_DOCUMENT";
26
+ const ADD_VIEWPORT_GATE = "ADD_VIEWPORT_GATE";
27
+ const REMOVE_VIEWPORT_GATE = "REMOVE_VIEWPORT_GATE";
28
+ function initViewportState(documentId) {
29
+ return { type: INIT_VIEWPORT_STATE, payload: { documentId } };
30
+ }
31
+ function cleanupViewportState(documentId) {
32
+ return { type: CLEANUP_VIEWPORT_STATE, payload: { documentId } };
33
+ }
34
+ function registerViewport(documentId) {
35
+ return { type: REGISTER_VIEWPORT, payload: { documentId } };
36
+ }
37
+ function unregisterViewport(documentId) {
38
+ return { type: UNREGISTER_VIEWPORT, payload: { documentId } };
39
+ }
21
40
  function setViewportGap(viewportGap) {
22
- return {
23
- type: SET_VIEWPORT_GAP,
24
- payload: viewportGap
25
- };
41
+ return { type: SET_VIEWPORT_GAP, payload: viewportGap };
26
42
  }
27
- function setViewportMetrics(viewportMetrics) {
28
- return {
29
- type: SET_VIEWPORT_METRICS,
30
- payload: viewportMetrics
31
- };
43
+ function setViewportMetrics(documentId, metrics) {
44
+ return { type: SET_VIEWPORT_METRICS, payload: { documentId, metrics } };
32
45
  }
33
- function setViewportScrollMetrics(scrollMetrics) {
34
- return {
35
- type: SET_VIEWPORT_SCROLL_METRICS,
36
- payload: scrollMetrics
37
- };
46
+ function setViewportScrollMetrics(documentId, scrollMetrics) {
47
+ return { type: SET_VIEWPORT_SCROLL_METRICS, payload: { documentId, scrollMetrics } };
38
48
  }
39
- function setScrollActivity(isScrolling) {
40
- return { type: SET_SCROLL_ACTIVITY, payload: isScrolling };
49
+ function setScrollActivity(documentId, isScrolling) {
50
+ return { type: SET_SCROLL_ACTIVITY, payload: { documentId, isScrolling } };
41
51
  }
42
- function setSmoothScrollActivity(isSmoothScrolling) {
43
- return { type: SET_SMOOTH_SCROLL_ACTIVITY, payload: isSmoothScrolling };
52
+ function setSmoothScrollActivity(documentId, isSmoothScrolling) {
53
+ return { type: SET_SMOOTH_SCROLL_ACTIVITY, payload: { documentId, isSmoothScrolling } };
44
54
  }
45
- const initialState = {
46
- viewportGap: 0,
55
+ function addViewportGate(documentId, key) {
56
+ return { type: ADD_VIEWPORT_GATE, payload: { documentId, key } };
57
+ }
58
+ function removeViewportGate(documentId, key) {
59
+ return { type: REMOVE_VIEWPORT_GATE, payload: { documentId, key } };
60
+ }
61
+ const initialViewportDocumentState = {
47
62
  viewportMetrics: {
48
63
  width: 0,
49
64
  height: 0,
@@ -53,50 +68,198 @@ const initialState = {
53
68
  clientHeight: 0,
54
69
  scrollWidth: 0,
55
70
  scrollHeight: 0,
56
- relativePosition: {
57
- x: 0,
58
- y: 0
59
- }
71
+ relativePosition: { x: 0, y: 0 }
60
72
  },
61
73
  isScrolling: false,
62
- isSmoothScrolling: false
74
+ isSmoothScrolling: false,
75
+ gates: /* @__PURE__ */ new Set()
76
+ };
77
+ const initialState = {
78
+ viewportGap: 0,
79
+ documents: {},
80
+ activeViewports: /* @__PURE__ */ new Set(),
81
+ activeDocumentId: null
63
82
  };
64
83
  const viewportReducer = (state = initialState, action) => {
65
84
  switch (action.type) {
66
- case SET_VIEWPORT_GAP:
67
- return { ...state, viewportGap: action.payload };
68
- case SET_VIEWPORT_METRICS:
85
+ // ─────────────────────────────────────────────────────────
86
+ // State Persistence (Document Lifecycle)
87
+ // ─────────────────────────────────────────────────────────
88
+ case INIT_VIEWPORT_STATE: {
89
+ const { documentId } = action.payload;
90
+ return {
91
+ ...state,
92
+ documents: {
93
+ ...state.documents,
94
+ [documentId]: { ...initialViewportDocumentState, gates: /* @__PURE__ */ new Set() }
95
+ }
96
+ };
97
+ }
98
+ case CLEANUP_VIEWPORT_STATE: {
99
+ const { documentId } = action.payload;
100
+ const { [documentId]: removed, ...remainingDocs } = state.documents;
101
+ const newActiveViewports = new Set(state.activeViewports);
102
+ newActiveViewports.delete(documentId);
103
+ return {
104
+ ...state,
105
+ documents: remainingDocs,
106
+ activeViewports: newActiveViewports,
107
+ activeDocumentId: state.activeDocumentId === documentId ? null : state.activeDocumentId
108
+ };
109
+ }
110
+ // ─────────────────────────────────────────────────────────
111
+ // Viewport Registration (DOM Lifecycle)
112
+ // ─────────────────────────────────────────────────────────
113
+ case REGISTER_VIEWPORT: {
114
+ const { documentId } = action.payload;
115
+ const newActiveViewports = new Set(state.activeViewports);
116
+ newActiveViewports.add(documentId);
117
+ return {
118
+ ...state,
119
+ activeViewports: newActiveViewports,
120
+ // Set as active if no active document
121
+ activeDocumentId: state.activeDocumentId ?? documentId
122
+ };
123
+ }
124
+ case UNREGISTER_VIEWPORT: {
125
+ const { documentId } = action.payload;
126
+ const newActiveViewports = new Set(state.activeViewports);
127
+ newActiveViewports.delete(documentId);
128
+ return {
129
+ ...state,
130
+ activeViewports: newActiveViewports
131
+ };
132
+ }
133
+ case SET_ACTIVE_VIEWPORT_DOCUMENT: {
134
+ return {
135
+ ...state,
136
+ activeDocumentId: action.payload
137
+ };
138
+ }
139
+ // ─────────────────────────────────────────────────────────
140
+ // Viewport Operations
141
+ // ─────────────────────────────────────────────────────────
142
+ case SET_VIEWPORT_GAP: {
143
+ return {
144
+ ...state,
145
+ viewportGap: action.payload
146
+ };
147
+ }
148
+ case SET_VIEWPORT_METRICS: {
149
+ const { documentId, metrics } = action.payload;
150
+ const viewport = state.documents[documentId];
151
+ if (!viewport) return state;
69
152
  return {
70
153
  ...state,
71
- viewportMetrics: {
72
- width: action.payload.width,
73
- height: action.payload.height,
74
- scrollTop: action.payload.scrollTop,
75
- scrollLeft: action.payload.scrollLeft,
76
- clientWidth: action.payload.clientWidth,
77
- clientHeight: action.payload.clientHeight,
78
- scrollWidth: action.payload.scrollWidth,
79
- scrollHeight: action.payload.scrollHeight,
80
- relativePosition: {
81
- x: action.payload.scrollWidth <= action.payload.clientWidth ? 0 : action.payload.scrollLeft / (action.payload.scrollWidth - action.payload.clientWidth),
82
- y: action.payload.scrollHeight <= action.payload.clientHeight ? 0 : action.payload.scrollTop / (action.payload.scrollHeight - action.payload.clientHeight)
154
+ documents: {
155
+ ...state.documents,
156
+ [documentId]: {
157
+ ...viewport,
158
+ viewportMetrics: {
159
+ width: metrics.width,
160
+ height: metrics.height,
161
+ scrollTop: metrics.scrollTop,
162
+ scrollLeft: metrics.scrollLeft,
163
+ clientWidth: metrics.clientWidth,
164
+ clientHeight: metrics.clientHeight,
165
+ scrollWidth: metrics.scrollWidth,
166
+ scrollHeight: metrics.scrollHeight,
167
+ relativePosition: {
168
+ x: metrics.scrollWidth <= metrics.clientWidth ? 0 : metrics.scrollLeft / (metrics.scrollWidth - metrics.clientWidth),
169
+ y: metrics.scrollHeight <= metrics.clientHeight ? 0 : metrics.scrollTop / (metrics.scrollHeight - metrics.clientHeight)
170
+ }
171
+ }
83
172
  }
84
173
  }
85
174
  };
86
- case SET_VIEWPORT_SCROLL_METRICS:
175
+ }
176
+ case SET_VIEWPORT_SCROLL_METRICS: {
177
+ const { documentId, scrollMetrics } = action.payload;
178
+ const viewport = state.documents[documentId];
179
+ if (!viewport) return state;
87
180
  return {
88
181
  ...state,
89
- viewportMetrics: {
90
- ...state.viewportMetrics,
91
- scrollTop: action.payload.scrollTop,
92
- scrollLeft: action.payload.scrollLeft
93
- },
94
- isScrolling: true
182
+ documents: {
183
+ ...state.documents,
184
+ [documentId]: {
185
+ ...viewport,
186
+ viewportMetrics: {
187
+ ...viewport.viewportMetrics,
188
+ scrollTop: scrollMetrics.scrollTop,
189
+ scrollLeft: scrollMetrics.scrollLeft
190
+ },
191
+ isScrolling: true
192
+ }
193
+ }
95
194
  };
96
- case SET_SCROLL_ACTIVITY:
97
- return { ...state, isScrolling: action.payload };
98
- case SET_SMOOTH_SCROLL_ACTIVITY:
99
- return { ...state, isSmoothScrolling: action.payload };
195
+ }
196
+ case SET_SCROLL_ACTIVITY: {
197
+ const { documentId, isScrolling } = action.payload;
198
+ const viewport = state.documents[documentId];
199
+ if (!viewport) return state;
200
+ return {
201
+ ...state,
202
+ documents: {
203
+ ...state.documents,
204
+ [documentId]: {
205
+ ...viewport,
206
+ isScrolling
207
+ }
208
+ }
209
+ };
210
+ }
211
+ case SET_SMOOTH_SCROLL_ACTIVITY: {
212
+ const { documentId, isSmoothScrolling } = action.payload;
213
+ const viewport = state.documents[documentId];
214
+ if (!viewport) return state;
215
+ return {
216
+ ...state,
217
+ documents: {
218
+ ...state.documents,
219
+ [documentId]: {
220
+ ...viewport,
221
+ isSmoothScrolling
222
+ }
223
+ }
224
+ };
225
+ }
226
+ // ─────────────────────────────────────────────────────────
227
+ // Named Gate Operations
228
+ // ─────────────────────────────────────────────────────────
229
+ case ADD_VIEWPORT_GATE: {
230
+ const { documentId, key } = action.payload;
231
+ const viewport = state.documents[documentId];
232
+ if (!viewport) return state;
233
+ const newGates = new Set(viewport.gates);
234
+ newGates.add(key);
235
+ return {
236
+ ...state,
237
+ documents: {
238
+ ...state.documents,
239
+ [documentId]: {
240
+ ...viewport,
241
+ gates: newGates
242
+ }
243
+ }
244
+ };
245
+ }
246
+ case REMOVE_VIEWPORT_GATE: {
247
+ const { documentId, key } = action.payload;
248
+ const viewport = state.documents[documentId];
249
+ if (!viewport) return state;
250
+ const newGates = new Set(viewport.gates);
251
+ newGates.delete(key);
252
+ return {
253
+ ...state,
254
+ documents: {
255
+ ...state.documents,
256
+ [documentId]: {
257
+ ...viewport,
258
+ gates: newGates
259
+ }
260
+ }
261
+ };
262
+ }
100
263
  default:
101
264
  return state;
102
265
  }
@@ -108,108 +271,333 @@ const _ViewportPlugin = class _ViewportPlugin extends BasePlugin {
108
271
  this.viewportResize$ = createBehaviorEmitter();
109
272
  this.viewportMetrics$ = createBehaviorEmitter();
110
273
  this.scrollMetrics$ = createBehaviorEmitter();
111
- this.scrollReq$ = createEmitter();
112
274
  this.scrollActivity$ = createBehaviorEmitter();
113
- this.rectProvider = null;
275
+ this.gateState$ = createBehaviorEmitter();
276
+ this.scrollRequests$ = /* @__PURE__ */ new Map();
277
+ this.rectProviders = /* @__PURE__ */ new Map();
114
278
  if (config.viewportGap) {
115
279
  this.dispatch(setViewportGap(config.viewportGap));
116
280
  }
117
281
  this.scrollEndDelay = config.scrollEndDelay || 100;
118
282
  }
283
+ // ─────────────────────────────────────────────────────────
284
+ // Document Lifecycle (from BasePlugin)
285
+ // ─────────────────────────────────────────────────────────
286
+ onDocumentLoadingStarted(documentId) {
287
+ this.dispatch(initViewportState(documentId));
288
+ this.scrollRequests$.set(documentId, createEmitter());
289
+ this.logger.debug(
290
+ "ViewportPlugin",
291
+ "DocumentOpened",
292
+ `Initialized viewport state for document: ${documentId}`
293
+ );
294
+ }
295
+ onDocumentClosed(documentId) {
296
+ var _a;
297
+ this.dispatch(cleanupViewportState(documentId));
298
+ (_a = this.scrollRequests$.get(documentId)) == null ? void 0 : _a.clear();
299
+ this.scrollRequests$.delete(documentId);
300
+ this.rectProviders.delete(documentId);
301
+ this.logger.debug(
302
+ "ViewportPlugin",
303
+ "DocumentClosed",
304
+ `Cleaned up viewport state for document: ${documentId}`
305
+ );
306
+ }
307
+ // ─────────────────────────────────────────────────────────
308
+ // Capability
309
+ // ─────────────────────────────────────────────────────────
119
310
  buildCapability() {
120
311
  return {
312
+ // Global
121
313
  getViewportGap: () => this.state.viewportGap,
122
- getMetrics: () => this.state.viewportMetrics,
123
- getBoundingRect: () => {
124
- var _a;
125
- return ((_a = this.rectProvider) == null ? void 0 : _a.call(this)) ?? {
126
- origin: { x: 0, y: 0 },
127
- size: { width: 0, height: 0 }
128
- };
129
- },
314
+ // Active document operations
315
+ getMetrics: () => this.getMetrics(),
130
316
  scrollTo: (pos) => this.scrollTo(pos),
131
- isScrolling: () => this.state.isScrolling,
132
- isSmoothScrolling: () => this.state.isSmoothScrolling,
133
- onScrollChange: this.scrollMetrics$.on,
317
+ isScrolling: () => this.isScrolling(),
318
+ isSmoothScrolling: () => this.isSmoothScrolling(),
319
+ isGated: (documentId) => this.isGated(documentId),
320
+ hasGate: (key, documentId) => this.hasGate(key, documentId),
321
+ getGates: (documentId) => this.getGates(documentId),
322
+ getBoundingRect: () => this.getBoundingRect(),
323
+ // Document-scoped operations
324
+ forDocument: (documentId) => this.createViewportScope(documentId),
325
+ gate: (key, documentId) => this.gate(key, documentId),
326
+ releaseGate: (key, documentId) => this.releaseGate(key, documentId),
327
+ // Check if viewport is currently mounted
328
+ isViewportMounted: (documentId) => this.state.activeViewports.has(documentId),
329
+ // Events
134
330
  onViewportChange: this.viewportMetrics$.on,
135
331
  onViewportResize: this.viewportResize$.on,
136
- onScrollActivity: this.scrollActivity$.on
332
+ onScrollChange: this.scrollMetrics$.on,
333
+ onScrollActivity: this.scrollActivity$.on,
334
+ onGateChange: this.gateState$.on
335
+ };
336
+ }
337
+ // ─────────────────────────────────────────────────────────
338
+ // Document Scoping
339
+ // ─────────────────────────────────────────────────────────
340
+ createViewportScope(documentId) {
341
+ return {
342
+ getMetrics: () => this.getMetrics(documentId),
343
+ scrollTo: (pos) => this.scrollTo(pos, documentId),
344
+ isScrolling: () => this.isScrolling(documentId),
345
+ isSmoothScrolling: () => this.isSmoothScrolling(documentId),
346
+ isGated: () => this.isGated(documentId),
347
+ hasGate: (key) => this.hasGate(key, documentId),
348
+ getGates: () => this.getGates(documentId),
349
+ gate: (key) => this.gate(key, documentId),
350
+ releaseGate: (key) => this.releaseGate(key, documentId),
351
+ getBoundingRect: () => this.getBoundingRect(documentId),
352
+ onViewportChange: (listener) => this.viewportMetrics$.on((event) => {
353
+ if (event.documentId === documentId) listener(event.metrics);
354
+ }),
355
+ onScrollChange: (listener) => this.scrollMetrics$.on((event) => {
356
+ if (event.documentId === documentId) listener(event.scrollMetrics);
357
+ }),
358
+ onScrollActivity: (listener) => this.scrollActivity$.on((event) => {
359
+ if (event.documentId === documentId) listener(event.activity);
360
+ }),
361
+ onGateChange: (listener) => this.gateState$.on((event) => {
362
+ if ((event == null ? void 0 : event.documentId) === documentId) listener(event);
363
+ })
137
364
  };
138
365
  }
139
- setViewportResizeMetrics(viewportMetrics) {
366
+ // ─────────────────────────────────────────────────────────
367
+ // Viewport Registration (Public API for components)
368
+ // ─────────────────────────────────────────────────────────
369
+ registerViewport(documentId) {
370
+ if (!this.state.documents[documentId]) {
371
+ throw new Error(
372
+ `Cannot register viewport for ${documentId}: document state not found. Document must be opened before registering viewport.`
373
+ );
374
+ }
375
+ if (!this.state.activeViewports.has(documentId)) {
376
+ this.dispatch(registerViewport(documentId));
377
+ this.logger.debug(
378
+ "ViewportPlugin",
379
+ "RegisterViewport",
380
+ `Registered viewport (DOM mounted) for document: ${documentId}`
381
+ );
382
+ }
383
+ }
384
+ unregisterViewport(documentId) {
140
385
  if (this.registry.isDestroyed()) return;
141
- this.dispatch(setViewportMetrics(viewportMetrics));
142
- this.viewportResize$.emit(this.state.viewportMetrics);
386
+ if (this.state.activeViewports.has(documentId)) {
387
+ this.dispatch(unregisterViewport(documentId));
388
+ this.rectProviders.delete(documentId);
389
+ this.logger.debug(
390
+ "ViewportPlugin",
391
+ "UnregisterViewport",
392
+ `Unregistered viewport (DOM unmounted) for document: ${documentId}. State preserved.`
393
+ );
394
+ }
395
+ }
396
+ // ─────────────────────────────────────────────────────────
397
+ // Per-Document Operations
398
+ // ─────────────────────────────────────────────────────────
399
+ setViewportResizeMetrics(documentId, metrics) {
400
+ if (this.registry.isDestroyed()) return;
401
+ this.dispatch(setViewportMetrics(documentId, metrics));
402
+ const viewport = this.state.documents[documentId];
403
+ if (viewport) {
404
+ this.viewportResize$.emit({
405
+ documentId,
406
+ metrics: viewport.viewportMetrics
407
+ });
408
+ }
143
409
  }
144
- setViewportScrollMetrics(scrollMetrics) {
410
+ setViewportScrollMetrics(documentId, scrollMetrics) {
145
411
  if (this.registry.isDestroyed()) return;
146
- if (scrollMetrics.scrollTop !== this.state.viewportMetrics.scrollTop || scrollMetrics.scrollLeft !== this.state.viewportMetrics.scrollLeft) {
147
- this.dispatch(setViewportScrollMetrics(scrollMetrics));
148
- this.bumpScrollActivity();
412
+ const viewport = this.state.documents[documentId];
413
+ if (!viewport) return;
414
+ if (scrollMetrics.scrollTop !== viewport.viewportMetrics.scrollTop || scrollMetrics.scrollLeft !== viewport.viewportMetrics.scrollLeft) {
415
+ this.dispatch(setViewportScrollMetrics(documentId, scrollMetrics));
416
+ this.bumpScrollActivity(documentId);
149
417
  this.scrollMetrics$.emit({
150
- scrollTop: scrollMetrics.scrollTop,
151
- scrollLeft: scrollMetrics.scrollLeft
418
+ documentId,
419
+ scrollMetrics
152
420
  });
153
421
  }
154
422
  }
155
- onScrollRequest(listener) {
156
- return this.scrollReq$.on(listener);
423
+ onScrollRequest(documentId, listener) {
424
+ const emitter = this.scrollRequests$.get(documentId);
425
+ if (!emitter) {
426
+ throw new Error(
427
+ `Cannot subscribe to scroll requests for ${documentId}: document state not initialized`
428
+ );
429
+ }
430
+ return emitter.on(listener);
157
431
  }
158
- registerBoundingRectProvider(provider) {
159
- this.rectProvider = provider;
432
+ registerBoundingRectProvider(documentId, provider) {
433
+ if (provider) {
434
+ this.rectProviders.set(documentId, provider);
435
+ } else {
436
+ this.rectProviders.delete(documentId);
437
+ }
438
+ }
439
+ // ─────────────────────────────────────────────────────────
440
+ // Public Gating API
441
+ // ─────────────────────────────────────────────────────────
442
+ gate(key, documentId) {
443
+ const viewport = this.state.documents[documentId];
444
+ if (!viewport) {
445
+ this.logger.warn(
446
+ "ViewportPlugin",
447
+ "GateViewport",
448
+ `Cannot gate viewport for ${documentId}: document not found`
449
+ );
450
+ return;
451
+ }
452
+ if (!viewport.gates.has(key)) {
453
+ this.dispatch(addViewportGate(documentId, key));
454
+ this.logger.debug(
455
+ "ViewportPlugin",
456
+ "GateAdded",
457
+ `Added gate '${key}' for document: ${documentId}. Total gates: ${viewport.gates.size + 1}`
458
+ );
459
+ }
460
+ }
461
+ releaseGate(key, documentId) {
462
+ const viewport = this.state.documents[documentId];
463
+ if (!viewport) {
464
+ this.logger.warn(
465
+ "ViewportPlugin",
466
+ "ReleaseGate",
467
+ `Cannot release gate for ${documentId}: document not found`
468
+ );
469
+ return;
470
+ }
471
+ if (viewport.gates.has(key)) {
472
+ this.dispatch(removeViewportGate(documentId, key));
473
+ this.logger.debug(
474
+ "ViewportPlugin",
475
+ "GateReleased",
476
+ `Released gate '${key}' for document: ${documentId}. Remaining gates: ${viewport.gates.size - 1}`
477
+ );
478
+ }
479
+ }
480
+ // ─────────────────────────────────────────────────────────
481
+ // Helper Methods
482
+ // ─────────────────────────────────────────────────────────
483
+ getViewportState(documentId) {
484
+ const id = documentId ?? this.getActiveDocumentId();
485
+ const viewport = this.state.documents[id];
486
+ if (!viewport) {
487
+ throw new Error(`Viewport state not found for document: ${id}`);
488
+ }
489
+ return viewport;
490
+ }
491
+ getMetrics(documentId) {
492
+ return this.getViewportState(documentId).viewportMetrics;
493
+ }
494
+ isScrolling(documentId) {
495
+ return this.getViewportState(documentId).isScrolling;
496
+ }
497
+ isSmoothScrolling(documentId) {
498
+ return this.getViewportState(documentId).isSmoothScrolling;
160
499
  }
161
- bumpScrollActivity() {
162
- this.debouncedDispatch(setScrollActivity(false), this.scrollEndDelay);
163
- this.debouncedDispatch(setSmoothScrollActivity(false), this.scrollEndDelay);
500
+ isGated(documentId) {
501
+ const viewport = this.getViewportState(documentId);
502
+ return viewport.gates.size > 0;
164
503
  }
165
- scrollTo(pos) {
504
+ hasGate(key, documentId) {
505
+ const viewport = this.getViewportState(documentId);
506
+ return viewport.gates.has(key);
507
+ }
508
+ getGates(documentId) {
509
+ const viewport = this.getViewportState(documentId);
510
+ return Array.from(viewport.gates);
511
+ }
512
+ getBoundingRect(documentId) {
513
+ const id = documentId ?? this.getActiveDocumentId();
514
+ const provider = this.rectProviders.get(id);
515
+ return (provider == null ? void 0 : provider()) ?? {
516
+ origin: { x: 0, y: 0 },
517
+ size: { width: 0, height: 0 }
518
+ };
519
+ }
520
+ scrollTo(pos, documentId) {
521
+ const id = documentId ?? this.getActiveDocumentId();
522
+ const viewport = this.getViewportState(id);
166
523
  const { x, y, center, behavior = "auto" } = pos;
167
524
  if (behavior === "smooth") {
168
- this.dispatch(setSmoothScrollActivity(true));
525
+ this.dispatch(setSmoothScrollActivity(id, true));
169
526
  }
527
+ let finalX = x;
528
+ let finalY = y;
170
529
  if (center) {
171
- const metrics = this.state.viewportMetrics;
172
- const centeredX = x - metrics.clientWidth / 2;
173
- const centeredY = y - metrics.clientHeight / 2;
174
- this.scrollReq$.emit({
175
- x: centeredX,
176
- y: centeredY,
177
- behavior
178
- });
179
- } else {
180
- this.scrollReq$.emit({
181
- x,
182
- y,
183
- behavior
184
- });
530
+ const metrics = viewport.viewportMetrics;
531
+ finalX = x - metrics.clientWidth / 2;
532
+ finalY = y - metrics.clientHeight / 2;
533
+ }
534
+ const emitter = this.scrollRequests$.get(id);
535
+ if (emitter) {
536
+ emitter.emit({ x: finalX, y: finalY, behavior });
185
537
  }
186
538
  }
187
- emitScrollActivity() {
188
- const scrollActivity = {
189
- isSmoothScrolling: this.state.isSmoothScrolling,
190
- isScrolling: this.state.isScrolling
191
- };
192
- this.scrollActivity$.emit(scrollActivity);
539
+ bumpScrollActivity(documentId) {
540
+ this.debouncedDispatch(setScrollActivity(documentId, false), this.scrollEndDelay);
541
+ this.debouncedDispatch(setSmoothScrollActivity(documentId, false), this.scrollEndDelay);
193
542
  }
194
- // Subscribe to store changes to notify onViewportChange
543
+ // ─────────────────────────────────────────────────────────
544
+ // State Change Handling
545
+ // ─────────────────────────────────────────────────────────
195
546
  onStoreUpdated(prevState, newState) {
196
- if (prevState !== newState) {
197
- this.viewportMetrics$.emit(newState.viewportMetrics);
198
- if (prevState.isScrolling !== newState.isScrolling || prevState.isSmoothScrolling !== newState.isSmoothScrolling) {
199
- this.emitScrollActivity();
547
+ for (const documentId in newState.documents) {
548
+ const prevViewport = prevState.documents[documentId];
549
+ const newViewport = newState.documents[documentId];
550
+ if (prevViewport !== newViewport) {
551
+ this.viewportMetrics$.emit({
552
+ documentId,
553
+ metrics: newViewport.viewportMetrics
554
+ });
555
+ if (prevViewport && (prevViewport.isScrolling !== newViewport.isScrolling || prevViewport.isSmoothScrolling !== newViewport.isSmoothScrolling)) {
556
+ this.scrollActivity$.emit({
557
+ documentId,
558
+ activity: {
559
+ isScrolling: newViewport.isScrolling,
560
+ isSmoothScrolling: newViewport.isSmoothScrolling
561
+ }
562
+ });
563
+ }
564
+ if (prevViewport && prevViewport.gates !== newViewport.gates) {
565
+ const prevGates = Array.from(prevViewport.gates);
566
+ const newGates = Array.from(newViewport.gates);
567
+ const addedGate = newGates.find((g) => !prevGates.includes(g));
568
+ const removedGate = prevGates.find((g) => !newGates.includes(g));
569
+ this.gateState$.emit({
570
+ documentId,
571
+ isGated: newViewport.gates.size > 0,
572
+ gates: newGates,
573
+ addedGate,
574
+ removedGate
575
+ });
576
+ this.logger.debug(
577
+ "ViewportPlugin",
578
+ "GateStateChanged",
579
+ `Gate state changed for document ${documentId}. Gates: [${newGates.join(", ")}], Gated: ${newViewport.gates.size > 0}`
580
+ );
581
+ }
200
582
  }
201
583
  }
202
584
  }
585
+ // ─────────────────────────────────────────────────────────
586
+ // Lifecycle
587
+ // ─────────────────────────────────────────────────────────
203
588
  async initialize(_config) {
589
+ this.logger.info("ViewportPlugin", "Initialize", "Viewport plugin initialized");
204
590
  }
205
591
  async destroy() {
206
- super.destroy();
207
592
  this.viewportMetrics$.clear();
208
593
  this.viewportResize$.clear();
209
594
  this.scrollMetrics$.clear();
210
- this.scrollReq$.clear();
211
595
  this.scrollActivity$.clear();
212
- this.rectProvider = null;
596
+ this.gateState$.clear();
597
+ this.scrollRequests$.forEach((emitter) => emitter.clear());
598
+ this.scrollRequests$.clear();
599
+ this.rectProviders.clear();
600
+ super.destroy();
213
601
  }
214
602
  };
215
603
  _ViewportPlugin.id = "viewport";