@embedpdf/angular 3.0.0-next.14 → 3.0.0-next.16
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/README.md +309 -0
- package/dist/README.md +309 -0
- package/dist/actions/index.d.ts +34 -16
- package/dist/anchored/index.d.ts +67 -0
- package/dist/annotation/index.d.ts +602 -0
- package/dist/attachment/index.d.ts +29 -0
- package/dist/commands/index.d.ts +109 -0
- package/dist/fesm2022/embedpdf-angular-actions.mjs +117 -36
- package/dist/fesm2022/embedpdf-angular-actions.mjs.map +1 -1
- package/dist/fesm2022/embedpdf-angular-anchored.mjs +148 -0
- package/dist/fesm2022/embedpdf-angular-anchored.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-annotation.mjs +1933 -0
- package/dist/fesm2022/embedpdf-angular-annotation.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-attachment.mjs +67 -0
- package/dist/fesm2022/embedpdf-angular-attachment.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-commands.mjs +235 -0
- package/dist/fesm2022/embedpdf-angular-commands.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-form.mjs +1074 -0
- package/dist/fesm2022/embedpdf-angular-form.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-history.mjs +54 -0
- package/dist/fesm2022/embedpdf-angular-history.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-i18n.mjs +100 -0
- package/dist/fesm2022/embedpdf-angular-i18n.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-interaction.mjs +157 -0
- package/dist/fesm2022/embedpdf-angular-interaction.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-link.mjs +306 -0
- package/dist/fesm2022/embedpdf-angular-link.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-measurement.mjs +106 -0
- package/dist/fesm2022/embedpdf-angular-measurement.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-metadata.mjs +77 -0
- package/dist/fesm2022/embedpdf-angular-metadata.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-outline.mjs +76 -0
- package/dist/fesm2022/embedpdf-angular-outline.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-page-edit.mjs +68 -0
- package/dist/fesm2022/embedpdf-angular-page-edit.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-page-view.mjs +297 -0
- package/dist/fesm2022/embedpdf-angular-page-view.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-redaction.mjs +93 -0
- package/dist/fesm2022/embedpdf-angular-redaction.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-render.mjs +331 -69
- package/dist/fesm2022/embedpdf-angular-render.mjs.map +1 -1
- package/dist/fesm2022/embedpdf-angular-runtime.mjs +1151 -390
- package/dist/fesm2022/embedpdf-angular-runtime.mjs.map +1 -1
- package/dist/fesm2022/embedpdf-angular-search.mjs +251 -0
- package/dist/fesm2022/embedpdf-angular-search.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-selection.mjs +477 -0
- package/dist/fesm2022/embedpdf-angular-selection.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-shell.mjs +177 -0
- package/dist/fesm2022/embedpdf-angular-shell.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-signature.mjs +120 -0
- package/dist/fesm2022/embedpdf-angular-signature.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-stage.mjs +688 -288
- package/dist/fesm2022/embedpdf-angular-stage.mjs.map +1 -1
- package/dist/fesm2022/embedpdf-angular-stamp.mjs +169 -0
- package/dist/fesm2022/embedpdf-angular-stamp.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-testing.mjs +89 -0
- package/dist/fesm2022/embedpdf-angular-testing.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-toolbar.mjs +1123 -0
- package/dist/fesm2022/embedpdf-angular-toolbar.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular-view-manager.mjs +69 -0
- package/dist/fesm2022/embedpdf-angular-view-manager.mjs.map +1 -0
- package/dist/fesm2022/embedpdf-angular.mjs +25 -3
- package/dist/fesm2022/embedpdf-angular.mjs.map +1 -1
- package/dist/form/index.d.ts +79 -0
- package/dist/history/index.d.ts +28 -0
- package/dist/i18n/index.d.ts +52 -0
- package/dist/index.d.ts +22 -0
- package/dist/interaction/index.d.ts +58 -0
- package/dist/link/index.d.ts +77 -0
- package/dist/measurement/index.d.ts +46 -0
- package/dist/metadata/index.d.ts +51 -0
- package/dist/outline/index.d.ts +32 -0
- package/dist/page-edit/index.d.ts +27 -0
- package/dist/page-view/index.d.ts +94 -0
- package/dist/redaction/index.d.ts +38 -0
- package/dist/render/index.d.ts +45 -6
- package/dist/runtime/index.d.ts +579 -199
- package/dist/search/index.d.ts +71 -0
- package/dist/selection/index.d.ts +141 -0
- package/dist/shell/index.d.ts +88 -0
- package/dist/signature/index.d.ts +45 -0
- package/dist/stage/index.d.ts +242 -141
- package/dist/stamp/index.d.ts +58 -0
- package/dist/testing/index.d.ts +53 -0
- package/dist/toolbar/index.d.ts +257 -0
- package/dist/view-manager/index.d.ts +23 -0
- package/package.json +145 -14
|
@@ -1,150 +1,421 @@
|
|
|
1
|
-
import { createKernel,
|
|
1
|
+
import { mergeSettings, viewerSettingsOf, VIEWER_WHOLE_SETTINGS, VIEWER_DEFAULTS, shallowEqual, DocumentsToken, PluginError, isLocalEngine, createKernel, isPluginError, returnsPromise, isReadMember, documentsState, documentState } from '@embedpdf/core';
|
|
2
2
|
export * from '@embedpdf/core';
|
|
3
|
+
import { browserPrinter, browserClock, pageClientSpace, pageLayersOf, createFontMounts, epdfThemeVariables } from '@embedpdf/web';
|
|
4
|
+
export { mountWebFont, saveFile } from '@embedpdf/web';
|
|
3
5
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { inject, PLATFORM_ID,
|
|
6
|
+
import { InjectionToken, signal, computed, inject, Injector, PLATFORM_ID, DestroyRef, untracked, runInInjectionContext, Injectable, effect, input, forwardRef, Directive, TemplateRef, ViewContainerRef, ChangeDetectorRef, afterNextRender, ElementRef, ChangeDetectionStrategy, Component, isDevMode } from '@angular/core';
|
|
5
7
|
import { isPlatformBrowser } from '@angular/common';
|
|
8
|
+
import { Observable } from 'rxjs';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* The adapter's numbered errors, written like Angular's own (`NG0201`): a code, what went wrong,
|
|
12
|
+
* how to fix it, and the page that explains it. A code keeps its meaning once it ships, so a
|
|
13
|
+
* search for `EPDF-101` always finds the right answer.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* Errors about the document (no document open yet, a permission missing) are the plugins'
|
|
16
|
+
* `PluginError`s, the same in every framework; these are only about how the app set up the
|
|
17
|
+
* adapter.
|
|
18
|
+
*/
|
|
19
|
+
const ERRORS_PAGE = 'https://www.embedpdf.com/docs/headless/angular/errors';
|
|
20
|
+
/** A setup mistake, with its code: `EPDF-101`. */
|
|
21
|
+
class EpdfError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
name = 'EpdfError';
|
|
24
|
+
constructor(code, problem) {
|
|
25
|
+
super(`${code}: ${problem} See ${ERRORS_PAGE}#${code.toLowerCase()}`);
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** EPDF-101: a service, component or directive of the viewer, with no viewer above it. */
|
|
30
|
+
const noViewerError = (what) => new EpdfError('EPDF-101', `${what} found no viewer. Add provideEmbedPdf() to this component's providers, or to your app config.`);
|
|
31
|
+
/** EPDF-102: a plugin's service, where no viewer above it has that plugin. */
|
|
32
|
+
const noPluginError = (service, feature) => new EpdfError('EPDF-102', `inject(${service}) found no viewer with its plugin. Add ${feature} to provideEmbedPdf() ` +
|
|
33
|
+
`in this component's providers, or in your app config.`);
|
|
34
|
+
/** EPDF-103: something that draws on a page, outside a page. */
|
|
35
|
+
const outsidePageError = (what) => new EpdfError('EPDF-103', `${what} draws on a page. Put it inside <ng-template epdfPage> in an <epdf-stage>.`);
|
|
36
|
+
/** EPDF-104: part of a Stage, outside one. */
|
|
37
|
+
const outsideStageError = (what) => new EpdfError('EPDF-104', `${what} belongs to a Stage. Put it inside <epdf-stage>.`);
|
|
38
|
+
/** EPDF-105: a call that needs the engine, made while rendering on the server. */
|
|
39
|
+
const onServerError = (what) => new EpdfError('EPDF-105', `${what} needs the engine, which runs in the browser only. Call it from an event or ` +
|
|
40
|
+
`afterNextRender(), or put the viewer in a @defer block.`);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The DI tokens the runtime's pieces share, in a file of their own so the pieces that provide
|
|
44
|
+
* them and the ones that read them never import each other.
|
|
45
|
+
*/
|
|
46
|
+
const EPDF_DOCUMENT_SCOPE = new InjectionToken('EPDF_DOCUMENT_SCOPE');
|
|
47
|
+
const EPDF_SETUP = new InjectionToken('EPDF_SETUP');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Settings before the kernel exists. With an engine that loads lazily, the app can read and
|
|
51
|
+
* change a plugin's settings (and the viewer's) before the kernel is there to keep them. This
|
|
52
|
+
* keeps the changes in order, shows what they add up to, and hands them to the kernel when it
|
|
53
|
+
* arrives, so `settings()` and `updateSettings()` work from the first moment, as they do in
|
|
54
|
+
* every other framework.
|
|
15
55
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
* The merge is the kernel's own (`mergeSettings` from `@embedpdf/core`), so the settings shown
|
|
57
|
+
* before the kernel exists are the ones it then has.
|
|
58
|
+
*/
|
|
59
|
+
/** One settings store's changes, kept until the kernel takes them. */
|
|
60
|
+
class SettingsBeforeStart {
|
|
61
|
+
changes = signal([], ...(ngDevMode ? [{ debugName: "changes" }] : []));
|
|
62
|
+
/** The settings the changes add up to, over what the app registered. */
|
|
63
|
+
current;
|
|
64
|
+
constructor(declaration) {
|
|
65
|
+
const registered = mergeSettings(declaration.defaults, declaration.registered, declaration.whole);
|
|
66
|
+
this.current = computed(() => this.changes().reduce((settings, change) => change === 'reset' ? registered : mergeSettings(settings, change, declaration.whole), registered), ...(ngDevMode ? [{ debugName: "current" }] : []));
|
|
67
|
+
}
|
|
68
|
+
update(change) {
|
|
69
|
+
this.changes.update((changes) => [...changes, change]);
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.changes.update((changes) => [...changes, 'reset']);
|
|
73
|
+
}
|
|
74
|
+
/** Hand the changes to the kernel's store, in order, and forget them. */
|
|
75
|
+
applyTo(store) {
|
|
76
|
+
for (const change of this.changes()) {
|
|
77
|
+
if (change === 'reset')
|
|
78
|
+
store.resetSettings();
|
|
79
|
+
else
|
|
80
|
+
store.updateSettings(change);
|
|
81
|
+
}
|
|
82
|
+
this.changes.set([]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The viewer behind `provideEmbedPdf()`: one per injector that provides it. In the browser it
|
|
88
|
+
* creates the kernel as soon as the engine is there (at once for an engine instance, later for a
|
|
89
|
+
* factory that loads one), starts it, opens the initial documents, runs the features' `setup`,
|
|
90
|
+
* and destroys it all with the injector. Given a kernel (`provideEmbedPdf({ kernel })`), it uses
|
|
91
|
+
* that one as it is, and leaves starting and destroying it to whoever made it.
|
|
92
|
+
*
|
|
93
|
+
* The kernel has one change stream; the host turns each change into a write of `revision`, and
|
|
94
|
+
* every signal in the adapter is a `computed()` over it (`read()`), which keeps its value, and
|
|
95
|
+
* wakes nothing, while what it reads is unchanged.
|
|
22
96
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
97
|
+
* On the server the host creates no kernel: every read gives its empty value and a call that
|
|
98
|
+
* needs the engine throws EPDF-105, so the page around the viewer still renders.
|
|
99
|
+
*/
|
|
100
|
+
/**
|
|
101
|
+
* The viewer's settings as the config gives them (a setting left out is its default), the same
|
|
102
|
+
* way every framework's viewer reads its props; what `resetSettings()` goes back to. A kernel
|
|
103
|
+
* given has its own.
|
|
26
104
|
*/
|
|
105
|
+
const registeredViewerSettings = (config) => viewerSettingsOf('kernel' in config ? {} : config);
|
|
106
|
+
/**
|
|
107
|
+
* Whether `kernel` has a plugin that provides `token`. A document plugin's token has its scope
|
|
108
|
+
* (any other token reads as a workspace one), and a workspace plugin's capability is there from
|
|
109
|
+
* the moment the kernel is created.
|
|
110
|
+
*/
|
|
111
|
+
const kernelProvides = (kernel, token) => kernel.scopeOf(token) === 'document' || kernel.tryCapability(token) !== null;
|
|
112
|
+
/** The settings of the plugin that provides `token` in `kernel`, or null when it has none. */
|
|
113
|
+
function kernelSettingsOf(kernel, token) {
|
|
114
|
+
try {
|
|
115
|
+
return kernel.settingsOf(token);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// The kernel's answer for a token no plugin provides, or a plugin without settings.
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** What the viewer is doing, as a kernel someone else started says it. */
|
|
123
|
+
const statusOfKernel = (status) => status === 'started' ? 'ready' : status === 'failed' ? 'error' : 'starting';
|
|
124
|
+
const isPromiseLike = (value) => typeof value.then === 'function';
|
|
27
125
|
class EpdfKernelHost {
|
|
126
|
+
setup = inject(EPDF_SETUP);
|
|
127
|
+
injector = inject(Injector);
|
|
128
|
+
/** Whether this is a browser. On the server the host creates no kernel. */
|
|
28
129
|
browser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
130
|
+
/** The kernel `provideEmbedPdf({ kernel })` was given: someone else's, used as it is. */
|
|
131
|
+
given = 'kernel' in this.setup.config ? this.setup.config.kernel : null;
|
|
132
|
+
/** Every plugin the features register, in their order: none for a kernel given, which has its own. */
|
|
133
|
+
plugins = this.given
|
|
134
|
+
? []
|
|
135
|
+
: this.setup.features.flatMap((feature) => feature.plugins);
|
|
136
|
+
kernelState = signal(null, ...(ngDevMode ? [{ debugName: "kernelState" }] : []));
|
|
137
|
+
revisionState = signal(0, ...(ngDevMode ? [{ debugName: "revisionState" }] : []));
|
|
138
|
+
statusState = signal('starting', ...(ngDevMode ? [{ debugName: "statusState" }] : []));
|
|
139
|
+
errorState = signal(null, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
140
|
+
listeners = new Set();
|
|
141
|
+
viewerSettingsBeforeStart = new SettingsBeforeStart({
|
|
142
|
+
defaults: VIEWER_DEFAULTS,
|
|
143
|
+
registered: registeredViewerSettings(this.setup.config),
|
|
144
|
+
whole: VIEWER_WHOLE_SETTINGS,
|
|
145
|
+
});
|
|
146
|
+
pluginSettings = new Map();
|
|
147
|
+
pluginSettingsBeforeStart = new Map();
|
|
148
|
+
engine = null;
|
|
34
149
|
ownsEngine = false;
|
|
35
|
-
|
|
36
|
-
initialDocuments;
|
|
37
|
-
booted = false;
|
|
150
|
+
stopListening = null;
|
|
38
151
|
destroyed = false;
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!this._kernel) {
|
|
62
|
-
if (!this.browser) {
|
|
63
|
-
throw new Error('[embedpdf] the viewer does not render on the server — wrap the EmbedPDF subtree in @defer (its placeholder renders during SSR) or a browser-only @if');
|
|
64
|
-
}
|
|
65
|
-
if (!this.config) {
|
|
66
|
-
throw new Error('[embedpdf] no kernel: wrap this subtree in <epdf-viewer> or add provideEmbedPdf(...) to your providers');
|
|
67
|
-
}
|
|
68
|
-
const { engine, plugins, initialDocuments } = this.config();
|
|
69
|
-
// A thunk is host-owned: call it now, destroy on teardown. An instance is
|
|
70
|
-
// borrowed and left untouched. Either way construction is synchronous and
|
|
71
|
-
// inert (engines boot lazily on first use).
|
|
72
|
-
this.ownsEngine = typeof engine === 'function';
|
|
73
|
-
this._engine = this.ownsEngine ? engine() : engine;
|
|
74
|
-
try {
|
|
75
|
-
this._kernel = createKernel({ engine: this._engine, plugins });
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
// ngOnDestroy() only tears down via `_kernel`, which stays null on this
|
|
79
|
-
// path — so an owned engine must be destroyed HERE or it leaks.
|
|
80
|
-
if (this.ownsEngine)
|
|
81
|
-
void this._engine.destroy();
|
|
82
|
-
this._engine = null;
|
|
83
|
-
this.ownsEngine = false;
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
// Warm up only once the kernel exists (a plugin-planning throw must not
|
|
87
|
-
// start a boot we'd immediately have to unwind) — `warmup()` overlaps the
|
|
88
|
-
// WASM/transport boot with the rest of initialization.
|
|
89
|
-
this._engine.warmup?.();
|
|
90
|
-
this.initialDocuments = initialDocuments;
|
|
91
|
-
this.unsubscribe = this._kernel.subscribe(() => this.tick.update((n) => n + 1));
|
|
92
|
-
}
|
|
93
|
-
return this._kernel;
|
|
94
|
-
}
|
|
95
|
-
/** Start the kernel and open initial documents in the background. Idempotent;
|
|
96
|
-
* a no-op on the server and after destroy. */
|
|
97
|
-
boot() {
|
|
98
|
-
if (this.booted || this.destroyed || !this.browser)
|
|
152
|
+
/**
|
|
153
|
+
* The kernel, once it exists: null while a lazily loaded engine is on its way, and on the
|
|
154
|
+
* server, where only a kernel given is there.
|
|
155
|
+
*/
|
|
156
|
+
kernel = this.kernelState.asReadonly();
|
|
157
|
+
/** Goes up on every change of the kernel's state. */
|
|
158
|
+
revision = this.revisionState.asReadonly();
|
|
159
|
+
/** A kernel given says it itself: someone else starts it. */
|
|
160
|
+
status = this.given
|
|
161
|
+
? this.read((kernel) => statusOfKernel(kernel.status()), () => 'starting')
|
|
162
|
+
: this.statusState.asReadonly();
|
|
163
|
+
/** Why the viewer failed to start, when `status()` is `'error'`. */
|
|
164
|
+
error = this.errorState.asReadonly();
|
|
165
|
+
/** The viewer's own settings, before the kernel exists and after. */
|
|
166
|
+
viewerSettings = this.read((kernel) => kernel.getSettings(), () => this.viewerSettingsBeforeStart.current(), shallowEqual);
|
|
167
|
+
constructor() {
|
|
168
|
+
inject(DestroyRef).onDestroy(() => this.destroy());
|
|
169
|
+
const { config } = this.setup;
|
|
170
|
+
// A kernel given is the kernel, wherever it was made.
|
|
171
|
+
if ('kernel' in config)
|
|
172
|
+
this.adopt(config.kernel);
|
|
173
|
+
if (!this.browser)
|
|
99
174
|
return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (this.destroyed)
|
|
114
|
-
return;
|
|
115
|
-
this.status.set('ready');
|
|
116
|
-
// Kernel-owned boot policy: all tabs appear immediately in array order;
|
|
117
|
-
// the `active` entry (else the first) is selected; per-document
|
|
118
|
-
// failures surface as that tab's `error`/`locked` status (this host's
|
|
119
|
-
// `error` signal reports only startup failures).
|
|
120
|
-
kernel.documents.openAll(this.initialDocuments ?? []);
|
|
121
|
-
})();
|
|
122
|
-
}
|
|
123
|
-
/** Read a value derived from the kernel, cached by equality — Angular's
|
|
124
|
-
* `useKernelValue`. The selector re-runs per kernel notification (cheap);
|
|
125
|
-
* `equal` decides whether dependents ever see it. */
|
|
126
|
-
value(select, equal = Object.is) {
|
|
175
|
+
if (!('kernel' in config))
|
|
176
|
+
this.start(config);
|
|
177
|
+
// After construction, because a feature's setup injects services, and they inject this host.
|
|
178
|
+
queueMicrotask(() => this.runFeatureSetups());
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A value read from the kernel, as a signal: `select` runs again after each change of the
|
|
182
|
+
* kernel's state, and the signal changes only when `equal` says the value did. `empty` is its
|
|
183
|
+
* value while there is no kernel.
|
|
184
|
+
*/
|
|
185
|
+
read(select, empty, equal = Object.is) {
|
|
127
186
|
return computed(() => {
|
|
128
|
-
this.
|
|
129
|
-
|
|
187
|
+
this.revisionState();
|
|
188
|
+
const kernel = this.kernelState();
|
|
189
|
+
return kernel ? select(kernel) : empty();
|
|
130
190
|
}, { equal });
|
|
131
191
|
}
|
|
132
|
-
|
|
192
|
+
/** Call `listener` after each change of the kernel's state, and once when the kernel arrives. */
|
|
193
|
+
subscribe(listener) {
|
|
194
|
+
this.listeners.add(listener);
|
|
195
|
+
return () => this.listeners.delete(listener);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* An event of the kernel itself (its settings, a plugin's settings store), as an RxJS
|
|
199
|
+
* stream. A subscription made before the kernel exists starts listening when it arrives.
|
|
200
|
+
*/
|
|
201
|
+
stream(pick) {
|
|
202
|
+
return new Observable((subscriber) => {
|
|
203
|
+
let listening = null;
|
|
204
|
+
const listen = () => {
|
|
205
|
+
const kernel = untracked(this.kernelState);
|
|
206
|
+
if (!kernel || listening)
|
|
207
|
+
return;
|
|
208
|
+
listening = new AbortController();
|
|
209
|
+
pick(kernel)((event) => subscriber.next(event), { signal: listening.signal });
|
|
210
|
+
};
|
|
211
|
+
const stop = this.subscribe(listen);
|
|
212
|
+
listen();
|
|
213
|
+
return () => {
|
|
214
|
+
stop();
|
|
215
|
+
listening?.abort();
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** Whether a plugin of this viewer provides `token` (the documents are always there). */
|
|
220
|
+
provides(token) {
|
|
221
|
+
if (token === DocumentsToken)
|
|
222
|
+
return true;
|
|
223
|
+
return this.given ? kernelProvides(this.given, token) : this.pluginFor(token) !== null;
|
|
224
|
+
}
|
|
225
|
+
/** Whether the plugin of this viewer that provides `token` has settings. */
|
|
226
|
+
hasSettings(token) {
|
|
227
|
+
return this.given
|
|
228
|
+
? kernelSettingsOf(this.given, token) !== null
|
|
229
|
+
: Boolean(this.pluginFor(token)?.settings);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The kernel, for `call` (`'zoomIn()'`) on `capability` (`'stage'`), which needs it now:
|
|
233
|
+
* `not-ready` while the engine is still on its way, EPDF-105 on the server.
|
|
234
|
+
*/
|
|
235
|
+
requireKernel(capability, call) {
|
|
236
|
+
const kernel = this.kernelState();
|
|
237
|
+
if (kernel)
|
|
238
|
+
return kernel;
|
|
239
|
+
if (!this.browser)
|
|
240
|
+
throw onServerError(call);
|
|
241
|
+
throw new PluginError('not-ready', capability, 'the viewer is still starting');
|
|
242
|
+
}
|
|
243
|
+
updateViewerSettings(change) {
|
|
244
|
+
const kernel = this.kernelState();
|
|
245
|
+
if (kernel)
|
|
246
|
+
kernel.updateSettings(change);
|
|
247
|
+
else
|
|
248
|
+
this.viewerSettingsBeforeStart.update(change);
|
|
249
|
+
}
|
|
250
|
+
resetViewerSettings() {
|
|
251
|
+
const kernel = this.kernelState();
|
|
252
|
+
if (kernel)
|
|
253
|
+
kernel.resetSettings();
|
|
254
|
+
else
|
|
255
|
+
this.viewerSettingsBeforeStart.reset();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* A plugin's settings, with or without a document, and before the kernel too: they belong to
|
|
259
|
+
* the plugin as the app registered it. One binding per plugin, shared by every service.
|
|
260
|
+
*/
|
|
261
|
+
settingsOf(token) {
|
|
262
|
+
const known = this.pluginSettings.get(token);
|
|
263
|
+
if (known)
|
|
264
|
+
return known;
|
|
265
|
+
const binding = this.given
|
|
266
|
+
? this.givenSettingsOf(this.given, token)
|
|
267
|
+
: this.registeredSettingsOf(token);
|
|
268
|
+
this.pluginSettings.set(token, binding);
|
|
269
|
+
return binding;
|
|
270
|
+
}
|
|
271
|
+
/** A registered plugin's settings, kept from before the kernel exists. */
|
|
272
|
+
registeredSettingsOf(token) {
|
|
273
|
+
const plugin = this.pluginFor(token);
|
|
274
|
+
const declaration = plugin?.settings;
|
|
275
|
+
if (!plugin || !declaration) {
|
|
276
|
+
throw new PluginError('unsupported', token.name, 'this plugin has no settings');
|
|
277
|
+
}
|
|
278
|
+
const before = new SettingsBeforeStart(declaration);
|
|
279
|
+
this.pluginSettingsBeforeStart.set(plugin, before);
|
|
280
|
+
const store = (kernel) => kernel.settingsOf(token);
|
|
281
|
+
return {
|
|
282
|
+
current: this.read((kernel) => store(kernel).getSettings(), () => before.current(), shallowEqual),
|
|
283
|
+
update: (change) => {
|
|
284
|
+
const kernel = this.kernelState();
|
|
285
|
+
if (kernel)
|
|
286
|
+
store(kernel).updateSettings(change);
|
|
287
|
+
else
|
|
288
|
+
before.update(change);
|
|
289
|
+
},
|
|
290
|
+
reset: () => {
|
|
291
|
+
const kernel = this.kernelState();
|
|
292
|
+
if (kernel)
|
|
293
|
+
store(kernel).resetSettings();
|
|
294
|
+
else
|
|
295
|
+
before.reset();
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** A plugin's settings in a kernel given, which is there from the start. */
|
|
300
|
+
givenSettingsOf(kernel, token) {
|
|
301
|
+
const store = kernelSettingsOf(kernel, token);
|
|
302
|
+
if (!store)
|
|
303
|
+
throw new PluginError('unsupported', token.name, 'this plugin has no settings');
|
|
304
|
+
const current = () => store.getSettings();
|
|
305
|
+
return {
|
|
306
|
+
current: this.read(current, current, shallowEqual),
|
|
307
|
+
update: (change) => store.updateSettings(change),
|
|
308
|
+
reset: () => store.resetSettings(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
pluginFor(token) {
|
|
312
|
+
return this.plugins.find((plugin) => plugin.token === token) ?? null;
|
|
313
|
+
}
|
|
314
|
+
// ── lifetime ────────────────────────────────────────────────────────────────
|
|
315
|
+
/** Follow a kernel someone else made: its changes, and nothing of its lifetime. */
|
|
316
|
+
adopt(kernel) {
|
|
317
|
+
this.stopListening = kernel.subscribe(() => this.changed());
|
|
318
|
+
this.kernelState.set(kernel);
|
|
319
|
+
this.changed();
|
|
320
|
+
}
|
|
321
|
+
start(config) {
|
|
322
|
+
const source = config.engine;
|
|
323
|
+
const owned = typeof source === 'function';
|
|
324
|
+
let made;
|
|
325
|
+
try {
|
|
326
|
+
made = owned ? source() : source;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
this.fail(error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (isPromiseLike(made)) {
|
|
333
|
+
made.then((engine) => this.attach(config, engine, owned), (error) => this.fail(error));
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this.attach(config, made, owned);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Create and start the kernel over `engine`. An engine this viewer made is destroyed with it. */
|
|
340
|
+
attach(config, engine, owned) {
|
|
341
|
+
if (this.destroyed) {
|
|
342
|
+
if (owned)
|
|
343
|
+
void engine.destroy();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Warming a local engine up now overlaps its WebAssembly boot with the plugins' setup.
|
|
347
|
+
if (isLocalEngine(engine))
|
|
348
|
+
engine.warmup();
|
|
349
|
+
let kernel;
|
|
350
|
+
try {
|
|
351
|
+
kernel = createKernel({
|
|
352
|
+
engine,
|
|
353
|
+
plugins: [...this.plugins],
|
|
354
|
+
settings: registeredViewerSettings(config),
|
|
355
|
+
clock: browserClock(),
|
|
356
|
+
printer: browserPrinter(),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (owned)
|
|
361
|
+
void engine.destroy();
|
|
362
|
+
this.fail(error);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.engine = engine;
|
|
366
|
+
this.ownsEngine = owned;
|
|
367
|
+
this.viewerSettingsBeforeStart.applyTo(kernel);
|
|
368
|
+
for (const [plugin, before] of this.pluginSettingsBeforeStart) {
|
|
369
|
+
before.applyTo(kernel.settingsOf(plugin.token));
|
|
370
|
+
}
|
|
371
|
+
this.stopListening = kernel.subscribe(() => this.changed());
|
|
372
|
+
this.kernelState.set(kernel);
|
|
373
|
+
this.changed();
|
|
374
|
+
kernel.start().then(() => {
|
|
375
|
+
if (this.destroyed)
|
|
376
|
+
return;
|
|
377
|
+
this.statusState.set('ready');
|
|
378
|
+
// Every tab appears at once, in order; a document that fails shows it in its own tab.
|
|
379
|
+
kernel.documents.openAll(config.initialDocuments ?? []);
|
|
380
|
+
}, (error) => {
|
|
381
|
+
if (!this.destroyed)
|
|
382
|
+
this.fail(error);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
changed() {
|
|
386
|
+
this.revisionState.update((revision) => revision + 1);
|
|
387
|
+
for (const listener of [...this.listeners])
|
|
388
|
+
listener();
|
|
389
|
+
}
|
|
390
|
+
fail(error) {
|
|
391
|
+
this.errorState.set(error);
|
|
392
|
+
this.statusState.set('error');
|
|
393
|
+
console.error('[embedpdf] the viewer failed to start', error);
|
|
394
|
+
}
|
|
395
|
+
runFeatureSetups() {
|
|
396
|
+
if (this.destroyed)
|
|
397
|
+
return;
|
|
398
|
+
for (const feature of this.setup.features) {
|
|
399
|
+
if (feature.setup)
|
|
400
|
+
runInInjectionContext(this.injector, feature.setup);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
destroy() {
|
|
133
404
|
if (this.destroyed)
|
|
134
405
|
return;
|
|
135
406
|
this.destroyed = true;
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
void
|
|
147
|
-
if (
|
|
407
|
+
this.stopListening?.();
|
|
408
|
+
this.listeners.clear();
|
|
409
|
+
// A kernel given is its maker's to destroy.
|
|
410
|
+
if (this.given)
|
|
411
|
+
return;
|
|
412
|
+
const kernel = this.kernelState();
|
|
413
|
+
const engine = this.engine;
|
|
414
|
+
const owned = this.ownsEngine;
|
|
415
|
+
// The kernel first, which closes every document's engine handle, then the engine it ran on.
|
|
416
|
+
if (kernel) {
|
|
417
|
+
void kernel.destroy().then(() => {
|
|
418
|
+
if (owned && engine)
|
|
148
419
|
void engine.destroy();
|
|
149
420
|
});
|
|
150
421
|
}
|
|
@@ -154,349 +425,839 @@ class EpdfKernelHost {
|
|
|
154
425
|
}
|
|
155
426
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfKernelHost, decorators: [{
|
|
156
427
|
type: Injectable
|
|
157
|
-
}] });
|
|
158
|
-
/**
|
|
159
|
-
* Host the kernel in an environment injector (route or application providers).
|
|
160
|
-
* The kernel's lifetime is the injector's — a route-level workspace tears down
|
|
161
|
-
* when the route does. Unlocks chrome OUTSIDE any viewer subtree: a toolbar in
|
|
162
|
-
* the app header and the stage in the main outlet share this one kernel.
|
|
163
|
-
*/
|
|
164
|
-
function provideEmbedPdf(config) {
|
|
165
|
-
return makeEnvironmentProviders([
|
|
166
|
-
{
|
|
167
|
-
provide: EpdfKernelHost,
|
|
168
|
-
useFactory: () => {
|
|
169
|
-
const host = new EpdfKernelHost();
|
|
170
|
-
host.connect(() => config);
|
|
171
|
-
host.boot();
|
|
172
|
-
return host;
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
]);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* DI tokens shared by the runtime's primitives and directives — split out so
|
|
180
|
-
* `inject.ts` (consumers) and `scope.ts` (providers) never import each other.
|
|
181
|
-
*/
|
|
182
|
-
const EPDF_DOCUMENT_SCOPE = new InjectionToken('EPDF_DOCUMENT_SCOPE');
|
|
428
|
+
}], ctorParameters: () => [] });
|
|
183
429
|
|
|
184
430
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
431
|
+
* How the adapter reaches a plugin: its capability token, resolved for one document (the one in
|
|
432
|
+
* scope, else the active one) after every change of the kernel. A `CapabilityBinding` turns that
|
|
433
|
+
* into what Angular code wants: signals for values, late-bound methods for calls, and RxJS
|
|
434
|
+
* streams for events. Services, the Stage and the page layers are all built on it.
|
|
187
435
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* construction — they return signals or lazy methods, so they are safe in
|
|
191
|
-
* component-hosted mode where the kernel materializes after construction.
|
|
436
|
+
* Resolution is a read, not a cache: a document becomes resolvable when it's ready, while its
|
|
437
|
+
* id stays the same, so a capability looked up once by id would go stale.
|
|
192
438
|
*/
|
|
193
|
-
|
|
194
|
-
function injectKernelHost() {
|
|
439
|
+
/** The viewer above this injection context; EPDF-101, naming `what`, when there is none. */
|
|
440
|
+
function injectKernelHost(what) {
|
|
195
441
|
const host = inject(EpdfKernelHost, { optional: true });
|
|
196
|
-
if (!host)
|
|
197
|
-
throw
|
|
198
|
-
}
|
|
442
|
+
if (!host)
|
|
443
|
+
throw noViewerError(what);
|
|
199
444
|
return host;
|
|
200
445
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
function injectKernelValue(select, equal) {
|
|
209
|
-
return injectKernelHost().value(select, equal);
|
|
446
|
+
const FOLLOW_ACTIVE = signal(null).asReadonly();
|
|
447
|
+
/**
|
|
448
|
+
* The document this injection context talks to: the id the nearest `[epdfDocumentScope]` (or a
|
|
449
|
+
* Stage with a `[document]`) gives, or null to follow the active document.
|
|
450
|
+
*/
|
|
451
|
+
function injectDocumentScope() {
|
|
452
|
+
return inject(EPDF_DOCUMENT_SCOPE, { optional: true })?.id ?? FOLLOW_ACTIVE;
|
|
210
453
|
}
|
|
211
|
-
|
|
212
|
-
|
|
454
|
+
/**
|
|
455
|
+
* A plugin's capability for this injection context's document, as a signal: null while it
|
|
456
|
+
* can't resolve (no such plugin, no document, or one that isn't ready yet). For plugin authors;
|
|
457
|
+
* apps use a plugin's service (`inject(EpdfSearch)`). Call it in an injection context.
|
|
458
|
+
*/
|
|
459
|
+
function injectCapability(token) {
|
|
460
|
+
const binding = new CapabilityBinding(injectKernelHost(`injectCapability(${token.name})`), () => token, injectDocumentScope());
|
|
461
|
+
return binding.capability;
|
|
213
462
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
463
|
+
/** One capability token, for one document, as signals, calls and streams. */
|
|
464
|
+
class CapabilityBinding {
|
|
465
|
+
host;
|
|
466
|
+
token;
|
|
467
|
+
documentId;
|
|
468
|
+
/** The capability, or null while it can't resolve. */
|
|
469
|
+
capability;
|
|
470
|
+
/**
|
|
471
|
+
* @param token the token to resolve; a function, because a component's token is an input
|
|
472
|
+
* @param documentId the document to resolve it for, or null for the active one
|
|
473
|
+
*/
|
|
474
|
+
constructor(host, token, documentId) {
|
|
475
|
+
this.host = host;
|
|
476
|
+
this.token = token;
|
|
477
|
+
this.documentId = documentId;
|
|
478
|
+
this.capability = host.read((kernel) => this.resolve(kernel), () => null);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* A value read through the capability, as a signal that changes only when `equal` says the
|
|
482
|
+
* value did. `empty` while the capability can't resolve, and for the one read that can land
|
|
483
|
+
* between a document closing and its capability going away.
|
|
484
|
+
*/
|
|
485
|
+
select(read, empty, equal = shallowEqual) {
|
|
486
|
+
return this.host.read((kernel) => {
|
|
487
|
+
const capability = this.resolve(kernel);
|
|
488
|
+
if (!capability)
|
|
489
|
+
return empty;
|
|
490
|
+
try {
|
|
491
|
+
return read(capability);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return empty;
|
|
495
|
+
}
|
|
496
|
+
}, () => empty, equal);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The capability, for `call`, which needs it now. Without a document a document plugin
|
|
500
|
+
* refuses with `not-ready` ("no document is open"), the same refusal every framework's
|
|
501
|
+
* stand-in gives (`standInFor` in `@embedpdf/core`); a token no plugin provides throws the
|
|
502
|
+
* kernel's reason, because that is a setup mistake.
|
|
503
|
+
*/
|
|
504
|
+
require(call) {
|
|
505
|
+
const token = this.token();
|
|
506
|
+
const kernel = this.host.requireKernel(token.name, call);
|
|
507
|
+
const documentId = untracked(this.documentId) ?? undefined;
|
|
508
|
+
const capability = kernel.tryCapability(token, documentId);
|
|
509
|
+
if (capability)
|
|
510
|
+
return capability;
|
|
511
|
+
if (kernel.scopeOf(token) === 'document') {
|
|
512
|
+
throw new PluginError('not-ready', token.name, 'no document is open');
|
|
513
|
+
}
|
|
514
|
+
return kernel.capability(token, documentId);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* The capability's method `name`, looked up when it's called, so a method kept in a field
|
|
518
|
+
* acts on the document current at that moment. The same function for the binding's lifetime.
|
|
519
|
+
*
|
|
520
|
+
* A read (a method named `get*`, `list*`, `is*`, `has*` or `can*`: `canUpdate()`,
|
|
521
|
+
* `getZoomLevel()`) is state too: called in a template, a `computed()` or an `effect()`, it's
|
|
522
|
+
* read again after each change of the kernel and of the document in scope, and wakes its
|
|
523
|
+
* reader only when its answer changes. So a button it disables follows the selection, the
|
|
524
|
+
* active document and its permissions. Other methods track nothing: an `effect()` that calls a
|
|
525
|
+
* verb doesn't run again because the verb changed the document.
|
|
526
|
+
*
|
|
527
|
+
* Without a document a method refuses with `not-ready`: one that returns a promise (the token's
|
|
528
|
+
* `promises`) returns the refusal as a rejected promise, so `.catch()` sees it; others throw.
|
|
529
|
+
*/
|
|
530
|
+
method(name) {
|
|
531
|
+
return this.late(String(name), (capability) => capability, name);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* The method `name` of the capability's namespace `namespace` (`custom.update()`), late-bound
|
|
535
|
+
* the way {@link method} is. A service gives a namespace an object of its own
|
|
536
|
+
* (`metadata.custom`), built from these, {@link select} and {@link stream}.
|
|
537
|
+
*/
|
|
538
|
+
namespaceMethod(namespace, name) {
|
|
539
|
+
return this.late(`${String(namespace)}.${String(name)}`, (capability) => capability[namespace], name);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* One of the capability's events, as an RxJS stream. A subscription follows the document:
|
|
543
|
+
* when it changes (another tab, a document that opens or closes), it moves to the new
|
|
544
|
+
* document's capability. Nothing is emitted while there is none.
|
|
545
|
+
*/
|
|
546
|
+
stream(pick) {
|
|
547
|
+
return new Observable((subscriber) => {
|
|
548
|
+
let current = null;
|
|
549
|
+
let listening = null;
|
|
550
|
+
const follow = () => {
|
|
551
|
+
const kernel = untracked(this.host.kernel);
|
|
552
|
+
const next = kernel ? untracked(() => this.resolve(kernel)) : null;
|
|
553
|
+
if (next === current)
|
|
554
|
+
return;
|
|
555
|
+
listening?.abort();
|
|
556
|
+
listening = null;
|
|
557
|
+
current = next;
|
|
558
|
+
if (!next)
|
|
559
|
+
return;
|
|
560
|
+
listening = new AbortController();
|
|
561
|
+
pick(next)((event) => subscriber.next(event), { signal: listening.signal });
|
|
562
|
+
};
|
|
563
|
+
const stop = this.host.subscribe(follow);
|
|
564
|
+
follow();
|
|
565
|
+
return () => {
|
|
566
|
+
stop();
|
|
567
|
+
listening?.abort();
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
resolve(kernel) {
|
|
572
|
+
return kernel.tryCapability(this.token(), this.documentId() ?? undefined);
|
|
573
|
+
}
|
|
574
|
+
/** `owner(capability)[name]`, looked up when called; a read tracks what its answer reads. */
|
|
575
|
+
late(label, owner, name) {
|
|
576
|
+
const call = `${label}()`;
|
|
577
|
+
const run = (args) => {
|
|
578
|
+
let target;
|
|
579
|
+
try {
|
|
580
|
+
target = owner(this.require(call));
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
// Not ready: a method that returns a promise rejects, as the capability would.
|
|
584
|
+
if (isPluginError(error, 'not-ready') && returnsPromise(this.token(), label)) {
|
|
585
|
+
return Promise.reject(error);
|
|
586
|
+
}
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
return target[name].apply(target, args);
|
|
590
|
+
};
|
|
591
|
+
if (!isReadMember(String(name))) {
|
|
592
|
+
return ((...args) => untracked(() => run(args)));
|
|
593
|
+
}
|
|
594
|
+
// A computed of its own for each call: whoever reads the check depends on its answer, not
|
|
595
|
+
// on every change of the kernel. Outside a reactive context it's one plain call.
|
|
596
|
+
return ((...args) => computed(() => {
|
|
597
|
+
this.documentId();
|
|
598
|
+
this.host.revision();
|
|
599
|
+
return run(args);
|
|
600
|
+
})());
|
|
601
|
+
}
|
|
220
602
|
}
|
|
603
|
+
|
|
221
604
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
605
|
+
* One service per plugin: `inject(EpdfSearch)` has the plugin's methods, every value of its
|
|
606
|
+
* State table as a signal (`search.hitCount()`), its events as RxJS streams
|
|
607
|
+
* (`search.completed$`), and, when the plugin has settings, `settings()`, `getSettings()`,
|
|
608
|
+
* `updateSettings()`, `resetSettings()` and `settingsChanged$`. `pluginService()` builds all of that from what the
|
|
609
|
+
* plugin declares already, so a plugin's service is a few lines; the package README's "Adding a
|
|
610
|
+
* plugin binding" shows one.
|
|
611
|
+
*
|
|
612
|
+
* A service acts on the document of the injector that created it: under `[epdfDocumentScope]`
|
|
613
|
+
* that document, else the active one. Its methods are looked up when they're called, so a
|
|
614
|
+
* method kept in a field acts on the document current at that moment.
|
|
615
|
+
*/
|
|
616
|
+
/**
|
|
617
|
+
* The base every plugin service extends. Its members are for the service's own class, which
|
|
618
|
+
* adds what the generic part can't: a signal with arguments (`hitsOn(page)`), a namespace
|
|
619
|
+
* (`custom.fields()`), a helper that needs the caller's injection context.
|
|
620
|
+
*/
|
|
621
|
+
class EpdfPluginService {
|
|
622
|
+
/** The plugin's capability for this service's document. */
|
|
623
|
+
binding;
|
|
624
|
+
constructor(spec) {
|
|
625
|
+
const host = inject(EpdfKernelHost, { optional: true });
|
|
626
|
+
// A plugin's service names its feature, whether the viewer is missing or only the plugin.
|
|
627
|
+
if (spec.feature && !host?.provides(spec.token))
|
|
628
|
+
throw noPluginError(spec.name, spec.feature);
|
|
629
|
+
if (!host)
|
|
630
|
+
throw noViewerError(`inject(${spec.name})`);
|
|
631
|
+
this.binding = new CapabilityBinding(host, () => spec.token, injectDocumentScope());
|
|
632
|
+
}
|
|
240
633
|
}
|
|
241
|
-
/**
|
|
242
|
-
*
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
634
|
+
/**
|
|
635
|
+
* The base class of a plugin's service, built from the plugin's declarations:
|
|
636
|
+
*
|
|
637
|
+
* @Injectable({ providedIn: 'root' })
|
|
638
|
+
* export class EpdfSearch extends pluginService({
|
|
639
|
+
* name: 'EpdfSearch',
|
|
640
|
+
* feature: 'withSearch()',
|
|
641
|
+
* token: SearchToken,
|
|
642
|
+
* state: searchState,
|
|
643
|
+
* methods: ['search', 'clear', 'goToHit'],
|
|
644
|
+
* events: ['onCompleted'],
|
|
645
|
+
* }) {}
|
|
646
|
+
*
|
|
647
|
+
* `providedIn: 'root'` is only the fallback that turns a missing viewer into EPDF-102: the
|
|
648
|
+
* service's feature provides it next to the viewer, and `[epdfDocumentScope]` again.
|
|
649
|
+
*/
|
|
650
|
+
function pluginService(spec) {
|
|
651
|
+
class PluginService extends EpdfPluginService {
|
|
652
|
+
constructor() {
|
|
653
|
+
super(spec);
|
|
654
|
+
const members = this;
|
|
655
|
+
const binding = this.binding;
|
|
656
|
+
if (spec.state) {
|
|
657
|
+
const state = spec.state;
|
|
658
|
+
// One read of the whole table, compared field by field, then one signal per field: a
|
|
659
|
+
// template that reads `hitCount()` wakes only when the hit count changes.
|
|
660
|
+
const whole = binding.select((capability) => state.read(capability), state.empty);
|
|
661
|
+
for (const field of Object.keys(state.empty)) {
|
|
662
|
+
members[field] = computed(() => whole()[field]);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const ensure = spec.ensure ?? spec.state?.ensure;
|
|
666
|
+
if (ensure) {
|
|
667
|
+
effect(() => {
|
|
668
|
+
const capability = binding.capability();
|
|
669
|
+
// The state says when the load fails (its status), so the rejection has nothing to add.
|
|
670
|
+
if (capability)
|
|
671
|
+
untracked(() => ensure(capability).catch(() => { }));
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
for (const name of spec.methods)
|
|
675
|
+
members[name] = binding.method(name);
|
|
676
|
+
for (const event of spec.events ?? []) {
|
|
677
|
+
const stream = `${event.charAt(2).toLowerCase()}${event.slice(3)}$`;
|
|
678
|
+
members[stream] = binding.stream((capability) => capability[event]);
|
|
679
|
+
}
|
|
680
|
+
// Settings belong to the plugin as the app registered it, not to a document: they work
|
|
681
|
+
// with no document open, and before the kernel exists.
|
|
682
|
+
const host = binding.host;
|
|
683
|
+
if (host.hasSettings(spec.token)) {
|
|
684
|
+
const settings = host.settingsOf(spec.token);
|
|
685
|
+
members['settings'] = settings.current;
|
|
686
|
+
// A read, like every `get*` method: a template or an effect that calls it follows it.
|
|
687
|
+
members['getSettings'] = () => settings.current();
|
|
688
|
+
members['updateSettings'] = settings.update;
|
|
689
|
+
members['resetSettings'] = settings.reset;
|
|
690
|
+
members['settingsChanged$'] = host.stream((kernel) => kernel.settingsOf(spec.token).onSettingsChanged);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return PluginService;
|
|
247
695
|
}
|
|
248
|
-
|
|
249
|
-
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The viewer's own services, which `provideEmbedPdf()` always has:
|
|
699
|
+
*
|
|
700
|
+
* inject(EpdfViewer) the viewer's settings (`identity`, `scope`, `accent`, `page`) and status
|
|
701
|
+
* inject(EpdfDocuments) open, close, unlock and download documents; every tab, as signals
|
|
702
|
+
* inject(EpdfDocument) the document this part of the template talks to, as signals
|
|
703
|
+
*/
|
|
704
|
+
/**
|
|
705
|
+
* The viewer itself: its settings, and whether it started. The config given to
|
|
706
|
+
* `provideEmbedPdf()` is read once; what may change while the app runs changes here:
|
|
707
|
+
*
|
|
708
|
+
* effect(() => viewer.updateSettings({ identity: auth.identity() }));
|
|
709
|
+
*
|
|
710
|
+
* Documents opened after a change of `identity` or `scope` use the new value; open ones keep
|
|
711
|
+
* theirs.
|
|
712
|
+
*/
|
|
713
|
+
class EpdfViewer {
|
|
714
|
+
host = injectKernelHost('inject(EpdfViewer)');
|
|
715
|
+
/** The viewer's settings: `identity`, `scope`, `accent` and `page`. */
|
|
716
|
+
settings = this.host.viewerSettings;
|
|
717
|
+
/** `'starting'`, `'ready'`, or `'error'` when the engine or a plugin failed to start. */
|
|
718
|
+
status = this.host.status;
|
|
719
|
+
/** Why the viewer failed to start, when `status()` is `'error'`. */
|
|
720
|
+
error = this.host.error;
|
|
721
|
+
/** Fires once for each call that changed a setting. */
|
|
722
|
+
settingsChanged$ = this.host.stream((kernel) => kernel.onSettingsChanged);
|
|
723
|
+
/** Merge `changes` into the viewer's settings: `page` merges, `identity` and `scope` replace. */
|
|
724
|
+
updateSettings(changes) {
|
|
725
|
+
this.host.updateViewerSettings(changes);
|
|
726
|
+
}
|
|
727
|
+
/** Go back to the settings `provideEmbedPdf()` was given. */
|
|
728
|
+
resetSettings() {
|
|
729
|
+
this.host.resetViewerSettings();
|
|
730
|
+
}
|
|
731
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
732
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, providedIn: 'root' });
|
|
250
733
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
734
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, decorators: [{
|
|
735
|
+
type: Injectable,
|
|
736
|
+
args: [{ providedIn: 'root' }]
|
|
737
|
+
}] });
|
|
738
|
+
/**
|
|
739
|
+
* The documents: open, close, unlock, retry and download them, and every tab with the active
|
|
740
|
+
* one as signals (`documents()`, `activeId()`). Under `[epdfDocumentScope]` the calls that
|
|
741
|
+
* leave out the document use that one: `download()` downloads the document in scope.
|
|
742
|
+
*/
|
|
743
|
+
class EpdfDocuments extends pluginService({
|
|
744
|
+
name: 'EpdfDocuments',
|
|
745
|
+
token: DocumentsToken,
|
|
746
|
+
state: documentsState,
|
|
747
|
+
methods: [
|
|
748
|
+
'open',
|
|
749
|
+
'canOpenFiles',
|
|
750
|
+
'openAll',
|
|
751
|
+
'retry',
|
|
752
|
+
'rename',
|
|
753
|
+
'unlock',
|
|
754
|
+
'close',
|
|
755
|
+
'closeAll',
|
|
756
|
+
'setActive',
|
|
757
|
+
'getActiveId',
|
|
758
|
+
'getActive',
|
|
759
|
+
'list',
|
|
760
|
+
'get',
|
|
761
|
+
'has',
|
|
762
|
+
'getCount',
|
|
763
|
+
'getOrder',
|
|
764
|
+
'setOrder',
|
|
765
|
+
'move',
|
|
766
|
+
'swap',
|
|
767
|
+
'download',
|
|
768
|
+
'downloadLayer',
|
|
769
|
+
'preparePrint',
|
|
770
|
+
'print',
|
|
771
|
+
'getPrintQuality',
|
|
772
|
+
'canDownload',
|
|
773
|
+
'canPrint',
|
|
774
|
+
'listPages',
|
|
775
|
+
'getPage',
|
|
776
|
+
'getPageIndex',
|
|
777
|
+
'getRevision',
|
|
778
|
+
],
|
|
779
|
+
events: [
|
|
780
|
+
'onOpened',
|
|
781
|
+
'onOpenFailed',
|
|
782
|
+
'onLocked',
|
|
783
|
+
'onClosed',
|
|
784
|
+
'onActiveChanged',
|
|
785
|
+
'onPagesChanged',
|
|
786
|
+
'onUnsavedChangesChanged',
|
|
787
|
+
],
|
|
788
|
+
}) {
|
|
789
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
790
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, providedIn: 'root' });
|
|
256
791
|
}
|
|
257
|
-
|
|
258
|
-
|
|
792
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocuments, decorators: [{
|
|
793
|
+
type: Injectable,
|
|
794
|
+
args: [{ providedIn: 'root' }]
|
|
795
|
+
}] });
|
|
796
|
+
const NO_PAGES = Object.freeze([]);
|
|
797
|
+
/**
|
|
798
|
+
* The document this part of the template talks to: the one `[epdfDocumentScope]` names, else
|
|
799
|
+
* the active one. Its fields are signals (`id()`, `name()`, `status()`, `pageCount()`,
|
|
800
|
+
* `hasUnsavedChanges()`, `passwordProvided()`, `error()`), and `pages()` its pages. With no
|
|
801
|
+
* document it reads as one still on its way: an empty `id` and `status() === 'loading'`.
|
|
802
|
+
*/
|
|
803
|
+
class EpdfDocument extends pluginService({
|
|
804
|
+
name: 'EpdfDocument',
|
|
805
|
+
token: DocumentsToken,
|
|
806
|
+
state: documentState,
|
|
807
|
+
methods: [],
|
|
808
|
+
}) {
|
|
809
|
+
/**
|
|
810
|
+
* The document's pages, in order: each with its `ref`, `index`, `label`, size and rotation.
|
|
811
|
+
* Needs no Stage. The same array until a page is added, removed, moved or rotated; empty
|
|
812
|
+
* with no document.
|
|
813
|
+
*/
|
|
814
|
+
pages = this.binding.select((documents) => documents.listPages(), NO_PAGES, Object.is);
|
|
815
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
816
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, providedIn: 'root' });
|
|
259
817
|
}
|
|
818
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocument, decorators: [{
|
|
819
|
+
type: Injectable,
|
|
820
|
+
args: [{ providedIn: 'root' }]
|
|
821
|
+
}] });
|
|
822
|
+
|
|
823
|
+
const scopedServices = [EpdfDocuments, EpdfDocument];
|
|
260
824
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* `injectSelector` stays strict (fail-fast) for code that KNOWS a document
|
|
265
|
-
* exists — e.g. anything behind the document gate.
|
|
825
|
+
* Every service that acts on one document: the viewer's own, and each feature's, added when a
|
|
826
|
+
* `provideEmbedPdf()` call names the feature. `[epdfDocumentScope]` and a Stage list this array
|
|
827
|
+
* in their `providers`, so a service injected under them acts on their document.
|
|
266
828
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return select(c);
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
return fallback;
|
|
829
|
+
* It is one array that grows, not a copy: Angular reads a directive's providers when its
|
|
830
|
+
* template is first created, which is after the `provideEmbedPdf()` calls in decorators and the
|
|
831
|
+
* app config ran. A service whose feature is first named later (a lazily loaded route's
|
|
832
|
+
* providers) is still provided by its viewer, and acts on the active document there.
|
|
833
|
+
*/
|
|
834
|
+
const EPDF_SCOPED_SERVICES = scopedServices;
|
|
835
|
+
/** The viewer, with the plugins its features register; or the viewer of a kernel given. */
|
|
836
|
+
function provideEmbedPdf(config, ...features) {
|
|
837
|
+
for (const feature of features) {
|
|
838
|
+
for (const service of feature.services ?? []) {
|
|
839
|
+
if (!scopedServices.includes(service))
|
|
840
|
+
scopedServices.push(service);
|
|
283
841
|
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
open: (input, options) => host.kernel.documents.open(input, options),
|
|
295
|
-
unlock: (id, input) => host.kernel.documents.unlock(id, input),
|
|
296
|
-
close: (id) => host.kernel.documents.close(id),
|
|
297
|
-
setActive: (id) => host.kernel.documents.setActive(id),
|
|
298
|
-
move: (id, toIndex) => host.kernel.documents.move(id, toIndex),
|
|
299
|
-
swap: (a, b) => host.kernel.documents.swap(a, b),
|
|
300
|
-
download: (id, opts) => host.kernel.documents.download(id, opts),
|
|
301
|
-
downloadLayer: (id) => host.kernel.documents.downloadLayer(id),
|
|
302
|
-
};
|
|
842
|
+
}
|
|
843
|
+
const setup = { config, features };
|
|
844
|
+
return [
|
|
845
|
+
{ provide: EPDF_SETUP, useValue: setup },
|
|
846
|
+
EpdfKernelHost,
|
|
847
|
+
EpdfViewer,
|
|
848
|
+
EpdfDocuments,
|
|
849
|
+
EpdfDocument,
|
|
850
|
+
...features.flatMap((feature) => [...(feature.services ?? []), ...(feature.providers ?? [])]),
|
|
851
|
+
];
|
|
303
852
|
}
|
|
304
853
|
|
|
305
854
|
/**
|
|
306
|
-
*
|
|
855
|
+
* Which document a part of the template talks to, and whether it has one:
|
|
307
856
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
857
|
+
* <div [epdfDocumentScope]="id"> everything inside talks to that document
|
|
858
|
+
* <epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
|
|
859
|
+
*
|
|
860
|
+
* The gate is a structural directive on purpose: a template really defers creating what's
|
|
861
|
+
* inside, which a conditional `<ng-content>` can't (Angular creates projected content whether
|
|
862
|
+
* or not it's shown). Document UI (the Stage, a page counter) goes behind it; workspace UI (a
|
|
863
|
+
* toolbar frame, an open button) stays outside and renders at once.
|
|
864
|
+
*/
|
|
865
|
+
/**
|
|
866
|
+
* Binds everything inside the element to one document: services injected there, the Stage
|
|
867
|
+
* and its layers act on it instead of the active document.
|
|
315
868
|
*/
|
|
316
869
|
class EpdfDocumentScope {
|
|
870
|
+
/** The document's id. */
|
|
317
871
|
id = input.required(...(ngDevMode ? [{ debugName: "id", alias: 'epdfDocumentScope' }] : [{ alias: 'epdfDocumentScope' }]));
|
|
318
872
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentScope, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
319
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentScope, isStandalone: true, selector: "[epdfDocumentScope]", inputs: { id: { classPropertyName: "id", publicName: "epdfDocumentScope", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
|
|
873
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentScope, isStandalone: true, selector: "[epdfDocumentScope]", inputs: { id: { classPropertyName: "id", publicName: "epdfDocumentScope", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
|
|
874
|
+
{ provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) },
|
|
875
|
+
// A service injected inside gets an instance of its own here, bound to this document.
|
|
876
|
+
EPDF_SCOPED_SERVICES,
|
|
877
|
+
], ngImport: i0 });
|
|
320
878
|
}
|
|
321
879
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentScope, decorators: [{
|
|
322
880
|
type: Directive,
|
|
323
881
|
args: [{
|
|
324
882
|
selector: '[epdfDocumentScope]',
|
|
325
|
-
|
|
326
|
-
|
|
883
|
+
providers: [
|
|
884
|
+
{ provide: EPDF_DOCUMENT_SCOPE, useExisting: forwardRef(() => EpdfDocumentScope) },
|
|
885
|
+
// A service injected inside gets an instance of its own here, bound to this document.
|
|
886
|
+
EPDF_SCOPED_SERVICES,
|
|
887
|
+
],
|
|
327
888
|
}]
|
|
328
889
|
}] });
|
|
329
890
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* `
|
|
891
|
+
* Renders its content only while this part of the template has a ready document, with that
|
|
892
|
+
* document as `let document`. Otherwise it renders `locked` for a document waiting for its
|
|
893
|
+
* password, `error` for one that couldn't be opened, and `fallback` for everything else (none
|
|
894
|
+
* open yet, still opening), each with the document as `let-document`. Without `locked` or
|
|
895
|
+
* `error` those states show `fallback` too.
|
|
333
896
|
*
|
|
334
|
-
* <
|
|
335
|
-
* <ng-template #
|
|
897
|
+
* <epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
|
|
898
|
+
* <ng-template #opening>Opening…</ng-template>
|
|
899
|
+
* <ng-template #locked let-document><app-password-form [document]="document" /></ng-template>
|
|
336
900
|
*
|
|
337
|
-
* The
|
|
338
|
-
*
|
|
901
|
+
* The content stays mounted when another ready document becomes the active one; only its
|
|
902
|
+
* `document` changes.
|
|
339
903
|
*/
|
|
340
904
|
class EpdfDocumentGate {
|
|
341
|
-
/** Shown while
|
|
905
|
+
/** Shown while there is no ready document: none is open, or it's still opening. */
|
|
342
906
|
fallback = input(null, ...(ngDevMode ? [{ debugName: "fallback", alias: 'epdfDocumentGateFallback' }] : [{
|
|
343
907
|
alias: 'epdfDocumentGateFallback',
|
|
344
908
|
}]));
|
|
909
|
+
/** Shown while the document waits for its password; `fallback` without it. */
|
|
910
|
+
locked = input(null, ...(ngDevMode ? [{ debugName: "locked", alias: 'epdfDocumentGateLocked' }] : [{
|
|
911
|
+
alias: 'epdfDocumentGateLocked',
|
|
912
|
+
}]));
|
|
913
|
+
/** Shown when the document couldn't be opened; `fallback` without it. */
|
|
914
|
+
error = input(null, ...(ngDevMode ? [{ debugName: "error", alias: 'epdfDocumentGateError' }] : [{
|
|
915
|
+
alias: 'epdfDocumentGateError',
|
|
916
|
+
}]));
|
|
917
|
+
static ngTemplateContextGuard(_gate, context) {
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
345
920
|
constructor() {
|
|
346
|
-
const
|
|
347
|
-
const hasDocument = computed(() => docId() !== null, ...(ngDevMode ? [{ debugName: "hasDocument" }] : []));
|
|
348
|
-
const container = inject(ViewContainerRef);
|
|
921
|
+
const document = new CapabilityBinding(injectKernelHost('*epdfDocumentGate'), () => DocumentsToken, injectDocumentScope()).select(documentState.read, documentState.empty);
|
|
349
922
|
const content = inject(TemplateRef);
|
|
923
|
+
const container = inject(ViewContainerRef);
|
|
924
|
+
const changes = inject(ChangeDetectorRef);
|
|
925
|
+
let shown = null;
|
|
350
926
|
effect(() => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
927
|
+
const current = document();
|
|
928
|
+
const template = this.templateFor(current, content);
|
|
929
|
+
untracked(() => {
|
|
930
|
+
if (shown && shown.template === template) {
|
|
931
|
+
// The same template for another document (a tab switch): keep the view, pass it on.
|
|
932
|
+
shown.view.context.$implicit = current;
|
|
933
|
+
shown.view.markForCheck();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
container.clear();
|
|
937
|
+
shown = template
|
|
938
|
+
? { template, view: container.createEmbeddedView(template, { $implicit: current }) }
|
|
939
|
+
: null;
|
|
940
|
+
changes.markForCheck();
|
|
941
|
+
});
|
|
362
942
|
});
|
|
363
943
|
}
|
|
944
|
+
templateFor(document, content) {
|
|
945
|
+
if (document.status === 'ready')
|
|
946
|
+
return content;
|
|
947
|
+
const locked = this.locked();
|
|
948
|
+
if (document.status === 'locked' && locked)
|
|
949
|
+
return locked;
|
|
950
|
+
const failed = this.error();
|
|
951
|
+
if (document.status === 'error' && failed)
|
|
952
|
+
return failed;
|
|
953
|
+
return this.fallback() ?? null;
|
|
954
|
+
}
|
|
364
955
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentGate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
365
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentGate, isStandalone: true, selector: "[epdfDocumentGate]", inputs: { fallback: { classPropertyName: "fallback", publicName: "epdfDocumentGateFallback", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
956
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfDocumentGate, isStandalone: true, selector: "[epdfDocumentGate]", inputs: { fallback: { classPropertyName: "fallback", publicName: "epdfDocumentGateFallback", isSignal: true, isRequired: false, transformFunction: null }, locked: { classPropertyName: "locked", publicName: "epdfDocumentGateLocked", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "epdfDocumentGateError", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
366
957
|
}
|
|
367
958
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfDocumentGate, decorators: [{
|
|
368
959
|
type: Directive,
|
|
369
|
-
args: [{ selector: '[epdfDocumentGate]'
|
|
960
|
+
args: [{ selector: '[epdfDocumentGate]' }]
|
|
370
961
|
}], ctorParameters: () => [] });
|
|
371
962
|
|
|
372
963
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
* `<ng-content>` cannot defer anything — real gating belongs to templates the
|
|
378
|
-
* APP owns: `@if (ready())` for boot chrome, `*epdfDocumentGate` (or
|
|
379
|
-
* `@if (documentId())`) for document UI.
|
|
964
|
+
* The page context: what a layer drawn on a page knows about that page. A layer reads only
|
|
965
|
+
* this, never the Stage, so the same layer works on a Stage's page and on a standalone page
|
|
966
|
+
* view. Inside `<ng-template epdfPage>`, `let-page` gives it to the template and `injectPage()`
|
|
967
|
+
* to a component.
|
|
380
968
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
/** The engine, as an instance OR a thunk ({@link EngineFactory}). An
|
|
386
|
-
* instance is borrowed (never destroyed here); a thunk is host-owned
|
|
387
|
-
* (constructed then destroyed with the viewer). See EmbedPdfConfig.
|
|
388
|
-
* Init-only: the kernel is built once from the first values; recreate the
|
|
389
|
-
* viewer (e.g. with @if) to swap it. */
|
|
390
|
-
engine = input.required(...(ngDevMode ? [{ debugName: "engine" }] : []));
|
|
391
|
-
plugins = input.required(...(ngDevMode ? [{ debugName: "plugins" }] : []));
|
|
392
|
-
initialDocuments = input(...(ngDevMode ? [undefined, { debugName: "initialDocuments" }] : []));
|
|
393
|
-
/** The host this viewer provides — exposed so a template reference can read
|
|
394
|
-
* `viewer.host.ready()` / `.status()` without extra imports. */
|
|
395
|
-
host = inject(EpdfKernelHost, { self: true });
|
|
396
|
-
constructor() {
|
|
397
|
-
// Deferred config: inputs are READ on first kernel access, which happens
|
|
398
|
-
// after construction (a template binding or effect) — never here.
|
|
399
|
-
this.host.connect(() => ({
|
|
400
|
-
engine: this.engine(),
|
|
401
|
-
plugins: this.plugins(),
|
|
402
|
-
initialDocuments: this.initialDocuments(),
|
|
403
|
-
}));
|
|
404
|
-
}
|
|
405
|
-
ngOnInit() {
|
|
406
|
-
// Inputs are set by now; materializes the kernel and starts it (no-op on
|
|
407
|
-
// the server — the SSR contract lives on the host).
|
|
408
|
-
this.host.boot();
|
|
409
|
-
}
|
|
410
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
411
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.2.4", type: EpdfViewer, isStandalone: true, selector: "epdf-viewer", inputs: { engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, plugins: { classPropertyName: "plugins", publicName: "plugins", isSignal: true, isRequired: true, transformFunction: null }, initialDocuments: { classPropertyName: "initialDocuments", publicName: "initialDocuments", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents;" }, providers: [EpdfKernelHost], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
412
|
-
}
|
|
413
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfViewer, decorators: [{
|
|
414
|
-
type: Component,
|
|
415
|
-
args: [{
|
|
416
|
-
selector: 'epdf-viewer',
|
|
417
|
-
standalone: true,
|
|
418
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
419
|
-
providers: [EpdfKernelHost],
|
|
420
|
-
// Layout-transparent, like React's element-less <Viewer>.
|
|
421
|
-
host: { style: 'display: contents;' },
|
|
422
|
-
template: `<ng-content />`,
|
|
423
|
-
}]
|
|
424
|
-
}], ctorParameters: () => [] });
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* PageContext — the seam. A layer depends ONLY on this, never on the Stage, so
|
|
428
|
-
* the same layer works inside a virtualized `<epdf-stage>` page and a future
|
|
429
|
-
* standalone `<epdf-page-view>`.
|
|
430
|
-
*
|
|
431
|
-
* THE STABILITY INVARIANT (the one performance rule of this adapter): a page's
|
|
432
|
-
* context is ONE STABLE OBJECT per mounted surface — identity never changes;
|
|
433
|
-
* the volatile parts (`transform`, `frame`, `pageIndex`) are signals INSIDE it.
|
|
434
|
-
* The per-page injector providing `EPDF_PAGE` is likewise created once. Camera
|
|
435
|
-
* frames then flow as signal writes — `NgTemplateOutlet` never recreates the
|
|
436
|
-
* embedded views, in-flight renders survive, and updates stay per-binding.
|
|
437
|
-
* Never rebuild the context or the injector per frame.
|
|
969
|
+
* One context per page on screen, for as long as the page is shown: its identity never
|
|
970
|
+
* changes, and what moves (`pageIndex`, `frame`, `transform`) are signals inside it. The
|
|
971
|
+
* template outlet then keeps every layer while the camera moves, and a camera frame updates
|
|
972
|
+
* only the bindings that read the transform. Never build a new context per frame.
|
|
438
973
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* `makePageContext` exactly).
|
|
974
|
+
* The coordinate math is `PageTransform` from `@embedpdf/core-geometry`, and the client-space
|
|
975
|
+
* conversions are `pageClientSpace` from `@embedpdf/web`, both shared by every framework.
|
|
442
976
|
*/
|
|
443
977
|
const EPDF_PAGE = new InjectionToken('EPDF_PAGE');
|
|
444
|
-
|
|
978
|
+
/** The page this component is drawn on; EPDF-103, naming `what`, outside a page. */
|
|
979
|
+
function injectPage(what = 'This component') {
|
|
445
980
|
const page = inject(EPDF_PAGE, { optional: true });
|
|
446
|
-
if (!page)
|
|
447
|
-
throw
|
|
448
|
-
}
|
|
981
|
+
if (!page)
|
|
982
|
+
throw outsidePageError(what);
|
|
449
983
|
return page;
|
|
450
984
|
}
|
|
451
|
-
/**
|
|
452
|
-
*
|
|
985
|
+
/**
|
|
986
|
+
* A page context from its parts. `documentId` and `ref` are functions because a component's
|
|
987
|
+
* inputs aren't set yet when it's constructed; both stay the same for the page's lifetime.
|
|
988
|
+
*/
|
|
453
989
|
function createPageContext(parts) {
|
|
454
990
|
const { transform, getRect } = parts;
|
|
455
991
|
return {
|
|
456
992
|
get documentId() {
|
|
457
993
|
return parts.documentId();
|
|
458
994
|
},
|
|
459
|
-
get
|
|
460
|
-
return parts.
|
|
995
|
+
get ref() {
|
|
996
|
+
return parts.ref();
|
|
997
|
+
},
|
|
998
|
+
get view() {
|
|
999
|
+
return parts.view();
|
|
461
1000
|
},
|
|
462
1001
|
pageIndex: parts.pageIndex,
|
|
463
1002
|
frame: parts.frame,
|
|
464
1003
|
transform,
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return transform().viewToContent({ x: clientX - r.left, y: clientY - r.top });
|
|
470
|
-
},
|
|
471
|
-
toClientPoint: (p) => {
|
|
472
|
-
// Exact inverse of `toContentPoint`, offset by the same live display-box
|
|
473
|
-
// origin — the two can never drift.
|
|
474
|
-
const r = getRect();
|
|
475
|
-
const v = transform().contentToView(p);
|
|
476
|
-
return { x: r.left + v.x, y: r.top + v.y };
|
|
477
|
-
},
|
|
478
|
-
toClientRect: (rect) => {
|
|
479
|
-
const r = getRect();
|
|
480
|
-
const v = transform().contentToViewRect(rect);
|
|
481
|
-
return { x: r.left + v.x, y: r.top + v.y, width: v.width, height: v.height };
|
|
482
|
-
},
|
|
1004
|
+
...(parts.getViewDemand ? { getViewDemand: parts.getViewDemand } : {}),
|
|
1005
|
+
// Client pixels to pixels in the page's box, then the transform undoes the turn and the scale,
|
|
1006
|
+
// against the transform the signal holds when a conversion runs.
|
|
1007
|
+
...pageClientSpace(transform, getRect),
|
|
483
1008
|
};
|
|
484
1009
|
}
|
|
485
1010
|
|
|
486
1011
|
/**
|
|
487
|
-
*
|
|
1012
|
+
* The Angular side of `@embedpdf/web`'s page layers: a layer says, for as long as it's there,
|
|
1013
|
+
* which part of its page it paints, and the render layer reads which parts are painted, to leave
|
|
1014
|
+
* them out of the page's picture.
|
|
1015
|
+
*/
|
|
1016
|
+
/**
|
|
1017
|
+
* Say, for as long as this component is there, that it paints `part` of its page (`pageRef()`,
|
|
1018
|
+
* the page context's `ref`; `null` while it paints nothing). Call it in an injection context.
|
|
1019
|
+
*/
|
|
1020
|
+
function paintsPagePart(pageRef, part) {
|
|
1021
|
+
effect((onCleanup) => {
|
|
1022
|
+
const ref = pageRef();
|
|
1023
|
+
if (ref)
|
|
1024
|
+
onCleanup(untracked(() => pageLayersOf(ref).paint(part)));
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Which parts of the page (`pageRef()`, the page context's `ref`) the layers on it paint: `null`
|
|
1029
|
+
* until the first render is done, when every layer made with this one has said what it paints,
|
|
1030
|
+
* so the page's picture is never asked for with a part another layer is about to take. Call it
|
|
1031
|
+
* in an injection context.
|
|
1032
|
+
*/
|
|
1033
|
+
function injectPaintedParts(pageRef) {
|
|
1034
|
+
const painted = signal(null, ...(ngDevMode ? [{ debugName: "painted" }] : []));
|
|
1035
|
+
const injector = inject(Injector);
|
|
1036
|
+
afterNextRender(() => {
|
|
1037
|
+
effect((onCleanup) => {
|
|
1038
|
+
const layers = pageLayersOf(pageRef());
|
|
1039
|
+
untracked(() => painted.set(layers.painted()));
|
|
1040
|
+
onCleanup(layers.subscribe(() => painted.set(layers.painted())));
|
|
1041
|
+
}, { injector });
|
|
1042
|
+
});
|
|
1043
|
+
return painted.asReadonly();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Scene shapes as SVG elements: `@embedpdf/web`'s element descriptions (`svgShapesOf`), one
|
|
1048
|
+
* element each, a clipping `svg` with its children. The annotation and form layers draw their
|
|
1049
|
+
* scenes through `<svg:g [epdfSvgShapes]>` inside an `<svg>`.
|
|
1050
|
+
*/
|
|
1051
|
+
/**
|
|
1052
|
+
* An SVG element's attributes from a record of SVG names (`stroke-width`), as `@embedpdf/web`
|
|
1053
|
+
* describes a scene node (templates can't spread). Set again when the record changes; an
|
|
1054
|
+
* attribute that left the record is removed.
|
|
1055
|
+
*/
|
|
1056
|
+
class EpdfSvgAttributes {
|
|
1057
|
+
attributes = input.required(...(ngDevMode ? [{ debugName: "attributes", alias: 'epdfSvgAttributes' }] : [{
|
|
1058
|
+
alias: 'epdfSvgAttributes',
|
|
1059
|
+
}]));
|
|
1060
|
+
constructor() {
|
|
1061
|
+
const element = inject(ElementRef).nativeElement;
|
|
1062
|
+
let shown = {};
|
|
1063
|
+
effect(() => {
|
|
1064
|
+
const next = this.attributes();
|
|
1065
|
+
for (const name of Object.keys(shown)) {
|
|
1066
|
+
if (!(name in next))
|
|
1067
|
+
element.removeAttribute(name);
|
|
1068
|
+
}
|
|
1069
|
+
for (const [name, value] of Object.entries(next)) {
|
|
1070
|
+
if (shown[name] !== value)
|
|
1071
|
+
element.setAttribute(name, String(value));
|
|
1072
|
+
}
|
|
1073
|
+
shown = next;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgAttributes, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1077
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfSvgAttributes, isStandalone: true, selector: "[epdfSvgAttributes]", inputs: { attributes: { classPropertyName: "attributes", publicName: "epdfSvgAttributes", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
1078
|
+
}
|
|
1079
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgAttributes, decorators: [{
|
|
1080
|
+
type: Directive,
|
|
1081
|
+
args: [{ selector: '[epdfSvgAttributes]' }]
|
|
1082
|
+
}], ctorParameters: () => [] });
|
|
1083
|
+
/** The shapes, in drawing order, in the `<svg:g>` it's on, and the faces their text draws in. */
|
|
1084
|
+
class EpdfSvgShapes {
|
|
1085
|
+
shapes = input.required(...(ngDevMode ? [{ debugName: "shapes", alias: 'epdfSvgShapes' }] : [{ alias: 'epdfSvgShapes' }]));
|
|
1086
|
+
constructor() {
|
|
1087
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID)))
|
|
1088
|
+
return;
|
|
1089
|
+
// The faces of this level's text, while it shows: a clipping `svg` mounts its children's.
|
|
1090
|
+
const mounts = createFontMounts();
|
|
1091
|
+
inject(DestroyRef).onDestroy(() => mounts.release());
|
|
1092
|
+
effect(() => mounts.mount(this.shapes().flatMap((shape) => (shape.program ? [shape.program] : []))));
|
|
1093
|
+
}
|
|
1094
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgShapes, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1095
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfSvgShapes, isStandalone: true, selector: "g[epdfSvgShapes]", inputs: { shapes: { classPropertyName: "shapes", publicName: "epdfSvgShapes", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
1096
|
+
@for (shape of shapes(); track $index) {
|
|
1097
|
+
@switch (shape.tag) {
|
|
1098
|
+
@case ('rect') {
|
|
1099
|
+
<svg:rect
|
|
1100
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1101
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1102
|
+
></svg:rect>
|
|
1103
|
+
}
|
|
1104
|
+
@case ('ellipse') {
|
|
1105
|
+
<svg:ellipse
|
|
1106
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1107
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1108
|
+
></svg:ellipse>
|
|
1109
|
+
}
|
|
1110
|
+
@case ('line') {
|
|
1111
|
+
<svg:line
|
|
1112
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1113
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1114
|
+
></svg:line>
|
|
1115
|
+
}
|
|
1116
|
+
@case ('path') {
|
|
1117
|
+
<svg:path
|
|
1118
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1119
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1120
|
+
></svg:path>
|
|
1121
|
+
}
|
|
1122
|
+
@case ('polygon') {
|
|
1123
|
+
<svg:polygon
|
|
1124
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1125
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1126
|
+
></svg:polygon>
|
|
1127
|
+
}
|
|
1128
|
+
@case ('polyline') {
|
|
1129
|
+
<svg:polyline
|
|
1130
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1131
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1132
|
+
></svg:polyline>
|
|
1133
|
+
}
|
|
1134
|
+
@case ('text') {
|
|
1135
|
+
<svg:text
|
|
1136
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1137
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1138
|
+
[textContent]="shape.text ?? ''"
|
|
1139
|
+
></svg:text>
|
|
1140
|
+
}
|
|
1141
|
+
@case ('svg') {
|
|
1142
|
+
<svg:svg [epdfSvgAttributes]="shape.attributes">
|
|
1143
|
+
<svg:g [epdfSvgShapes]="shape.children ?? []"></svg:g>
|
|
1144
|
+
</svg:svg>
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EpdfSvgShapes, selector: "g[epdfSvgShapes]", inputs: ["epdfSvgShapes"] }, { kind: "directive", type: EpdfSvgAttributes, selector: "[epdfSvgAttributes]", inputs: ["epdfSvgAttributes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1149
|
+
}
|
|
1150
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfSvgShapes, decorators: [{
|
|
1151
|
+
type: Component,
|
|
1152
|
+
args: [{
|
|
1153
|
+
selector: 'g[epdfSvgShapes]',
|
|
1154
|
+
imports: [EpdfSvgAttributes],
|
|
1155
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1156
|
+
template: `
|
|
1157
|
+
@for (shape of shapes(); track $index) {
|
|
1158
|
+
@switch (shape.tag) {
|
|
1159
|
+
@case ('rect') {
|
|
1160
|
+
<svg:rect
|
|
1161
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1162
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1163
|
+
></svg:rect>
|
|
1164
|
+
}
|
|
1165
|
+
@case ('ellipse') {
|
|
1166
|
+
<svg:ellipse
|
|
1167
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1168
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1169
|
+
></svg:ellipse>
|
|
1170
|
+
}
|
|
1171
|
+
@case ('line') {
|
|
1172
|
+
<svg:line
|
|
1173
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1174
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1175
|
+
></svg:line>
|
|
1176
|
+
}
|
|
1177
|
+
@case ('path') {
|
|
1178
|
+
<svg:path
|
|
1179
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1180
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1181
|
+
></svg:path>
|
|
1182
|
+
}
|
|
1183
|
+
@case ('polygon') {
|
|
1184
|
+
<svg:polygon
|
|
1185
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1186
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1187
|
+
></svg:polygon>
|
|
1188
|
+
}
|
|
1189
|
+
@case ('polyline') {
|
|
1190
|
+
<svg:polyline
|
|
1191
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1192
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1193
|
+
></svg:polyline>
|
|
1194
|
+
}
|
|
1195
|
+
@case ('text') {
|
|
1196
|
+
<svg:text
|
|
1197
|
+
[epdfSvgAttributes]="shape.attributes"
|
|
1198
|
+
[style.mix-blend-mode]="shape.blend ?? null"
|
|
1199
|
+
[textContent]="shape.text ?? ''"
|
|
1200
|
+
></svg:text>
|
|
1201
|
+
}
|
|
1202
|
+
@case ('svg') {
|
|
1203
|
+
<svg:svg [epdfSvgAttributes]="shape.attributes">
|
|
1204
|
+
<svg:g [epdfSvgShapes]="shape.children ?? []"></svg:g>
|
|
1205
|
+
</svg:svg>
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
`,
|
|
1210
|
+
}]
|
|
1211
|
+
}], ctorParameters: () => [] });
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Development-time guardrails. Each warning fires once (by key), says what is wrong and what to
|
|
1215
|
+
* do instead, and stays quiet in production builds (Angular's `isDevMode()`).
|
|
1216
|
+
*/
|
|
1217
|
+
const warned = new Set();
|
|
1218
|
+
/** Warn once per key. Quiet in production. */
|
|
1219
|
+
function devWarn(key, message) {
|
|
1220
|
+
if (!isDevMode() || warned.has(key))
|
|
1221
|
+
return;
|
|
1222
|
+
warned.add(key);
|
|
1223
|
+
console.warn(`[embedpdf] ${message}`);
|
|
1224
|
+
}
|
|
1225
|
+
/** For tests: forget every warning, so one can fire again. */
|
|
1226
|
+
function resetDevWarnings() {
|
|
1227
|
+
warned.clear();
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* `epdfTheme()`: a theme written with setting names, as the CSS variables for the element
|
|
1232
|
+
* around your viewer. The variables and their lookup are framework-free (`@embedpdf/web`); this
|
|
1233
|
+
* types the result for Angular's `[style]` binding, which sets custom properties as they are.
|
|
1234
|
+
*/
|
|
1235
|
+
/**
|
|
1236
|
+
* The CSS variables of a theme: `<div [style]="theme()">` with
|
|
1237
|
+
* `theme = computed(() => epdfTheme({ accent: this.accent() }))`. Everything inside follows
|
|
1238
|
+
* them, and a CSS variable set in a stylesheet still wins over a setting.
|
|
1239
|
+
*/
|
|
1240
|
+
function epdfTheme(theme) {
|
|
1241
|
+
return epdfThemeVariables(theme);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* @embedpdf/angular/runtime: the viewer and what every feature builds on.
|
|
488
1246
|
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
1247
|
+
* provideEmbedPdf(config, ...features) the viewer, as providers
|
|
1248
|
+
* inject(EpdfViewer) its settings and status
|
|
1249
|
+
* inject(EpdfDocuments), EpdfDocument the documents, as methods, signals and streams
|
|
1250
|
+
* [epdfDocumentScope], *epdfDocumentGate which document a part of the template talks to
|
|
1251
|
+
* pluginService(), EpdfPluginService how a plugin's service is built
|
|
1252
|
+
* EPDF_PAGE, injectPage() the page a layer draws on
|
|
1253
|
+
* paintsPagePart(), injectPaintedParts() which parts of a page its layers paint
|
|
1254
|
+
* <svg:g [epdfSvgShapes]> a scene's shapes as SVG elements
|
|
494
1255
|
*/
|
|
495
|
-
//
|
|
1256
|
+
// The core's types and helpers, so app code has one import for the viewer.
|
|
496
1257
|
|
|
497
1258
|
/**
|
|
498
1259
|
* Generated bundle index. Do not edit.
|
|
499
1260
|
*/
|
|
500
1261
|
|
|
501
|
-
export { EPDF_DOCUMENT_SCOPE, EPDF_PAGE, EpdfDocumentGate, EpdfDocumentScope, EpdfKernelHost,
|
|
1262
|
+
export { CapabilityBinding, EPDF_DOCUMENT_SCOPE, EPDF_PAGE, EPDF_SCOPED_SERVICES, EPDF_SETUP, EpdfDocument, EpdfDocumentGate, EpdfDocumentScope, EpdfDocuments, EpdfError, EpdfKernelHost, EpdfPluginService, EpdfSvgAttributes, EpdfSvgShapes, EpdfViewer, createPageContext, devWarn, epdfTheme, injectCapability, injectDocumentScope, injectKernelHost, injectPage, injectPaintedParts, noPluginError, noViewerError, onServerError, outsidePageError, outsideStageError, paintsPagePart, pluginService, provideEmbedPdf, resetDevWarnings };
|
|
502
1263
|
//# sourceMappingURL=embedpdf-angular-runtime.mjs.map
|