@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.
Files changed (87) hide show
  1. package/README.md +309 -0
  2. package/dist/README.md +309 -0
  3. package/dist/actions/index.d.ts +34 -16
  4. package/dist/anchored/index.d.ts +67 -0
  5. package/dist/annotation/index.d.ts +602 -0
  6. package/dist/attachment/index.d.ts +29 -0
  7. package/dist/commands/index.d.ts +109 -0
  8. package/dist/fesm2022/embedpdf-angular-actions.mjs +117 -36
  9. package/dist/fesm2022/embedpdf-angular-actions.mjs.map +1 -1
  10. package/dist/fesm2022/embedpdf-angular-anchored.mjs +148 -0
  11. package/dist/fesm2022/embedpdf-angular-anchored.mjs.map +1 -0
  12. package/dist/fesm2022/embedpdf-angular-annotation.mjs +1933 -0
  13. package/dist/fesm2022/embedpdf-angular-annotation.mjs.map +1 -0
  14. package/dist/fesm2022/embedpdf-angular-attachment.mjs +67 -0
  15. package/dist/fesm2022/embedpdf-angular-attachment.mjs.map +1 -0
  16. package/dist/fesm2022/embedpdf-angular-commands.mjs +235 -0
  17. package/dist/fesm2022/embedpdf-angular-commands.mjs.map +1 -0
  18. package/dist/fesm2022/embedpdf-angular-form.mjs +1074 -0
  19. package/dist/fesm2022/embedpdf-angular-form.mjs.map +1 -0
  20. package/dist/fesm2022/embedpdf-angular-history.mjs +54 -0
  21. package/dist/fesm2022/embedpdf-angular-history.mjs.map +1 -0
  22. package/dist/fesm2022/embedpdf-angular-i18n.mjs +100 -0
  23. package/dist/fesm2022/embedpdf-angular-i18n.mjs.map +1 -0
  24. package/dist/fesm2022/embedpdf-angular-interaction.mjs +157 -0
  25. package/dist/fesm2022/embedpdf-angular-interaction.mjs.map +1 -0
  26. package/dist/fesm2022/embedpdf-angular-link.mjs +306 -0
  27. package/dist/fesm2022/embedpdf-angular-link.mjs.map +1 -0
  28. package/dist/fesm2022/embedpdf-angular-measurement.mjs +106 -0
  29. package/dist/fesm2022/embedpdf-angular-measurement.mjs.map +1 -0
  30. package/dist/fesm2022/embedpdf-angular-metadata.mjs +77 -0
  31. package/dist/fesm2022/embedpdf-angular-metadata.mjs.map +1 -0
  32. package/dist/fesm2022/embedpdf-angular-outline.mjs +76 -0
  33. package/dist/fesm2022/embedpdf-angular-outline.mjs.map +1 -0
  34. package/dist/fesm2022/embedpdf-angular-page-edit.mjs +68 -0
  35. package/dist/fesm2022/embedpdf-angular-page-edit.mjs.map +1 -0
  36. package/dist/fesm2022/embedpdf-angular-page-view.mjs +297 -0
  37. package/dist/fesm2022/embedpdf-angular-page-view.mjs.map +1 -0
  38. package/dist/fesm2022/embedpdf-angular-redaction.mjs +93 -0
  39. package/dist/fesm2022/embedpdf-angular-redaction.mjs.map +1 -0
  40. package/dist/fesm2022/embedpdf-angular-render.mjs +331 -69
  41. package/dist/fesm2022/embedpdf-angular-render.mjs.map +1 -1
  42. package/dist/fesm2022/embedpdf-angular-runtime.mjs +1151 -390
  43. package/dist/fesm2022/embedpdf-angular-runtime.mjs.map +1 -1
  44. package/dist/fesm2022/embedpdf-angular-search.mjs +251 -0
  45. package/dist/fesm2022/embedpdf-angular-search.mjs.map +1 -0
  46. package/dist/fesm2022/embedpdf-angular-selection.mjs +477 -0
  47. package/dist/fesm2022/embedpdf-angular-selection.mjs.map +1 -0
  48. package/dist/fesm2022/embedpdf-angular-shell.mjs +177 -0
  49. package/dist/fesm2022/embedpdf-angular-shell.mjs.map +1 -0
  50. package/dist/fesm2022/embedpdf-angular-signature.mjs +120 -0
  51. package/dist/fesm2022/embedpdf-angular-signature.mjs.map +1 -0
  52. package/dist/fesm2022/embedpdf-angular-stage.mjs +688 -288
  53. package/dist/fesm2022/embedpdf-angular-stage.mjs.map +1 -1
  54. package/dist/fesm2022/embedpdf-angular-stamp.mjs +169 -0
  55. package/dist/fesm2022/embedpdf-angular-stamp.mjs.map +1 -0
  56. package/dist/fesm2022/embedpdf-angular-testing.mjs +89 -0
  57. package/dist/fesm2022/embedpdf-angular-testing.mjs.map +1 -0
  58. package/dist/fesm2022/embedpdf-angular-toolbar.mjs +1123 -0
  59. package/dist/fesm2022/embedpdf-angular-toolbar.mjs.map +1 -0
  60. package/dist/fesm2022/embedpdf-angular-view-manager.mjs +69 -0
  61. package/dist/fesm2022/embedpdf-angular-view-manager.mjs.map +1 -0
  62. package/dist/fesm2022/embedpdf-angular.mjs +25 -3
  63. package/dist/fesm2022/embedpdf-angular.mjs.map +1 -1
  64. package/dist/form/index.d.ts +79 -0
  65. package/dist/history/index.d.ts +28 -0
  66. package/dist/i18n/index.d.ts +52 -0
  67. package/dist/index.d.ts +22 -0
  68. package/dist/interaction/index.d.ts +58 -0
  69. package/dist/link/index.d.ts +77 -0
  70. package/dist/measurement/index.d.ts +46 -0
  71. package/dist/metadata/index.d.ts +51 -0
  72. package/dist/outline/index.d.ts +32 -0
  73. package/dist/page-edit/index.d.ts +27 -0
  74. package/dist/page-view/index.d.ts +94 -0
  75. package/dist/redaction/index.d.ts +38 -0
  76. package/dist/render/index.d.ts +45 -6
  77. package/dist/runtime/index.d.ts +579 -199
  78. package/dist/search/index.d.ts +71 -0
  79. package/dist/selection/index.d.ts +141 -0
  80. package/dist/shell/index.d.ts +88 -0
  81. package/dist/signature/index.d.ts +45 -0
  82. package/dist/stage/index.d.ts +242 -141
  83. package/dist/stamp/index.d.ts +58 -0
  84. package/dist/testing/index.d.ts +53 -0
  85. package/dist/toolbar/index.d.ts +257 -0
  86. package/dist/view-manager/index.d.ts +23 -0
  87. package/package.json +145 -14
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # @embedpdf/angular
2
+
3
+ The Angular adapter for EmbedPDF's headless viewer: providers to set it up, one service per
4
+ plugin, and standalone components for the pages and the layers on them. Zoneless and `OnPush`
5
+ throughout, built on signals, server-rendering safe (the engine only starts in the browser).
6
+
7
+ ```ts
8
+ @Component({
9
+ selector: 'app-contract',
10
+ imports: [
11
+ EpdfDocumentGate,
12
+ EpdfStage,
13
+ EpdfPageTemplate,
14
+ EpdfRenderLayer,
15
+ PercentPipe,
16
+ ],
17
+ providers: [
18
+ provideEmbedPdf(
19
+ {
20
+ engine: () => localEngine(),
21
+ initialDocuments: [{ source: { kind: 'url', url: '/c.pdf' } }],
22
+ },
23
+ withStage(),
24
+ withRender(),
25
+ ),
26
+ ],
27
+ template: `
28
+ <section *epdfDocumentGate="let document; fallback: opening">
29
+ <button (click)="stage.zoomIn()">+</button>
30
+ {{ stage.zoomLevel() | percent }}
31
+ <epdf-stage #stage="epdfStage" style="height: 600px">
32
+ <ng-template epdfPage><epdf-render-layer /></ng-template>
33
+ </epdf-stage>
34
+ </section>
35
+ <ng-template #opening>Opening…</ng-template>
36
+ `,
37
+ })
38
+ export class Contract {
39
+ private readonly documents = inject(EpdfDocuments); // the component that provides can inject
40
+ }
41
+ ```
42
+
43
+ The docs: [embedpdf.com/docs/headless/angular](https://www.embedpdf.com/docs/headless/angular).
44
+
45
+ ## Entry points
46
+
47
+ One secondary entry point per feature, so a bundle carries only the features it imports.
48
+
49
+ | Entry point | What it has |
50
+ | :------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `@embedpdf/angular/runtime` | `provideEmbedPdf()`, `EpdfViewer`, `EpdfDocuments`, `EpdfDocument`, `[epdfDocumentScope]`, `*epdfDocumentGate`, the numbered errors, and what features build on: `pluginService()`, `CapabilityBinding`, `injectCapability()`, `EPDF_PAGE` / `injectPage()`, `paintsPagePart()` / `injectPaintedParts()`, `epdfTheme()` |
52
+ | `@embedpdf/angular/stage` | `withStage()`, `<epdf-stage>`, `<ng-template epdfPage>` / `epdfPageChrome`, `<epdf-scrollbar>` |
53
+ | `@embedpdf/angular/render` | `withRender()`, `EpdfRender`, `<epdf-render-layer>` |
54
+ | `@embedpdf/angular/interaction` | `withInteraction()`, `withFeedback()`, `EpdfInteraction` (with `overrideCursor()`), `<epdf-page-pointer-source>` |
55
+ | `@embedpdf/angular/page-view` | `<epdf-page-view [page] [width]>`, one page without a Stage, with `epdfFallback` / `epdfPageChrome` templates |
56
+ | `@embedpdf/angular/selection` | `withSelection()`, `EpdfSelection`, `<epdf-selection-layer>`, `<epdf-selection-menu>`, `<epdf-selection-handles>`, `<epdf-selection-clipboard>`, `copySelection()` |
57
+ | `@embedpdf/angular/search` | `withSearch()`, `EpdfSearch` (with `hits()` and `hitsOn(page)`), `<epdf-search-layer (hitClick)>` |
58
+ | `@embedpdf/angular/link` | `withLink()`, `EpdfLink`, `<epdf-link-layer>` with `<ng-template epdfLink let-link let-native="native">` |
59
+ | `@embedpdf/angular/testing` | `EpdfStageHarness`, `EpdfSearchLayerHarness`: test harnesses on `@angular/cdk/testing` |
60
+ | `@embedpdf/angular/anchored` | `<epdf-anchored>`, and `EPDF_PROJECTOR`, the binding a page surface provides for it |
61
+ | `@embedpdf/angular/actions` | `withActions()`, `withActionsUi(handlers)`, `EpdfActions` |
62
+ | `@embedpdf/angular/metadata` | `withMetadata()`, `EpdfMetadata` (`fields()`, and your own fields as `custom.fields()`) |
63
+ | `@embedpdf/angular/page-edit` | `withPageEdit()`, `EpdfPageEdit` |
64
+ | `@embedpdf/angular/view-manager` | `withViewManager()`, `EpdfViewManager` (`panes()`, `focusedPaneId()`) |
65
+ | `@embedpdf/angular/commands` | `withCommands()`, `withCommandShortcuts()`, `standardCommands`, `EpdfCommands` (with `commandOf(id)`), `[epdfCommand]` on your own buttons, `[epdfCommandShortcuts]` for the keys inside one element |
66
+ | `@embedpdf/angular/toolbar` | `<epdf-toolbar [bar]>`, its parts as templates (`epdfToolbarCommand`, `epdfToolbarCustom`, `epdfToolbarCollapsed`, `epdfToolbarGroupTrigger`, `epdfToolbarSeparator`, `epdfToolbarOverflowTrigger`, `epdfToolbarOverflowMenu`), and the bar's vocabulary (`group()`, `item()`, `custom()`, …) |
67
+ | `@embedpdf/angular/shell` | `withShell()`, `EpdfShell` (with `surface(id)`: `isOpen()`, `props()`, `open()`, `close()`, `toggle()`), `epdfPanelToggle` with `epdfPanelToggleExclusive` |
68
+ | `@embedpdf/angular/i18n` | `withI18n()`, `EpdfI18n` (with `translator()`), the `epdfT` pipe (`EpdfTPipe`) |
69
+ | `@embedpdf/angular/annotation` | `withAnnotation()`, `withFilePicker()`, `EpdfAnnotation` (with `watch(filter)`, `anchorOf(ref)`, `selection.properties()`, `tools.defaultsOf(id)`), `EpdfComments` (`threads()`, `threadOf(ref)`), `<epdf-annotation-layer>` with `<ng-template [epdfAnnotation]>` / `epdfHandle` / `epdfRotationHandle`, `[epdfRichTextEditor]`, `<epdf-annotation-menu>`, `<epdf-annotation-draft-menu>`, `<epdf-annotation-rotation-badge>` |
70
+ | `@embedpdf/angular/stamp` | `withStamp()`, `EpdfStamp` (with `libraries()`, `assetsOf(filter)`, `previewUrlOf(id)`), `persistStampLibraries()` / `restoreStampLibraries()` over the service, `indexedDbByteStore()` |
71
+ | `@embedpdf/angular/measurement` | `withMeasurement()`, `EpdfMeasurement` (with `scaleOf(page)` and `readoutOf(ref)`) |
72
+ | `@embedpdf/angular/redaction` | `withRedaction()`, `EpdfRedaction` (with `pending()` and `pendingOn(page)`) |
73
+ | `@embedpdf/angular/form` | `withForm()`, `EpdfForm` (with `valueOf(ref)`, and `controlOf(ref)`: a field as a Reactive Forms `FormControl`, which needs `@angular/forms`), `<epdf-form-layer>` |
74
+ | `@embedpdf/angular/signature` | `withSignature()`, `EpdfSignature` (with `signerRows()`), and the signers (`webCryptoSigner`, `remoteSigner`, `personalSigner`, `createTestSigner`) |
75
+
76
+ `scripts/check-parity.mjs` (part of `test`) lists the React entry points this package doesn't
77
+ have yet.
78
+
79
+ ## How it fits together
80
+
81
+ - **`provideEmbedPdf(config, ...features)`** returns providers. Each injector that gets them (a
82
+ component, a route, the app) has one viewer: `EpdfKernelHost` creates the kernel in the browser
83
+ once the engine is there (a factory may return a promise), starts it, opens
84
+ `initialDocuments`, and destroys it with the injector.
85
+ - **Signals.** The kernel has one change stream; the host turns each change into a signal write
86
+ (`revision`), and every value the adapter shows is a `computed()` over it that keeps its value
87
+ while nothing it reads changed (`host.read()`, `binding.select()`).
88
+ - **Services.** A plugin's service (`inject(EpdfSearch)`) has the plugin's methods, its State
89
+ table as signals, its events as RxJS streams (`completed$`), and its settings. `pluginService()`
90
+ builds that from the plugin's declarations. A service acts on the document of the injector
91
+ that created it: `[epdfDocumentScope]` (and a Stage with a `[document]`) provide every service
92
+ again, bound to their document; elsewhere a service follows the active document.
93
+ - **The read rule, the same in every adapter.** A method named `get*`, `list*`, `is*`, `has*` or
94
+ `can*` (the capability vocabulary's reads, `docs/conventions/naming.md`) is a read: called in a
95
+ template, a `computed()` or an `effect()`, it is read again after each change of the kernel and
96
+ wakes its reader only when its answer changes (`[disabled]="!metadata.canUpdate()"`). Every
97
+ other method tracks nothing. Without a document a method refuses with `not-ready`: one that
98
+ returns a promise (`form.setValue`, `signature.placeMark`) rejects, so `.catch()` sees it, and
99
+ the others throw. Which methods return a promise comes from the plugin's token (`promises`).
100
+ - **Components** have signal inputs and outputs; the Stage is its own API, reached from around it
101
+ with `#stage="epdfStage"` or `viewChild(EpdfStage)`, and from inside it with
102
+ `inject(EpdfStageRef)`, which anywhere else drives the main view of the document in scope.
103
+ **Templates** replace React's render functions: `<ng-template epdfPage let-page>`.
104
+ - **Errors** a developer can make while setting up have a number and the fix:
105
+ `EPDF-101` (no viewer), `EPDF-102` (a plugin's service without its feature), `EPDF-103` (page UI
106
+ outside a page), `EPDF-104` (Stage UI outside a Stage), `EPDF-105` (an engine call on the
107
+ server). They link to `/docs/headless/angular/errors`.
108
+
109
+ ## Adding a plugin binding
110
+
111
+ The recipe for a plugin's Angular side, with search as the example. React's binding
112
+ (`packages/framework/react/src/<plugin>.tsx`) is the reference for behavior; the plugin's
113
+ `contract.ts` and `state.ts` are the API; the page's Angular snippets
114
+ (`docs/content/snippets/<area>/<name>.angular.ts`) are the spec for the names.
115
+
116
+ ### 1. The entry point
117
+
118
+ `search/ng-package.json` (copy `render/ng-package.json`), `search/src/public_api.ts`, an
119
+ `./search` entry in `package.json`'s `exports` (the same shape as the others), a line in
120
+ `src/public_api.ts`, and remove `search` from `PENDING` in `scripts/check-parity.mjs`. The entry
121
+ re-exports the plugin, so app code has one import for the feature:
122
+
123
+ ```ts
124
+ /** @embedpdf/angular/search: … one line per thing it has. */
125
+ export * from '@embedpdf/plugin-search';
126
+ export * from './search';
127
+ export * from './search-layer';
128
+ ```
129
+
130
+ ### 2. The service and the feature
131
+
132
+ ```ts
133
+ @Injectable({ providedIn: 'root' })
134
+ export class EpdfSearch extends pluginService({
135
+ name: 'EpdfSearch', // as errors show it
136
+ feature: 'withSearch()', // as errors show it
137
+ token: SearchToken,
138
+ state: searchState, // every field becomes a signal: search.hitCount()
139
+ methods: ['search', 'clear', 'next', 'previous', 'goToHit'], // the page's Methods table
140
+ events: ['onCompleted', 'onActiveHitChanged'], // completed$, activeHitChanged$
141
+ }) {
142
+ // What the generic part can't build, written by hand with the protected `binding`:
143
+ /** A value with arguments is a method that returns a signal. */
144
+ hitsOn(page: PageRef | (() => PageRef)): Signal<readonly SearchHit[]> {
145
+ const pageOf = typeof page === 'function' ? page : () => page;
146
+ return this.binding.select(
147
+ (search) => search.listHitsOn(pageOf()),
148
+ NO_HITS,
149
+ );
150
+ }
151
+ }
152
+
153
+ export function withSearch(options?: SearchConfig): EmbedPdfFeature {
154
+ return { plugins: [searchPlugin(options)], services: [EpdfSearch] };
155
+ }
156
+ ```
157
+
158
+ - `settings()`, `getSettings()`, `updateSettings()`, `resetSettings()` and `settingsChanged$`
159
+ come by themselves when the plugin declares settings; they work before any document opens.
160
+ Don't list `getSettings` in `methods`.
161
+ - Methods are looked up when called, so a method kept in a field acts on the document current
162
+ then; without a document a document plugin's method refuses with `not-ready`.
163
+ - A check (`can…()`, `is…()`, `has…()`) is state: read in a template, a `computed()` or an
164
+ `effect()`, it is read again after each change of the kernel and wakes its reader only when
165
+ its answer changes, so `[disabled]="!metadata.canUpdate()"` follows the document. Other methods
166
+ track nothing.
167
+ - `providedIn: 'root'` is only the fallback that turns a missing feature into EPDF-102; the
168
+ feature's `services` provides it next to the viewer and under every document scope.
169
+ - A namespace (`metadata.custom`, `annotation.selection`) is an object of its own on the service,
170
+ built the same way (`this.binding.namespaceMethod('custom', 'update')`, `this.binding.select(…)`,
171
+ `this.binding.stream(…)`; `metadata.ts` has one). A namespace is
172
+ never itself a signal, and a State value never shares a name with the service or a namespace
173
+ (`EpdfMetadata` has `fields()` and `custom.fields()`).
174
+ - A helper that needs the caller's lifetime (`interaction.overrideCursor(() => spec)`) creates
175
+ its `effect()` in the caller's injection context; say so in its doc comment.
176
+ - App-wide setup that React does with a hook (`useCommandShortcuts()`) is a feature:
177
+ `withCommandShortcuts()` returns `{ plugins: [], setup: () => { … } }`; `setup` runs once in the
178
+ providers' injection context, after the viewer is created.
179
+
180
+ ### 3. A component, with inputs and outputs
181
+
182
+ ```ts
183
+ @Component({
184
+ selector: 'epdf-search-layer',
185
+ changeDetection: ChangeDetectionStrategy.OnPush,
186
+ template: `@for (hit of hits(); track hit.index) { … (click)="hitClick.emit(hit)" }`,
187
+ })
188
+ export class EpdfSearchLayer {
189
+ readonly hitClick = output<SearchHit>(); // (hitClick)
190
+ …
191
+ }
192
+ ```
193
+
194
+ React's props become `input()`s (`<RenderLayer tiles>` → `[tiles]`), `onX` callbacks `output()`s
195
+ named `x` (`onHitClick` → `(hitClick)`), and a controlled value (`zoom` + `onZoomChange`) an input and an output named
196
+ `zoom` / `zoomChange`, so `[(zoom)]` works with a signal. Every component is standalone and
197
+ `OnPush`, and touches the DOM only in the browser (`isPlatformBrowser(inject(PLATFORM_ID))`).
198
+
199
+ ### 4. A layer drawn on each page
200
+
201
+ A layer goes inside `<ng-template epdfPage>` and reads its page with `injectPage()`. It resolves
202
+ its plugin for the page's own document:
203
+
204
+ ```ts
205
+ export class EpdfSearchLayer {
206
+ private readonly page = injectPage('<epdf-search-layer>'); // EPDF-103 outside a page
207
+ private readonly search = new CapabilityBinding(
208
+ injectKernelHost('<epdf-search-layer>'),
209
+ () => SearchToken,
210
+ () => this.page.documentId,
211
+ );
212
+ protected readonly hits = this.search.select(
213
+ (search) => search.listHitsOn(this.page.ref),
214
+ NO_HITS,
215
+ );
216
+ // page.transform() places page coordinates on screen: page.transform().pageToViewRect(rect)
217
+ }
218
+ ```
219
+
220
+ `page.transform()`, `page.frame()` and `page.pageIndex()` are signals that move with the
221
+ camera; the context itself never changes. A layer that paints a part of the page itself (the
222
+ annotations, the form fields) says so with `paintsPagePart()`, and the render layer leaves that
223
+ part out of the page's picture.
224
+
225
+ ### 5. Custom drawing with a template
226
+
227
+ React's render prop (`renderLink={(link) => …}`) becomes a marker directive on an
228
+ `<ng-template>`, typed with a context guard, that the component reads with `contentChild()`:
229
+
230
+ ```ts
231
+ export interface EpdfLinkTemplateContext {
232
+ $implicit: LinkInfo;
233
+ }
234
+
235
+ @Directive({ selector: 'ng-template[epdfLink]' })
236
+ export class EpdfLinkTemplate {
237
+ readonly template = inject<TemplateRef<EpdfLinkTemplateContext>>(TemplateRef);
238
+ static ngTemplateContextGuard(_: EpdfLinkTemplate, context: unknown): context is EpdfLinkTemplateContext {
239
+ return true;
240
+ }
241
+ }
242
+
243
+ // In the layer: the app's template when there is one, the built-in look otherwise.
244
+ private readonly custom = contentChild(EpdfLinkTemplate);
245
+ // <ng-container [ngTemplateOutlet]="custom()?.template ?? builtIn" [ngTemplateOutletContext]="{ $implicit: link }" />
246
+ ```
247
+
248
+ The app writes `<epdf-link-layer><ng-template epdfLink let-link>…</ng-template></epdf-link-layer>`.
249
+ A name that would clash gets a prefix (the toolbar's command template is `epdfToolbarCommand`,
250
+ because `epdfCommand` is the directive on your own buttons). Floating UI next to something on a
251
+ page uses `<epdf-anchored [anchor]>` inside the Stage.
252
+
253
+ A template can't draw "nothing, use the default" the way a React render prop returning
254
+ `undefined` can: a template you give draws every part of its kind. The toolbar's items of your own
255
+ are templates by name (`epdfToolbarCustom="page-number"`), and an item with no template of its name
256
+ is a native `<slot name>` socket with its command's button inside.
257
+
258
+ Two rules for templates inside a page template (`<ng-template epdfPage>`), which Angular creates
259
+ once per page: an entry the template registers (an annotation renderer) is interned, so the same
260
+ rule gives the same entry on every page and its behaviors register once; and a marker directive's
261
+ inputs are never `input.required`, because a template inside `@if` enters `contentChildren` before
262
+ its inputs are set.
263
+
264
+ ### 6. A test
265
+
266
+ Tests run in vitest with happy-dom, compiling components just in time with `TestBed`, zoneless
267
+ (`vitest.config.mts` runs Angular's JIT transform on this package's files, so signal inputs and
268
+ queries work). `test/fixtures.ts` has a fake engine, a small document plugin, and `viewerHost()`
269
+ / `mount()`:
270
+
271
+ ```ts
272
+ it('reads the hit count, and empty without a document', async () => {
273
+ const fixture = await mount(
274
+ viewerHost({ template: '<epdf-search-layer />', imports: [EpdfSearchLayer], features: [withSearch()] }),
275
+ );
276
+ const search = fixture.debugElement.injector.get(EpdfSearch);
277
+ expect(search.hitCount()).toBe(0);
278
+ await kernelOf(fixture).documents.open(bytesInput('a'));
279
+ …
280
+ });
281
+ ```
282
+
283
+ Port the React tests that test the binding (not the plugin): empty state without a document,
284
+ signals that change only when their value does, the document scope, streams that follow the
285
+ document, outputs, the setup errors.
286
+
287
+ ### 7. Checks
288
+
289
+ ```sh
290
+ pnpm --filter @embedpdf/angular test # parity + vitest
291
+ pnpm --filter @embedpdf/angular typecheck # ngc strictTemplates + the tests with tsc
292
+ pnpm --filter @embedpdf/angular build # ng-packagr, then the built declarations as an app reads them
293
+ pnpm --filter @embedpdf/docs-content snippets # your pages, per framework
294
+ pnpm --filter @embedpdf/docs-content samples <area> # your examples
295
+ ```
296
+
297
+ The build ends with `scripts/check-declarations.mjs`: it imports every entry point from `dist`
298
+ with `skipLibCheck: false` and fails on any error there. The usual one is an import that doesn't
299
+ resolve, which an app (`skipLibCheck: true`) never reports: the service whose type needed it
300
+ quietly loses its members. It happens when a declaration needs a type its source file doesn't
301
+ import, and the compiler names it through another module: the entry point's own public name, or
302
+ the package that declares it, which may not be a dependency of this one. Import that type into
303
+ the file (`services.ts` imports `PluginServiceClass`), or give the member an explicit type.
304
+
305
+ An Angular example (`docs/content/samples/<area>/<name>.angular.ts`) exports its root as `App`
306
+ with `selector: 'demo-root'`, uses the React example's stylesheet with
307
+ `styleUrl: './<name>.css'` and `encapsulation: ViewEncapsulation.None`, and exports every
308
+ component in the file: `ngc` stops reporting template errors anywhere once a file has an
309
+ unexported component.
package/dist/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # @embedpdf/angular
2
+
3
+ The Angular adapter for EmbedPDF's headless viewer: providers to set it up, one service per
4
+ plugin, and standalone components for the pages and the layers on them. Zoneless and `OnPush`
5
+ throughout, built on signals, server-rendering safe (the engine only starts in the browser).
6
+
7
+ ```ts
8
+ @Component({
9
+ selector: 'app-contract',
10
+ imports: [
11
+ EpdfDocumentGate,
12
+ EpdfStage,
13
+ EpdfPageTemplate,
14
+ EpdfRenderLayer,
15
+ PercentPipe,
16
+ ],
17
+ providers: [
18
+ provideEmbedPdf(
19
+ {
20
+ engine: () => localEngine(),
21
+ initialDocuments: [{ source: { kind: 'url', url: '/c.pdf' } }],
22
+ },
23
+ withStage(),
24
+ withRender(),
25
+ ),
26
+ ],
27
+ template: `
28
+ <section *epdfDocumentGate="let document; fallback: opening">
29
+ <button (click)="stage.zoomIn()">+</button>
30
+ {{ stage.zoomLevel() | percent }}
31
+ <epdf-stage #stage="epdfStage" style="height: 600px">
32
+ <ng-template epdfPage><epdf-render-layer /></ng-template>
33
+ </epdf-stage>
34
+ </section>
35
+ <ng-template #opening>Opening…</ng-template>
36
+ `,
37
+ })
38
+ export class Contract {
39
+ private readonly documents = inject(EpdfDocuments); // the component that provides can inject
40
+ }
41
+ ```
42
+
43
+ The docs: [embedpdf.com/docs/headless/angular](https://www.embedpdf.com/docs/headless/angular).
44
+
45
+ ## Entry points
46
+
47
+ One secondary entry point per feature, so a bundle carries only the features it imports.
48
+
49
+ | Entry point | What it has |
50
+ | :------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `@embedpdf/angular/runtime` | `provideEmbedPdf()`, `EpdfViewer`, `EpdfDocuments`, `EpdfDocument`, `[epdfDocumentScope]`, `*epdfDocumentGate`, the numbered errors, and what features build on: `pluginService()`, `CapabilityBinding`, `injectCapability()`, `EPDF_PAGE` / `injectPage()`, `paintsPagePart()` / `injectPaintedParts()`, `epdfTheme()` |
52
+ | `@embedpdf/angular/stage` | `withStage()`, `<epdf-stage>`, `<ng-template epdfPage>` / `epdfPageChrome`, `<epdf-scrollbar>` |
53
+ | `@embedpdf/angular/render` | `withRender()`, `EpdfRender`, `<epdf-render-layer>` |
54
+ | `@embedpdf/angular/interaction` | `withInteraction()`, `withFeedback()`, `EpdfInteraction` (with `overrideCursor()`), `<epdf-page-pointer-source>` |
55
+ | `@embedpdf/angular/page-view` | `<epdf-page-view [page] [width]>`, one page without a Stage, with `epdfFallback` / `epdfPageChrome` templates |
56
+ | `@embedpdf/angular/selection` | `withSelection()`, `EpdfSelection`, `<epdf-selection-layer>`, `<epdf-selection-menu>`, `<epdf-selection-handles>`, `<epdf-selection-clipboard>`, `copySelection()` |
57
+ | `@embedpdf/angular/search` | `withSearch()`, `EpdfSearch` (with `hits()` and `hitsOn(page)`), `<epdf-search-layer (hitClick)>` |
58
+ | `@embedpdf/angular/link` | `withLink()`, `EpdfLink`, `<epdf-link-layer>` with `<ng-template epdfLink let-link let-native="native">` |
59
+ | `@embedpdf/angular/testing` | `EpdfStageHarness`, `EpdfSearchLayerHarness`: test harnesses on `@angular/cdk/testing` |
60
+ | `@embedpdf/angular/anchored` | `<epdf-anchored>`, and `EPDF_PROJECTOR`, the binding a page surface provides for it |
61
+ | `@embedpdf/angular/actions` | `withActions()`, `withActionsUi(handlers)`, `EpdfActions` |
62
+ | `@embedpdf/angular/metadata` | `withMetadata()`, `EpdfMetadata` (`fields()`, and your own fields as `custom.fields()`) |
63
+ | `@embedpdf/angular/page-edit` | `withPageEdit()`, `EpdfPageEdit` |
64
+ | `@embedpdf/angular/view-manager` | `withViewManager()`, `EpdfViewManager` (`panes()`, `focusedPaneId()`) |
65
+ | `@embedpdf/angular/commands` | `withCommands()`, `withCommandShortcuts()`, `standardCommands`, `EpdfCommands` (with `commandOf(id)`), `[epdfCommand]` on your own buttons, `[epdfCommandShortcuts]` for the keys inside one element |
66
+ | `@embedpdf/angular/toolbar` | `<epdf-toolbar [bar]>`, its parts as templates (`epdfToolbarCommand`, `epdfToolbarCustom`, `epdfToolbarCollapsed`, `epdfToolbarGroupTrigger`, `epdfToolbarSeparator`, `epdfToolbarOverflowTrigger`, `epdfToolbarOverflowMenu`), and the bar's vocabulary (`group()`, `item()`, `custom()`, …) |
67
+ | `@embedpdf/angular/shell` | `withShell()`, `EpdfShell` (with `surface(id)`: `isOpen()`, `props()`, `open()`, `close()`, `toggle()`), `epdfPanelToggle` with `epdfPanelToggleExclusive` |
68
+ | `@embedpdf/angular/i18n` | `withI18n()`, `EpdfI18n` (with `translator()`), the `epdfT` pipe (`EpdfTPipe`) |
69
+ | `@embedpdf/angular/annotation` | `withAnnotation()`, `withFilePicker()`, `EpdfAnnotation` (with `watch(filter)`, `anchorOf(ref)`, `selection.properties()`, `tools.defaultsOf(id)`), `EpdfComments` (`threads()`, `threadOf(ref)`), `<epdf-annotation-layer>` with `<ng-template [epdfAnnotation]>` / `epdfHandle` / `epdfRotationHandle`, `[epdfRichTextEditor]`, `<epdf-annotation-menu>`, `<epdf-annotation-draft-menu>`, `<epdf-annotation-rotation-badge>` |
70
+ | `@embedpdf/angular/stamp` | `withStamp()`, `EpdfStamp` (with `libraries()`, `assetsOf(filter)`, `previewUrlOf(id)`), `persistStampLibraries()` / `restoreStampLibraries()` over the service, `indexedDbByteStore()` |
71
+ | `@embedpdf/angular/measurement` | `withMeasurement()`, `EpdfMeasurement` (with `scaleOf(page)` and `readoutOf(ref)`) |
72
+ | `@embedpdf/angular/redaction` | `withRedaction()`, `EpdfRedaction` (with `pending()` and `pendingOn(page)`) |
73
+ | `@embedpdf/angular/form` | `withForm()`, `EpdfForm` (with `valueOf(ref)`, and `controlOf(ref)`: a field as a Reactive Forms `FormControl`, which needs `@angular/forms`), `<epdf-form-layer>` |
74
+ | `@embedpdf/angular/signature` | `withSignature()`, `EpdfSignature` (with `signerRows()`), and the signers (`webCryptoSigner`, `remoteSigner`, `personalSigner`, `createTestSigner`) |
75
+
76
+ `scripts/check-parity.mjs` (part of `test`) lists the React entry points this package doesn't
77
+ have yet.
78
+
79
+ ## How it fits together
80
+
81
+ - **`provideEmbedPdf(config, ...features)`** returns providers. Each injector that gets them (a
82
+ component, a route, the app) has one viewer: `EpdfKernelHost` creates the kernel in the browser
83
+ once the engine is there (a factory may return a promise), starts it, opens
84
+ `initialDocuments`, and destroys it with the injector.
85
+ - **Signals.** The kernel has one change stream; the host turns each change into a signal write
86
+ (`revision`), and every value the adapter shows is a `computed()` over it that keeps its value
87
+ while nothing it reads changed (`host.read()`, `binding.select()`).
88
+ - **Services.** A plugin's service (`inject(EpdfSearch)`) has the plugin's methods, its State
89
+ table as signals, its events as RxJS streams (`completed$`), and its settings. `pluginService()`
90
+ builds that from the plugin's declarations. A service acts on the document of the injector
91
+ that created it: `[epdfDocumentScope]` (and a Stage with a `[document]`) provide every service
92
+ again, bound to their document; elsewhere a service follows the active document.
93
+ - **The read rule, the same in every adapter.** A method named `get*`, `list*`, `is*`, `has*` or
94
+ `can*` (the capability vocabulary's reads, `docs/conventions/naming.md`) is a read: called in a
95
+ template, a `computed()` or an `effect()`, it is read again after each change of the kernel and
96
+ wakes its reader only when its answer changes (`[disabled]="!metadata.canUpdate()"`). Every
97
+ other method tracks nothing. Without a document a method refuses with `not-ready`: one that
98
+ returns a promise (`form.setValue`, `signature.placeMark`) rejects, so `.catch()` sees it, and
99
+ the others throw. Which methods return a promise comes from the plugin's token (`promises`).
100
+ - **Components** have signal inputs and outputs; the Stage is its own API, reached from around it
101
+ with `#stage="epdfStage"` or `viewChild(EpdfStage)`, and from inside it with
102
+ `inject(EpdfStageRef)`, which anywhere else drives the main view of the document in scope.
103
+ **Templates** replace React's render functions: `<ng-template epdfPage let-page>`.
104
+ - **Errors** a developer can make while setting up have a number and the fix:
105
+ `EPDF-101` (no viewer), `EPDF-102` (a plugin's service without its feature), `EPDF-103` (page UI
106
+ outside a page), `EPDF-104` (Stage UI outside a Stage), `EPDF-105` (an engine call on the
107
+ server). They link to `/docs/headless/angular/errors`.
108
+
109
+ ## Adding a plugin binding
110
+
111
+ The recipe for a plugin's Angular side, with search as the example. React's binding
112
+ (`packages/framework/react/src/<plugin>.tsx`) is the reference for behavior; the plugin's
113
+ `contract.ts` and `state.ts` are the API; the page's Angular snippets
114
+ (`docs/content/snippets/<area>/<name>.angular.ts`) are the spec for the names.
115
+
116
+ ### 1. The entry point
117
+
118
+ `search/ng-package.json` (copy `render/ng-package.json`), `search/src/public_api.ts`, an
119
+ `./search` entry in `package.json`'s `exports` (the same shape as the others), a line in
120
+ `src/public_api.ts`, and remove `search` from `PENDING` in `scripts/check-parity.mjs`. The entry
121
+ re-exports the plugin, so app code has one import for the feature:
122
+
123
+ ```ts
124
+ /** @embedpdf/angular/search: … one line per thing it has. */
125
+ export * from '@embedpdf/plugin-search';
126
+ export * from './search';
127
+ export * from './search-layer';
128
+ ```
129
+
130
+ ### 2. The service and the feature
131
+
132
+ ```ts
133
+ @Injectable({ providedIn: 'root' })
134
+ export class EpdfSearch extends pluginService({
135
+ name: 'EpdfSearch', // as errors show it
136
+ feature: 'withSearch()', // as errors show it
137
+ token: SearchToken,
138
+ state: searchState, // every field becomes a signal: search.hitCount()
139
+ methods: ['search', 'clear', 'next', 'previous', 'goToHit'], // the page's Methods table
140
+ events: ['onCompleted', 'onActiveHitChanged'], // completed$, activeHitChanged$
141
+ }) {
142
+ // What the generic part can't build, written by hand with the protected `binding`:
143
+ /** A value with arguments is a method that returns a signal. */
144
+ hitsOn(page: PageRef | (() => PageRef)): Signal<readonly SearchHit[]> {
145
+ const pageOf = typeof page === 'function' ? page : () => page;
146
+ return this.binding.select(
147
+ (search) => search.listHitsOn(pageOf()),
148
+ NO_HITS,
149
+ );
150
+ }
151
+ }
152
+
153
+ export function withSearch(options?: SearchConfig): EmbedPdfFeature {
154
+ return { plugins: [searchPlugin(options)], services: [EpdfSearch] };
155
+ }
156
+ ```
157
+
158
+ - `settings()`, `getSettings()`, `updateSettings()`, `resetSettings()` and `settingsChanged$`
159
+ come by themselves when the plugin declares settings; they work before any document opens.
160
+ Don't list `getSettings` in `methods`.
161
+ - Methods are looked up when called, so a method kept in a field acts on the document current
162
+ then; without a document a document plugin's method refuses with `not-ready`.
163
+ - A check (`can…()`, `is…()`, `has…()`) is state: read in a template, a `computed()` or an
164
+ `effect()`, it is read again after each change of the kernel and wakes its reader only when
165
+ its answer changes, so `[disabled]="!metadata.canUpdate()"` follows the document. Other methods
166
+ track nothing.
167
+ - `providedIn: 'root'` is only the fallback that turns a missing feature into EPDF-102; the
168
+ feature's `services` provides it next to the viewer and under every document scope.
169
+ - A namespace (`metadata.custom`, `annotation.selection`) is an object of its own on the service,
170
+ built the same way (`this.binding.namespaceMethod('custom', 'update')`, `this.binding.select(…)`,
171
+ `this.binding.stream(…)`; `metadata.ts` has one). A namespace is
172
+ never itself a signal, and a State value never shares a name with the service or a namespace
173
+ (`EpdfMetadata` has `fields()` and `custom.fields()`).
174
+ - A helper that needs the caller's lifetime (`interaction.overrideCursor(() => spec)`) creates
175
+ its `effect()` in the caller's injection context; say so in its doc comment.
176
+ - App-wide setup that React does with a hook (`useCommandShortcuts()`) is a feature:
177
+ `withCommandShortcuts()` returns `{ plugins: [], setup: () => { … } }`; `setup` runs once in the
178
+ providers' injection context, after the viewer is created.
179
+
180
+ ### 3. A component, with inputs and outputs
181
+
182
+ ```ts
183
+ @Component({
184
+ selector: 'epdf-search-layer',
185
+ changeDetection: ChangeDetectionStrategy.OnPush,
186
+ template: `@for (hit of hits(); track hit.index) { … (click)="hitClick.emit(hit)" }`,
187
+ })
188
+ export class EpdfSearchLayer {
189
+ readonly hitClick = output<SearchHit>(); // (hitClick)
190
+ …
191
+ }
192
+ ```
193
+
194
+ React's props become `input()`s (`<RenderLayer tiles>` → `[tiles]`), `onX` callbacks `output()`s
195
+ named `x` (`onHitClick` → `(hitClick)`), and a controlled value (`zoom` + `onZoomChange`) an input and an output named
196
+ `zoom` / `zoomChange`, so `[(zoom)]` works with a signal. Every component is standalone and
197
+ `OnPush`, and touches the DOM only in the browser (`isPlatformBrowser(inject(PLATFORM_ID))`).
198
+
199
+ ### 4. A layer drawn on each page
200
+
201
+ A layer goes inside `<ng-template epdfPage>` and reads its page with `injectPage()`. It resolves
202
+ its plugin for the page's own document:
203
+
204
+ ```ts
205
+ export class EpdfSearchLayer {
206
+ private readonly page = injectPage('<epdf-search-layer>'); // EPDF-103 outside a page
207
+ private readonly search = new CapabilityBinding(
208
+ injectKernelHost('<epdf-search-layer>'),
209
+ () => SearchToken,
210
+ () => this.page.documentId,
211
+ );
212
+ protected readonly hits = this.search.select(
213
+ (search) => search.listHitsOn(this.page.ref),
214
+ NO_HITS,
215
+ );
216
+ // page.transform() places page coordinates on screen: page.transform().pageToViewRect(rect)
217
+ }
218
+ ```
219
+
220
+ `page.transform()`, `page.frame()` and `page.pageIndex()` are signals that move with the
221
+ camera; the context itself never changes. A layer that paints a part of the page itself (the
222
+ annotations, the form fields) says so with `paintsPagePart()`, and the render layer leaves that
223
+ part out of the page's picture.
224
+
225
+ ### 5. Custom drawing with a template
226
+
227
+ React's render prop (`renderLink={(link) => …}`) becomes a marker directive on an
228
+ `<ng-template>`, typed with a context guard, that the component reads with `contentChild()`:
229
+
230
+ ```ts
231
+ export interface EpdfLinkTemplateContext {
232
+ $implicit: LinkInfo;
233
+ }
234
+
235
+ @Directive({ selector: 'ng-template[epdfLink]' })
236
+ export class EpdfLinkTemplate {
237
+ readonly template = inject<TemplateRef<EpdfLinkTemplateContext>>(TemplateRef);
238
+ static ngTemplateContextGuard(_: EpdfLinkTemplate, context: unknown): context is EpdfLinkTemplateContext {
239
+ return true;
240
+ }
241
+ }
242
+
243
+ // In the layer: the app's template when there is one, the built-in look otherwise.
244
+ private readonly custom = contentChild(EpdfLinkTemplate);
245
+ // <ng-container [ngTemplateOutlet]="custom()?.template ?? builtIn" [ngTemplateOutletContext]="{ $implicit: link }" />
246
+ ```
247
+
248
+ The app writes `<epdf-link-layer><ng-template epdfLink let-link>…</ng-template></epdf-link-layer>`.
249
+ A name that would clash gets a prefix (the toolbar's command template is `epdfToolbarCommand`,
250
+ because `epdfCommand` is the directive on your own buttons). Floating UI next to something on a
251
+ page uses `<epdf-anchored [anchor]>` inside the Stage.
252
+
253
+ A template can't draw "nothing, use the default" the way a React render prop returning
254
+ `undefined` can: a template you give draws every part of its kind. The toolbar's items of your own
255
+ are templates by name (`epdfToolbarCustom="page-number"`), and an item with no template of its name
256
+ is a native `<slot name>` socket with its command's button inside.
257
+
258
+ Two rules for templates inside a page template (`<ng-template epdfPage>`), which Angular creates
259
+ once per page: an entry the template registers (an annotation renderer) is interned, so the same
260
+ rule gives the same entry on every page and its behaviors register once; and a marker directive's
261
+ inputs are never `input.required`, because a template inside `@if` enters `contentChildren` before
262
+ its inputs are set.
263
+
264
+ ### 6. A test
265
+
266
+ Tests run in vitest with happy-dom, compiling components just in time with `TestBed`, zoneless
267
+ (`vitest.config.mts` runs Angular's JIT transform on this package's files, so signal inputs and
268
+ queries work). `test/fixtures.ts` has a fake engine, a small document plugin, and `viewerHost()`
269
+ / `mount()`:
270
+
271
+ ```ts
272
+ it('reads the hit count, and empty without a document', async () => {
273
+ const fixture = await mount(
274
+ viewerHost({ template: '<epdf-search-layer />', imports: [EpdfSearchLayer], features: [withSearch()] }),
275
+ );
276
+ const search = fixture.debugElement.injector.get(EpdfSearch);
277
+ expect(search.hitCount()).toBe(0);
278
+ await kernelOf(fixture).documents.open(bytesInput('a'));
279
+ …
280
+ });
281
+ ```
282
+
283
+ Port the React tests that test the binding (not the plugin): empty state without a document,
284
+ signals that change only when their value does, the document scope, streams that follow the
285
+ document, outputs, the setup errors.
286
+
287
+ ### 7. Checks
288
+
289
+ ```sh
290
+ pnpm --filter @embedpdf/angular test # parity + vitest
291
+ pnpm --filter @embedpdf/angular typecheck # ngc strictTemplates + the tests with tsc
292
+ pnpm --filter @embedpdf/angular build # ng-packagr, then the built declarations as an app reads them
293
+ pnpm --filter @embedpdf/docs-content snippets # your pages, per framework
294
+ pnpm --filter @embedpdf/docs-content samples <area> # your examples
295
+ ```
296
+
297
+ The build ends with `scripts/check-declarations.mjs`: it imports every entry point from `dist`
298
+ with `skipLibCheck: false` and fails on any error there. The usual one is an import that doesn't
299
+ resolve, which an app (`skipLibCheck: true`) never reports: the service whose type needed it
300
+ quietly loses its members. It happens when a declaration needs a type its source file doesn't
301
+ import, and the compiler names it through another module: the entry point's own public name, or
302
+ the package that declares it, which may not be a dependency of this one. Import that type into
303
+ the file (`services.ts` imports `PluginServiceClass`), or give the member an explicit type.
304
+
305
+ An Angular example (`docs/content/samples/<area>/<name>.angular.ts`) exports its root as `App`
306
+ with `selector: 'demo-root'`, uses the React example's stylesheet with
307
+ `styleUrl: './<name>.css'` and `encapsulation: ViewEncapsulation.None`, and exports every
308
+ component in the file: `ngc` stops reporting template errors anywhere once a file has an
309
+ unexported component.