@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.
- package/LICENSE +192 -0
- package/dist/LICENSE +192 -0
- package/dist/fesm2022/embedpdf-angular-render.mjs +119 -0
- package/dist/fesm2022/embedpdf-angular-render.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-runtime.mjs +502 -0
- package/dist/fesm2022/embedpdf-angular-runtime.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-stage.mjs +580 -0
- package/dist/fesm2022/embedpdf-angular-stage.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular.mjs +14 -0
- package/dist/fesm2022/embedpdf-angular.mjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/render/index.d.ts +19 -0
- package/dist/runtime/index.d.ts +265 -0
- package/dist/stage/index.d.ts +167 -0
- package/package.json +64 -0
|
@@ -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
|