kviewer 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -1
- package/dist/runtime/annotation/engine/editor/editor.d.ts +17 -0
- package/dist/runtime/annotation/engine/editor/editor.js +44 -15
- package/dist/runtime/annotation/engine/tools/signature.js +1 -1
- package/dist/runtime/annotation/engine/tools/stamp.d.ts +8 -0
- package/dist/runtime/annotation/engine/tools/stamp.js +27 -2
- package/dist/runtime/annotation/engine/types.d.ts +25 -0
- package/dist/runtime/annotation/pdf-export/export.js +0 -3
- package/dist/runtime/annotation/utils/signature_image.d.ts +26 -0
- package/dist/runtime/annotation/utils/signature_image.js +20 -0
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/FormFieldLayer.vue +6 -1
- package/dist/runtime/components/Viewer.d.vue.ts +56 -2
- package/dist/runtime/components/Viewer.vue +68 -2
- package/dist/runtime/components/Viewer.vue.d.ts +56 -2
- package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -9
- package/dist/runtime/components/ViewerTabs.vue +10 -2
- package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -9
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +4 -2
- package/dist/runtime/components/form-fields/FormSignatureField.vue +15 -2
- package/dist/runtime/components/modals/FreeTextModal.vue +1 -1
- package/dist/runtime/components/modals/SignatureDrawModal.vue +170 -4
- package/dist/runtime/components/tools/ToolProperties.vue +98 -101
- package/dist/runtime/composables/useFormFields.d.ts +17 -1
- package/dist/runtime/composables/useFormFields.js +48 -9
- package/dist/runtime/composables/useViewerSearch.js +1 -1
- package/dist/runtime/composables/useViewerState.d.ts +1 -0
- package/dist/runtime/composables/useViewerState.js +12 -13
- package/dist/runtime/embed/bridge-client.d.ts +19 -3
- package/dist/runtime/embed/bridge-client.js +24 -0
- package/dist/runtime/embed/bridge-host.d.ts +10 -1
- package/dist/runtime/embed/bridge-host.js +20 -0
- package/dist/runtime/embed/protocol.d.ts +28 -1
- package/dist/runtime/i18n/messages.d.ts +16 -2
- package/dist/runtime/i18n/messages.js +16 -2
- package/dist/runtime/public-types.d.ts +1 -1
- package/dist/runtime/toolbar-tools.d.ts +1 -1
- package/dist/runtime/toolbar-tools.js +8 -0
- package/package.json +18 -18
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ By default the toolbar is fully built-in. Pass `tools` to choose which buttons r
|
|
|
88
88
|
</template>
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
`separator`/`spacer` are layout primitives (`spacer` right-aligns what follows). `tools` controls buttons only — it never gates programmatic `state.selectTool(...)` access. Form-edit mode is API-only (`v-model:form-edit-mode` / `setFormEditMode()` / `toggleFormEditMode()`); there is no built-in toggle button. See the [toolbar configuration docs](https://
|
|
91
|
+
`separator`/`spacer` are layout primitives (`spacer` right-aligns what follows). `tools` controls buttons only — it never gates programmatic `state.selectTool(...)` access. Form-edit mode is API-only (`v-model:form-edit-mode` / `setFormEditMode()` / `toggleFormEditMode()`); there is no built-in toggle button. See the [toolbar configuration docs](https://kviewer.wrench.kabema-digital.de/api/kviewer#configuring-the-toolbar) for the full tool-ID list and the array that reproduces the default toolbar.
|
|
92
92
|
|
|
93
93
|
## Viewer Ref API
|
|
94
94
|
|
|
@@ -185,6 +185,18 @@ A page counts as read once its top edge scrolls into view (computed from geometr
|
|
|
185
185
|
|
|
186
186
|
The same surface is available over the [embed bridge](src/runtime/embed): `client.allPagesRead()`, `client.getViewedPages()`, `client.resetViewedPages()`, and the `all-pages-read` / `viewedPages-changed` events via `client.on(...)`.
|
|
187
187
|
|
|
188
|
+
## Migrating to v0.3.0
|
|
189
|
+
|
|
190
|
+
The built-in **click-to-sign** flow on signature fields is now **opt-in** (previously always on). Without it, signature fields render as passive markers (signature icon + a localized "Signature" label) and clicking them does nothing — so an embedding e-sign product's own signing process stays the only entry point.
|
|
191
|
+
|
|
192
|
+
To restore the previous behavior:
|
|
193
|
+
|
|
194
|
+
```vue
|
|
195
|
+
<KViewer :source="pdfUrl" click-to-sign />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
or at runtime via `viewer.setClickToSign(true)` / the embed bridge's `client.setClickToSign(true)`. A field's `promptText` now only applies to the click-to-sign placeholder; signature values set programmatically always render.
|
|
199
|
+
|
|
188
200
|
## Development
|
|
189
201
|
|
|
190
202
|
<details>
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -37,7 +37,17 @@ const module$1 = defineNuxtModule({
|
|
|
37
37
|
"konva",
|
|
38
38
|
"pdf-lib",
|
|
39
39
|
"pdfjs-dist/legacy/build/pdf.mjs",
|
|
40
|
-
"pdfjs-dist/web/pdf_viewer.mjs"
|
|
40
|
+
"pdfjs-dist/web/pdf_viewer.mjs",
|
|
41
|
+
// unicode-font.ts registers @pdf-lib/fontkit for Unicode font
|
|
42
|
+
// embedding. Its ESM build does `import pako from 'pako'`, but pako 1.x
|
|
43
|
+
// is CJS with no `default` export — so unless fontkit is pre-bundled
|
|
44
|
+
// (where esbuild applies CJS interop and synthesizes the default), the
|
|
45
|
+
// browser receives the raw pako index.js and throws "does not provide
|
|
46
|
+
// an export named 'default'". The `kviewer >` prefix anchors resolution
|
|
47
|
+
// through this module, so it works even when pnpm leaves fontkit
|
|
48
|
+
// un-hoisted in the consumer (a bare '@pdf-lib/fontkit' entry would not
|
|
49
|
+
// resolve from the consumer root and Vite would silently skip it).
|
|
50
|
+
"kviewer > @pdf-lib/fontkit"
|
|
41
51
|
);
|
|
42
52
|
});
|
|
43
53
|
nuxt.options.runtimeConfig.public.kviewer = {
|
|
@@ -33,6 +33,8 @@ export declare abstract class Editor {
|
|
|
33
33
|
currentShapeGroup: IShapeGroup | null;
|
|
34
34
|
protected getStylusModeEnabled?: () => boolean;
|
|
35
35
|
protected freehandGroupingDelay: number;
|
|
36
|
+
private lastPointerDownAt;
|
|
37
|
+
private lastPointerDownPos;
|
|
36
38
|
static MinSize: number;
|
|
37
39
|
constructor({ userName, konvaStage, pageNumber, annotation, onAdd, editorType, onChange, getStylusModeEnabled, freehandGroupingDelay, }: IEditorOptions & {
|
|
38
40
|
editorType: AnnotationType;
|
|
@@ -40,6 +42,13 @@ export declare abstract class Editor {
|
|
|
40
42
|
private dispatchAddEvent;
|
|
41
43
|
private dispatchChangedEvent;
|
|
42
44
|
protected enableEditMode(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Returns true when this pointer-down is a duplicate of the immediately
|
|
47
|
+
* preceding one (same spot, within {@link DUPLICATE_DOWN_MS}). See the
|
|
48
|
+
* constant docs: this catches the iOS ghost click and stylus contact
|
|
49
|
+
* bounce that would otherwise commit two overlapping annotations per tap.
|
|
50
|
+
*/
|
|
51
|
+
private isDuplicatePointerDown;
|
|
43
52
|
protected disableEditMode(): void;
|
|
44
53
|
protected getBgLayer(konvaStage?: Konva.Stage): Konva.Layer;
|
|
45
54
|
protected delShapeGroup(id: string): void;
|
|
@@ -62,6 +71,14 @@ export declare abstract class Editor {
|
|
|
62
71
|
activate(konvaStage: Konva.Stage, annotation: IAnnotationType): void;
|
|
63
72
|
rebindEvents(): void;
|
|
64
73
|
addSerializedGroupToLayer(konvaStage: Konva.Stage, konvaString: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Attach a rehydrated group to the page layer AND register it in
|
|
76
|
+
* shapeGroupStore. Registration is what lets the painter resolve this
|
|
77
|
+
* editor as the group's owner (findEditorForGroupId) — without it,
|
|
78
|
+
* drag/transform changes to the group are silently never persisted.
|
|
79
|
+
* Subclasses that override addSerializedGroupToLayer must call this.
|
|
80
|
+
*/
|
|
81
|
+
protected adoptHydratedGroup(konvaStage: Konva.Stage, ghostGroup: Konva.Group): void;
|
|
65
82
|
deleteGroup(id: string, konvaStage: Konva.Stage): void;
|
|
66
83
|
updateStyle(annotationStore: IAnnotationStore, style: IAnnotationStyle): void;
|
|
67
84
|
static Timer: Map<number, {
|
|
@@ -2,6 +2,8 @@ import Konva from "konva";
|
|
|
2
2
|
import { formatTimestamp, generateUUID } from "../utils.js";
|
|
3
3
|
import { SHAPE_GROUP_NAME } from "../const.js";
|
|
4
4
|
import { isStylusTouch } from "../input-device.js";
|
|
5
|
+
const DUPLICATE_DOWN_MS = 400;
|
|
6
|
+
const DUPLICATE_DOWN_DISTANCE = 12;
|
|
5
7
|
export class Editor {
|
|
6
8
|
userName;
|
|
7
9
|
id;
|
|
@@ -15,6 +17,8 @@ export class Editor {
|
|
|
15
17
|
currentShapeGroup;
|
|
16
18
|
getStylusModeEnabled;
|
|
17
19
|
freehandGroupingDelay;
|
|
20
|
+
lastPointerDownAt = 0;
|
|
21
|
+
lastPointerDownPos = null;
|
|
18
22
|
static MinSize = 8;
|
|
19
23
|
constructor({
|
|
20
24
|
userName,
|
|
@@ -87,7 +91,9 @@ export class Editor {
|
|
|
87
91
|
enableEditMode() {
|
|
88
92
|
const stylusEnabled = this.getStylusModeEnabled?.() ?? false;
|
|
89
93
|
this.konvaStage.on("mousedown", (e) => {
|
|
90
|
-
if (e.evt.button
|
|
94
|
+
if (e.evt.button !== 0) return;
|
|
95
|
+
if (this.isDuplicatePointerDown()) return;
|
|
96
|
+
this.mouseDownHandler(e);
|
|
91
97
|
});
|
|
92
98
|
this.konvaStage.on("mousemove", (e) => {
|
|
93
99
|
this.mouseMoveHandler(e);
|
|
@@ -97,29 +103,42 @@ export class Editor {
|
|
|
97
103
|
});
|
|
98
104
|
this.konvaStage.on("touchstart", (e) => {
|
|
99
105
|
if (e.evt.touches.length !== 1) return;
|
|
100
|
-
if (stylusEnabled)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
106
|
+
if (stylusEnabled && !isStylusTouch(e.evt)) return;
|
|
107
|
+
e.evt.preventDefault();
|
|
108
|
+
if (this.isDuplicatePointerDown()) return;
|
|
104
109
|
this.mouseDownHandler(e);
|
|
105
110
|
});
|
|
106
111
|
this.konvaStage.on("touchmove", (e) => {
|
|
107
112
|
if (e.evt.touches.length !== 1) return;
|
|
108
|
-
if (stylusEnabled)
|
|
109
|
-
|
|
110
|
-
if (this.isPainting) e.evt.preventDefault();
|
|
111
|
-
}
|
|
113
|
+
if (stylusEnabled && !isStylusTouch(e.evt)) return;
|
|
114
|
+
if (this.isPainting) e.evt.preventDefault();
|
|
112
115
|
this.mouseMoveHandler(e);
|
|
113
116
|
});
|
|
114
117
|
this.konvaStage.on("touchend", (e) => {
|
|
115
|
-
if (stylusEnabled)
|
|
116
|
-
|
|
117
|
-
const isStylus = touch && "touchType" in touch && touch.touchType === "stylus";
|
|
118
|
-
if (!isStylus) return;
|
|
119
|
-
}
|
|
118
|
+
if (stylusEnabled && !isStylusTouch(e.evt)) return;
|
|
119
|
+
e.evt.preventDefault();
|
|
120
120
|
this.mouseUpHandler(e);
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns true when this pointer-down is a duplicate of the immediately
|
|
125
|
+
* preceding one (same spot, within {@link DUPLICATE_DOWN_MS}). See the
|
|
126
|
+
* constant docs: this catches the iOS ghost click and stylus contact
|
|
127
|
+
* bounce that would otherwise commit two overlapping annotations per tap.
|
|
128
|
+
*/
|
|
129
|
+
isDuplicatePointerDown() {
|
|
130
|
+
const pos = this.konvaStage.getRelativePointerPosition();
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const prevAt = this.lastPointerDownAt;
|
|
133
|
+
const prevPos = this.lastPointerDownPos;
|
|
134
|
+
this.lastPointerDownAt = now;
|
|
135
|
+
this.lastPointerDownPos = pos ? { x: pos.x, y: pos.y } : null;
|
|
136
|
+
if (!pos || !prevPos) return false;
|
|
137
|
+
if (now - prevAt > DUPLICATE_DOWN_MS) return false;
|
|
138
|
+
const dx = pos.x - prevPos.x;
|
|
139
|
+
const dy = pos.y - prevPos.y;
|
|
140
|
+
return dx * dx + dy * dy <= DUPLICATE_DOWN_DISTANCE * DUPLICATE_DOWN_DISTANCE;
|
|
141
|
+
}
|
|
123
142
|
disableEditMode() {
|
|
124
143
|
this.isPainting = false;
|
|
125
144
|
this.konvaStage.off("click mousedown mousemove mouseup touchstart touchmove touchend");
|
|
@@ -211,11 +230,21 @@ export class Editor {
|
|
|
211
230
|
this.enableEditMode();
|
|
212
231
|
}
|
|
213
232
|
addSerializedGroupToLayer(konvaStage, konvaString) {
|
|
214
|
-
this.konvaStage = konvaStage;
|
|
215
233
|
const ghostGroup = Konva.Node.create(konvaString);
|
|
216
234
|
if (ghostGroup instanceof Konva.Container) {
|
|
217
235
|
ghostGroup.find((node) => node instanceof Konva.Shape).forEach((shape) => shape.strokeScaleEnabled(true));
|
|
218
236
|
}
|
|
237
|
+
this.adoptHydratedGroup(konvaStage, ghostGroup);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Attach a rehydrated group to the page layer AND register it in
|
|
241
|
+
* shapeGroupStore. Registration is what lets the painter resolve this
|
|
242
|
+
* editor as the group's owner (findEditorForGroupId) — without it,
|
|
243
|
+
* drag/transform changes to the group are silently never persisted.
|
|
244
|
+
* Subclasses that override addSerializedGroupToLayer must call this.
|
|
245
|
+
*/
|
|
246
|
+
adoptHydratedGroup(konvaStage, ghostGroup) {
|
|
247
|
+
this.konvaStage = konvaStage;
|
|
219
248
|
const id = ghostGroup.id();
|
|
220
249
|
this.getBgLayer(konvaStage).add(ghostGroup);
|
|
221
250
|
if (this.shapeGroupStore.has(id)) return;
|
|
@@ -9,6 +9,14 @@ export declare class EditorStamp extends Editor {
|
|
|
9
9
|
private stampHeight;
|
|
10
10
|
constructor(editorOptions: IEditorOptions, defaultStampUrl: string | null);
|
|
11
11
|
private createCursorImg;
|
|
12
|
+
/**
|
|
13
|
+
* True when an identical stamp (same source image) already sits under the
|
|
14
|
+
* tap point. Guards against dropping a second, overlapping copy on a spot
|
|
15
|
+
* that's already stamped — e.g. a flaky stylus misses a tap, the user
|
|
16
|
+
* re-sweeps the column, and re-hits cells that already carry a cross.
|
|
17
|
+
* Distance is compared in stage-content units so it is zoom-independent.
|
|
18
|
+
*/
|
|
19
|
+
private hasIdenticalStampNear;
|
|
12
20
|
protected mouseDownHandler(e: KonvaEventObject<MouseEvent | TouchEvent>): void;
|
|
13
21
|
activateWithStamp(konvaStage: Konva.Stage, annotation: IAnnotationType, stampUrl: string | null, stampWidth?: number, stampHeight?: number): void;
|
|
14
22
|
addSerializedGroupToLayer(konvaStage: Konva.Stage, konvaString: string): void;
|
|
@@ -21,12 +21,37 @@ export class EditorStamp extends Editor {
|
|
|
21
21
|
const scale = this.konvaStage?.scale()?.x ?? 1;
|
|
22
22
|
showCursorPreview(this.stampUrl, Math.round(baseSize * scale));
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* True when an identical stamp (same source image) already sits under the
|
|
26
|
+
* tap point. Guards against dropping a second, overlapping copy on a spot
|
|
27
|
+
* that's already stamped — e.g. a flaky stylus misses a tap, the user
|
|
28
|
+
* re-sweeps the column, and re-hits cells that already carry a cross.
|
|
29
|
+
* Distance is compared in stage-content units so it is zoom-independent.
|
|
30
|
+
*/
|
|
31
|
+
hasIdenticalStampNear(pos, size) {
|
|
32
|
+
const threshold = Math.max(8, size * 0.5);
|
|
33
|
+
const t2 = threshold * threshold;
|
|
34
|
+
const images = this.konvaStage.find("Image");
|
|
35
|
+
for (const img of images) {
|
|
36
|
+
if (img.getAttr("base64") !== this.stampUrl) continue;
|
|
37
|
+
const rect = img.getClientRect({ relativeTo: this.konvaStage });
|
|
38
|
+
const cx = rect.x + rect.width / 2;
|
|
39
|
+
const cy = rect.y + rect.height / 2;
|
|
40
|
+
const dx = cx - pos.x;
|
|
41
|
+
const dy = cy - pos.y;
|
|
42
|
+
if (dx * dx + dy * dy <= t2) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
24
46
|
mouseDownHandler(e) {
|
|
25
47
|
if (e.currentTarget !== this.konvaStage) return;
|
|
48
|
+
const pos = this.konvaStage.getRelativePointerPosition();
|
|
49
|
+
if (!pos) return;
|
|
50
|
+
const estimatedSize = this.stampWidth ?? this.stampHeight ?? 120;
|
|
51
|
+
if (this.hasIdenticalStampNear(pos, estimatedSize)) return;
|
|
26
52
|
this.stampImage = null;
|
|
27
53
|
this.currentShapeGroup = this.createShapeGroup();
|
|
28
54
|
this.getBgLayer().add(this.currentShapeGroup.konvaGroup);
|
|
29
|
-
const pos = this.konvaStage.getRelativePointerPosition();
|
|
30
55
|
Konva.Image.fromURL(this.stampUrl, async (image) => {
|
|
31
56
|
let newWidth;
|
|
32
57
|
let newHeight;
|
|
@@ -83,7 +108,7 @@ export class EditorStamp extends Editor {
|
|
|
83
108
|
oldText.moveToTop();
|
|
84
109
|
}
|
|
85
110
|
});
|
|
86
|
-
this.
|
|
111
|
+
this.adoptHydratedGroup(konvaStage, ghostGroup);
|
|
87
112
|
}
|
|
88
113
|
mouseMoveHandler() {
|
|
89
114
|
}
|
|
@@ -294,6 +294,31 @@ export interface AddFormFieldPayload {
|
|
|
294
294
|
/** 0 = left, 1 = center, 2 = right. */
|
|
295
295
|
textAlignment?: number;
|
|
296
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Preset properties applied to fields the user places via the placement
|
|
299
|
+
* tools. `pageNumber`/`rect`/`fieldType` come from the gesture;
|
|
300
|
+
* `fieldName`/`buttonValue` stay auto-generated so name uniqueness and
|
|
301
|
+
* radio grouping keep working — rename via the `field-placed` event and
|
|
302
|
+
* `updateFormField()` instead.
|
|
303
|
+
*/
|
|
304
|
+
export type PlacedFieldDefaultProps = Partial<Omit<AddFormFieldPayload, 'pageNumber' | 'rect' | 'fieldType' | 'fieldName' | 'buttonValue'>>;
|
|
305
|
+
/** Per-field-type presets for tool placements, keyed by {@link FormFieldType}. */
|
|
306
|
+
export type PlacedFieldDefaults = Partial<Record<FormFieldType, PlacedFieldDefaultProps>>;
|
|
307
|
+
/**
|
|
308
|
+
* Signed-state snapshot of one signature widget. `signed` is true when the
|
|
309
|
+
* widget holds a signature image applied in this session OR the source PDF
|
|
310
|
+
* already carries a digital signature for the field (`signedInSource`).
|
|
311
|
+
* Lets a host (e.g. an e-sign app) gate finalization on all fields being
|
|
312
|
+
* signed — see `getSignedStatus()` / the `signedStatus-changed` event.
|
|
313
|
+
*/
|
|
314
|
+
export interface SignatureFieldStatus {
|
|
315
|
+
id: string;
|
|
316
|
+
fieldName: string;
|
|
317
|
+
pageNumber: number;
|
|
318
|
+
signed: boolean;
|
|
319
|
+
/** Present when the field is tagged with a host role. */
|
|
320
|
+
roleId?: string;
|
|
321
|
+
}
|
|
297
322
|
export interface DetectedShapeRect {
|
|
298
323
|
x: number;
|
|
299
324
|
y: number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixel helpers for signature images uploaded from disk.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of DOM APIs (they take plain pixel buffers, not canvases) so
|
|
5
|
+
* the behaviour is unit-testable without a headless browser — the rest
|
|
6
|
+
* of the upload flow lives in SignatureDrawModal.vue.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Scanned or photographed signatures arrive on opaque white paper,
|
|
10
|
+
* which would cover page content once stamped onto a PDF. Map luminance
|
|
11
|
+
* to alpha so near-white goes fully transparent and the anti-aliased
|
|
12
|
+
* edges of the strokes fade out instead of leaving a hard halo.
|
|
13
|
+
*/
|
|
14
|
+
export declare const WHITE_CUTOFF = 240;
|
|
15
|
+
export declare const WHITE_SOFT_EDGE = 180;
|
|
16
|
+
/** Mutates `data` (RGBA, as returned by ImageData) in place. */
|
|
17
|
+
export declare function knockOutWhite(data: Uint8ClampedArray, cutoff?: number, softEdge?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Scale `width`×`height` down to fit inside `maxWidth`×`maxHeight`,
|
|
20
|
+
* preserving aspect ratio. Never scales up: a signature smaller than the
|
|
21
|
+
* box is left at its natural size rather than being blown up and blurred.
|
|
22
|
+
*/
|
|
23
|
+
export declare function fitWithin(width: number, height: number, maxWidth: number, maxHeight: number): {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const WHITE_CUTOFF = 240;
|
|
2
|
+
export const WHITE_SOFT_EDGE = 180;
|
|
3
|
+
export function knockOutWhite(data, cutoff = WHITE_CUTOFF, softEdge = WHITE_SOFT_EDGE) {
|
|
4
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
5
|
+
const luminance = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
6
|
+
if (luminance >= cutoff) {
|
|
7
|
+
data[i + 3] = 0;
|
|
8
|
+
} else if (luminance > softEdge) {
|
|
9
|
+
const ratio = (cutoff - luminance) / (cutoff - softEdge);
|
|
10
|
+
data[i + 3] = Math.round(data[i + 3] * ratio);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function fitWithin(width, height, maxWidth, maxHeight) {
|
|
15
|
+
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
|
|
16
|
+
return {
|
|
17
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
18
|
+
height: Math.max(1, Math.round(height * scale))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.kviewer-text-layer{left:0;line-height:1;overflow:hidden;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:1}.kviewer-text-layer--interactive{cursor:text;-webkit-user-select:text;-moz-user-select:text;user-select:text}.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer *{cursor:text!important}.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_selector_hover{cursor:pointer!important}.kviewer-form-layer{left:0;overflow:hidden;position:absolute;top:0}.kviewer-form-field{box-sizing:border-box;position:absolute}.kviewer-form-input{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 14%,transparent);border:1px solid transparent;box-sizing:border-box;color:#000;display:block;font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;height:100%;line-height:normal;margin:0;outline:none;padding:1px 2px;resize:none;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;vertical-align:top}.kviewer-form-input:hover{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 22%,transparent);border-color:rgba(0,0,0,.2)}.kviewer-form-input:focus{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 28%,transparent);border-color:var(--kvw-field-color,var(--color-primary,#3b82f6));outline:2px solid var(--kvw-field-color,var(--color-primary,#3b82f6))}.kviewer-form-uselect{align-items:center;background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 14%,transparent);border:1px solid transparent;border-radius:0;box-sizing:border-box;color:#000;cursor:pointer;display:flex;height:100%;line-height:1;padding:0 11px 0 2px;width:100%}.kviewer-form-uselect:hover{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 22%,transparent);border-color:rgba(0,0,0,.2)}.kviewer-form-uselect:focus-visible{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 28%,transparent);outline:2px solid var(--kvw-field-color,var(--color-primary,#3b82f6));outline-offset:-2px}.kviewer-form-uselect:disabled{cursor:default}.kviewer-form-listbox{overflow-y:auto;padding:0}.kviewer-form-listbox option{padding:1px 3px}.kviewer-form-editable-combo{height:100%;position:relative;width:100%}.kviewer-form-editable-combo input{height:100%;width:100%}.kviewer-form-checkbox,.kviewer-form-radio{align-items:center;background:#fff;box-sizing:border-box;cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-checkbox input,.kviewer-form-radio input{accent-color:var(--kvw-field-color,var(--color-primary,#3b82f6));cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-checkbox--detected{background:transparent}.kviewer-form-checkbox--circle{border-radius:50%}.kviewer-form-checkbox__icon{color:#1a1a1a;height:65%;width:65%}.kviewer-form-checkbox--circle .kviewer-form-checkbox__icon{height:50%;width:50%}.kviewer-form-radio input{accent-color:var(--kvw-field-color,var(--color-primary,#3b82f6));cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-button{align-items:center;background:#c8c8c8;border:1px solid rgba(0,0,0,.35);box-sizing:border-box;color:#000;cursor:pointer;display:flex;font-family:inherit;font-weight:700;height:100%;justify-content:center;line-height:1;padding:0 6px;text-align:center;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.kviewer-form-button:hover{background:#bdbdbd}.kviewer-form-button:active{background:#b0b0b0}.kviewer-form-signature{align-items:center;background:rgba(255,255,200,.15);border:1px dashed rgba(0,0,0,.2);cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-signature:hover{background:color-mix(in srgb,var(--kvw-field-color,#3b82f6) 8%,transparent);border-color:var(--kvw-field-color,var(--color-primary,#3b82f6))}.kviewer-form-signature--filled{background:transparent;border-color:transparent;border-style:solid}.kviewer-form-signature__preview{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain}.kviewer-form-signature__placeholder{color:rgba(0,0,0,.4);font-size:10px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}
|
|
1
|
+
.kviewer-text-layer{left:0;line-height:1;overflow:hidden;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:1}.kviewer-text-layer--interactive{cursor:text;-webkit-user-select:text;-moz-user-select:text;user-select:text}.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer *{cursor:text!important}.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_selector_hover{cursor:pointer!important}.kviewer-form-layer{left:0;overflow:hidden;position:absolute;top:0}.kviewer-form-field{box-sizing:border-box;position:absolute}.kviewer-form-input{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 14%,transparent);border:1px solid transparent;box-sizing:border-box;color:#000;display:block;font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;height:100%;line-height:normal;margin:0;outline:none;padding:1px 2px;resize:none;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;vertical-align:top}.kviewer-form-input:hover{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 22%,transparent);border-color:rgba(0,0,0,.2)}.kviewer-form-input:focus{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 28%,transparent);border-color:var(--kvw-field-color,var(--color-primary,#3b82f6));outline:2px solid var(--kvw-field-color,var(--color-primary,#3b82f6))}.kviewer-form-uselect{align-items:center;background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 14%,transparent);border:1px solid transparent;border-radius:0;box-sizing:border-box;color:#000;cursor:pointer;display:flex;height:100%;line-height:1;padding:0 11px 0 2px;width:100%}.kviewer-form-uselect:hover{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 22%,transparent);border-color:rgba(0,0,0,.2)}.kviewer-form-uselect:focus-visible{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 28%,transparent);outline:2px solid var(--kvw-field-color,var(--color-primary,#3b82f6));outline-offset:-2px}.kviewer-form-uselect:disabled{cursor:default}.kviewer-form-listbox{overflow-y:auto;padding:0}.kviewer-form-listbox option{padding:1px 3px}.kviewer-form-editable-combo{height:100%;position:relative;width:100%}.kviewer-form-editable-combo input{height:100%;width:100%}.kviewer-form-checkbox,.kviewer-form-radio{align-items:center;background:#fff;box-sizing:border-box;cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-checkbox input,.kviewer-form-radio input{accent-color:var(--kvw-field-color,var(--color-primary,#3b82f6));cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-checkbox--detected{background:transparent}.kviewer-form-checkbox--circle{border-radius:50%}.kviewer-form-checkbox__icon{color:#1a1a1a;height:65%;width:65%}.kviewer-form-checkbox--circle .kviewer-form-checkbox__icon{height:50%;width:50%}.kviewer-form-radio input{accent-color:var(--kvw-field-color,var(--color-primary,#3b82f6));cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-button{align-items:center;background:#c8c8c8;border:1px solid rgba(0,0,0,.35);box-sizing:border-box;color:#000;cursor:pointer;display:flex;font-family:inherit;font-weight:700;height:100%;justify-content:center;line-height:1;padding:0 6px;text-align:center;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.kviewer-form-button:hover{background:#bdbdbd}.kviewer-form-button:active{background:#b0b0b0}.kviewer-form-signature{align-items:center;background:rgba(255,255,200,.15);border:1px dashed rgba(0,0,0,.2);cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-signature:hover{background:color-mix(in srgb,var(--kvw-field-color,#3b82f6) 8%,transparent);border-color:var(--kvw-field-color,var(--color-primary,#3b82f6))}.kviewer-form-signature--filled{background:transparent;border-color:transparent;border-style:solid}.kviewer-form-signature__preview{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain}.kviewer-form-signature__placeholder{color:rgba(0,0,0,.4);font-size:10px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kviewer-form-signature--static{cursor:default}.kviewer-form-signature--static:hover{background:rgba(255,255,200,.15);border-color:rgba(0,0,0,.2)}.kviewer-form-signature__marker{align-items:center;display:inline-flex;gap:4px}.kviewer-form-signature__marker-icon{flex:0 0 auto;height:12px;width:12px}
|
|
@@ -48,7 +48,12 @@ const state = useViewerState();
|
|
|
48
48
|
const fields = computed(() => formFields.getFieldsForPage(props.pageNumber));
|
|
49
49
|
const selectedFieldId = formFields.selectedPlacedFieldId;
|
|
50
50
|
watch(() => state.formEditMode.value, (enabled) => {
|
|
51
|
-
if (
|
|
51
|
+
if (enabled) return;
|
|
52
|
+
const id = formFields.selectedPlacedFieldId.value;
|
|
53
|
+
if (!id) return;
|
|
54
|
+
const def = formFields.getFieldById(id);
|
|
55
|
+
const keepsChrome = formFields.editablePlacedFields.value && def?.origin === "placed";
|
|
56
|
+
if (!keepsChrome) formFields.selectPlacedField(null);
|
|
52
57
|
});
|
|
53
58
|
watch(() => state.activeTool.value, (tool) => {
|
|
54
59
|
const isFormFieldTool = typeof tool === "number" && (tool === AnnotationType.FORM_TEXT || tool === AnnotationType.FORM_CHECKBOX || tool === AnnotationType.FORM_RADIO || tool === AnnotationType.FORM_SIGNATURE);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ViewMode } from '../composables/useViewerState.js';
|
|
2
|
-
import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
|
|
2
|
+
import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
|
|
3
3
|
import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
|
|
4
4
|
import type { ViewerMenuItem } from '../menu-items.js';
|
|
5
5
|
import type { ViewerToolEntry } from '../toolbar-tools.js';
|
|
@@ -35,6 +35,21 @@ type __VLS_Props = {
|
|
|
35
35
|
* drives this — typically by binding it to a role picker in its own
|
|
36
36
|
* UI. Supports v-model via `v-model:active-role-id`. */
|
|
37
37
|
activeRoleId?: string | null;
|
|
38
|
+
/** When true, clicking an unsigned signature field opens kviewer's own
|
|
39
|
+
* signing flow (saved signature or draw modal). Off by default so hosts
|
|
40
|
+
* that own the signing process (e.g. an embedding e-sign app) don't get
|
|
41
|
+
* a competing canvas — fields then render as passive markers. */
|
|
42
|
+
clickToSign?: boolean;
|
|
43
|
+
/** When true, user-placed form fields show selection chrome and can be
|
|
44
|
+
* moved/resized/deleted in hand mode even outside form-edit mode.
|
|
45
|
+
* Fields parsed from the source PDF are unaffected. */
|
|
46
|
+
editablePlacedFields?: boolean;
|
|
47
|
+
/** Per-field-type presets merged into every field the user places via
|
|
48
|
+
* the placement tools (e.g. a signature `promptText` or `lockAction`).
|
|
49
|
+
* `fieldName` stays auto-generated — rename via the `field-placed`
|
|
50
|
+
* event and `updateFormField()`. Programmatic `addFormField()` calls
|
|
51
|
+
* are not affected. */
|
|
52
|
+
fieldDefaults?: PlacedFieldDefaults;
|
|
38
53
|
/** Execute embedded JavaScript inside the PDF — field-level AA scripts
|
|
39
54
|
* (Mouse Up, Calculate, Format, Validate, Keystroke), document-level
|
|
40
55
|
* open actions, and page-level scripts. Powered by pdf.js's QuickJS
|
|
@@ -98,6 +113,32 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
98
113
|
removeFormField: (id: string) => void;
|
|
99
114
|
/** All form-field definitions (parsed, detected, and placed), flat. */
|
|
100
115
|
getFormFields: () => FormFieldDefinition[];
|
|
116
|
+
/** Replace the per-field-type presets applied to tool placements.
|
|
117
|
+
* Programmatic counterpart to the `fieldDefaults` prop. */
|
|
118
|
+
setFieldDefaults: (defaults: PlacedFieldDefaults) => void;
|
|
119
|
+
/** Enable/disable kviewer's built-in click-to-sign flow at runtime.
|
|
120
|
+
* Programmatic counterpart to the `clickToSign` prop. */
|
|
121
|
+
setClickToSign: (enabled: boolean) => void;
|
|
122
|
+
/** Subscribe to tool placements (imperative mirror of the
|
|
123
|
+
* `field-placed` event, used by the embed bridge). Returns an
|
|
124
|
+
* unsubscribe function. */
|
|
125
|
+
onFieldPlaced: (cb: (field: FormFieldDefinition) => void) => (() => void);
|
|
126
|
+
/** True once every signature field in the document is signed — via a
|
|
127
|
+
* value applied in this session or a digital signature already in the
|
|
128
|
+
* source PDF. Vacuously true when the document has no signature
|
|
129
|
+
* fields, so combine with `getSignatureFieldStatus().length` if you
|
|
130
|
+
* need "at least one". Use to gate e.g. envelope finalization. */
|
|
131
|
+
getSignedStatus: () => boolean;
|
|
132
|
+
/** Per-widget signed-state snapshot (id, fieldName, pageNumber,
|
|
133
|
+
* signed, roleId). */
|
|
134
|
+
getSignatureFieldStatus: () => SignatureFieldStatus[];
|
|
135
|
+
/** Subscribe to signed-status changes (imperative mirror of the
|
|
136
|
+
* `signed-status-changed` event, used by the embed bridge). Returns
|
|
137
|
+
* an unsubscribe function. */
|
|
138
|
+
onSignedStatusChanged: (cb: (payload: {
|
|
139
|
+
allSigned: boolean;
|
|
140
|
+
fields: SignatureFieldStatus[];
|
|
141
|
+
}) => void) => (() => void);
|
|
101
142
|
/** Reactive form-edit-mode flag — read with `.value`. */
|
|
102
143
|
formEditMode: import("vue").Ref<boolean, boolean>;
|
|
103
144
|
/** Programmatically enter or leave form-edit mode. */
|
|
@@ -114,20 +155,32 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
114
155
|
resetViewedPages: () => void;
|
|
115
156
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
116
157
|
"all-pages-read": () => any;
|
|
158
|
+
"field-placed": (field: FormFieldDefinition) => any;
|
|
117
159
|
"update:formEditMode": (value: boolean) => any;
|
|
118
160
|
"update:activeRoleId": (value: string | null) => any;
|
|
119
161
|
"update:viewedPages": (pages: number[]) => any;
|
|
162
|
+
"signed-status-changed": (payload: {
|
|
163
|
+
allSigned: boolean;
|
|
164
|
+
fields: SignatureFieldStatus[];
|
|
165
|
+
}) => any;
|
|
120
166
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
121
167
|
"onAll-pages-read"?: (() => any) | undefined;
|
|
168
|
+
"onField-placed"?: ((field: FormFieldDefinition) => any) | undefined;
|
|
122
169
|
"onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
|
|
123
170
|
"onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
|
|
124
171
|
"onUpdate:viewedPages"?: ((pages: number[]) => any) | undefined;
|
|
172
|
+
"onSigned-status-changed"?: ((payload: {
|
|
173
|
+
allSigned: boolean;
|
|
174
|
+
fields: SignatureFieldStatus[];
|
|
175
|
+
}) => any) | undefined;
|
|
125
176
|
}>, {
|
|
126
177
|
userName: string;
|
|
127
178
|
freehandGroupingDelay: number;
|
|
128
179
|
zoom: number;
|
|
180
|
+
clickToSign: boolean;
|
|
129
181
|
readonly: boolean;
|
|
130
182
|
active: boolean;
|
|
183
|
+
locale: KviewerLocale;
|
|
131
184
|
menuItems: ViewerMenuItem[];
|
|
132
185
|
tools: ViewerToolEntry[];
|
|
133
186
|
scripting: boolean;
|
|
@@ -138,7 +191,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
138
191
|
formEditMode: boolean;
|
|
139
192
|
roleColors: Record<string, string>;
|
|
140
193
|
activeRoleId: string | null;
|
|
141
|
-
|
|
194
|
+
editablePlacedFields: boolean;
|
|
195
|
+
fieldDefaults: PlacedFieldDefaults;
|
|
142
196
|
messages: KviewerMessageOverrides;
|
|
143
197
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
144
198
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -242,6 +242,9 @@ const props = defineProps({
|
|
|
242
242
|
formEditMode: { type: Boolean, required: false, default: void 0 },
|
|
243
243
|
roleColors: { type: Object, required: false, default: void 0 },
|
|
244
244
|
activeRoleId: { type: [String, null], required: false, default: void 0 },
|
|
245
|
+
clickToSign: { type: Boolean, required: false, default: false },
|
|
246
|
+
editablePlacedFields: { type: Boolean, required: false, default: false },
|
|
247
|
+
fieldDefaults: { type: Object, required: false, default: void 0 },
|
|
245
248
|
scripting: { type: Boolean, required: false, default: false },
|
|
246
249
|
menuItems: { type: Array, required: false, default: void 0 },
|
|
247
250
|
tools: { type: Array, required: false, default: void 0 },
|
|
@@ -252,7 +255,9 @@ const slots = useSlots();
|
|
|
252
255
|
const toolSlotNames = computed(
|
|
253
256
|
() => Object.keys(slots).filter((name) => name.startsWith("tool-"))
|
|
254
257
|
);
|
|
255
|
-
const emit = defineEmits(["update:formEditMode", "update:activeRoleId", "update:viewedPages", "all-pages-read"]);
|
|
258
|
+
const emit = defineEmits(["update:formEditMode", "update:activeRoleId", "update:viewedPages", "all-pages-read", "field-placed", "signed-status-changed"]);
|
|
259
|
+
const fieldPlacedCallbacks = /* @__PURE__ */ new Set();
|
|
260
|
+
const signedStatusCallbacks = /* @__PURE__ */ new Set();
|
|
256
261
|
const viewerRoot = ref(null);
|
|
257
262
|
const scrollContainer = ref(null);
|
|
258
263
|
const { state: viewerState, setScrollToPageFn, setDownloadPdfFn, setApplyScaleFn } = provideViewerState();
|
|
@@ -262,6 +267,9 @@ watchEffect(() => {
|
|
|
262
267
|
watchEffect(() => {
|
|
263
268
|
viewerState.shapeDetection.value = props.shapeDetection ?? false;
|
|
264
269
|
});
|
|
270
|
+
watchEffect(() => {
|
|
271
|
+
viewerState.clickToSign.value = props.clickToSign ?? false;
|
|
272
|
+
});
|
|
265
273
|
watch(
|
|
266
274
|
() => props.formEditMode,
|
|
267
275
|
(v) => {
|
|
@@ -293,6 +301,28 @@ watch(
|
|
|
293
301
|
},
|
|
294
302
|
{ immediate: true }
|
|
295
303
|
);
|
|
304
|
+
watch(
|
|
305
|
+
() => props.fieldDefaults,
|
|
306
|
+
(v) => {
|
|
307
|
+
formFieldsState.setPlacedFieldDefaults(v ?? {});
|
|
308
|
+
},
|
|
309
|
+
{ immediate: true }
|
|
310
|
+
);
|
|
311
|
+
watch(
|
|
312
|
+
() => props.editablePlacedFields,
|
|
313
|
+
(v) => {
|
|
314
|
+
formFieldsState.setEditablePlacedFields(v ?? false);
|
|
315
|
+
},
|
|
316
|
+
{ immediate: true }
|
|
317
|
+
);
|
|
318
|
+
const signatureStatus = computed(() => formFieldsState.getSignatureFieldStatus());
|
|
319
|
+
const signatureStatusKey = (fields) => fields.map((s) => `${s.id}|${s.fieldName}|${s.signed ? 1 : 0}`).join(";");
|
|
320
|
+
watch(signatureStatus, (next, prev) => {
|
|
321
|
+
if (prev && signatureStatusKey(next) === signatureStatusKey(prev)) return;
|
|
322
|
+
const payload = { allSigned: next.every((s) => s.signed), fields: next };
|
|
323
|
+
emit("signed-status-changed", payload);
|
|
324
|
+
for (const cb of signedStatusCallbacks) cb(payload);
|
|
325
|
+
});
|
|
296
326
|
watch(
|
|
297
327
|
() => props.activeRoleId,
|
|
298
328
|
(v) => {
|
|
@@ -918,7 +948,12 @@ onMounted(() => {
|
|
|
918
948
|
userName: props.userName,
|
|
919
949
|
onRequestTextInput,
|
|
920
950
|
onPlaceField: (payload) => {
|
|
921
|
-
formFieldsState.addPlacedField(payload);
|
|
951
|
+
const def = formFieldsState.addPlacedField(payload);
|
|
952
|
+
if (!viewerState.formEditMode.value && formFieldsState.editablePlacedFields.value) {
|
|
953
|
+
formFieldsState.selectPlacedField(def.id);
|
|
954
|
+
}
|
|
955
|
+
emit("field-placed", def);
|
|
956
|
+
for (const cb of fieldPlacedCallbacks) cb(def);
|
|
922
957
|
viewerState.selectTool("hand");
|
|
923
958
|
},
|
|
924
959
|
getFormFieldPreviewColor: () => {
|
|
@@ -1070,6 +1105,37 @@ defineExpose({
|
|
|
1070
1105
|
removeFormField: (id) => formFieldsState.removePlacedField(id),
|
|
1071
1106
|
/** All form-field definitions (parsed, detected, and placed), flat. */
|
|
1072
1107
|
getFormFields: () => formFieldsState.getAllFields(),
|
|
1108
|
+
/** Replace the per-field-type presets applied to tool placements.
|
|
1109
|
+
* Programmatic counterpart to the `fieldDefaults` prop. */
|
|
1110
|
+
setFieldDefaults: (defaults) => formFieldsState.setPlacedFieldDefaults(defaults),
|
|
1111
|
+
/** Enable/disable kviewer's built-in click-to-sign flow at runtime.
|
|
1112
|
+
* Programmatic counterpart to the `clickToSign` prop. */
|
|
1113
|
+
setClickToSign: (enabled) => {
|
|
1114
|
+
viewerState.clickToSign.value = enabled;
|
|
1115
|
+
},
|
|
1116
|
+
/** Subscribe to tool placements (imperative mirror of the
|
|
1117
|
+
* `field-placed` event, used by the embed bridge). Returns an
|
|
1118
|
+
* unsubscribe function. */
|
|
1119
|
+
onFieldPlaced: (cb) => {
|
|
1120
|
+
fieldPlacedCallbacks.add(cb);
|
|
1121
|
+
return () => fieldPlacedCallbacks.delete(cb);
|
|
1122
|
+
},
|
|
1123
|
+
/** True once every signature field in the document is signed — via a
|
|
1124
|
+
* value applied in this session or a digital signature already in the
|
|
1125
|
+
* source PDF. Vacuously true when the document has no signature
|
|
1126
|
+
* fields, so combine with `getSignatureFieldStatus().length` if you
|
|
1127
|
+
* need "at least one". Use to gate e.g. envelope finalization. */
|
|
1128
|
+
getSignedStatus: () => formFieldsState.getSignatureFieldStatus().every((s) => s.signed),
|
|
1129
|
+
/** Per-widget signed-state snapshot (id, fieldName, pageNumber,
|
|
1130
|
+
* signed, roleId). */
|
|
1131
|
+
getSignatureFieldStatus: () => formFieldsState.getSignatureFieldStatus(),
|
|
1132
|
+
/** Subscribe to signed-status changes (imperative mirror of the
|
|
1133
|
+
* `signed-status-changed` event, used by the embed bridge). Returns
|
|
1134
|
+
* an unsubscribe function. */
|
|
1135
|
+
onSignedStatusChanged: (cb) => {
|
|
1136
|
+
signedStatusCallbacks.add(cb);
|
|
1137
|
+
return () => signedStatusCallbacks.delete(cb);
|
|
1138
|
+
},
|
|
1073
1139
|
/** Reactive form-edit-mode flag — read with `.value`. */
|
|
1074
1140
|
formEditMode: viewerState.formEditMode,
|
|
1075
1141
|
/** Programmatically enter or leave form-edit mode. */
|