@embedpdf/angular 3.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.
@@ -0,0 +1,580 @@
1
+ import { StageToken, wheelZoomFactor, settingsEqual } from '@embedpdf/plugin-stage';
2
+ export * from '@embedpdf/plugin-stage';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, TemplateRef, Directive, input, computed, viewChild, Injector, ChangeDetectionStrategy, Component, contentChild, PLATFORM_ID, ElementRef, NgZone, effect } from '@angular/core';
5
+ import { NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
6
+ import { createPageContext, EPDF_PAGE, injectKernelHost, injectOptionalCapabilityFor, injectOptionalCapability, injectDocumentId, injectOptionalSelectorFor, injectCapability, injectSelector, shallowArray } from '@embedpdf/angular/runtime';
7
+ import { InteractionToken } from '@embedpdf/plugin-interaction';
8
+
9
+ /**
10
+ * The render-prop translation: React's `children: (page) => ReactNode` becomes
11
+ * marker directives on `<ng-template>`, typed so `let-page` infers
12
+ * `EpdfPageContext` in the template.
13
+ *
14
+ * <epdf-stage>
15
+ * <ng-template epdfPage> page-space content (rotates)
16
+ * <ng-template epdfPageChrome> box-space chrome (never rotates)
17
+ *
18
+ * The `$implicit` context is the SAME stable object the per-page injector
19
+ * provides as `EPDF_PAGE` — `let-page` is sugar for inline chrome; components
20
+ * inside the template just call `injectPage()`.
21
+ */
22
+ /** PAGE-SPACE content for each visible page (RenderLayer, annotations,
23
+ * markers). Rendered inside the page's content frame, so it ROTATES with the
24
+ * page's display rotation — coordinates are plain PDF points. */
25
+ class EpdfPageTemplate {
26
+ template = inject(TemplateRef);
27
+ static ngTemplateContextGuard(_dir, ctx) {
28
+ return true;
29
+ }
30
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfPageTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
31
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.4", type: EpdfPageTemplate, isStandalone: true, selector: "ng-template[epdfPage]", ngImport: i0 });
32
+ }
33
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfPageTemplate, decorators: [{
34
+ type: Directive,
35
+ args: [{ selector: 'ng-template[epdfPage]', standalone: true }]
36
+ }] });
37
+ /** BOX-SPACE chrome for each visible page (page-number label, selection
38
+ * border, per-page buttons). Rendered into the OUTER box (content + reserved
39
+ * `pageFrame`), so it does NOT rotate and the reserved bands are plain
40
+ * regions (`bottom: 0; height: page.frame().bottom`). */
41
+ class EpdfPageChrome {
42
+ template = inject(TemplateRef);
43
+ static ngTemplateContextGuard(_dir, ctx) {
44
+ return true;
45
+ }
46
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfPageChrome, deps: [], target: i0.ɵɵFactoryTarget.Directive });
47
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.4", type: EpdfPageChrome, isStandalone: true, selector: "ng-template[epdfPageChrome]", ngImport: i0 });
48
+ }
49
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfPageChrome, decorators: [{
50
+ type: Directive,
51
+ args: [{ selector: 'ng-template[epdfPageChrome]', standalone: true }]
52
+ }] });
53
+
54
+ /**
55
+ * INTERNAL page shell — one per visible page, tracked by `pon` so the instance
56
+ * (and everything below) survives camera motion and page reorders.
57
+ *
58
+ * THE STABILITY INVARIANT (see page-context.ts): `page` (the context) and
59
+ * `pageInjector` are created ONCE per surface. Camera frames arrive as new
60
+ * `vp` input values; only the signals inside the context change. Changing the
61
+ * injector or context object identity would make `NgTemplateOutlet` recreate
62
+ * the embedded views — destroying layers at 120Hz. Don't.
63
+ *
64
+ * Geometry mirrors React's PageSurface: the outer box = content footprint +
65
+ * reserved chrome bands; the shadow is axis-aligned and stays put under
66
+ * rotation; the content wrapper is the ONLY thing rotation turns, and carries
67
+ * no transform at rotation 0 so it pixel-snaps like the shadow behind it.
68
+ * All numbers come from the transform — never re-derive `* zoom` / `* dpr`.
69
+ */
70
+ class EpdfPageSurface {
71
+ vp = input.required(...(ngDevMode ? [{ debugName: "vp" }] : []));
72
+ frame = input.required(...(ngDevMode ? [{ debugName: "frame" }] : []));
73
+ documentId = input.required(...(ngDevMode ? [{ debugName: "documentId" }] : []));
74
+ pageTpl = input.required(...(ngDevMode ? [{ debugName: "pageTpl" }] : []));
75
+ chromeTpl = input(null, ...(ngDevMode ? [{ debugName: "chromeTpl" }] : []));
76
+ t = computed(() => this.vp().transform, ...(ngDevMode ? [{ debugName: "t" }] : []));
77
+ // Outer box = display footprint + reserved chrome bands; screenX/screenY are
78
+ // the device-snapped footprint top-left, so the box sits one frame further out.
79
+ left = computed(() => this.vp().screenX - this.frame().left, ...(ngDevMode ? [{ debugName: "left" }] : []));
80
+ top = computed(() => this.vp().screenY - this.frame().top, ...(ngDevMode ? [{ debugName: "top" }] : []));
81
+ outerWidth = computed(() => this.t().viewWidth + this.frame().left + this.frame().right, ...(ngDevMode ? [{ debugName: "outerWidth" }] : []));
82
+ outerHeight = computed(() => this.t().viewHeight + this.frame().top + this.frame().bottom, ...(ngDevMode ? [{ debugName: "outerHeight" }] : []));
83
+ // Center the (possibly rotated) content box on the display box and rotate
84
+ // about its center — NO translate(), so rotation 0 carries no transform.
85
+ contentLeft = computed(() => this.frame().left + (this.t().viewWidth - this.t().contentWidth) / 2, ...(ngDevMode ? [{ debugName: "contentLeft" }] : []));
86
+ contentTop = computed(() => this.frame().top + (this.t().viewHeight - this.t().contentHeight) / 2, ...(ngDevMode ? [{ debugName: "contentTop" }] : []));
87
+ rotate = computed(() => {
88
+ const rotation = this.vp().rotation;
89
+ return rotation ? `rotate(${rotation}deg)` : null;
90
+ }, ...(ngDevMode ? [{ debugName: "rotate" }] : []));
91
+ contentEl = viewChild.required('content');
92
+ /** ONE stable context per surface — volatile parts are signals inside it. */
93
+ page = createPageContext({
94
+ documentId: () => this.documentId(),
95
+ pon: () => this.vp().pon,
96
+ pageIndex: computed(() => this.vp().pageIndex),
97
+ frame: this.frame,
98
+ transform: this.t,
99
+ getRect: () => this.contentEl().nativeElement.getBoundingClientRect(),
100
+ });
101
+ /** ONE stable injector per surface (see the invariant above), parented to
102
+ * this surface's node injector so layers also reach the kernel host. */
103
+ pageInjector = Injector.create({
104
+ providers: [{ provide: EPDF_PAGE, useValue: this.page }],
105
+ parent: inject(Injector),
106
+ });
107
+ templateContext = { $implicit: this.page };
108
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfPageSurface, deps: [], target: i0.ɵɵFactoryTarget.Component });
109
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfPageSurface, isStandalone: true, selector: "epdf-page-surface", inputs: { vp: { classPropertyName: "vp", publicName: "vp", isSignal: true, isRequired: true, transformFunction: null }, frame: { classPropertyName: "frame", publicName: "frame", isSignal: true, isRequired: true, transformFunction: null }, documentId: { classPropertyName: "documentId", publicName: "documentId", isSignal: true, isRequired: true, transformFunction: null }, pageTpl: { classPropertyName: "pageTpl", publicName: "pageTpl", isSignal: true, isRequired: true, transformFunction: null }, chromeTpl: { classPropertyName: "chromeTpl", publicName: "chromeTpl", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.left.px": "left()", "style.top.px": "top()", "style.width.px": "outerWidth()", "style.height.px": "outerHeight()" }, styleAttribute: "position: absolute; display: block;" }, viewQueries: [{ propertyName: "contentEl", first: true, predicate: ["content"], descendants: true, isSignal: true }], ngImport: i0, template: `
110
+ <!-- drop shadow ONLY — axis-aligned at the content box (inset by the frame),
111
+ transparent fill so it can never peek out behind the bitmap, and it
112
+ stays put under rotation. -->
113
+ <div
114
+ style="position: absolute; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);"
115
+ [style.left.px]="frame().left"
116
+ [style.top.px]="frame().top"
117
+ [style.width.px]="t().viewWidth"
118
+ [style.height.px]="t().viewHeight"
119
+ ></div>
120
+ <!-- the page: white backing + content as ONE box — the only thing rotation
121
+ turns. We render our own selection highlights, so native selection is
122
+ suppressed on the whole page subtree. -->
123
+ <div
124
+ #content
125
+ style="position: absolute; background: #fff; user-select: none; -webkit-user-select: none;"
126
+ [style.left.px]="contentLeft()"
127
+ [style.top.px]="contentTop()"
128
+ [style.width.px]="t().contentWidth"
129
+ [style.height.px]="t().contentHeight"
130
+ [style.transform]="rotate()"
131
+ >
132
+ <ng-container
133
+ [ngTemplateOutlet]="pageTpl()"
134
+ [ngTemplateOutletContext]="templateContext"
135
+ [ngTemplateOutletInjector]="pageInjector"
136
+ />
137
+ </div>
138
+ <!-- box-space chrome — fills the outer box, NEVER rotates. Bands are plain
139
+ regions: a label is bottom: 0; height: frame().bottom. -->
140
+ @if (chromeTpl(); as chrome) {
141
+ <ng-container
142
+ [ngTemplateOutlet]="chrome"
143
+ [ngTemplateOutletContext]="templateContext"
144
+ [ngTemplateOutletInjector]="pageInjector"
145
+ />
146
+ }
147
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
148
+ }
149
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfPageSurface, decorators: [{
150
+ type: Component,
151
+ args: [{
152
+ selector: 'epdf-page-surface',
153
+ standalone: true,
154
+ imports: [NgTemplateOutlet],
155
+ changeDetection: ChangeDetectionStrategy.OnPush,
156
+ host: {
157
+ style: 'position: absolute; display: block;',
158
+ '[style.left.px]': 'left()',
159
+ '[style.top.px]': 'top()',
160
+ '[style.width.px]': 'outerWidth()',
161
+ '[style.height.px]': 'outerHeight()',
162
+ },
163
+ template: `
164
+ <!-- drop shadow ONLY — axis-aligned at the content box (inset by the frame),
165
+ transparent fill so it can never peek out behind the bitmap, and it
166
+ stays put under rotation. -->
167
+ <div
168
+ style="position: absolute; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);"
169
+ [style.left.px]="frame().left"
170
+ [style.top.px]="frame().top"
171
+ [style.width.px]="t().viewWidth"
172
+ [style.height.px]="t().viewHeight"
173
+ ></div>
174
+ <!-- the page: white backing + content as ONE box — the only thing rotation
175
+ turns. We render our own selection highlights, so native selection is
176
+ suppressed on the whole page subtree. -->
177
+ <div
178
+ #content
179
+ style="position: absolute; background: #fff; user-select: none; -webkit-user-select: none;"
180
+ [style.left.px]="contentLeft()"
181
+ [style.top.px]="contentTop()"
182
+ [style.width.px]="t().contentWidth"
183
+ [style.height.px]="t().contentHeight"
184
+ [style.transform]="rotate()"
185
+ >
186
+ <ng-container
187
+ [ngTemplateOutlet]="pageTpl()"
188
+ [ngTemplateOutletContext]="templateContext"
189
+ [ngTemplateOutletInjector]="pageInjector"
190
+ />
191
+ </div>
192
+ <!-- box-space chrome — fills the outer box, NEVER rotates. Bands are plain
193
+ regions: a label is bottom: 0; height: frame().bottom. -->
194
+ @if (chromeTpl(); as chrome) {
195
+ <ng-container
196
+ [ngTemplateOutlet]="chrome"
197
+ [ngTemplateOutletContext]="templateContext"
198
+ [ngTemplateOutletInjector]="pageInjector"
199
+ />
200
+ }
201
+ `,
202
+ }]
203
+ }] });
204
+
205
+ /**
206
+ * Robust multi-click counter. `pointerdown.detail` is 0/1 in several browsers,
207
+ * so we count clicks ourselves from timing + proximity — the standard
208
+ * double/triple detection. Input normalization belongs in the adapter; the
209
+ * hub/handlers stay pure.
210
+ *
211
+ * INTERNAL for now — moves to the `/interaction` entry point (its React home)
212
+ * when that vertical lands.
213
+ */
214
+ function createClickCounter(maxGapMs = 400, maxDistPx = 6) {
215
+ let last = 0;
216
+ let lastX = 0;
217
+ let lastY = 0;
218
+ let count = 0;
219
+ return (now, x, y) => {
220
+ count = now - last <= maxGapMs && Math.hypot(x - lastX, y - lastY) <= maxDistPx ? count + 1 : 1;
221
+ last = now;
222
+ lastX = x;
223
+ lastY = y;
224
+ return count;
225
+ };
226
+ }
227
+
228
+ /**
229
+ * `<epdf-stage>` — virtualizes and positions page surfaces by the camera, and
230
+ * stamps each one with YOUR `<ng-template epdfPage>` — you bring the layers.
231
+ *
232
+ * NULL-SAFE BY DESIGN (a deliberate deviation from React's strict Stage):
233
+ * Angular instantiates projected content eagerly, so a stage constructed
234
+ * before any document exists must not crash — it renders zero pages and its
235
+ * listeners no-op until the document arrives. Teach the document gate anyway:
236
+ * document CHROME (toolbars, panels) still belongs behind `*epdfDocumentGate`.
237
+ *
238
+ * ZONE RULE: every DOM listener here attaches OUTSIDE Angular's zone — a
239
+ * 120Hz pointermove stream must never trigger app-wide change detection.
240
+ * State flows kernel → tick signal → computeds → bindings, so the adapter is
241
+ * zone-agnostic (zoneless recommended, zone.js tolerated).
242
+ */
243
+ const EMPTY_PAGES = [];
244
+ const NO_FRAME = { top: 0, right: 0, bottom: 0, left: 0 };
245
+ const frameEqual = (a, b) => a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left;
246
+ class EpdfStage {
247
+ /**
248
+ * Route this Stage's pointer events to the interaction hub (page-resolved via
249
+ * `pageAt`) instead of the built-in drag-to-pan. Pan then becomes the `pan`
250
+ * tool's job and dragging in `pointer` mode selects text (incl. across pages).
251
+ * Pair with `stagePlugin({ interaction: true })`. Default false (built-in pan).
252
+ */
253
+ interaction = input(false, ...(ngDevMode ? [{ debugName: "interaction" }] : []));
254
+ /**
255
+ * Ambient ZOOM gestures on this stage: ctrl/cmd+wheel and trackpad pinch
256
+ * (Safari gesture events included). Default true. Turn OFF for follower
257
+ * lenses with a fixed magnification (a thumbnail rail should scroll under
258
+ * cmd+wheel, not zoom); pinches are still swallowed either way.
259
+ */
260
+ zoomGestures = input(true, ...(ngDevMode ? [{ debugName: "zoomGestures" }] : []));
261
+ /** The stage lens to drive (default: the main StageToken). */
262
+ token = input(StageToken, ...(ngDevMode ? [{ debugName: "token" }] : []));
263
+ host = injectKernelHost();
264
+ stage = injectOptionalCapabilityFor(() => this.token());
265
+ ix = injectOptionalCapability(InteractionToken);
266
+ useHub = computed(() => this.interaction() && this.ix() !== null, ...(ngDevMode ? [{ debugName: "useHub" }] : []));
267
+ // The hub's resolved cursor (text/grab/…), applied to the viewport when driving.
268
+ hubCursor = this.host.value(() => this.ix()?.cursor() ?? 'default');
269
+ cursor = computed(() => (this.useHub() ? this.hubCursor() : null), ...(ngDevMode ? [{ debugName: "cursor" }] : []));
270
+ documentId = injectDocumentId();
271
+ // visiblePages already folds in the camera (each page carries its
272
+ // device-snapped screenX/screenY + transform), so panning re-emits the list —
273
+ // no separate camera subscription needed for positioning. The capability
274
+ // memoizes, so Object.is equality suffices.
275
+ pages = injectOptionalSelectorFor(() => this.token(), (c) => c.visiblePages(), EMPTY_PAGES);
276
+ // Reserved chrome bands (screen px), uniform across pages — the frame the
277
+ // outer box reserves and the chrome template paints into.
278
+ frame = injectOptionalSelectorFor(() => this.token(), (c) => c.pageFrame(), NO_FRAME, frameEqual);
279
+ pageTemplateDir = contentChild(EpdfPageTemplate, ...(ngDevMode ? [{ debugName: "pageTemplateDir" }] : []));
280
+ chromeTemplateDir = contentChild(EpdfPageChrome, ...(ngDevMode ? [{ debugName: "chromeTemplateDir" }] : []));
281
+ pageTemplate = computed(() => this.pageTemplateDir()?.template ?? null, ...(ngDevMode ? [{ debugName: "pageTemplate" }] : []));
282
+ chromeTemplate = computed(() => this.chromeTemplateDir()?.template ?? null, ...(ngDevMode ? [{ debugName: "chromeTemplate" }] : []));
283
+ constructor() {
284
+ if (!isPlatformBrowser(inject(PLATFORM_ID)))
285
+ return;
286
+ const el = inject(ElementRef).nativeElement;
287
+ const zone = inject(NgZone);
288
+ // Rebinds when the stage capability (document open/close/switch, token
289
+ // change), hub, or gesture flags change — the Angular spelling of the React
290
+ // Stage effect and its dependency array.
291
+ effect((onCleanup) => {
292
+ const stage = this.stage();
293
+ const ix = this.ix();
294
+ const useHub = this.interaction() && !!ix;
295
+ const zoomGestures = this.zoomGestures();
296
+ if (!stage)
297
+ return; // no document yet — nothing to drive
298
+ const cleanups = [];
299
+ zone.runOutsideAngular(() => {
300
+ // Only report the viewport size. Initial placement (home) is the Stage
301
+ // plugin's job — the shell stays dumb.
302
+ const setViewport = () => stage.setViewport({ width: el.clientWidth, height: el.clientHeight });
303
+ const resizeObserver = new ResizeObserver(setViewport);
304
+ resizeObserver.observe(el);
305
+ setViewport();
306
+ cleanups.push(() => resizeObserver.disconnect());
307
+ // Report the device pixel ratio so page transforms render crisp. dppx
308
+ // changes (zoom, dragging between monitors) fire the media query;
309
+ // re-subscribe each time since the query value itself moves.
310
+ let mq = null;
311
+ const reportDpr = () => {
312
+ stage.setDevicePixelRatio(window.devicePixelRatio || 1);
313
+ mq?.removeEventListener('change', reportDpr);
314
+ mq = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
315
+ mq.addEventListener('change', reportDpr);
316
+ };
317
+ reportDpr();
318
+ cleanups.push(() => mq?.removeEventListener('change', reportDpr));
319
+ // Wheel is ambient navigation in BOTH modes: ctrl/meta zooms (classified
320
+ // per input by wheelZoomFactor), else scrolls. With zoom gestures off, a
321
+ // zoom-wheel falls through to ordinary pan.
322
+ const onWheel = (e) => {
323
+ e.preventDefault();
324
+ const r = el.getBoundingClientRect();
325
+ if (zoomGestures && (e.ctrlKey || e.metaKey)) {
326
+ stage.zoomAround({ x: e.clientX - r.left, y: e.clientY - r.top }, wheelZoomFactor(e));
327
+ }
328
+ else {
329
+ const dx = e.shiftKey ? e.deltaY : e.deltaX;
330
+ const dy = e.shiftKey ? e.deltaX : e.deltaY;
331
+ stage.panBy(-dx, -dy);
332
+ }
333
+ };
334
+ el.addEventListener('wheel', onWheel, { passive: false });
335
+ cleanups.push(() => el.removeEventListener('wheel', onWheel));
336
+ // Safari never synthesizes ctrl+wheel for trackpad pinch — it fires
337
+ // proprietary gesture events carrying an ABSOLUTE scale; convert to the
338
+ // per-event ratio the camera physics wants. Feature-detected, so Chrome
339
+ // and Firefox pay nothing. preventDefault runs even with zoom gestures
340
+ // off — a pinch over the stage must never zoom the page itself.
341
+ let lastScale = 1;
342
+ const onGestureStart = (e) => {
343
+ e.preventDefault();
344
+ lastScale = e.scale ?? 1;
345
+ };
346
+ const onGestureChange = (e) => {
347
+ e.preventDefault();
348
+ const g = e;
349
+ const scale = g.scale ?? 1;
350
+ if (zoomGestures && scale > 0) {
351
+ const r = el.getBoundingClientRect();
352
+ stage.zoomAround({ x: g.clientX - r.left, y: g.clientY - r.top }, scale / lastScale);
353
+ }
354
+ lastScale = scale;
355
+ };
356
+ if ('GestureEvent' in window) {
357
+ el.addEventListener('gesturestart', onGestureStart);
358
+ el.addEventListener('gesturechange', onGestureChange);
359
+ el.addEventListener('gestureend', onGestureStart); // reset the base
360
+ cleanups.push(() => {
361
+ el.removeEventListener('gesturestart', onGestureStart);
362
+ el.removeEventListener('gesturechange', onGestureChange);
363
+ el.removeEventListener('gestureend', onGestureStart);
364
+ });
365
+ }
366
+ if (useHub && ix) {
367
+ // Forward to the hub: pan/select/etc. become tool-gated handlers.
368
+ // `pageAt` resolves the page per event, so a drag can cross pages.
369
+ const clicks = createClickCounter();
370
+ const forward = (phase, e, clickCount = 1) => {
371
+ const r = el.getBoundingClientRect();
372
+ const viewport = { x: e.clientX - r.left, y: e.clientY - r.top };
373
+ ix.dispatch({
374
+ phase,
375
+ viewport,
376
+ page: stage.pageAt(viewport) ?? undefined,
377
+ // Page-anchored gestures (annotation move/resize) track the origin
378
+ // page's frame through this even when the cursor is off that page.
379
+ project: (pon) => stage.pointOnPage(pon, viewport),
380
+ modifiers: { shift: e.shiftKey, alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey },
381
+ clickCount,
382
+ });
383
+ };
384
+ let dragging = false;
385
+ const down = (e) => {
386
+ if (e.button !== 0)
387
+ return;
388
+ dragging = true;
389
+ forward('down', e, clicks(Date.now(), e.clientX, e.clientY));
390
+ };
391
+ const hover = (e) => {
392
+ if (!dragging)
393
+ forward('move', e); // cursor feedback, no gesture
394
+ };
395
+ const windowMove = (e) => {
396
+ if (dragging)
397
+ forward('move', e);
398
+ };
399
+ const up = (e) => {
400
+ if (!dragging)
401
+ return;
402
+ dragging = false;
403
+ forward('up', e);
404
+ };
405
+ el.addEventListener('pointerdown', down);
406
+ el.addEventListener('pointermove', hover);
407
+ window.addEventListener('pointermove', windowMove);
408
+ window.addEventListener('pointerup', up);
409
+ cleanups.push(() => {
410
+ el.removeEventListener('pointerdown', down);
411
+ el.removeEventListener('pointermove', hover);
412
+ window.removeEventListener('pointermove', windowMove);
413
+ window.removeEventListener('pointerup', up);
414
+ });
415
+ }
416
+ else {
417
+ // Built-in drag-to-pan (no interaction hub).
418
+ let dragging = false;
419
+ let lastX = 0;
420
+ let lastY = 0;
421
+ const down = (e) => {
422
+ if (e.button !== 0)
423
+ return;
424
+ dragging = true;
425
+ lastX = e.clientX;
426
+ lastY = e.clientY;
427
+ };
428
+ const move = (e) => {
429
+ if (!dragging)
430
+ return;
431
+ stage.panBy(e.clientX - lastX, e.clientY - lastY);
432
+ lastX = e.clientX;
433
+ lastY = e.clientY;
434
+ };
435
+ const up = () => {
436
+ dragging = false;
437
+ };
438
+ el.addEventListener('pointerdown', down);
439
+ window.addEventListener('pointermove', move);
440
+ window.addEventListener('pointerup', up);
441
+ cleanups.push(() => {
442
+ el.removeEventListener('pointerdown', down);
443
+ window.removeEventListener('pointermove', move);
444
+ window.removeEventListener('pointerup', up);
445
+ });
446
+ }
447
+ });
448
+ onCleanup(() => cleanups.forEach((fn) => fn()));
449
+ });
450
+ }
451
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfStage, deps: [], target: i0.ɵɵFactoryTarget.Component });
452
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfStage, isStandalone: true, selector: "epdf-stage", inputs: { interaction: { classPropertyName: "interaction", publicName: "interaction", isSignal: true, isRequired: false, transformFunction: null }, zoomGestures: { classPropertyName: "zoomGestures", publicName: "zoomGestures", isSignal: true, isRequired: false, transformFunction: null }, token: { classPropertyName: "token", publicName: "token", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.cursor": "cursor()" }, styleAttribute: "position: relative; overflow: hidden; touch-action: none; display: block;" }, queries: [{ propertyName: "pageTemplateDir", first: true, predicate: EpdfPageTemplate, descendants: true, isSignal: true }, { propertyName: "chromeTemplateDir", first: true, predicate: EpdfPageChrome, descendants: true, isSignal: true }], ngImport: i0, template: `
453
+ @if (pageTemplate(); as tpl) {
454
+ @for (p of pages(); track p.pon) {
455
+ <epdf-page-surface
456
+ [vp]="p"
457
+ [frame]="frame()"
458
+ [documentId]="documentId() ?? ''"
459
+ [pageTpl]="tpl"
460
+ [chromeTpl]="chromeTemplate()"
461
+ />
462
+ }
463
+ }
464
+ <!-- viewport-space overlay UI: anything projected into <epdf-stage>
465
+ renders above the pages (menus, controls, scrollbars). -->
466
+ <ng-content />
467
+ `, isInline: true, dependencies: [{ kind: "component", type: EpdfPageSurface, selector: "epdf-page-surface", inputs: ["vp", "frame", "documentId", "pageTpl", "chromeTpl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
468
+ }
469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfStage, decorators: [{
470
+ type: Component,
471
+ args: [{
472
+ selector: 'epdf-stage',
473
+ standalone: true,
474
+ imports: [EpdfPageSurface],
475
+ changeDetection: ChangeDetectionStrategy.OnPush,
476
+ host: {
477
+ style: 'position: relative; overflow: hidden; touch-action: none; display: block;',
478
+ '[style.cursor]': 'cursor()',
479
+ },
480
+ template: `
481
+ @if (pageTemplate(); as tpl) {
482
+ @for (p of pages(); track p.pon) {
483
+ <epdf-page-surface
484
+ [vp]="p"
485
+ [frame]="frame()"
486
+ [documentId]="documentId() ?? ''"
487
+ [pageTpl]="tpl"
488
+ [chromeTpl]="chromeTemplate()"
489
+ />
490
+ }
491
+ }
492
+ <!-- viewport-space overlay UI: anything projected into <epdf-stage>
493
+ renders above the pages (menus, controls, scrollbars). -->
494
+ <ng-content />
495
+ `,
496
+ }]
497
+ }], ctorParameters: () => [] });
498
+
499
+ /** Late-binding method passthrough: exact capability signature, current doc. */
500
+ const lazy = (cap, key) => ((...args) => cap()[key](...args));
501
+ function injectStage(token = StageToken) {
502
+ return injectCapability(token);
503
+ }
504
+ function injectZoom(token = StageToken) {
505
+ const s = injectCapability(token);
506
+ return {
507
+ zoom: injectSelector(token, (c) => c.zoomLevel()),
508
+ /** Active zoom intent: 'automatic' | 'fit-page' | 'fit-width' | 'fit-all' | 'custom'. */
509
+ mode: injectSelector(token, (c) => c.zoomMode()),
510
+ zoomIn: lazy(s, 'zoomIn'),
511
+ zoomOut: lazy(s, 'zoomOut'),
512
+ fitWidth: lazy(s, 'fitWidth'),
513
+ fitPage: lazy(s, 'fitPage'),
514
+ fitAll: lazy(s, 'fitAll'),
515
+ automatic: lazy(s, 'automatic'),
516
+ zoomTo: lazy(s, 'zoomTo'),
517
+ };
518
+ }
519
+ function injectPages(token = StageToken) {
520
+ const s = injectCapability(token);
521
+ return {
522
+ currentPage: injectSelector(token, (c) => c.currentPage()),
523
+ pageCount: injectSelector(token, (c) => c.pageCount()),
524
+ goToPage: lazy(s, 'goToPage'),
525
+ next: lazy(s, 'next'),
526
+ prev: lazy(s, 'prev'),
527
+ reveal: lazy(s, 'reveal'),
528
+ };
529
+ }
530
+ function injectLayout(token = StageToken) {
531
+ const s = injectCapability(token);
532
+ return {
533
+ flow: injectSelector(token, (c) => c.flow()),
534
+ layout: injectSelector(token, (c) => c.layout()),
535
+ spread: injectSelector(token, (c) => c.spread()),
536
+ sizing: injectSelector(token, (c) => c.sizing()),
537
+ bounded: injectSelector(token, (c) => c.bounded()),
538
+ setFlow: lazy(s, 'setFlow'),
539
+ setLayout: lazy(s, 'setLayout'),
540
+ setSpread: lazy(s, 'setSpread'),
541
+ setSizing: lazy(s, 'setSizing'),
542
+ setBounded: lazy(s, 'setBounded'),
543
+ };
544
+ }
545
+ /** The document's page list (with PDF labels) + the current item's pages — the
546
+ * data for page thumbnails / worksheet-style page tabs. */
547
+ function injectPageList(token = StageToken) {
548
+ return {
549
+ pages: injectSelector(token, (c) => c.pages(), (a, b) => a.length === b.length && a.every((p, i) => p.pon === b[i].pon && p.label === b[i].label)),
550
+ currentItemPages: injectSelector(token, (c) => c.currentItemPages(), shallowArray),
551
+ };
552
+ }
553
+ /**
554
+ * All Stage settings + the batch `update`. The seam for "presets are a customer
555
+ * concern": keep your own `Partial<StageSettings>` objects and apply them with
556
+ * `update(preset)` (one anchor-preserving change).
557
+ */
558
+ function injectStageSettings(token = StageToken) {
559
+ const s = injectCapability(token);
560
+ return {
561
+ // settingsEqual derives from the plugin's settings registry — a new setting
562
+ // is covered automatically, without this package spelling out the shape.
563
+ settings: injectSelector(token, (c) => c.settings(), settingsEqual),
564
+ update: lazy(s, 'update'),
565
+ reset: lazy(s, 'resetView'),
566
+ };
567
+ }
568
+
569
+ /**
570
+ * @embedpdf/angular/stage — the viewport trunk: `<epdf-stage>` + page
571
+ * templates + the stage facade inject functions.
572
+ */
573
+ // One-line-per-feature: registration travels with the UI.
574
+
575
+ /**
576
+ * Generated bundle index. Do not edit.
577
+ */
578
+
579
+ export { EpdfPageChrome, EpdfPageSurface, EpdfPageTemplate, EpdfStage, injectLayout, injectPageList, injectPages, injectStage, injectStageSettings, injectZoom };
580
+ //# sourceMappingURL=embedpdf-angular-stage.mjs.map