@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
|
@@ -0,0 +1,1933 @@
|
|
|
1
|
+
import { AnnotationToken, annotationState, annotationPlugin, ANNOTATION_DEFAULTS } from '@embedpdf/plugin-annotation';
|
|
2
|
+
export * from '@embedpdf/plugin-annotation';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { computed, Injectable, inject, DestroyRef, input, ChangeDetectionStrategy, Component, Directive, booleanAttribute, TemplateRef, InjectionToken, afterRenderEffect, untracked, ElementRef, Injector, contentChildren, contentChild, signal, PLATFORM_ID, effect } from '@angular/core';
|
|
5
|
+
import { EpdfPluginService, injectDocumentScope, pluginService, CapabilityBinding, injectKernelHost, devWarn, injectPage, EpdfSvgShapes, paintsPagePart, outsideStageError } from '@embedpdf/angular/runtime';
|
|
6
|
+
import { AnnotationToken as AnnotationToken$1, previewBucket } from '@embedpdf/plugin-annotation/contract/host';
|
|
7
|
+
import { enrichCommentThreads, sameAnnotationAnchor, pickRequestedFile, annotationChromePaint, chromeInPixels, sceneViewBox, svgShapesOf, ghostOpacity, rasterInFrame, createTextBoxEditorFollower, freeTextBoxOf, textBoxEditorScaleOf, ownTextBoxOf, createShownUrls, editingTextKeyOf, frameInPixels, bakedAppearanceOf, pictureOrPainting, shownAppearance, annotationDrawingOf, lookFrameOf, layerTextBoxesOf, rectInPixels, registerRendererBehaviors, loadAppearanceUrls, loadObjectUrl, sameSelectionAnchor, sameCreationDraftAnchor, sameRotationAnchor, paintDefault, paint } from '@embedpdf/web';
|
|
8
|
+
import { annotationKey } from '@embedpdf/core';
|
|
9
|
+
import { NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
|
|
10
|
+
import { scene, MITER_LIMIT, hasLiveDrawing } from '@embedpdf/core-annotation';
|
|
11
|
+
import { InteractionToken } from '@embedpdf/plugin-interaction/contract';
|
|
12
|
+
import { RenderToken } from '@embedpdf/plugin-render/contract/host';
|
|
13
|
+
import { samePagePlacement } from '@embedpdf/core-geometry';
|
|
14
|
+
import { EPDF_PROJECTOR, EpdfAnchored } from '@embedpdf/angular/anchored';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Comments and replies, as their own service: `inject(EpdfComments)`. A comments sidebar needs
|
|
18
|
+
* the threads and the verbs on them, not the rest of the annotation API, so they get a service
|
|
19
|
+
* of their own (React's `useComments()` and `useCommentThreads()`). `withAnnotation()` provides
|
|
20
|
+
* it.
|
|
21
|
+
*
|
|
22
|
+
* The threads come with where their page is shown (`pageIndex`, `pageLabel`): the join of the
|
|
23
|
+
* plugin's threads and the document's page list is `@embedpdf/web`'s, the same for every
|
|
24
|
+
* framework.
|
|
25
|
+
*/
|
|
26
|
+
const NO_THREADS = Object.freeze([]);
|
|
27
|
+
const NO_PAGES = Object.freeze([]);
|
|
28
|
+
/**
|
|
29
|
+
* The comments of the document in scope (`[epdfDocumentScope]`), else the active one: every
|
|
30
|
+
* thread as `threads()`, one as `threadOf(ref)`, the verbs (`reply()`, `setText()`,
|
|
31
|
+
* `setStatus()`, `setMarked()`, `delete()`, `deleteThread()`), the checks (`canReply(ref)`, …),
|
|
32
|
+
* which follow the document and its permissions in a template, and `threadChanged$`. Without a
|
|
33
|
+
* document the threads are empty and the verbs refuse with `not-ready`.
|
|
34
|
+
*/
|
|
35
|
+
class EpdfComments extends EpdfPluginService {
|
|
36
|
+
constructor() {
|
|
37
|
+
super({ name: 'EpdfComments', feature: 'withAnnotation()', token: AnnotationToken });
|
|
38
|
+
}
|
|
39
|
+
/** Every thread, in reading order, read once. In a template, use `threads()`. */
|
|
40
|
+
listThreads = this.binding.namespaceMethod('comments', 'listThreads');
|
|
41
|
+
/** The thread a comment is in (its first comment, a reply or a review state), read once, or null. */
|
|
42
|
+
getThread = this.binding.namespaceMethod('comments', 'getThread');
|
|
43
|
+
/** Reply to the thread's first comment, whichever comment you pass. Resolves `{ annotation }`. */
|
|
44
|
+
reply = this.binding.namespaceMethod('comments', 'reply');
|
|
45
|
+
/** Change a comment's text. */
|
|
46
|
+
setText = this.binding.namespaceMethod('comments', 'setText');
|
|
47
|
+
/** Set the user's review state on a thread, such as `'accepted'`. */
|
|
48
|
+
setStatus = this.binding.namespaceMethod('comments', 'setStatus');
|
|
49
|
+
/** Check a thread off for the user, or clear the check mark. */
|
|
50
|
+
setMarked = this.binding.namespaceMethod('comments', 'setMarked');
|
|
51
|
+
/** Delete a comment with its replies; the first comment of a thread deletes the thread. */
|
|
52
|
+
delete = this.binding.namespaceMethod('comments', 'delete');
|
|
53
|
+
/** Delete a whole thread, all of it or none. Resolves `{ deleted, failed }`. */
|
|
54
|
+
deleteThread = this.binding.namespaceMethod('comments', 'deleteThread');
|
|
55
|
+
/** Whether the user may reply to this thread. */
|
|
56
|
+
canReply = this.binding.namespaceMethod('comments', 'canReply');
|
|
57
|
+
/** Whether the user may change this comment's text. */
|
|
58
|
+
canSetText = this.binding.namespaceMethod('comments', 'canSetText');
|
|
59
|
+
/** Whether the user may set a review state on this thread. */
|
|
60
|
+
canSetStatus = this.binding.namespaceMethod('comments', 'canSetStatus');
|
|
61
|
+
/** Whether the user may check this thread off. */
|
|
62
|
+
canSetMarked = this.binding.namespaceMethod('comments', 'canSetMarked');
|
|
63
|
+
/** Whether the user may delete this comment. */
|
|
64
|
+
canDelete = this.binding.namespaceMethod('comments', 'canDelete');
|
|
65
|
+
/** Whether the user may delete every comment in this thread. */
|
|
66
|
+
canDeleteThread = this.binding.namespaceMethod('comments', 'canDeleteThread');
|
|
67
|
+
/** A thread changed through this viewer: a reply, a text, a review state, a check mark or a delete. */
|
|
68
|
+
threadChanged$ = this.binding.stream((annotation) => annotation.comments.onThreadChanged);
|
|
69
|
+
scope = injectDocumentScope();
|
|
70
|
+
rawThreads = this.binding.select((annotation) => annotation.comments.listThreads(), NO_THREADS);
|
|
71
|
+
/** The pages of this service's document: a page moved or deleted moves its threads' labels. */
|
|
72
|
+
pages = this.binding.host.read((kernel) => {
|
|
73
|
+
const id = this.scope() ?? kernel.documents.getActiveId();
|
|
74
|
+
return id ? kernel.documents.listPages(id) : NO_PAGES;
|
|
75
|
+
}, () => NO_PAGES);
|
|
76
|
+
/**
|
|
77
|
+
* Every thread in the document, in reading order (by page, then from the top), each with its
|
|
78
|
+
* page's `pageIndex` and `pageLabel`. Own, remote and reloaded changes all show. The same
|
|
79
|
+
* array until a thread or a page changes; empty without a document.
|
|
80
|
+
*/
|
|
81
|
+
threads = computed(() => enrichCommentThreads(this.rawThreads(), this.pages()), ...(ngDevMode ? [{ debugName: "threads" }] : []));
|
|
82
|
+
/**
|
|
83
|
+
* The thread a comment is in, whether you pass its first comment, a reply or a review state,
|
|
84
|
+
* with its page's label; null for null, or a comment in no thread. Pass the ref, or a function
|
|
85
|
+
* that reads it (`() => this.note().ref`).
|
|
86
|
+
*/
|
|
87
|
+
threadOf(ref) {
|
|
88
|
+
const refOf = typeof ref === 'function' ? ref : () => ref;
|
|
89
|
+
const thread = this.binding.select((annotation) => {
|
|
90
|
+
const member = refOf();
|
|
91
|
+
return member ? annotation.comments.getThread(member) : null;
|
|
92
|
+
}, null, Object.is);
|
|
93
|
+
return computed(() => {
|
|
94
|
+
const found = thread();
|
|
95
|
+
if (!found)
|
|
96
|
+
return null;
|
|
97
|
+
const root = annotationKey(found.root.ref);
|
|
98
|
+
return this.threads().find((view) => annotationKey(view.root.ref) === root) ?? null;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfComments, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
102
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfComments, providedIn: 'root' });
|
|
103
|
+
}
|
|
104
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfComments, decorators: [{
|
|
105
|
+
type: Injectable,
|
|
106
|
+
args: [{ providedIn: 'root' }]
|
|
107
|
+
}], ctorParameters: () => [] });
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The annotation plugin's service and feature:
|
|
111
|
+
*
|
|
112
|
+
* withAnnotation(options) the plugin, for provideEmbedPdf() (it needs withInteraction())
|
|
113
|
+
* inject(EpdfAnnotation) create, change and delete annotations; the State table as
|
|
114
|
+
* signals (`status()`, `selected()`, `hovered()`, `editing()`);
|
|
115
|
+
* the parts (`selection`, `draft`, `text`, `tools`, `stamps`,
|
|
116
|
+
* `links`); the events as streams (`created$`, …); the settings;
|
|
117
|
+
* and the reads a template follows (`watch(filter)`,
|
|
118
|
+
* `anchorOf(ref)`, `tools.defaultsOf(id)`)
|
|
119
|
+
*
|
|
120
|
+
* The service is both the plugin's API and its state, so the React hooks around it become
|
|
121
|
+
* members: `useAnnotationList(filter)` is `annotation.watch(filter)`, `useAnnotationAnchor(ref)`
|
|
122
|
+
* is `annotation.anchorOf(ref)`, `useAnnotationDefaults(id)` is `annotation.tools.defaultsOf(id)`
|
|
123
|
+
* and `useAnnotationProperties()` is `annotation.selection.properties()`. Each of them takes a
|
|
124
|
+
* value or a function that reads signals (`() => this.stamp().ref`), and follows it.
|
|
125
|
+
*/
|
|
126
|
+
const NO_ANNOTATIONS = Object.freeze([]);
|
|
127
|
+
const NO_DEFAULTS = Object.freeze({});
|
|
128
|
+
const NO_PROPERTIES = Object.freeze({
|
|
129
|
+
properties: [],
|
|
130
|
+
values: {},
|
|
131
|
+
mixed: [],
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Annotations: `create()`, `update()`, `delete()` and the rest of the Methods tables, the
|
|
135
|
+
* selecting page's State table as signals (`annotation.selected()`), the parts the capability
|
|
136
|
+
* has (`annotation.selection.set(refs)`, `annotation.tools.updateDefaults(id, changes)`), its
|
|
137
|
+
* events as streams (`annotation.created$`), and its settings (`annotation.settings()`,
|
|
138
|
+
* `annotation.updateSettings({ chrome })`). Comments have their own service, `EpdfComments`.
|
|
139
|
+
*
|
|
140
|
+
* Without a document the signals read empty and the methods refuse with `not-ready`. The checks
|
|
141
|
+
* (`canCreate()`, `canDelete(ref)`, `selection.canGroup()`, `stamps.isArmed()`) are state:
|
|
142
|
+
* read in a template, they follow the document, its permissions and the selection.
|
|
143
|
+
*/
|
|
144
|
+
class EpdfAnnotation extends pluginService({
|
|
145
|
+
name: 'EpdfAnnotation',
|
|
146
|
+
feature: 'withAnnotation()',
|
|
147
|
+
token: AnnotationToken,
|
|
148
|
+
state: annotationState,
|
|
149
|
+
methods: [
|
|
150
|
+
'get',
|
|
151
|
+
'list',
|
|
152
|
+
'getAt',
|
|
153
|
+
'isPending',
|
|
154
|
+
'getStatus',
|
|
155
|
+
'getHovered',
|
|
156
|
+
'refresh',
|
|
157
|
+
'create',
|
|
158
|
+
'createFromSelection',
|
|
159
|
+
'update',
|
|
160
|
+
'delete',
|
|
161
|
+
'reorder',
|
|
162
|
+
'export',
|
|
163
|
+
'import',
|
|
164
|
+
'downloadResource',
|
|
165
|
+
'setFilePickerProvider',
|
|
166
|
+
'canRead',
|
|
167
|
+
'canCreate',
|
|
168
|
+
'canUpdate',
|
|
169
|
+
'canDelete',
|
|
170
|
+
'cancel',
|
|
171
|
+
],
|
|
172
|
+
events: [
|
|
173
|
+
'onCreated',
|
|
174
|
+
'onUpdated',
|
|
175
|
+
'onDeleted',
|
|
176
|
+
'onReordered',
|
|
177
|
+
'onResynced',
|
|
178
|
+
'onWriteFailed',
|
|
179
|
+
'onSelectionChanged',
|
|
180
|
+
'onDraftChanged',
|
|
181
|
+
'onEditingChanged',
|
|
182
|
+
'onHoverChanged',
|
|
183
|
+
],
|
|
184
|
+
}) {
|
|
185
|
+
/** The same capability, typed with the reads only framework code has (an annotation's anchor). */
|
|
186
|
+
host = new CapabilityBinding(this.binding.host, () => AnnotationToken$1, injectDocumentScope());
|
|
187
|
+
/** The selection: `set(refs)`, `update(changes)`, `delete()`, … and `properties()`. */
|
|
188
|
+
selection = {
|
|
189
|
+
set: this.binding.namespaceMethod('selection', 'set'),
|
|
190
|
+
add: this.binding.namespaceMethod('selection', 'add'),
|
|
191
|
+
selectAll: this.binding.namespaceMethod('selection', 'selectAll'),
|
|
192
|
+
selectInRect: this.binding.namespaceMethod('selection', 'selectInRect'),
|
|
193
|
+
clear: this.binding.namespaceMethod('selection', 'clear'),
|
|
194
|
+
list: this.binding.namespaceMethod('selection', 'list'),
|
|
195
|
+
update: this.binding.namespaceMethod('selection', 'update'),
|
|
196
|
+
updateLink: this.binding.namespaceMethod('selection', 'updateLink'),
|
|
197
|
+
getProperties: this.binding.namespaceMethod('selection', 'getProperties'),
|
|
198
|
+
delete: this.binding.namespaceMethod('selection', 'delete'),
|
|
199
|
+
rotateBy: this.binding.namespaceMethod('selection', 'rotateBy'),
|
|
200
|
+
resetRotation: this.binding.namespaceMethod('selection', 'resetRotation'),
|
|
201
|
+
group: this.binding.namespaceMethod('selection', 'group'),
|
|
202
|
+
ungroup: this.binding.namespaceMethod('selection', 'ungroup'),
|
|
203
|
+
canGroup: this.binding.namespaceMethod('selection', 'canGroup'),
|
|
204
|
+
canUngroup: this.binding.namespaceMethod('selection', 'canUngroup'),
|
|
205
|
+
getAnchor: this.binding.namespaceMethod('selection', 'getAnchor'),
|
|
206
|
+
getRotationAnchor: this.binding.namespaceMethod('selection', 'getRotationAnchor'),
|
|
207
|
+
// The plugin hands out the same object while nothing changes.
|
|
208
|
+
properties: this.binding.select((annotation) => annotation.selection.getProperties(), NO_PROPERTIES, Object.is),
|
|
209
|
+
};
|
|
210
|
+
/** A polygon or polyline being drawn: `get()`, `finish()`, `cancel()`. */
|
|
211
|
+
draft = {
|
|
212
|
+
get: this.binding.namespaceMethod('draft', 'get'),
|
|
213
|
+
finish: this.binding.namespaceMethod('draft', 'finish'),
|
|
214
|
+
cancel: this.binding.namespaceMethod('draft', 'cancel'),
|
|
215
|
+
};
|
|
216
|
+
/** Typing in a text box: `begin(ref)`, `end()`, `getEditing()`, `toggleFormat(format)`. */
|
|
217
|
+
text = {
|
|
218
|
+
begin: this.binding.namespaceMethod('text', 'begin'),
|
|
219
|
+
end: this.binding.namespaceMethod('text', 'end'),
|
|
220
|
+
getEditing: this.binding.namespaceMethod('text', 'getEditing'),
|
|
221
|
+
toggleFormat: this.binding.namespaceMethod('text', 'toggleFormat'),
|
|
222
|
+
};
|
|
223
|
+
/** The tools: `list()`, `register(tool)`, `updateDefaults(id, changes)`, `defaultsOf(id)`, … */
|
|
224
|
+
tools = {
|
|
225
|
+
list: this.binding.namespaceMethod('tools', 'list'),
|
|
226
|
+
get: this.binding.namespaceMethod('tools', 'get'),
|
|
227
|
+
register: this.binding.namespaceMethod('tools', 'register'),
|
|
228
|
+
getDefaults: this.binding.namespaceMethod('tools', 'getDefaults'),
|
|
229
|
+
updateDefaults: this.binding.namespaceMethod('tools', 'updateDefaults'),
|
|
230
|
+
getProperties: this.binding.namespaceMethod('tools', 'getProperties'),
|
|
231
|
+
// The plugin hands out the same object until the tool's defaults change.
|
|
232
|
+
defaultsOf: (id) => {
|
|
233
|
+
const idOf = typeof id === 'function' ? id : () => id;
|
|
234
|
+
return this.binding.select((annotation) => annotation.tools.getDefaults(idOf()), NO_DEFAULTS, Object.is);
|
|
235
|
+
},
|
|
236
|
+
propertiesOf: (id) => {
|
|
237
|
+
const idOf = typeof id === 'function' ? id : () => id;
|
|
238
|
+
return this.binding.select((annotation) => annotation.tools.getProperties(idOf()), NO_PROPERTIES);
|
|
239
|
+
},
|
|
240
|
+
defaultsChanged$: this.binding.stream((annotation) => annotation.tools.onDefaultsChanged),
|
|
241
|
+
};
|
|
242
|
+
/** Stamps from bytes: `arm(stamp)`, `disarm()`, `isArmed()`, `place(stamp, placement)`. */
|
|
243
|
+
stamps = {
|
|
244
|
+
arm: this.binding.namespaceMethod('stamps', 'arm'),
|
|
245
|
+
disarm: this.binding.namespaceMethod('stamps', 'disarm'),
|
|
246
|
+
isArmed: this.binding.namespaceMethod('stamps', 'isArmed'),
|
|
247
|
+
place: this.binding.namespaceMethod('stamps', 'place'),
|
|
248
|
+
};
|
|
249
|
+
/** An annotation's link: `get(ref)`, `set(ref, target)`, `clear(ref)`. */
|
|
250
|
+
links = {
|
|
251
|
+
get: this.binding.namespaceMethod('links', 'get'),
|
|
252
|
+
set: this.binding.namespaceMethod('links', 'set'),
|
|
253
|
+
clear: this.binding.namespaceMethod('links', 'clear'),
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* The annotations matching `filter` (some pages, one kind), in drawing order, as the user sees
|
|
257
|
+
* them: a signal that keeps the same array while they stay the same. Pass the filter, or a
|
|
258
|
+
* function that reads it (`() => ({ pages: [this.page()] })`). Empty without a document.
|
|
259
|
+
* Call it once, in a field or the constructor, and read the signal in your template.
|
|
260
|
+
*/
|
|
261
|
+
watch(filter) {
|
|
262
|
+
const filterOf = typeof filter === 'function' ? filter : () => filter;
|
|
263
|
+
return this.binding.select((annotation) => annotation.list(filterOf() ?? undefined), NO_ANNOTATIONS);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* An anchor for `<epdf-anchored>` that keeps a card or a badge on one annotation: its page and
|
|
267
|
+
* the box around what it shows, following a move as it happens. Pass the ref, or a function
|
|
268
|
+
* that reads it (`() => this.hovered()?.ref ?? null`). Null for null, or an annotation that
|
|
269
|
+
* isn't here. It changes when the annotation moves, not while people scroll or zoom.
|
|
270
|
+
*/
|
|
271
|
+
anchorOf(ref) {
|
|
272
|
+
const refOf = typeof ref === 'function' ? ref : () => ref;
|
|
273
|
+
return this.host.select((annotation) => {
|
|
274
|
+
const target = refOf();
|
|
275
|
+
return target ? annotation.getAnnotationAnchor(target) : null;
|
|
276
|
+
}, null, sameAnnotationAnchor);
|
|
277
|
+
}
|
|
278
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotation, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
279
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotation, providedIn: 'root' });
|
|
280
|
+
}
|
|
281
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotation, decorators: [{
|
|
282
|
+
type: Injectable,
|
|
283
|
+
args: [{ providedIn: 'root' }]
|
|
284
|
+
}] });
|
|
285
|
+
/**
|
|
286
|
+
* The annotation plugin: drawing, selecting, changing and saving annotations, and comments. It
|
|
287
|
+
* works with the interaction plugin, so give `withInteraction()` too, and an
|
|
288
|
+
* `<epdf-annotation-layer>` in each page:
|
|
289
|
+
* `provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction(), withAnnotation())`.
|
|
290
|
+
*/
|
|
291
|
+
function withAnnotation(options) {
|
|
292
|
+
return { plugins: [annotationPlugin(options)], services: [EpdfAnnotation, EpdfComments] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* `withFilePicker(provider?)`: where the `stamp` and `attachment` tools get their file after a
|
|
297
|
+
* click. The plugin has no DOM, so it asks a provider, and this feature gives it one: the
|
|
298
|
+
* browser's file dialog by default, or your own function (a stamp library, a cloud drive).
|
|
299
|
+
*
|
|
300
|
+
* provideEmbedPdf({ engine }, withStage(), withInteraction(), withAnnotation(), withFilePicker())
|
|
301
|
+
*
|
|
302
|
+
* React installs it with a hook in an always-mounted component; in Angular it is app-wide setup,
|
|
303
|
+
* so it is a feature of `provideEmbedPdf()`.
|
|
304
|
+
*/
|
|
305
|
+
/**
|
|
306
|
+
* The browser's file dialog, filtered by the tool's `accept`: what `withFilePicker()` installs
|
|
307
|
+
* when you give it nothing. A picked `File` carries its own name and type.
|
|
308
|
+
*/
|
|
309
|
+
const filePickerProvider = pickRequestedFile;
|
|
310
|
+
/**
|
|
311
|
+
* The file dialog behind every click-then-pick tool, for every document the viewer opens.
|
|
312
|
+
* Without an argument it is the browser's dialog ({@link filePickerProvider}); pass your own
|
|
313
|
+
* function to pick from your library (it gets the tool, the page and the point, and returns the
|
|
314
|
+
* file or `null` to cancel), or `null` to make those tools do nothing.
|
|
315
|
+
*/
|
|
316
|
+
function withFilePicker(provider = filePickerProvider) {
|
|
317
|
+
return { plugins: [], setup: () => installFilePicker(provider) };
|
|
318
|
+
}
|
|
319
|
+
/** Give every open document's annotation plugin the provider, for as long as the viewer lives. */
|
|
320
|
+
function installFilePicker(provider) {
|
|
321
|
+
const host = injectKernelHost('withFilePicker()');
|
|
322
|
+
if (!host.provides(AnnotationToken)) {
|
|
323
|
+
devWarn('file-picker-without-annotation', 'withFilePicker() has no annotation plugin to give its file dialog to. Add withAnnotation() to provideEmbedPdf().');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// One install per document's plugin, removed when the document closes.
|
|
327
|
+
const installed = new Map();
|
|
328
|
+
// On every change of the kernel, not in an effect: a document is given the provider in the
|
|
329
|
+
// same change that makes it ready, before anyone can click with a stamp tool.
|
|
330
|
+
const follow = () => {
|
|
331
|
+
const kernel = host.kernel();
|
|
332
|
+
if (!kernel)
|
|
333
|
+
return;
|
|
334
|
+
const open = new Set();
|
|
335
|
+
for (const document of kernel.documents.list()) {
|
|
336
|
+
const annotation = kernel.tryCapability(AnnotationToken, document.id);
|
|
337
|
+
if (!annotation)
|
|
338
|
+
continue;
|
|
339
|
+
open.add(annotation);
|
|
340
|
+
if (!installed.has(annotation)) {
|
|
341
|
+
installed.set(annotation, annotation.setFilePickerProvider(provider));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const [annotation, remove] of installed) {
|
|
345
|
+
if (open.has(annotation))
|
|
346
|
+
continue;
|
|
347
|
+
remove();
|
|
348
|
+
installed.delete(annotation);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const stop = host.subscribe(follow);
|
|
352
|
+
follow();
|
|
353
|
+
inject(DestroyRef).onDestroy(() => {
|
|
354
|
+
stop();
|
|
355
|
+
for (const remove of installed.values())
|
|
356
|
+
remove();
|
|
357
|
+
installed.clear();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* What the annotation layer's parts read about their page: the plugin's host side for the page's
|
|
363
|
+
* own document, and the page's view as signals that change only when what they hold does. A
|
|
364
|
+
* Stage hands every page a new transform on every frame, also during a plain scroll, which moves
|
|
365
|
+
* the page but changes nothing inside it; these keep a scroll from redrawing the annotations.
|
|
366
|
+
*/
|
|
367
|
+
/**
|
|
368
|
+
* The annotation plugin's host side (the reads only framework code has: items, chrome, text
|
|
369
|
+
* boxes), for the document the page belongs to. Call it in an injection context.
|
|
370
|
+
*/
|
|
371
|
+
function annotationHostOf(page, what) {
|
|
372
|
+
return new CapabilityBinding(injectKernelHost(what), () => AnnotationToken$1, () => page.documentId);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The page's zoom and turn, for the plugin's reads: it projects what keeps its size or stays
|
|
376
|
+
* upright on screen (`noZoom`, `noRotate`). `zoom`, not `viewScale`: 1 is the page's 100%.
|
|
377
|
+
*/
|
|
378
|
+
function pageViewOf(page) {
|
|
379
|
+
return computed(() => {
|
|
380
|
+
const transform = page.transform();
|
|
381
|
+
return { zoom: transform.zoom, rotation: transform.rotation };
|
|
382
|
+
}, { equal: (left, right) => left.zoom === right.zoom && left.rotation === right.rotation });
|
|
383
|
+
}
|
|
384
|
+
/** The page's transform for placing things in its pixels: the same while only a scroll moves it. */
|
|
385
|
+
function pagePixelsOf(page) {
|
|
386
|
+
return computed(() => page.transform(), { equal: samePagePlacement });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The selection chrome's settings, for the parts that paint it: the plugin's `chrome` settings
|
|
391
|
+
* and the viewer's accent, which a chrome color left `null` follows. Settings belong to the
|
|
392
|
+
* plugin, not to a document, so they read before any document opens.
|
|
393
|
+
*/
|
|
394
|
+
/** The chrome settings and the accent, as signals. Call it in an injection context. */
|
|
395
|
+
function injectChromeSettings() {
|
|
396
|
+
const host = injectKernelHost('<epdf-annotation-layer>');
|
|
397
|
+
// Without the plugin there is nothing to paint; the defaults keep the reads simple.
|
|
398
|
+
const settings = host.provides(AnnotationToken)
|
|
399
|
+
? host.settingsOf(AnnotationToken).current
|
|
400
|
+
: computed(() => ANNOTATION_DEFAULTS);
|
|
401
|
+
return {
|
|
402
|
+
chrome: computed(() => settings().chrome),
|
|
403
|
+
accent: computed(() => host.viewerSettings().accent),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The selection's chrome on one page: its outline, handles and rotation handle, the guides of a
|
|
409
|
+
* snapped move or a turn, and the box dragged to select. The plugin lists the chrome in page
|
|
410
|
+
* points, already sized for the page's zoom; `@embedpdf/web` puts it in the layer's pixels and
|
|
411
|
+
* paints it from the `chrome` settings, each color, width and dash behind its
|
|
412
|
+
* `--epdf-annotation-*` variable. This component only draws: the viewer decides where a handle
|
|
413
|
+
* can be grabbed, so handles drawn by the app's templates can't break dragging.
|
|
414
|
+
*
|
|
415
|
+
* Paint goes in `style`: an SVG attribute doesn't read `var()`.
|
|
416
|
+
*/
|
|
417
|
+
const NO_NODES = Object.freeze([]);
|
|
418
|
+
class EpdfAnnotationChrome {
|
|
419
|
+
/** The app's handle template, drawn in place of the layer's handles. */
|
|
420
|
+
handle = input(null, ...(ngDevMode ? [{ debugName: "handle" }] : []));
|
|
421
|
+
/** The app's rotation handle template, drawn in place of the layer's. */
|
|
422
|
+
rotationHandle = input(null, ...(ngDevMode ? [{ debugName: "rotationHandle" }] : []));
|
|
423
|
+
page = injectPage('<epdf-annotation-layer>');
|
|
424
|
+
annotation = annotationHostOf(this.page, '<epdf-annotation-layer>');
|
|
425
|
+
pixels = pagePixelsOf(this.page);
|
|
426
|
+
settings = injectChromeSettings();
|
|
427
|
+
chrome = computed(() => this.settings.chrome(), ...(ngDevMode ? [{ debugName: "chrome" }] : []));
|
|
428
|
+
paint = computed(() => annotationChromePaint(this.settings.chrome(), this.settings.accent()), ...(ngDevMode ? [{ debugName: "paint" }] : []));
|
|
429
|
+
// The page's view scale turns the screen-pixel chrome settings into page units inside the
|
|
430
|
+
// plugin (the rotation handle's offset, the grab zones): the same size on screen at every
|
|
431
|
+
// zoom. What it lists comes back in page points; `chromeInPixels` places it.
|
|
432
|
+
listed = this.annotation.select((annotation) => {
|
|
433
|
+
const transform = this.pixels();
|
|
434
|
+
return annotation.listChromeNodes(this.page.ref, transform.viewScale, transform.rotation, transform.zoom);
|
|
435
|
+
}, NO_NODES);
|
|
436
|
+
nodes = computed(() => {
|
|
437
|
+
const handle = this.handle();
|
|
438
|
+
const rotationHandle = this.rotationHandle();
|
|
439
|
+
const chrome = this.settings.chrome();
|
|
440
|
+
return chromeInPixels(this.listed(), this.pixels()).map((node) => {
|
|
441
|
+
if (node.kind === 'handle' && handle) {
|
|
442
|
+
const custom = {
|
|
443
|
+
at: node.at,
|
|
444
|
+
size: chrome.handles.size,
|
|
445
|
+
rotation: node.rotation,
|
|
446
|
+
kind: node.role,
|
|
447
|
+
active: node.active,
|
|
448
|
+
};
|
|
449
|
+
return { ...node, custom };
|
|
450
|
+
}
|
|
451
|
+
if (node.kind === 'rotation-handle' && rotationHandle) {
|
|
452
|
+
// A drawn rotation handle doesn't turn, and isn't told when it's dragged.
|
|
453
|
+
const custom = {
|
|
454
|
+
at: node.at,
|
|
455
|
+
from: node.from,
|
|
456
|
+
size: chrome.rotationHandle.size,
|
|
457
|
+
rotation: 0,
|
|
458
|
+
active: false,
|
|
459
|
+
};
|
|
460
|
+
return { ...node, custom };
|
|
461
|
+
}
|
|
462
|
+
return node;
|
|
463
|
+
});
|
|
464
|
+
}, ...(ngDevMode ? [{ debugName: "nodes" }] : []));
|
|
465
|
+
hasCustom = computed(() => this.nodes().some((node) => node.custom), ...(ngDevMode ? [{ debugName: "hasCustom" }] : []));
|
|
466
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationChrome, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
467
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfAnnotationChrome, isStandalone: true, selector: "epdf-annotation-chrome", inputs: { handle: { classPropertyName: "handle", publicName: "handle", isSignal: true, isRequired: false, transformFunction: null }, rotationHandle: { classPropertyName: "rotationHandle", publicName: "rotationHandle", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
468
|
+
<svg style="position: absolute; inset: 0; overflow: visible; pointer-events: none">
|
|
469
|
+
@for (node of nodes(); track $index) {
|
|
470
|
+
@switch (node.kind) {
|
|
471
|
+
@case ('handle') {
|
|
472
|
+
@if (node.kind === 'handle' && !node.custom) {
|
|
473
|
+
@if (chrome().handles.shape === 'circle') {
|
|
474
|
+
<svg:circle
|
|
475
|
+
[attr.cx]="node.at.x"
|
|
476
|
+
[attr.cy]="node.at.y"
|
|
477
|
+
[attr.r]="chrome().handles.size / 2"
|
|
478
|
+
stroke-width="1.5"
|
|
479
|
+
[style.fill]="paint().handle.fill"
|
|
480
|
+
[style.stroke]="paint().handle.stroke"
|
|
481
|
+
></svg:circle>
|
|
482
|
+
} @else {
|
|
483
|
+
<!-- The square rides a turned box: it spins about itself. -->
|
|
484
|
+
<svg:rect
|
|
485
|
+
[attr.x]="node.at.x - chrome().handles.size / 2"
|
|
486
|
+
[attr.y]="node.at.y - chrome().handles.size / 2"
|
|
487
|
+
[attr.width]="chrome().handles.size"
|
|
488
|
+
[attr.height]="chrome().handles.size"
|
|
489
|
+
[attr.transform]="
|
|
490
|
+
node.rotation ? 'rotate(' + node.rotation + ' ' + node.at.x + ' ' + node.at.y + ')' : null
|
|
491
|
+
"
|
|
492
|
+
stroke-width="1.5"
|
|
493
|
+
[style.fill]="paint().handle.fill"
|
|
494
|
+
[style.stroke]="paint().handle.stroke"
|
|
495
|
+
></svg:rect>
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
@case ('guide') {
|
|
500
|
+
@if (node.kind === 'guide') {
|
|
501
|
+
<svg:line
|
|
502
|
+
[attr.x1]="node.from.x"
|
|
503
|
+
[attr.y1]="node.from.y"
|
|
504
|
+
[attr.x2]="node.to.x"
|
|
505
|
+
[attr.y2]="node.to.y"
|
|
506
|
+
shape-rendering="crispEdges"
|
|
507
|
+
[style.stroke]="paint().guide.stroke"
|
|
508
|
+
[style.stroke-width]="paint().guide.strokeWidth"
|
|
509
|
+
[style.stroke-dasharray]="paint().guide.strokeDasharray"
|
|
510
|
+
></svg:line>
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
@case ('turned-outline') {
|
|
514
|
+
@if (node.kind === 'turned-outline') {
|
|
515
|
+
<svg:polygon
|
|
516
|
+
[attr.points]="node.points"
|
|
517
|
+
fill="none"
|
|
518
|
+
[style.stroke]="paint().outline.stroke"
|
|
519
|
+
[style.stroke-width]="paint().outline.strokeWidth"
|
|
520
|
+
[style.stroke-dasharray]="paint().outline.strokeDasharray"
|
|
521
|
+
></svg:polygon>
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
@case ('rotation-guides') {
|
|
525
|
+
@if (node.kind === 'rotation-guides') {
|
|
526
|
+
<svg:g>
|
|
527
|
+
@for (line of node.lines; track $index) {
|
|
528
|
+
<svg:line
|
|
529
|
+
[attr.x1]="line.from.x"
|
|
530
|
+
[attr.y1]="line.from.y"
|
|
531
|
+
[attr.x2]="line.to.x"
|
|
532
|
+
[attr.y2]="line.to.y"
|
|
533
|
+
[attr.opacity]="line.opacity"
|
|
534
|
+
[style.stroke]="paint().rotationGuide.stroke"
|
|
535
|
+
[style.stroke-width]="paint().rotationGuide.strokeWidth"
|
|
536
|
+
[style.stroke-dasharray]="paint().rotationGuide.strokeDasharray"
|
|
537
|
+
></svg:line>
|
|
538
|
+
}
|
|
539
|
+
</svg:g>
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
@case ('rotation-handle') {
|
|
543
|
+
@if (node.kind === 'rotation-handle' && !node.custom) {
|
|
544
|
+
<svg:g>
|
|
545
|
+
@if (chrome().rotationHandle.stalk) {
|
|
546
|
+
<svg:line
|
|
547
|
+
[attr.x1]="node.from.x"
|
|
548
|
+
[attr.y1]="node.from.y"
|
|
549
|
+
[attr.x2]="node.at.x"
|
|
550
|
+
[attr.y2]="node.at.y"
|
|
551
|
+
stroke-width="1"
|
|
552
|
+
[style.stroke]="paint().rotationHandle.stroke"
|
|
553
|
+
></svg:line>
|
|
554
|
+
}
|
|
555
|
+
<svg:circle
|
|
556
|
+
[attr.cx]="node.at.x"
|
|
557
|
+
[attr.cy]="node.at.y"
|
|
558
|
+
[attr.r]="chrome().rotationHandle.size / 2"
|
|
559
|
+
stroke-width="1.5"
|
|
560
|
+
[style.fill]="paint().rotationHandle.fill"
|
|
561
|
+
[style.stroke]="paint().rotationHandle.stroke"
|
|
562
|
+
></svg:circle>
|
|
563
|
+
</svg:g>
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
<!-- The box dragged to select keeps its own look (a see-through accent fill, always
|
|
567
|
+
dashed); the selection outline follows the settings. -->
|
|
568
|
+
@case ('marquee') {
|
|
569
|
+
@if (node.kind === 'marquee') {
|
|
570
|
+
<svg:rect
|
|
571
|
+
[attr.x]="node.box.left"
|
|
572
|
+
[attr.y]="node.box.top"
|
|
573
|
+
[attr.width]="node.box.width"
|
|
574
|
+
[attr.height]="node.box.height"
|
|
575
|
+
stroke-width="1"
|
|
576
|
+
stroke-dasharray="4 3"
|
|
577
|
+
[style.fill]="paint().marquee.fill"
|
|
578
|
+
[style.stroke]="paint().marquee.stroke"
|
|
579
|
+
></svg:rect>
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
@case ('outline') {
|
|
583
|
+
@if (node.kind === 'outline') {
|
|
584
|
+
<svg:rect
|
|
585
|
+
[attr.x]="node.box.left"
|
|
586
|
+
[attr.y]="node.box.top"
|
|
587
|
+
[attr.width]="node.box.width"
|
|
588
|
+
[attr.height]="node.box.height"
|
|
589
|
+
fill="none"
|
|
590
|
+
[style.stroke]="paint().outline.stroke"
|
|
591
|
+
[style.stroke-width]="paint().outline.strokeWidth"
|
|
592
|
+
[style.stroke-dasharray]="paint().outline.strokeDasharray"
|
|
593
|
+
></svg:rect>
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
</svg>
|
|
599
|
+
<!-- The app's own handles draw as HTML over the page; the layer's as SVG. -->
|
|
600
|
+
@if (hasCustom()) {
|
|
601
|
+
<div style="position: absolute; inset: 0; pointer-events: none">
|
|
602
|
+
@for (node of nodes(); track $index) {
|
|
603
|
+
@if (node.custom; as props) {
|
|
604
|
+
@if (node.kind === 'handle') {
|
|
605
|
+
<ng-container
|
|
606
|
+
[ngTemplateOutlet]="handle()"
|
|
607
|
+
[ngTemplateOutletContext]="{ $implicit: props }"
|
|
608
|
+
/>
|
|
609
|
+
} @else {
|
|
610
|
+
<ng-container
|
|
611
|
+
[ngTemplateOutlet]="rotationHandle()"
|
|
612
|
+
[ngTemplateOutletContext]="{ $implicit: props }"
|
|
613
|
+
/>
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
</div>
|
|
618
|
+
}
|
|
619
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
620
|
+
}
|
|
621
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationChrome, decorators: [{
|
|
622
|
+
type: Component,
|
|
623
|
+
args: [{
|
|
624
|
+
selector: 'epdf-annotation-chrome',
|
|
625
|
+
imports: [NgTemplateOutlet],
|
|
626
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
627
|
+
host: { style: 'display: contents' },
|
|
628
|
+
template: `
|
|
629
|
+
<svg style="position: absolute; inset: 0; overflow: visible; pointer-events: none">
|
|
630
|
+
@for (node of nodes(); track $index) {
|
|
631
|
+
@switch (node.kind) {
|
|
632
|
+
@case ('handle') {
|
|
633
|
+
@if (node.kind === 'handle' && !node.custom) {
|
|
634
|
+
@if (chrome().handles.shape === 'circle') {
|
|
635
|
+
<svg:circle
|
|
636
|
+
[attr.cx]="node.at.x"
|
|
637
|
+
[attr.cy]="node.at.y"
|
|
638
|
+
[attr.r]="chrome().handles.size / 2"
|
|
639
|
+
stroke-width="1.5"
|
|
640
|
+
[style.fill]="paint().handle.fill"
|
|
641
|
+
[style.stroke]="paint().handle.stroke"
|
|
642
|
+
></svg:circle>
|
|
643
|
+
} @else {
|
|
644
|
+
<!-- The square rides a turned box: it spins about itself. -->
|
|
645
|
+
<svg:rect
|
|
646
|
+
[attr.x]="node.at.x - chrome().handles.size / 2"
|
|
647
|
+
[attr.y]="node.at.y - chrome().handles.size / 2"
|
|
648
|
+
[attr.width]="chrome().handles.size"
|
|
649
|
+
[attr.height]="chrome().handles.size"
|
|
650
|
+
[attr.transform]="
|
|
651
|
+
node.rotation ? 'rotate(' + node.rotation + ' ' + node.at.x + ' ' + node.at.y + ')' : null
|
|
652
|
+
"
|
|
653
|
+
stroke-width="1.5"
|
|
654
|
+
[style.fill]="paint().handle.fill"
|
|
655
|
+
[style.stroke]="paint().handle.stroke"
|
|
656
|
+
></svg:rect>
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
@case ('guide') {
|
|
661
|
+
@if (node.kind === 'guide') {
|
|
662
|
+
<svg:line
|
|
663
|
+
[attr.x1]="node.from.x"
|
|
664
|
+
[attr.y1]="node.from.y"
|
|
665
|
+
[attr.x2]="node.to.x"
|
|
666
|
+
[attr.y2]="node.to.y"
|
|
667
|
+
shape-rendering="crispEdges"
|
|
668
|
+
[style.stroke]="paint().guide.stroke"
|
|
669
|
+
[style.stroke-width]="paint().guide.strokeWidth"
|
|
670
|
+
[style.stroke-dasharray]="paint().guide.strokeDasharray"
|
|
671
|
+
></svg:line>
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
@case ('turned-outline') {
|
|
675
|
+
@if (node.kind === 'turned-outline') {
|
|
676
|
+
<svg:polygon
|
|
677
|
+
[attr.points]="node.points"
|
|
678
|
+
fill="none"
|
|
679
|
+
[style.stroke]="paint().outline.stroke"
|
|
680
|
+
[style.stroke-width]="paint().outline.strokeWidth"
|
|
681
|
+
[style.stroke-dasharray]="paint().outline.strokeDasharray"
|
|
682
|
+
></svg:polygon>
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
@case ('rotation-guides') {
|
|
686
|
+
@if (node.kind === 'rotation-guides') {
|
|
687
|
+
<svg:g>
|
|
688
|
+
@for (line of node.lines; track $index) {
|
|
689
|
+
<svg:line
|
|
690
|
+
[attr.x1]="line.from.x"
|
|
691
|
+
[attr.y1]="line.from.y"
|
|
692
|
+
[attr.x2]="line.to.x"
|
|
693
|
+
[attr.y2]="line.to.y"
|
|
694
|
+
[attr.opacity]="line.opacity"
|
|
695
|
+
[style.stroke]="paint().rotationGuide.stroke"
|
|
696
|
+
[style.stroke-width]="paint().rotationGuide.strokeWidth"
|
|
697
|
+
[style.stroke-dasharray]="paint().rotationGuide.strokeDasharray"
|
|
698
|
+
></svg:line>
|
|
699
|
+
}
|
|
700
|
+
</svg:g>
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
@case ('rotation-handle') {
|
|
704
|
+
@if (node.kind === 'rotation-handle' && !node.custom) {
|
|
705
|
+
<svg:g>
|
|
706
|
+
@if (chrome().rotationHandle.stalk) {
|
|
707
|
+
<svg:line
|
|
708
|
+
[attr.x1]="node.from.x"
|
|
709
|
+
[attr.y1]="node.from.y"
|
|
710
|
+
[attr.x2]="node.at.x"
|
|
711
|
+
[attr.y2]="node.at.y"
|
|
712
|
+
stroke-width="1"
|
|
713
|
+
[style.stroke]="paint().rotationHandle.stroke"
|
|
714
|
+
></svg:line>
|
|
715
|
+
}
|
|
716
|
+
<svg:circle
|
|
717
|
+
[attr.cx]="node.at.x"
|
|
718
|
+
[attr.cy]="node.at.y"
|
|
719
|
+
[attr.r]="chrome().rotationHandle.size / 2"
|
|
720
|
+
stroke-width="1.5"
|
|
721
|
+
[style.fill]="paint().rotationHandle.fill"
|
|
722
|
+
[style.stroke]="paint().rotationHandle.stroke"
|
|
723
|
+
></svg:circle>
|
|
724
|
+
</svg:g>
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
<!-- The box dragged to select keeps its own look (a see-through accent fill, always
|
|
728
|
+
dashed); the selection outline follows the settings. -->
|
|
729
|
+
@case ('marquee') {
|
|
730
|
+
@if (node.kind === 'marquee') {
|
|
731
|
+
<svg:rect
|
|
732
|
+
[attr.x]="node.box.left"
|
|
733
|
+
[attr.y]="node.box.top"
|
|
734
|
+
[attr.width]="node.box.width"
|
|
735
|
+
[attr.height]="node.box.height"
|
|
736
|
+
stroke-width="1"
|
|
737
|
+
stroke-dasharray="4 3"
|
|
738
|
+
[style.fill]="paint().marquee.fill"
|
|
739
|
+
[style.stroke]="paint().marquee.stroke"
|
|
740
|
+
></svg:rect>
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
@case ('outline') {
|
|
744
|
+
@if (node.kind === 'outline') {
|
|
745
|
+
<svg:rect
|
|
746
|
+
[attr.x]="node.box.left"
|
|
747
|
+
[attr.y]="node.box.top"
|
|
748
|
+
[attr.width]="node.box.width"
|
|
749
|
+
[attr.height]="node.box.height"
|
|
750
|
+
fill="none"
|
|
751
|
+
[style.stroke]="paint().outline.stroke"
|
|
752
|
+
[style.stroke-width]="paint().outline.strokeWidth"
|
|
753
|
+
[style.stroke-dasharray]="paint().outline.strokeDasharray"
|
|
754
|
+
></svg:rect>
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
</svg>
|
|
760
|
+
<!-- The app's own handles draw as HTML over the page; the layer's as SVG. -->
|
|
761
|
+
@if (hasCustom()) {
|
|
762
|
+
<div style="position: absolute; inset: 0; pointer-events: none">
|
|
763
|
+
@for (node of nodes(); track $index) {
|
|
764
|
+
@if (node.custom; as props) {
|
|
765
|
+
@if (node.kind === 'handle') {
|
|
766
|
+
<ng-container
|
|
767
|
+
[ngTemplateOutlet]="handle()"
|
|
768
|
+
[ngTemplateOutletContext]="{ $implicit: props }"
|
|
769
|
+
/>
|
|
770
|
+
} @else {
|
|
771
|
+
<ng-container
|
|
772
|
+
[ngTemplateOutlet]="rotationHandle()"
|
|
773
|
+
[ngTemplateOutletContext]="{ $implicit: props }"
|
|
774
|
+
/>
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
</div>
|
|
779
|
+
}
|
|
780
|
+
`,
|
|
781
|
+
}]
|
|
782
|
+
}] });
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* How the layer draws one annotation, the parts its template is built from:
|
|
786
|
+
*
|
|
787
|
+
* [epdfAnnotationFrame] the box an annotation draws into, placed and turned like it
|
|
788
|
+
* <epdf-annotation-native> its own drawing in that box: the engine's picture or its scene
|
|
789
|
+
*
|
|
790
|
+
* No bounds math and no per-kind logic: the core computes each item's frame, box and scene,
|
|
791
|
+
* and `@embedpdf/web` turns them into pixels and SVG elements, the same for every framework.
|
|
792
|
+
*/
|
|
793
|
+
/**
|
|
794
|
+
* The box an annotation draws into, in the page layer: placed, sized and turned like the
|
|
795
|
+
* annotation (`item.frame`), inside the page layer that the page itself turns. Its blend is on
|
|
796
|
+
* the box: a turned box groups what is in it, so blending an element inside would stop
|
|
797
|
+
* blending with the page. It takes the pointer only for a look that does (`interactive`), and
|
|
798
|
+
* `inert` keeps the pointer and the focus out of a look that only draws.
|
|
799
|
+
*/
|
|
800
|
+
class EpdfAnnotationFrame {
|
|
801
|
+
frame = input.required(...(ngDevMode ? [{ debugName: "frame", alias: 'epdfAnnotationFrame' }] : [{ alias: 'epdfAnnotationFrame' }]));
|
|
802
|
+
blend = input(undefined, ...(ngDevMode ? [{ debugName: "blend", alias: 'frameBlend' }] : [{ alias: 'frameBlend' }]));
|
|
803
|
+
interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive", alias: 'frameInteractive' }] : [{ alias: 'frameInteractive' }]));
|
|
804
|
+
inert = input(false, ...(ngDevMode ? [{ debugName: "inert", alias: 'frameInert' }] : [{ alias: 'frameInert' }]));
|
|
805
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationFrame, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
806
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfAnnotationFrame, isStandalone: true, selector: "[epdfAnnotationFrame]", inputs: { frame: { classPropertyName: "frame", publicName: "epdfAnnotationFrame", isSignal: true, isRequired: true, transformFunction: null }, blend: { classPropertyName: "blend", publicName: "frameBlend", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "frameInteractive", isSignal: true, isRequired: false, transformFunction: null }, inert: { classPropertyName: "inert", publicName: "frameInert", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.left.px": "frame().left", "style.top.px": "frame().top", "style.width.px": "frame().width", "style.height.px": "frame().height", "style.transform": "frame().transform ?? null", "style.mix-blend-mode": "blend() ?? null", "style.pointer-events": "interactive() ? 'auto' : 'none'", "attr.inert": "inert() ? '' : null" }, styleAttribute: "position: absolute; transform-origin: center" }, ngImport: i0 });
|
|
807
|
+
}
|
|
808
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationFrame, decorators: [{
|
|
809
|
+
type: Directive,
|
|
810
|
+
args: [{
|
|
811
|
+
selector: '[epdfAnnotationFrame]',
|
|
812
|
+
host: {
|
|
813
|
+
style: 'position: absolute; transform-origin: center',
|
|
814
|
+
'[style.left.px]': 'frame().left',
|
|
815
|
+
'[style.top.px]': 'frame().top',
|
|
816
|
+
'[style.width.px]': 'frame().width',
|
|
817
|
+
'[style.height.px]': 'frame().height',
|
|
818
|
+
'[style.transform]': 'frame().transform ?? null',
|
|
819
|
+
'[style.mix-blend-mode]': 'blend() ?? null',
|
|
820
|
+
// An interactive look takes the pointer: the layer's own `none` ends here.
|
|
821
|
+
'[style.pointer-events]': "interactive() ? 'auto' : 'none'",
|
|
822
|
+
'[attr.inert]': "inert() ? '' : null",
|
|
823
|
+
},
|
|
824
|
+
}]
|
|
825
|
+
}] });
|
|
826
|
+
/**
|
|
827
|
+
* The scene, filling the item's frame: drawn upright in it, the frame turns it. Nothing until
|
|
828
|
+
* the item has area (the 0×0 drawing at a press). The SVG and its viewBox stay proportional:
|
|
829
|
+
* never clamp either one.
|
|
830
|
+
*/
|
|
831
|
+
class EpdfAnnotationScene {
|
|
832
|
+
item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
|
|
833
|
+
viewBox = computed(() => sceneViewBox(this.item().box), ...(ngDevMode ? [{ debugName: "viewBox" }] : []));
|
|
834
|
+
shapes = computed(() => svgShapesOf(scene(this.item()), { miterLimit: MITER_LIMIT }), ...(ngDevMode ? [{ debugName: "shapes" }] : []));
|
|
835
|
+
/** A ghost is see-through as a whole, so its fill and stroke don't stack. */
|
|
836
|
+
opacity = computed(() => {
|
|
837
|
+
const item = this.item();
|
|
838
|
+
return item.source === 'ghost' ? ghostOpacity(item.ghostOpacity ?? 0.5) : null;
|
|
839
|
+
}, ...(ngDevMode ? [{ debugName: "opacity" }] : []));
|
|
840
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationScene, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
841
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfAnnotationScene, isStandalone: true, selector: "epdf-annotation-scene", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
842
|
+
@if (viewBox(); as viewBox) {
|
|
843
|
+
<svg
|
|
844
|
+
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none"
|
|
845
|
+
[attr.viewBox]="viewBox"
|
|
846
|
+
[style.opacity]="opacity()"
|
|
847
|
+
>
|
|
848
|
+
<svg:g [epdfSvgShapes]="shapes()"></svg:g>
|
|
849
|
+
</svg>
|
|
850
|
+
}
|
|
851
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EpdfSvgShapes, selector: "g[epdfSvgShapes]", inputs: ["epdfSvgShapes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
852
|
+
}
|
|
853
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationScene, decorators: [{
|
|
854
|
+
type: Component,
|
|
855
|
+
args: [{
|
|
856
|
+
selector: 'epdf-annotation-scene',
|
|
857
|
+
imports: [EpdfSvgShapes],
|
|
858
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
859
|
+
host: { style: 'display: contents' },
|
|
860
|
+
template: `
|
|
861
|
+
@if (viewBox(); as viewBox) {
|
|
862
|
+
<svg
|
|
863
|
+
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none"
|
|
864
|
+
[attr.viewBox]="viewBox"
|
|
865
|
+
[style.opacity]="opacity()"
|
|
866
|
+
>
|
|
867
|
+
<svg:g [epdfSvgShapes]="shapes()"></svg:g>
|
|
868
|
+
</svg>
|
|
869
|
+
}
|
|
870
|
+
`,
|
|
871
|
+
}]
|
|
872
|
+
}] });
|
|
873
|
+
/**
|
|
874
|
+
* An annotation's own drawing, filling its frame: the engine's picture where the core places it
|
|
875
|
+
* (following a move as it happens), or its scene, as the layer's one rule picks (`shows`,
|
|
876
|
+
* `pictureOrPainting`).
|
|
877
|
+
*/
|
|
878
|
+
class EpdfAnnotationNative {
|
|
879
|
+
item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
|
|
880
|
+
/** What it shows: the engine's picture, its scene, or nothing yet. */
|
|
881
|
+
shows = input(null, ...(ngDevMode ? [{ debugName: "shows" }] : []));
|
|
882
|
+
picture = computed(() => {
|
|
883
|
+
const shows = this.shows();
|
|
884
|
+
return shows?.kind === 'picture' ? shows.picture.url : null;
|
|
885
|
+
}, ...(ngDevMode ? [{ debugName: "picture" }] : []));
|
|
886
|
+
raster = computed(() => {
|
|
887
|
+
const item = this.item();
|
|
888
|
+
return item.raster ? rasterInFrame(item.raster, item.frame) : null;
|
|
889
|
+
}, ...(ngDevMode ? [{ debugName: "raster" }] : []));
|
|
890
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationNative, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
891
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfAnnotationNative, isStandalone: true, selector: "epdf-annotation-native", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, shows: { classPropertyName: "shows", publicName: "shows", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
892
|
+
@if (shows()?.kind === 'painting') {
|
|
893
|
+
<epdf-annotation-scene [item]="item()" />
|
|
894
|
+
} @else if (picture() && raster(); as box) {
|
|
895
|
+
<!-- An explicit size, never clamped by a global img reset (a landscape stamp on a
|
|
896
|
+
turned page is wider than its containing block); the turn the engine took out
|
|
897
|
+
of the picture goes back on about its middle. -->
|
|
898
|
+
<img
|
|
899
|
+
alt=""
|
|
900
|
+
draggable="false"
|
|
901
|
+
style="position: absolute; max-width: none; max-height: none; pointer-events: none; transform-origin: center"
|
|
902
|
+
[attr.src]="picture()"
|
|
903
|
+
[style.left]="box.left"
|
|
904
|
+
[style.top]="box.top"
|
|
905
|
+
[style.width]="box.width"
|
|
906
|
+
[style.height]="box.height"
|
|
907
|
+
[style.transform]="box.transform ?? null"
|
|
908
|
+
/>
|
|
909
|
+
}
|
|
910
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EpdfAnnotationScene, selector: "epdf-annotation-scene", inputs: ["item"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
911
|
+
}
|
|
912
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationNative, decorators: [{
|
|
913
|
+
type: Component,
|
|
914
|
+
args: [{
|
|
915
|
+
selector: 'epdf-annotation-native',
|
|
916
|
+
imports: [EpdfAnnotationScene],
|
|
917
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
918
|
+
host: { style: 'display: contents' },
|
|
919
|
+
template: `
|
|
920
|
+
@if (shows()?.kind === 'painting') {
|
|
921
|
+
<epdf-annotation-scene [item]="item()" />
|
|
922
|
+
} @else if (picture() && raster(); as box) {
|
|
923
|
+
<!-- An explicit size, never clamped by a global img reset (a landscape stamp on a
|
|
924
|
+
turned page is wider than its containing block); the turn the engine took out
|
|
925
|
+
of the picture goes back on about its middle. -->
|
|
926
|
+
<img
|
|
927
|
+
alt=""
|
|
928
|
+
draggable="false"
|
|
929
|
+
style="position: absolute; max-width: none; max-height: none; pointer-events: none; transform-origin: center"
|
|
930
|
+
[attr.src]="picture()"
|
|
931
|
+
[style.left]="box.left"
|
|
932
|
+
[style.top]="box.top"
|
|
933
|
+
[style.width]="box.width"
|
|
934
|
+
[style.height]="box.height"
|
|
935
|
+
[style.transform]="box.transform ?? null"
|
|
936
|
+
/>
|
|
937
|
+
}
|
|
938
|
+
`,
|
|
939
|
+
}]
|
|
940
|
+
}] });
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* The templates `<epdf-annotation-layer>` draws with, in place of React's `renderers` and
|
|
944
|
+
* `components` props:
|
|
945
|
+
*
|
|
946
|
+
* <epdf-annotation-layer>
|
|
947
|
+
* <ng-template [epdfAnnotation]="isNote" let-annotation let-frame="frame">… your look
|
|
948
|
+
* <ng-template epdfHandle let-handle>… your handles
|
|
949
|
+
* <ng-template epdfRotationHandle let-handle>… your rotation handle
|
|
950
|
+
* </epdf-annotation-layer>
|
|
951
|
+
*
|
|
952
|
+
* A look (`epdfAnnotation`) is a renderer: which annotations it draws (a function of the
|
|
953
|
+
* annotation, or a subtype such as `'stamp'`), whether it takes the pointer
|
|
954
|
+
* (`epdfAnnotationInteractive`) and whether the layer scales it with the page
|
|
955
|
+
* (`epdfAnnotationScale`). The first look that matches an annotation draws it.
|
|
956
|
+
*
|
|
957
|
+
* Which drawing each annotation gets, and the behaviors an interactive look registers, are
|
|
958
|
+
* `@embedpdf/web`'s, keyed by the identity of each renderer entry. The page template, and with it
|
|
959
|
+
* every template inside it, is made once per page on screen, so the entries here are interned:
|
|
960
|
+
* the same rule (the same function or subtype, the same options) is the same entry on every
|
|
961
|
+
* page, registered once.
|
|
962
|
+
*/
|
|
963
|
+
const subtypeMatchers = new Map();
|
|
964
|
+
const ALWAYS = {};
|
|
965
|
+
const NEVER = {};
|
|
966
|
+
const SCALED = {};
|
|
967
|
+
const UNSCALED = {};
|
|
968
|
+
const looks = new WeakMap();
|
|
969
|
+
const behaviors = new Map();
|
|
970
|
+
/** A table under `key` in `table`, made on first use. */
|
|
971
|
+
function tableIn(table, key, make) {
|
|
972
|
+
let found = table.get(key);
|
|
973
|
+
if (!found)
|
|
974
|
+
table.set(key, (found = make()));
|
|
975
|
+
return found;
|
|
976
|
+
}
|
|
977
|
+
/** The one entry for a look's rule: the same match, interactivity and scaling give the same object. */
|
|
978
|
+
function lookRendererOf(match, interactive, scale) {
|
|
979
|
+
let matcher;
|
|
980
|
+
if (typeof match === 'function') {
|
|
981
|
+
matcher = match;
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
matcher = subtypeMatchers.get(match) ?? ((annotation) => annotation.subtype === match);
|
|
985
|
+
subtypeMatchers.set(match, matcher);
|
|
986
|
+
}
|
|
987
|
+
const interactiveKey = typeof interactive === 'function' ? interactive : interactive ? ALWAYS : NEVER;
|
|
988
|
+
const byInteractive = tableIn(looks, matcher, () => new WeakMap());
|
|
989
|
+
const byScale = tableIn(byInteractive, interactiveKey, () => new WeakMap());
|
|
990
|
+
return tableIn(byScale, scale ? SCALED : UNSCALED, () => Object.freeze({ for: matcher, interactive, scale }));
|
|
991
|
+
}
|
|
992
|
+
/** `epdfAnnotationInteractive` with no value (`<ng-template … epdfAnnotationInteractive>`) is `true`. */
|
|
993
|
+
function interactiveAttribute(value) {
|
|
994
|
+
if (typeof value === 'function')
|
|
995
|
+
return value;
|
|
996
|
+
return value === '' || value === true || value === 'true';
|
|
997
|
+
}
|
|
998
|
+
// ── the marker directives ────────────────────────────────────────────────────
|
|
999
|
+
/**
|
|
1000
|
+
* Your own look for some annotations: `<ng-template [epdfAnnotation]="isNote" let-annotation>`.
|
|
1001
|
+
* Give it a function that picks annotations by any field, or a subtype
|
|
1002
|
+
* (`epdfAnnotation="stamp"`). Without `epdfAnnotationInteractive` it only draws: the layer keeps
|
|
1003
|
+
* handling the pointer (select, move, resize). With it (or a function the layer asks whenever it
|
|
1004
|
+
* matters) your template takes the pointer, and the annotation can't be selected or moved while
|
|
1005
|
+
* it does. `[epdfAnnotationScale]="false"` draws at the size on screen instead of the
|
|
1006
|
+
* annotation's 100% size.
|
|
1007
|
+
*/
|
|
1008
|
+
class EpdfAnnotationTemplate {
|
|
1009
|
+
/**
|
|
1010
|
+
* Which annotations it draws: a function of the annotation, or a subtype. (Not a required
|
|
1011
|
+
* input: a template inside `@if` is in the layer's query a moment before its inputs are set.)
|
|
1012
|
+
*/
|
|
1013
|
+
match = input(undefined, ...(ngDevMode ? [{ debugName: "match", alias: 'epdfAnnotation' }] : [{ alias: 'epdfAnnotation' }]));
|
|
1014
|
+
/** Whether it takes the pointer: present (or `true`), or a function of `{ annotation, toolId }`. */
|
|
1015
|
+
interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive", alias: 'epdfAnnotationInteractive', transform: interactiveAttribute }] : [{ alias: 'epdfAnnotationInteractive', transform: interactiveAttribute }]));
|
|
1016
|
+
/** Scale what you draw with the page (the default), or draw at the size on screen (`false`). */
|
|
1017
|
+
scale = input(true, ...(ngDevMode ? [{ debugName: "scale", alias: 'epdfAnnotationScale', transform: booleanAttribute }] : [{ alias: 'epdfAnnotationScale', transform: booleanAttribute }]));
|
|
1018
|
+
template = inject(TemplateRef);
|
|
1019
|
+
/** Its renderer entry, the same object on every page for the same rule; null until it has one. */
|
|
1020
|
+
renderer = computed(() => {
|
|
1021
|
+
const match = this.match();
|
|
1022
|
+
return match ? lookRendererOf(match, this.interactive(), this.scale()) : null;
|
|
1023
|
+
}, ...(ngDevMode ? [{ debugName: "renderer" }] : []));
|
|
1024
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1028
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfAnnotationTemplate, isStandalone: true, selector: "ng-template[epdfAnnotation]", inputs: { match: { classPropertyName: "match", publicName: "epdfAnnotation", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "epdfAnnotationInteractive", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "epdfAnnotationScale", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1029
|
+
}
|
|
1030
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationTemplate, decorators: [{
|
|
1031
|
+
type: Directive,
|
|
1032
|
+
args: [{ selector: 'ng-template[epdfAnnotation]' }]
|
|
1033
|
+
}] });
|
|
1034
|
+
/**
|
|
1035
|
+
* What a sibling plugin's behavior owns, drawn by your template while the behavior is engaged:
|
|
1036
|
+
* `<ng-template epdfAnnotationBehavior="form-fill" let-annotation let-item="item">`. The
|
|
1037
|
+
* template places its own controls and takes the input.
|
|
1038
|
+
*/
|
|
1039
|
+
class EpdfAnnotationBehaviorTemplate {
|
|
1040
|
+
/** The behavior's id, as its plugin registers it. */
|
|
1041
|
+
behavior = input(undefined, ...(ngDevMode ? [{ debugName: "behavior", alias: 'epdfAnnotationBehavior' }] : [{ alias: 'epdfAnnotationBehavior' }]));
|
|
1042
|
+
template = inject(TemplateRef);
|
|
1043
|
+
/** Its renderer entry, the same object for the same behavior; null until it has one. */
|
|
1044
|
+
renderer = computed(() => {
|
|
1045
|
+
const behavior = this.behavior();
|
|
1046
|
+
if (!behavior)
|
|
1047
|
+
return null;
|
|
1048
|
+
let entry = behaviors.get(behavior);
|
|
1049
|
+
if (!entry)
|
|
1050
|
+
behaviors.set(behavior, (entry = Object.freeze({ behavior })));
|
|
1051
|
+
return entry;
|
|
1052
|
+
}, ...(ngDevMode ? [{ debugName: "renderer" }] : []));
|
|
1053
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationBehaviorTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1057
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfAnnotationBehaviorTemplate, isStandalone: true, selector: "ng-template[epdfAnnotationBehavior]", inputs: { behavior: { classPropertyName: "behavior", publicName: "epdfAnnotationBehavior", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1058
|
+
}
|
|
1059
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationBehaviorTemplate, decorators: [{
|
|
1060
|
+
type: Directive,
|
|
1061
|
+
args: [{ selector: 'ng-template[epdfAnnotationBehavior]' }]
|
|
1062
|
+
}] });
|
|
1063
|
+
/**
|
|
1064
|
+
* Each handle, drawn your way in place of the layer's: `<ng-template epdfHandle let-handle>`.
|
|
1065
|
+
* The viewer still decides where a handle can be grabbed, so what you draw can't break dragging.
|
|
1066
|
+
*/
|
|
1067
|
+
class EpdfHandleTemplate {
|
|
1068
|
+
template = inject(TemplateRef);
|
|
1069
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfHandleTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1073
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.4", type: EpdfHandleTemplate, isStandalone: true, selector: "ng-template[epdfHandle]", ngImport: i0 });
|
|
1074
|
+
}
|
|
1075
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfHandleTemplate, decorators: [{
|
|
1076
|
+
type: Directive,
|
|
1077
|
+
args: [{ selector: 'ng-template[epdfHandle]' }]
|
|
1078
|
+
}] });
|
|
1079
|
+
/** The rotation handle, drawn your way: `<ng-template epdfRotationHandle let-handle>`. */
|
|
1080
|
+
class EpdfRotationHandleTemplate {
|
|
1081
|
+
template = inject(TemplateRef);
|
|
1082
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfRotationHandleTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1086
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.4", type: EpdfRotationHandleTemplate, isStandalone: true, selector: "ng-template[epdfRotationHandle]", ngImport: i0 });
|
|
1087
|
+
}
|
|
1088
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfRotationHandleTemplate, decorators: [{
|
|
1089
|
+
type: Directive,
|
|
1090
|
+
args: [{ selector: 'ng-template[epdfRotationHandle]' }]
|
|
1091
|
+
}] });
|
|
1092
|
+
/**
|
|
1093
|
+
* The rotation badge's content, drawn your way: inside `<epdf-annotation-rotation-badge>`,
|
|
1094
|
+
* `<ng-template epdfRotationBadge let-rotation>{{ rotation.angle }}°</ng-template>`.
|
|
1095
|
+
*/
|
|
1096
|
+
class EpdfRotationBadgeTemplate {
|
|
1097
|
+
template = inject(TemplateRef);
|
|
1098
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfRotationBadgeTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1102
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.4", type: EpdfRotationBadgeTemplate, isStandalone: true, selector: "ng-template[epdfRotationBadge]", ngImport: i0 });
|
|
1103
|
+
}
|
|
1104
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfRotationBadgeTemplate, decorators: [{
|
|
1105
|
+
type: Directive,
|
|
1106
|
+
args: [{ selector: 'ng-template[epdfRotationBadge]' }]
|
|
1107
|
+
}] });
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Free text boxes: the element someone types in. `@embedpdf/web`'s text box editor makes an
|
|
1111
|
+
* element the editor of a text box (rich text, focus and `contentEditable` following the
|
|
1112
|
+
* plugin's `editing`, presses kept inside), and its follower attaches again for another document
|
|
1113
|
+
* and takes the box and the scale; this file binds the follower to signals.
|
|
1114
|
+
*
|
|
1115
|
+
* [epdfFreeTextBox] the layer's own text box, one per free text on the page (internal)
|
|
1116
|
+
* [epdfRichTextEditor] your own element in a look you draw: it becomes the box's editor
|
|
1117
|
+
*
|
|
1118
|
+
* The editor owns the element's children: Angular puts none in it, and the element is never
|
|
1119
|
+
* made again while someone types (the caret would jump).
|
|
1120
|
+
*/
|
|
1121
|
+
/**
|
|
1122
|
+
* How much the layer scales the look around an element: 1 outside a look, or in one drawn at its
|
|
1123
|
+
* size on screen. The layer provides it to each scaled look, so a rich text editor inside draws
|
|
1124
|
+
* its text at the annotation's 100% size and lets the look's scale do the rest.
|
|
1125
|
+
*/
|
|
1126
|
+
const EPDF_LOOK_SCALE = new InjectionToken('EPDF_LOOK_SCALE');
|
|
1127
|
+
/**
|
|
1128
|
+
* Make `element` the editor of the text box `item()` reads, at `scale()` pixels per point: the
|
|
1129
|
+
* binding attaches when the plugin is there (again for another document), and redraws only
|
|
1130
|
+
* when the text differs. Call it in an injection context; it lets go when the caller goes.
|
|
1131
|
+
*
|
|
1132
|
+
* It follows after the render, not before: when typing starts, the look's frame around the
|
|
1133
|
+
* element stops being inert in that same render, and an element inside an inert one can't take
|
|
1134
|
+
* focus (the editor's `focus()` would do nothing, and the keys would go to the page).
|
|
1135
|
+
*/
|
|
1136
|
+
function bindTextBoxEditor(element, annotation, item, scale) {
|
|
1137
|
+
const follower = createTextBoxEditorFollower();
|
|
1138
|
+
afterRenderEffect({
|
|
1139
|
+
write: () => {
|
|
1140
|
+
const host = annotation();
|
|
1141
|
+
const next = item();
|
|
1142
|
+
const at = scale();
|
|
1143
|
+
untracked(() => follower.follow(element, host, next, at));
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
inject(DestroyRef).onDestroy(() => follower.detach());
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* The layer's own text box for one free text: the engine's text plate (the box inset by its
|
|
1150
|
+
* padding), styled like the box's body and laid out at a legible size (`freeTextBoxOf`). The
|
|
1151
|
+
* box's fill and border are the vector scene's, under it. It takes the pointer only while
|
|
1152
|
+
* someone types in it; otherwise clicks fall through to the shapes (select, move, resize).
|
|
1153
|
+
*/
|
|
1154
|
+
class EpdfFreeTextBox {
|
|
1155
|
+
/** The text box, as the plugin lists it for the page. */
|
|
1156
|
+
item = input.required(...(ngDevMode ? [{ debugName: "item", alias: 'epdfFreeTextBox' }] : [{ alias: 'epdfFreeTextBox' }]));
|
|
1157
|
+
/** The outline color while someone types: the chrome's `textOutline`, painted. */
|
|
1158
|
+
outline = input.required(...(ngDevMode ? [{ debugName: "outline" }] : []));
|
|
1159
|
+
page = injectPage('[epdfFreeTextBox]');
|
|
1160
|
+
pixels = pagePixelsOf(this.page);
|
|
1161
|
+
box = computed(() => freeTextBoxOf(this.item(), this.pixels(), this.outline()), ...(ngDevMode ? [{ debugName: "box" }] : []));
|
|
1162
|
+
constructor() {
|
|
1163
|
+
const annotation = annotationHostOf(this.page, '[epdfFreeTextBox]').capability;
|
|
1164
|
+
bindTextBoxEditor(inject(ElementRef).nativeElement, annotation, this.item, computed(() => this.box().scale));
|
|
1165
|
+
}
|
|
1166
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfFreeTextBox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1167
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfFreeTextBox, isStandalone: true, selector: "[epdfFreeTextBox]", inputs: { item: { classPropertyName: "item", publicName: "epdfFreeTextBox", isSignal: true, isRequired: true, transformFunction: null }, outline: { classPropertyName: "outline", publicName: "outline", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "style": "box().style" } }, ngImport: i0 });
|
|
1168
|
+
}
|
|
1169
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfFreeTextBox, decorators: [{
|
|
1170
|
+
type: Directive,
|
|
1171
|
+
args: [{
|
|
1172
|
+
selector: '[epdfFreeTextBox]',
|
|
1173
|
+
// Property by property: the editor's own line height on the element stays.
|
|
1174
|
+
host: { '[style]': 'box().style' },
|
|
1175
|
+
}]
|
|
1176
|
+
}], ctorParameters: () => [] });
|
|
1177
|
+
/**
|
|
1178
|
+
* Your own element as the editor of a text box you draw yourself, in a look
|
|
1179
|
+
* (`<ng-template [epdfAnnotation]="isTextBox" let-annotation>`), inside the element that draws
|
|
1180
|
+
* the box's frame:
|
|
1181
|
+
*
|
|
1182
|
+
* <div class="text-box" [class.editing]="editor.editing()">
|
|
1183
|
+
* <div [epdfRichTextEditor]="annotation" #editor="epdfRichTextEditor"></div>
|
|
1184
|
+
* </div>
|
|
1185
|
+
*
|
|
1186
|
+
* It draws the runs, turns typing back into runs, keeps the caret when the style changes, and
|
|
1187
|
+
* handles pasting, input methods and the format shortcuts; `text.toggleFormat()` and
|
|
1188
|
+
* `selection.update()` work on it as on the layer's own box. It gives the element the box's
|
|
1189
|
+
* body style (font, size, color and alignment), filling its parent at a legible size
|
|
1190
|
+
* (`ownTextBoxOf`), so draw the box's fill, border and padding on the parent; while someone
|
|
1191
|
+
* types it takes the pointer. Leave the element empty: the editor fills it.
|
|
1192
|
+
*/
|
|
1193
|
+
class EpdfRichTextEditor {
|
|
1194
|
+
/** The free text annotation this element edits: the template's `let-annotation`. */
|
|
1195
|
+
annotation = input.required(...(ngDevMode ? [{ debugName: "annotation", alias: 'epdfRichTextEditor' }] : [{ alias: 'epdfRichTextEditor' }]));
|
|
1196
|
+
page = injectPage('[epdfRichTextEditor]');
|
|
1197
|
+
host = annotationHostOf(this.page, '[epdfRichTextEditor]');
|
|
1198
|
+
lookScale = inject(EPDF_LOOK_SCALE, { optional: true });
|
|
1199
|
+
view = pageViewOf(this.page);
|
|
1200
|
+
pixels = pagePixelsOf(this.page);
|
|
1201
|
+
/** The annotation's text box, saved and idle too (this element draws its text), or null. */
|
|
1202
|
+
item = this.host.select((annotation) => annotation.getTextItem(this.annotation().ref, this.view()), null);
|
|
1203
|
+
/** Pixels per point where the element is: in a scaled look, at the annotation's 100% size. */
|
|
1204
|
+
scale = computed(() => textBoxEditorScaleOf(this.item(), this.pixels(), this.lookScale?.() ?? 1), ...(ngDevMode ? [{ debugName: "scale" }] : []));
|
|
1205
|
+
/** The element's style and the scale its text is laid out at, or null while there is no box. */
|
|
1206
|
+
own = computed(() => {
|
|
1207
|
+
const item = this.item();
|
|
1208
|
+
return item ? ownTextBoxOf(item, this.scale()) : null;
|
|
1209
|
+
}, ...(ngDevMode ? [{ debugName: "own" }] : []));
|
|
1210
|
+
/** True while someone types in the box. */
|
|
1211
|
+
editing = computed(() => this.item()?.editing ?? false, ...(ngDevMode ? [{ debugName: "editing" }] : []));
|
|
1212
|
+
constructor() {
|
|
1213
|
+
bindTextBoxEditor(inject(ElementRef).nativeElement, this.host.capability, this.item, computed(() => this.own()?.scale ?? this.scale()));
|
|
1214
|
+
}
|
|
1215
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfRichTextEditor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1216
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.4", type: EpdfRichTextEditor, isStandalone: true, selector: "[epdfRichTextEditor]", inputs: { annotation: { classPropertyName: "annotation", publicName: "epdfRichTextEditor", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "style": "own()?.style ?? null" } }, exportAs: ["epdfRichTextEditor"], ngImport: i0 });
|
|
1217
|
+
}
|
|
1218
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfRichTextEditor, decorators: [{
|
|
1219
|
+
type: Directive,
|
|
1220
|
+
args: [{
|
|
1221
|
+
selector: '[epdfRichTextEditor]',
|
|
1222
|
+
exportAs: 'epdfRichTextEditor',
|
|
1223
|
+
// Property by property: the editor's own line height on the element stays.
|
|
1224
|
+
host: { '[style]': 'own()?.style ?? null' },
|
|
1225
|
+
}]
|
|
1226
|
+
}], ctorParameters: () => [] });
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* `<epdf-annotation-layer>`: a page's annotations, the selection's outline and handles, the
|
|
1230
|
+
* tool's preview, and the text boxes being typed in. Put it in each page, above the rendered
|
|
1231
|
+
* page; while it's there, the render layer leaves the annotations out of the page's picture:
|
|
1232
|
+
*
|
|
1233
|
+
* <ng-template epdfPage>
|
|
1234
|
+
* <epdf-render-layer />
|
|
1235
|
+
* <epdf-annotation-layer />
|
|
1236
|
+
* </ng-template>
|
|
1237
|
+
*
|
|
1238
|
+
* Pure paint: it reads the plugin's items and chrome for the page and draws them. The pointer
|
|
1239
|
+
* reaches the plugin through the interaction hub (the Stage forwards it), and so does the
|
|
1240
|
+
* cursor. Each annotation is one drawing: its vector scene, the engine's baked picture, or a
|
|
1241
|
+
* look of yours from an `<ng-template [epdfAnnotation]>` inside the layer, which may keep the
|
|
1242
|
+
* layer's own drawing (`native`) and add to it. `<ng-template epdfHandle>` and
|
|
1243
|
+
* `<ng-template epdfRotationHandle>` draw the handles your way.
|
|
1244
|
+
*
|
|
1245
|
+
* What isn't Angular is `@embedpdf/web`'s, the same for every framework: which drawing each
|
|
1246
|
+
* annotation gets and the behaviors interactive looks register, the frames and chrome in
|
|
1247
|
+
* pixels, the scene as SVG elements, the text box editor, and the object URLs of baked pictures
|
|
1248
|
+
* and the stamp ghost.
|
|
1249
|
+
*/
|
|
1250
|
+
const NO_ITEMS = Object.freeze([]);
|
|
1251
|
+
const NO_TEXTS = Object.freeze([]);
|
|
1252
|
+
class EpdfAnnotationLayer {
|
|
1253
|
+
page = injectPage('<epdf-annotation-layer>');
|
|
1254
|
+
injector = inject(Injector);
|
|
1255
|
+
annotation = annotationHostOf(this.page, '<epdf-annotation-layer>');
|
|
1256
|
+
interaction = new CapabilityBinding(injectKernelHost('<epdf-annotation-layer>'), () => InteractionToken, () => this.page.documentId);
|
|
1257
|
+
render = new CapabilityBinding(injectKernelHost('<epdf-annotation-layer>'), () => RenderToken, () => this.page.documentId);
|
|
1258
|
+
chrome = injectChromeSettings();
|
|
1259
|
+
// ── your templates ───────────────────────────────────────────────────────────
|
|
1260
|
+
looks = contentChildren(EpdfAnnotationTemplate, ...(ngDevMode ? [{ debugName: "looks", descendants: true }] : [{ descendants: true }]));
|
|
1261
|
+
behaviors = contentChildren(EpdfAnnotationBehaviorTemplate, ...(ngDevMode ? [{ debugName: "behaviors", descendants: true }] : [{
|
|
1262
|
+
descendants: true,
|
|
1263
|
+
}]));
|
|
1264
|
+
handleTemplate = contentChild(EpdfHandleTemplate, ...(ngDevMode ? [{ debugName: "handleTemplate", descendants: true }] : [{ descendants: true }]));
|
|
1265
|
+
rotationHandleTemplate = contentChild(EpdfRotationHandleTemplate, ...(ngDevMode ? [{ debugName: "rotationHandleTemplate", descendants: true }] : [{
|
|
1266
|
+
descendants: true,
|
|
1267
|
+
}]));
|
|
1268
|
+
/**
|
|
1269
|
+
* The renderer entries, in the order the templates are written: the first look that matches
|
|
1270
|
+
* wins. The same array while every entry stays the same.
|
|
1271
|
+
*/
|
|
1272
|
+
renderers = computed(() => [
|
|
1273
|
+
...this.behaviors().map((template) => template.renderer()),
|
|
1274
|
+
...this.looks().map((template) => template.renderer()),
|
|
1275
|
+
].filter((entry) => entry !== null), ...(ngDevMode ? [{ debugName: "renderers", equal: (left, right) => left.length === right.length && left.every((entry, index) => entry === right[index]) }] : [{
|
|
1276
|
+
equal: (left, right) => left.length === right.length && left.every((entry, index) => entry === right[index]),
|
|
1277
|
+
}]));
|
|
1278
|
+
/** Goes up when the behaviors register again, so each drawing is decided again. */
|
|
1279
|
+
registrations = signal(0, ...(ngDevMode ? [{ debugName: "registrations" }] : []));
|
|
1280
|
+
// ── what the page shows ──────────────────────────────────────────────────────
|
|
1281
|
+
view = pageViewOf(this.page);
|
|
1282
|
+
pixels = pagePixelsOf(this.page);
|
|
1283
|
+
items = this.annotation.select((annotation) => annotation.listPageItems(this.page.ref, this.view()), NO_ITEMS);
|
|
1284
|
+
allTexts = this.annotation.select((annotation) => annotation.listTextItems(this.page.ref, this.view()), NO_TEXTS);
|
|
1285
|
+
/** The active tool: an interactive look's function is asked about it, so a tool change redraws. */
|
|
1286
|
+
activeToolId = this.interaction.select((interaction) => interaction.getActiveToolId() ?? '', '');
|
|
1287
|
+
/** The pictures' URLs, by annotation and state; `null` until the page's first ones arrive. */
|
|
1288
|
+
urls = signal(null, ...(ngDevMode ? [{ debugName: "urls" }] : []));
|
|
1289
|
+
/** The pictures shown stay valid until the next ones are shown. */
|
|
1290
|
+
shown = createShownUrls();
|
|
1291
|
+
// A move or a turn leaves the epoch as it is (the same pixels, placed elsewhere), and live
|
|
1292
|
+
// gestures don't touch it: nothing loads mid-drag.
|
|
1293
|
+
appearanceEpoch = this.annotation.select((annotation) => annotation.getAppearanceEpoch(this.page.ref), '');
|
|
1294
|
+
// Zoom steps inside one rung of the document's appearance lattice bake nothing again.
|
|
1295
|
+
// Without the render plugin, only this session's pictures load.
|
|
1296
|
+
appearanceScale = this.render.select((render) => render.getAppearanceScale(this.page.transform().renderScale), 0);
|
|
1297
|
+
bakeScale = computed(() => this.render.capability() ? this.appearanceScale() : this.page.transform().renderScale, ...(ngDevMode ? [{ debugName: "bakeScale" }] : []));
|
|
1298
|
+
/** The text box being typed in, by key: while it is, a look's editor takes the keys. */
|
|
1299
|
+
editingKey = computed(() => editingTextKeyOf(this.allTexts(), annotationKey), ...(ngDevMode ? [{ debugName: "editingKey" }] : []));
|
|
1300
|
+
/** One injector per look, by item: kept while the item is drawn, so the look is never made again. */
|
|
1301
|
+
lookInjectors = new Map();
|
|
1302
|
+
drawings = computed(() => {
|
|
1303
|
+
this.activeToolId();
|
|
1304
|
+
this.registrations();
|
|
1305
|
+
const annotation = this.annotation.capability();
|
|
1306
|
+
const renderers = this.renderers();
|
|
1307
|
+
const looks = this.looks();
|
|
1308
|
+
const behaviors = this.behaviors();
|
|
1309
|
+
const pixels = this.pixels();
|
|
1310
|
+
const urls = this.urls();
|
|
1311
|
+
const editingKey = this.editingKey();
|
|
1312
|
+
const drawings = this.items().map((item) => {
|
|
1313
|
+
const frame = frameInPixels(item.frame, pixels);
|
|
1314
|
+
const state = item.annotation?.appearanceState ?? null;
|
|
1315
|
+
const drawn = {
|
|
1316
|
+
item,
|
|
1317
|
+
frame,
|
|
1318
|
+
shows: pictureOrPainting({
|
|
1319
|
+
pending: item.source !== 'baked',
|
|
1320
|
+
picture: urls && shownAppearance(urls, item.id, state),
|
|
1321
|
+
version: item.pictureVersion ?? 0,
|
|
1322
|
+
loaded: urls !== null,
|
|
1323
|
+
paint: () => (hasLiveDrawing(item) ? item : null),
|
|
1324
|
+
}),
|
|
1325
|
+
appearance: urls && bakedAppearanceOf(urls, item.id, state),
|
|
1326
|
+
look: null,
|
|
1327
|
+
owner: null,
|
|
1328
|
+
interactive: false,
|
|
1329
|
+
inert: false,
|
|
1330
|
+
};
|
|
1331
|
+
const record = item.annotation;
|
|
1332
|
+
if (!record || !annotation)
|
|
1333
|
+
return drawn;
|
|
1334
|
+
// Ownership beats looks: an engaged behavior's template is authoritative (a form
|
|
1335
|
+
// plugin's controls own their DOM); looks apply only to what the layer owns, and draw
|
|
1336
|
+
// without the pointer. While its text box is typed in, a look's editor takes the keys.
|
|
1337
|
+
const typing = editingKey !== null && annotationKey(record.ref) === editingKey;
|
|
1338
|
+
const drawing = annotationDrawingOf(record, annotation, renderers, typing);
|
|
1339
|
+
switch (drawing.kind) {
|
|
1340
|
+
case 'native':
|
|
1341
|
+
return { ...drawn, inert: drawing.inert };
|
|
1342
|
+
case 'owned': {
|
|
1343
|
+
const template = behaviors.find((behavior) => behavior.renderer() === drawing.entry);
|
|
1344
|
+
return template ? { ...drawn, owner: { template, annotation: record } } : drawn;
|
|
1345
|
+
}
|
|
1346
|
+
case 'look': {
|
|
1347
|
+
const template = looks.find((look) => look.renderer() === drawing.entry);
|
|
1348
|
+
if (!template)
|
|
1349
|
+
return drawn;
|
|
1350
|
+
const scaled = drawing.entry.scale !== false;
|
|
1351
|
+
return {
|
|
1352
|
+
...drawn,
|
|
1353
|
+
look: {
|
|
1354
|
+
template,
|
|
1355
|
+
annotation: record,
|
|
1356
|
+
frame: lookFrameOf(frame, scaled),
|
|
1357
|
+
scaled,
|
|
1358
|
+
injector: this.lookInjectorOf(item.id),
|
|
1359
|
+
},
|
|
1360
|
+
interactive: drawing.interactive,
|
|
1361
|
+
inert: drawing.inert,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
// Forget the injectors of looks no longer drawn: a look drawn again starts afresh.
|
|
1367
|
+
const drawnLooks = new Set(drawings.flatMap((drawn) => (drawn.look ? [drawn.item.id] : [])));
|
|
1368
|
+
for (const id of this.lookInjectors.keys()) {
|
|
1369
|
+
if (!drawnLooks.has(id))
|
|
1370
|
+
this.lookInjectors.delete(id);
|
|
1371
|
+
}
|
|
1372
|
+
return drawings;
|
|
1373
|
+
}, ...(ngDevMode ? [{ debugName: "drawings" }] : []));
|
|
1374
|
+
/** How much the layer scales each look, by item: 1 for a look drawn at its size on screen. */
|
|
1375
|
+
lookScales = computed(() => {
|
|
1376
|
+
const scales = new Map();
|
|
1377
|
+
for (const drawn of this.drawings()) {
|
|
1378
|
+
if (drawn.look)
|
|
1379
|
+
scales.set(drawn.item.id, drawn.look.scaled ? drawn.frame.scale : 1);
|
|
1380
|
+
}
|
|
1381
|
+
return scales;
|
|
1382
|
+
}, ...(ngDevMode ? [{ debugName: "lookScales" }] : []));
|
|
1383
|
+
/** The text boxes the layer draws: not the ones a look of yours draws, which edits them there. */
|
|
1384
|
+
texts = computed(() => layerTextBoxesOf(this.allTexts(), this.items(), this.renderers(), annotationKey), ...(ngDevMode ? [{ debugName: "texts" }] : []));
|
|
1385
|
+
textOutline = computed(() => annotationChromePaint(this.chrome.chrome(), this.chrome.accent()).textOutline, ...(ngDevMode ? [{ debugName: "textOutline" }] : []));
|
|
1386
|
+
// ── the armed stamp's ghost ──────────────────────────────────────────────────
|
|
1387
|
+
imageGhost = this.annotation.select((annotation) => annotation.getImageGhost(this.page.ref), null);
|
|
1388
|
+
armedStamp = this.annotation.select((annotation) => annotation.getArmedStamp(), null, Object.is);
|
|
1389
|
+
/**
|
|
1390
|
+
* The ghost is a picture of vector artwork, sharp at one size: the size class that covers the
|
|
1391
|
+
* box's device width. The plugin keeps one picture per class, so a zoom never renders per frame.
|
|
1392
|
+
*/
|
|
1393
|
+
ghostBucket = computed(() => {
|
|
1394
|
+
const ghost = this.imageGhost();
|
|
1395
|
+
return ghost ? previewBucket(ghost.box.width * this.page.transform().renderScale) : 0;
|
|
1396
|
+
}, ...(ngDevMode ? [{ debugName: "ghostBucket" }] : []));
|
|
1397
|
+
ghostUrl = signal(null, ...(ngDevMode ? [{ debugName: "ghostUrl" }] : []));
|
|
1398
|
+
ghost = computed(() => {
|
|
1399
|
+
const ghost = this.imageGhost();
|
|
1400
|
+
const url = this.ghostUrl();
|
|
1401
|
+
if (!ghost || !url)
|
|
1402
|
+
return null;
|
|
1403
|
+
return {
|
|
1404
|
+
url,
|
|
1405
|
+
box: rectInPixels(ghost.box, this.pixels()),
|
|
1406
|
+
opacity: ghostOpacity(ghost.opacity),
|
|
1407
|
+
rotation: ghost.rot,
|
|
1408
|
+
};
|
|
1409
|
+
}, ...(ngDevMode ? [{ debugName: "ghost" }] : []));
|
|
1410
|
+
constructor() {
|
|
1411
|
+
// While it's here, the page's picture leaves the annotations to it.
|
|
1412
|
+
paintsPagePart(() => this.page.ref, 'annotations');
|
|
1413
|
+
this.registerBehaviors();
|
|
1414
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID)))
|
|
1415
|
+
return;
|
|
1416
|
+
inject(DestroyRef).onDestroy(() => this.shown.release());
|
|
1417
|
+
this.loadAppearances();
|
|
1418
|
+
this.loadGhost();
|
|
1419
|
+
}
|
|
1420
|
+
/** The injector a look's template gets for one item: the look's scale, followed live. */
|
|
1421
|
+
lookInjectorOf(id) {
|
|
1422
|
+
let injector = this.lookInjectors.get(id);
|
|
1423
|
+
if (!injector) {
|
|
1424
|
+
const scale = computed(() => this.lookScales().get(id) ?? 1, ...(ngDevMode ? [{ debugName: "scale" }] : []));
|
|
1425
|
+
injector = Injector.create({
|
|
1426
|
+
providers: [{ provide: EPDF_LOOK_SCALE, useValue: scale }],
|
|
1427
|
+
parent: this.injector,
|
|
1428
|
+
});
|
|
1429
|
+
this.lookInjectors.set(id, injector);
|
|
1430
|
+
}
|
|
1431
|
+
return injector;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* An interactive look takes the pointer through a behavior the plugin knows, registered once
|
|
1435
|
+
* per document and look however many pages show the layer (`@embedpdf/web` counts them). The
|
|
1436
|
+
* looks are the same entries on every page, so keep the functions you give `epdfAnnotation`
|
|
1437
|
+
* and `epdfAnnotationInteractive` in fields: a function made anew registers again.
|
|
1438
|
+
*/
|
|
1439
|
+
registerBehaviors() {
|
|
1440
|
+
effect((onCleanup) => {
|
|
1441
|
+
const annotation = this.annotation.capability();
|
|
1442
|
+
const renderers = this.renderers();
|
|
1443
|
+
if (annotation && renderers.length > 0) {
|
|
1444
|
+
onCleanup(untracked(() => registerRendererBehaviors(annotation, renderers, () => untracked(this.activeToolId))));
|
|
1445
|
+
}
|
|
1446
|
+
this.registrations.update((count) => count + 1);
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* The baked pictures, loaded again when the page's appearance epoch or the bake scale changes;
|
|
1451
|
+
* the ones shown stay valid until the next are shown.
|
|
1452
|
+
*/
|
|
1453
|
+
loadAppearances() {
|
|
1454
|
+
effect((onCleanup) => {
|
|
1455
|
+
const annotation = this.annotation.capability();
|
|
1456
|
+
const scale = this.bakeScale();
|
|
1457
|
+
this.appearanceEpoch();
|
|
1458
|
+
if (!annotation || !scale)
|
|
1459
|
+
return;
|
|
1460
|
+
const page = this.page.ref;
|
|
1461
|
+
onCleanup(untracked(() => loadAppearanceUrls(this.shown, (abort) => annotation.renderAppearances(page, scale, abort), annotationKey, (urls) => this.urls.set(urls))));
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* The armed stamp's ghost picture, for its size class. The previous class's picture stays up
|
|
1466
|
+
* until this one is there, so a zoom across a class boundary never flickers; a disarm drops it.
|
|
1467
|
+
*/
|
|
1468
|
+
loadGhost() {
|
|
1469
|
+
effect((onCleanup) => {
|
|
1470
|
+
const annotation = this.annotation.capability();
|
|
1471
|
+
const bucket = this.ghostBucket();
|
|
1472
|
+
this.armedStamp();
|
|
1473
|
+
if (!annotation || !bucket) {
|
|
1474
|
+
this.ghostUrl.set(null);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
onCleanup(untracked(() => loadObjectUrl(() => annotation.renderArmedStampPreview(bucket), (url) => this.ghostUrl.set(url))));
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationLayer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1481
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfAnnotationLayer, isStandalone: true, selector: "epdf-annotation-layer", host: { styleAttribute: "position: absolute; inset: 0; display: block; pointer-events: none" }, queries: [{ propertyName: "looks", predicate: EpdfAnnotationTemplate, descendants: true, isSignal: true }, { propertyName: "behaviors", predicate: EpdfAnnotationBehaviorTemplate, descendants: true, isSignal: true }, { propertyName: "handleTemplate", first: true, predicate: EpdfHandleTemplate, descendants: true, isSignal: true }, { propertyName: "rotationHandleTemplate", first: true, predicate: EpdfRotationHandleTemplate, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1482
|
+
@for (drawn of drawings(); track drawn.item.id) {
|
|
1483
|
+
<!-- The layer's own drawing of it, which a look may keep: its \`native\`. -->
|
|
1484
|
+
<ng-template #native>
|
|
1485
|
+
<epdf-annotation-native [item]="drawn.item" [shows]="drawn.shows" />
|
|
1486
|
+
</ng-template>
|
|
1487
|
+
@if (drawn.owner; as owner) {
|
|
1488
|
+
<!-- A sibling plugin's controls place themselves in the layer and take the input;
|
|
1489
|
+
the annotation's own drawing keeps its frame. -->
|
|
1490
|
+
<ng-template #framedNative>
|
|
1491
|
+
<div [epdfAnnotationFrame]="drawn.frame" [frameBlend]="drawn.item.blend">
|
|
1492
|
+
<ng-container [ngTemplateOutlet]="native" />
|
|
1493
|
+
</div>
|
|
1494
|
+
</ng-template>
|
|
1495
|
+
<ng-container
|
|
1496
|
+
[ngTemplateOutlet]="owner.template.template"
|
|
1497
|
+
[ngTemplateOutletContext]="{
|
|
1498
|
+
$implicit: owner.annotation,
|
|
1499
|
+
annotation: owner.annotation,
|
|
1500
|
+
item: drawn.item,
|
|
1501
|
+
page: page,
|
|
1502
|
+
native: framedNative,
|
|
1503
|
+
hovered: drawn.item.hovered ?? false,
|
|
1504
|
+
selected: drawn.item.selected,
|
|
1505
|
+
interactive: true
|
|
1506
|
+
}"
|
|
1507
|
+
/>
|
|
1508
|
+
} @else {
|
|
1509
|
+
@if (drawn.look; as look) {
|
|
1510
|
+
<div
|
|
1511
|
+
[epdfAnnotationFrame]="drawn.frame"
|
|
1512
|
+
[frameBlend]="drawn.item.blend"
|
|
1513
|
+
[frameInteractive]="drawn.interactive"
|
|
1514
|
+
[frameInert]="drawn.inert"
|
|
1515
|
+
>
|
|
1516
|
+
<ng-template #lookTemplate>
|
|
1517
|
+
<ng-container
|
|
1518
|
+
[ngTemplateOutlet]="look.template.template"
|
|
1519
|
+
[ngTemplateOutletInjector]="look.injector"
|
|
1520
|
+
[ngTemplateOutletContext]="{
|
|
1521
|
+
$implicit: look.annotation,
|
|
1522
|
+
annotation: look.annotation,
|
|
1523
|
+
frame: look.frame,
|
|
1524
|
+
native: native,
|
|
1525
|
+
appearance: drawn.appearance,
|
|
1526
|
+
hovered: drawn.item.hovered ?? false,
|
|
1527
|
+
selected: drawn.item.selected,
|
|
1528
|
+
interactive: drawn.interactive
|
|
1529
|
+
}"
|
|
1530
|
+
/>
|
|
1531
|
+
</ng-template>
|
|
1532
|
+
@if (look.scaled) {
|
|
1533
|
+
<!-- Drawn at the annotation's 100% size, scaled with the page as a whole. -->
|
|
1534
|
+
<div
|
|
1535
|
+
style="position: absolute; left: 0; top: 0; transform-origin: 0 0"
|
|
1536
|
+
[style.width.px]="drawn.frame.design.width"
|
|
1537
|
+
[style.height.px]="drawn.frame.design.height"
|
|
1538
|
+
[style.transform]="'scale(' + drawn.frame.scale + ')'"
|
|
1539
|
+
>
|
|
1540
|
+
<ng-container [ngTemplateOutlet]="lookTemplate" />
|
|
1541
|
+
</div>
|
|
1542
|
+
} @else {
|
|
1543
|
+
<ng-container [ngTemplateOutlet]="lookTemplate" />
|
|
1544
|
+
}
|
|
1545
|
+
</div>
|
|
1546
|
+
} @else {
|
|
1547
|
+
<!-- Inert when an engaged behavior has no template of yours: its plugin owns the
|
|
1548
|
+
input (a link's anchor takes the click), and the annotation keeps its own look. -->
|
|
1549
|
+
<div
|
|
1550
|
+
[epdfAnnotationFrame]="drawn.frame"
|
|
1551
|
+
[frameBlend]="drawn.item.blend"
|
|
1552
|
+
[frameInert]="drawn.inert"
|
|
1553
|
+
>
|
|
1554
|
+
<ng-container [ngTemplateOutlet]="native" />
|
|
1555
|
+
</div>
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
@for (text of texts(); track text.id) {
|
|
1560
|
+
<div [epdfFreeTextBox]="text" [outline]="textOutline()"></div>
|
|
1561
|
+
}
|
|
1562
|
+
@if (ghost(); as ghost) {
|
|
1563
|
+
<!-- The armed stamp, see-through, in the box a click would place it in. -->
|
|
1564
|
+
<img
|
|
1565
|
+
alt=""
|
|
1566
|
+
draggable="false"
|
|
1567
|
+
style="position: absolute; max-width: none; max-height: none; pointer-events: none; transform-origin: center"
|
|
1568
|
+
[attr.src]="ghost.url"
|
|
1569
|
+
[style.left.px]="ghost.box.left"
|
|
1570
|
+
[style.top.px]="ghost.box.top"
|
|
1571
|
+
[style.width.px]="ghost.box.width"
|
|
1572
|
+
[style.height.px]="ghost.box.height"
|
|
1573
|
+
[style.opacity]="ghost.opacity"
|
|
1574
|
+
[style.transform]="ghost.rotation ? 'rotate(' + ghost.rotation + 'deg)' : null"
|
|
1575
|
+
/>
|
|
1576
|
+
}
|
|
1577
|
+
<epdf-annotation-chrome
|
|
1578
|
+
[handle]="handleTemplate()?.template ?? null"
|
|
1579
|
+
[rotationHandle]="rotationHandleTemplate()?.template ?? null"
|
|
1580
|
+
/>
|
|
1581
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: EpdfAnnotationFrame, selector: "[epdfAnnotationFrame]", inputs: ["epdfAnnotationFrame", "frameBlend", "frameInteractive", "frameInert"] }, { kind: "component", type: EpdfAnnotationNative, selector: "epdf-annotation-native", inputs: ["item", "shows"] }, { kind: "component", type: EpdfAnnotationChrome, selector: "epdf-annotation-chrome", inputs: ["handle", "rotationHandle"] }, { kind: "directive", type: EpdfFreeTextBox, selector: "[epdfFreeTextBox]", inputs: ["epdfFreeTextBox", "outline"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1582
|
+
}
|
|
1583
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationLayer, decorators: [{
|
|
1584
|
+
type: Component,
|
|
1585
|
+
args: [{
|
|
1586
|
+
selector: 'epdf-annotation-layer',
|
|
1587
|
+
imports: [
|
|
1588
|
+
NgTemplateOutlet,
|
|
1589
|
+
EpdfAnnotationFrame,
|
|
1590
|
+
EpdfAnnotationNative,
|
|
1591
|
+
EpdfAnnotationChrome,
|
|
1592
|
+
EpdfFreeTextBox,
|
|
1593
|
+
],
|
|
1594
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1595
|
+
host: { style: 'position: absolute; inset: 0; display: block; pointer-events: none' },
|
|
1596
|
+
template: `
|
|
1597
|
+
@for (drawn of drawings(); track drawn.item.id) {
|
|
1598
|
+
<!-- The layer's own drawing of it, which a look may keep: its \`native\`. -->
|
|
1599
|
+
<ng-template #native>
|
|
1600
|
+
<epdf-annotation-native [item]="drawn.item" [shows]="drawn.shows" />
|
|
1601
|
+
</ng-template>
|
|
1602
|
+
@if (drawn.owner; as owner) {
|
|
1603
|
+
<!-- A sibling plugin's controls place themselves in the layer and take the input;
|
|
1604
|
+
the annotation's own drawing keeps its frame. -->
|
|
1605
|
+
<ng-template #framedNative>
|
|
1606
|
+
<div [epdfAnnotationFrame]="drawn.frame" [frameBlend]="drawn.item.blend">
|
|
1607
|
+
<ng-container [ngTemplateOutlet]="native" />
|
|
1608
|
+
</div>
|
|
1609
|
+
</ng-template>
|
|
1610
|
+
<ng-container
|
|
1611
|
+
[ngTemplateOutlet]="owner.template.template"
|
|
1612
|
+
[ngTemplateOutletContext]="{
|
|
1613
|
+
$implicit: owner.annotation,
|
|
1614
|
+
annotation: owner.annotation,
|
|
1615
|
+
item: drawn.item,
|
|
1616
|
+
page: page,
|
|
1617
|
+
native: framedNative,
|
|
1618
|
+
hovered: drawn.item.hovered ?? false,
|
|
1619
|
+
selected: drawn.item.selected,
|
|
1620
|
+
interactive: true
|
|
1621
|
+
}"
|
|
1622
|
+
/>
|
|
1623
|
+
} @else {
|
|
1624
|
+
@if (drawn.look; as look) {
|
|
1625
|
+
<div
|
|
1626
|
+
[epdfAnnotationFrame]="drawn.frame"
|
|
1627
|
+
[frameBlend]="drawn.item.blend"
|
|
1628
|
+
[frameInteractive]="drawn.interactive"
|
|
1629
|
+
[frameInert]="drawn.inert"
|
|
1630
|
+
>
|
|
1631
|
+
<ng-template #lookTemplate>
|
|
1632
|
+
<ng-container
|
|
1633
|
+
[ngTemplateOutlet]="look.template.template"
|
|
1634
|
+
[ngTemplateOutletInjector]="look.injector"
|
|
1635
|
+
[ngTemplateOutletContext]="{
|
|
1636
|
+
$implicit: look.annotation,
|
|
1637
|
+
annotation: look.annotation,
|
|
1638
|
+
frame: look.frame,
|
|
1639
|
+
native: native,
|
|
1640
|
+
appearance: drawn.appearance,
|
|
1641
|
+
hovered: drawn.item.hovered ?? false,
|
|
1642
|
+
selected: drawn.item.selected,
|
|
1643
|
+
interactive: drawn.interactive
|
|
1644
|
+
}"
|
|
1645
|
+
/>
|
|
1646
|
+
</ng-template>
|
|
1647
|
+
@if (look.scaled) {
|
|
1648
|
+
<!-- Drawn at the annotation's 100% size, scaled with the page as a whole. -->
|
|
1649
|
+
<div
|
|
1650
|
+
style="position: absolute; left: 0; top: 0; transform-origin: 0 0"
|
|
1651
|
+
[style.width.px]="drawn.frame.design.width"
|
|
1652
|
+
[style.height.px]="drawn.frame.design.height"
|
|
1653
|
+
[style.transform]="'scale(' + drawn.frame.scale + ')'"
|
|
1654
|
+
>
|
|
1655
|
+
<ng-container [ngTemplateOutlet]="lookTemplate" />
|
|
1656
|
+
</div>
|
|
1657
|
+
} @else {
|
|
1658
|
+
<ng-container [ngTemplateOutlet]="lookTemplate" />
|
|
1659
|
+
}
|
|
1660
|
+
</div>
|
|
1661
|
+
} @else {
|
|
1662
|
+
<!-- Inert when an engaged behavior has no template of yours: its plugin owns the
|
|
1663
|
+
input (a link's anchor takes the click), and the annotation keeps its own look. -->
|
|
1664
|
+
<div
|
|
1665
|
+
[epdfAnnotationFrame]="drawn.frame"
|
|
1666
|
+
[frameBlend]="drawn.item.blend"
|
|
1667
|
+
[frameInert]="drawn.inert"
|
|
1668
|
+
>
|
|
1669
|
+
<ng-container [ngTemplateOutlet]="native" />
|
|
1670
|
+
</div>
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
@for (text of texts(); track text.id) {
|
|
1675
|
+
<div [epdfFreeTextBox]="text" [outline]="textOutline()"></div>
|
|
1676
|
+
}
|
|
1677
|
+
@if (ghost(); as ghost) {
|
|
1678
|
+
<!-- The armed stamp, see-through, in the box a click would place it in. -->
|
|
1679
|
+
<img
|
|
1680
|
+
alt=""
|
|
1681
|
+
draggable="false"
|
|
1682
|
+
style="position: absolute; max-width: none; max-height: none; pointer-events: none; transform-origin: center"
|
|
1683
|
+
[attr.src]="ghost.url"
|
|
1684
|
+
[style.left.px]="ghost.box.left"
|
|
1685
|
+
[style.top.px]="ghost.box.top"
|
|
1686
|
+
[style.width.px]="ghost.box.width"
|
|
1687
|
+
[style.height.px]="ghost.box.height"
|
|
1688
|
+
[style.opacity]="ghost.opacity"
|
|
1689
|
+
[style.transform]="ghost.rotation ? 'rotate(' + ghost.rotation + 'deg)' : null"
|
|
1690
|
+
/>
|
|
1691
|
+
}
|
|
1692
|
+
<epdf-annotation-chrome
|
|
1693
|
+
[handle]="handleTemplate()?.template ?? null"
|
|
1694
|
+
[rotationHandle]="rotationHandleTemplate()?.template ?? null"
|
|
1695
|
+
/>
|
|
1696
|
+
`,
|
|
1697
|
+
}]
|
|
1698
|
+
}], ctorParameters: () => [] });
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Your UI next to annotations, inside `<epdf-stage>`, each on `<epdf-anchored>`, so it moves
|
|
1702
|
+
* with the pages in the same frame and a click in it never reaches the page:
|
|
1703
|
+
*
|
|
1704
|
+
* <epdf-annotation-menu> your menu next to the selection
|
|
1705
|
+
* <epdf-annotation-draft-menu #menu> buttons over a polygon being drawn (`menu.draft()`)
|
|
1706
|
+
* <epdf-annotation-rotation-badge> the angle next to the pointer while turning
|
|
1707
|
+
*
|
|
1708
|
+
* They only place things: what's in them is yours, built from `inject(EpdfAnnotation)`.
|
|
1709
|
+
*/
|
|
1710
|
+
/** The annotation plugin of the document this part of the template talks to. */
|
|
1711
|
+
function annotationInScope(what) {
|
|
1712
|
+
return new CapabilityBinding(injectKernelHost(what), () => AnnotationToken$1, injectDocumentScope());
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Your menu next to the selection: one menu, also for a selection over several pages, clear of
|
|
1716
|
+
* the rotation handle. It hides while the selection is dragged, resized or turned. Put it inside
|
|
1717
|
+
* `<epdf-stage>`, next to the page template, with what the menu holds between its tags:
|
|
1718
|
+
*
|
|
1719
|
+
* <epdf-annotation-menu placement="bottom">
|
|
1720
|
+
* <button (click)="annotation.selection.delete()">Delete</button>
|
|
1721
|
+
* </epdf-annotation-menu>
|
|
1722
|
+
*/
|
|
1723
|
+
class EpdfAnnotationMenu {
|
|
1724
|
+
/** Which side of the selection: `'top'` (the default), `'bottom'`, `'left'` or `'right'`, or a corner. */
|
|
1725
|
+
placement = input('top', ...(ngDevMode ? [{ debugName: "placement" }] : []));
|
|
1726
|
+
/** Screen pixels between the selection and the menu. */
|
|
1727
|
+
gap = input(15, ...(ngDevMode ? [{ debugName: "gap" }] : []));
|
|
1728
|
+
annotation = annotationInScope('<epdf-annotation-menu>');
|
|
1729
|
+
projection = inject(EPDF_PROJECTOR, { optional: true });
|
|
1730
|
+
/**
|
|
1731
|
+
* The selection's anchor as its page is shown: the rotation handle sits a fixed number of
|
|
1732
|
+
* pixels off the box, so where it is on the page depends on the zoom. Read again with the
|
|
1733
|
+
* projection, in the same pass as the pages.
|
|
1734
|
+
*/
|
|
1735
|
+
selection = this.annotation.select((annotation) => {
|
|
1736
|
+
const anchor = annotation.selection.getAnchor();
|
|
1737
|
+
if (!anchor)
|
|
1738
|
+
return null;
|
|
1739
|
+
this.projection?.revision();
|
|
1740
|
+
const view = this.projection?.projector()?.viewEnv(anchor.page);
|
|
1741
|
+
return view ? annotation.getSelectionAnchorIn(view) : anchor;
|
|
1742
|
+
}, null, sameSelectionAnchor);
|
|
1743
|
+
anchor = computed(() => {
|
|
1744
|
+
const anchor = this.selection();
|
|
1745
|
+
if (!anchor)
|
|
1746
|
+
return null;
|
|
1747
|
+
return {
|
|
1748
|
+
page: anchor.page,
|
|
1749
|
+
bounds: anchor.bounds,
|
|
1750
|
+
...(anchor.rotationHandle ? { avoid: [anchor.rotationHandle] } : {}),
|
|
1751
|
+
};
|
|
1752
|
+
}, ...(ngDevMode ? [{ debugName: "anchor" }] : []));
|
|
1753
|
+
constructor() {
|
|
1754
|
+
if (!this.projection)
|
|
1755
|
+
throw outsideStageError('<epdf-annotation-menu>');
|
|
1756
|
+
}
|
|
1757
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1758
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.2.4", type: EpdfAnnotationMenu, isStandalone: true, selector: "epdf-annotation-menu", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
1759
|
+
<epdf-anchored [anchor]="anchor()" [placement]="placement()" [gap]="gap()">
|
|
1760
|
+
<ng-content />
|
|
1761
|
+
</epdf-anchored>
|
|
1762
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EpdfAnchored, selector: "epdf-anchored", inputs: ["anchor", "placement", "gap", "pinned"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1763
|
+
}
|
|
1764
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationMenu, decorators: [{
|
|
1765
|
+
type: Component,
|
|
1766
|
+
args: [{
|
|
1767
|
+
selector: 'epdf-annotation-menu',
|
|
1768
|
+
imports: [EpdfAnchored],
|
|
1769
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1770
|
+
template: `
|
|
1771
|
+
<epdf-anchored [anchor]="anchor()" [placement]="placement()" [gap]="gap()">
|
|
1772
|
+
<ng-content />
|
|
1773
|
+
</epdf-anchored>
|
|
1774
|
+
`,
|
|
1775
|
+
}]
|
|
1776
|
+
}], ctorParameters: () => [] });
|
|
1777
|
+
/**
|
|
1778
|
+
* Buttons over a polygon or polyline being drawn, which helps on touch screens. `draft()` is the
|
|
1779
|
+
* shape being drawn (`pointCount`, `minPoints`, `canFinish`, …), or null; the verbs are the
|
|
1780
|
+
* annotation service's `draft.finish()` and `draft.cancel()`:
|
|
1781
|
+
*
|
|
1782
|
+
* <epdf-annotation-draft-menu #menu="epdfAnnotationDraftMenu">
|
|
1783
|
+
* <button [disabled]="!menu.draft()?.canFinish" (click)="annotation.draft.finish()">Done</button>
|
|
1784
|
+
* </epdf-annotation-draft-menu>
|
|
1785
|
+
*/
|
|
1786
|
+
class EpdfAnnotationDraftMenu {
|
|
1787
|
+
/** Which side of the shape being drawn. */
|
|
1788
|
+
placement = input('top', ...(ngDevMode ? [{ debugName: "placement" }] : []));
|
|
1789
|
+
/** Screen pixels between the shape and the menu. */
|
|
1790
|
+
gap = input(8, ...(ngDevMode ? [{ debugName: "gap" }] : []));
|
|
1791
|
+
annotation = annotationInScope('<epdf-annotation-draft-menu>');
|
|
1792
|
+
/** The shape being drawn: where it is, its points so far and whether it can end; or null. */
|
|
1793
|
+
draft = this.annotation.select((annotation) => annotation.draft.get(), null, sameCreationDraftAnchor);
|
|
1794
|
+
constructor() {
|
|
1795
|
+
if (!inject(EPDF_PROJECTOR, { optional: true })) {
|
|
1796
|
+
throw outsideStageError('<epdf-annotation-draft-menu>');
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationDraftMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1800
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.2.4", type: EpdfAnnotationDraftMenu, isStandalone: true, selector: "epdf-annotation-draft-menu", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["epdfAnnotationDraftMenu"], ngImport: i0, template: `
|
|
1801
|
+
<epdf-anchored [anchor]="draft()" [placement]="placement()" [gap]="gap()">
|
|
1802
|
+
<ng-content />
|
|
1803
|
+
</epdf-anchored>
|
|
1804
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EpdfAnchored, selector: "epdf-anchored", inputs: ["anchor", "placement", "gap", "pinned"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1805
|
+
}
|
|
1806
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationDraftMenu, decorators: [{
|
|
1807
|
+
type: Component,
|
|
1808
|
+
args: [{
|
|
1809
|
+
selector: 'epdf-annotation-draft-menu',
|
|
1810
|
+
exportAs: 'epdfAnnotationDraftMenu',
|
|
1811
|
+
imports: [EpdfAnchored],
|
|
1812
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1813
|
+
template: `
|
|
1814
|
+
<epdf-anchored [anchor]="draft()" [placement]="placement()" [gap]="gap()">
|
|
1815
|
+
<ng-content />
|
|
1816
|
+
</epdf-anchored>
|
|
1817
|
+
`,
|
|
1818
|
+
}]
|
|
1819
|
+
}], ctorParameters: () => [] });
|
|
1820
|
+
/**
|
|
1821
|
+
* The angle next to the pointer while a selection is turned, upright however the page is shown.
|
|
1822
|
+
* It shows the angle in the `chrome.readout` colors when `chrome.readout.enabled`; give it an
|
|
1823
|
+
* `<ng-template epdfRotationBadge let-rotation>` to draw it your way. `rotation()` is the turn
|
|
1824
|
+
* in progress (`page`, `at`, `angle`), or null.
|
|
1825
|
+
*/
|
|
1826
|
+
class EpdfAnnotationRotationBadge {
|
|
1827
|
+
/** Which side of the pointer. */
|
|
1828
|
+
placement = input('right', ...(ngDevMode ? [{ debugName: "placement" }] : []));
|
|
1829
|
+
/** Screen pixels between the pointer and the badge. */
|
|
1830
|
+
gap = input(16, ...(ngDevMode ? [{ debugName: "gap" }] : []));
|
|
1831
|
+
annotation = annotationInScope('<epdf-annotation-rotation-badge>');
|
|
1832
|
+
settings = injectChromeSettings();
|
|
1833
|
+
custom = contentChild(EpdfRotationBadgeTemplate, ...(ngDevMode ? [{ debugName: "custom" }] : []));
|
|
1834
|
+
/** The turn in progress: its page, the pointer's point and the angle so far; or null. */
|
|
1835
|
+
rotation = this.annotation.select((annotation) => annotation.selection.getRotationAnchor(), null, sameRotationAnchor);
|
|
1836
|
+
/** Shown while turning, with a template of yours or with the readout on. */
|
|
1837
|
+
shown = computed(() => {
|
|
1838
|
+
const rotation = this.rotation();
|
|
1839
|
+
return rotation && (this.custom() || this.settings.chrome().readout.enabled) ? rotation : null;
|
|
1840
|
+
}, ...(ngDevMode ? [{ debugName: "shown" }] : []));
|
|
1841
|
+
/** A box of no size at the pointer. */
|
|
1842
|
+
anchor = computed(() => {
|
|
1843
|
+
const rotation = this.rotation();
|
|
1844
|
+
return rotation
|
|
1845
|
+
? { page: rotation.page, bounds: { ...rotation.at, width: 0, height: 0 } }
|
|
1846
|
+
: null;
|
|
1847
|
+
}, ...(ngDevMode ? [{ debugName: "anchor" }] : []));
|
|
1848
|
+
fontFamily = paintDefault('font-mono');
|
|
1849
|
+
background = computed(() => paint('readout-background', this.settings.chrome().readout.background), ...(ngDevMode ? [{ debugName: "background" }] : []));
|
|
1850
|
+
color = computed(() => paint('readout-color', this.settings.chrome().readout.color), ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
1851
|
+
constructor() {
|
|
1852
|
+
if (!inject(EPDF_PROJECTOR, { optional: true })) {
|
|
1853
|
+
throw outsideStageError('<epdf-annotation-rotation-badge>');
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationRotationBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1857
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.4", type: EpdfAnnotationRotationBadge, isStandalone: true, selector: "epdf-annotation-rotation-badge", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "custom", first: true, predicate: EpdfRotationBadgeTemplate, descendants: true, isSignal: true }], exportAs: ["epdfAnnotationRotationBadge"], ngImport: i0, template: `
|
|
1858
|
+
@if (shown(); as rotation) {
|
|
1859
|
+
<epdf-anchored [anchor]="anchor()" [placement]="placement()" [gap]="gap()">
|
|
1860
|
+
@if (custom(); as custom) {
|
|
1861
|
+
<ng-container
|
|
1862
|
+
[ngTemplateOutlet]="custom.template"
|
|
1863
|
+
[ngTemplateOutletContext]="{ $implicit: rotation }"
|
|
1864
|
+
/>
|
|
1865
|
+
} @else {
|
|
1866
|
+
<div
|
|
1867
|
+
style="pointer-events: none; white-space: nowrap; border-radius: 4px; padding: 2px 6px; font-size: 12px"
|
|
1868
|
+
[style.font-family]="fontFamily"
|
|
1869
|
+
[style.background]="background()"
|
|
1870
|
+
[style.color]="color()"
|
|
1871
|
+
>
|
|
1872
|
+
{{ rotation.angle }}°
|
|
1873
|
+
</div>
|
|
1874
|
+
}
|
|
1875
|
+
</epdf-anchored>
|
|
1876
|
+
}
|
|
1877
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EpdfAnchored, selector: "epdf-anchored", inputs: ["anchor", "placement", "gap", "pinned"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1878
|
+
}
|
|
1879
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: EpdfAnnotationRotationBadge, decorators: [{
|
|
1880
|
+
type: Component,
|
|
1881
|
+
args: [{
|
|
1882
|
+
selector: 'epdf-annotation-rotation-badge',
|
|
1883
|
+
exportAs: 'epdfAnnotationRotationBadge',
|
|
1884
|
+
imports: [EpdfAnchored, NgTemplateOutlet],
|
|
1885
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1886
|
+
template: `
|
|
1887
|
+
@if (shown(); as rotation) {
|
|
1888
|
+
<epdf-anchored [anchor]="anchor()" [placement]="placement()" [gap]="gap()">
|
|
1889
|
+
@if (custom(); as custom) {
|
|
1890
|
+
<ng-container
|
|
1891
|
+
[ngTemplateOutlet]="custom.template"
|
|
1892
|
+
[ngTemplateOutletContext]="{ $implicit: rotation }"
|
|
1893
|
+
/>
|
|
1894
|
+
} @else {
|
|
1895
|
+
<div
|
|
1896
|
+
style="pointer-events: none; white-space: nowrap; border-radius: 4px; padding: 2px 6px; font-size: 12px"
|
|
1897
|
+
[style.font-family]="fontFamily"
|
|
1898
|
+
[style.background]="background()"
|
|
1899
|
+
[style.color]="color()"
|
|
1900
|
+
>
|
|
1901
|
+
{{ rotation.angle }}°
|
|
1902
|
+
</div>
|
|
1903
|
+
}
|
|
1904
|
+
</epdf-anchored>
|
|
1905
|
+
}
|
|
1906
|
+
`,
|
|
1907
|
+
}]
|
|
1908
|
+
}], ctorParameters: () => [] });
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* @embedpdf/angular/annotation: drawing, selecting, changing and saving annotations.
|
|
1912
|
+
*
|
|
1913
|
+
* withAnnotation(options) the plugin, for provideEmbedPdf() (with withInteraction())
|
|
1914
|
+
* withFilePicker(provider?) the file dialog behind the stamp and attachment tools
|
|
1915
|
+
* inject(EpdfAnnotation) the API, the State table as signals, the events as
|
|
1916
|
+
* streams, and `watch()`, `anchorOf()`, `tools.defaultsOf()`
|
|
1917
|
+
* inject(EpdfComments) threads and replies: `threads()`, `threadOf(ref)`, `reply()`, …
|
|
1918
|
+
* <epdf-annotation-layer> the annotations, the selection and the text boxes, in each page
|
|
1919
|
+
* <ng-template [epdfAnnotation]> your own look for some annotations
|
|
1920
|
+
* <ng-template epdfHandle>, epdfRotationHandle your own handles
|
|
1921
|
+
* [epdfRichTextEditor] your element as a text box's editor, in a look
|
|
1922
|
+
* <epdf-annotation-menu> your menu next to the selection, inside <epdf-stage>
|
|
1923
|
+
* <epdf-annotation-draft-menu> buttons over a polygon being drawn
|
|
1924
|
+
* <epdf-annotation-rotation-badge> the angle while a selection is turned
|
|
1925
|
+
*/
|
|
1926
|
+
// The plugin's types and helpers, so app code has one import for the feature.
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Generated bundle index. Do not edit.
|
|
1930
|
+
*/
|
|
1931
|
+
|
|
1932
|
+
export { EpdfAnnotation, EpdfAnnotationBehaviorTemplate, EpdfAnnotationDraftMenu, EpdfAnnotationLayer, EpdfAnnotationMenu, EpdfAnnotationRotationBadge, EpdfAnnotationTemplate, EpdfComments, EpdfHandleTemplate, EpdfRichTextEditor, EpdfRotationBadgeTemplate, EpdfRotationHandleTemplate, filePickerProvider, withAnnotation, withFilePicker };
|
|
1933
|
+
//# sourceMappingURL=embedpdf-angular-annotation.mjs.map
|