kviewer 0.2.4 → 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.
Files changed (40) hide show
  1. package/README.md +13 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/annotation/engine/editor/editor.d.ts +17 -0
  4. package/dist/runtime/annotation/engine/editor/editor.js +44 -15
  5. package/dist/runtime/annotation/engine/tools/signature.js +1 -1
  6. package/dist/runtime/annotation/engine/tools/stamp.d.ts +8 -0
  7. package/dist/runtime/annotation/engine/tools/stamp.js +27 -2
  8. package/dist/runtime/annotation/engine/types.d.ts +25 -0
  9. package/dist/runtime/annotation/pdf-export/export.js +0 -3
  10. package/dist/runtime/annotation/utils/signature_image.d.ts +26 -0
  11. package/dist/runtime/annotation/utils/signature_image.js +20 -0
  12. package/dist/runtime/assets/kviewer.css +1 -1
  13. package/dist/runtime/components/FormFieldLayer.vue +6 -1
  14. package/dist/runtime/components/Viewer.d.vue.ts +56 -2
  15. package/dist/runtime/components/Viewer.vue +68 -2
  16. package/dist/runtime/components/Viewer.vue.d.ts +56 -2
  17. package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -9
  18. package/dist/runtime/components/ViewerTabs.vue +10 -2
  19. package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -9
  20. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +4 -2
  21. package/dist/runtime/components/form-fields/FormSignatureField.vue +15 -2
  22. package/dist/runtime/components/modals/FreeTextModal.vue +1 -1
  23. package/dist/runtime/components/modals/SignatureDrawModal.vue +170 -4
  24. package/dist/runtime/components/tools/ToolProperties.vue +98 -101
  25. package/dist/runtime/composables/useFormFields.d.ts +17 -1
  26. package/dist/runtime/composables/useFormFields.js +48 -9
  27. package/dist/runtime/composables/useViewerSearch.js +1 -1
  28. package/dist/runtime/composables/useViewerState.d.ts +1 -0
  29. package/dist/runtime/composables/useViewerState.js +12 -13
  30. package/dist/runtime/embed/bridge-client.d.ts +19 -3
  31. package/dist/runtime/embed/bridge-client.js +24 -0
  32. package/dist/runtime/embed/bridge-host.d.ts +10 -1
  33. package/dist/runtime/embed/bridge-host.js +20 -0
  34. package/dist/runtime/embed/protocol.d.ts +28 -1
  35. package/dist/runtime/i18n/messages.d.ts +16 -2
  36. package/dist/runtime/i18n/messages.js +16 -2
  37. package/dist/runtime/public-types.d.ts +1 -1
  38. package/dist/runtime/toolbar-tools.d.ts +1 -1
  39. package/dist/runtime/toolbar-tools.js +8 -0
  40. 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://github.com/uppt/nuxt-kviewer) for the full tool-ID list and the array that reproduces the default toolbar.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.2.4",
4
+ "version": "0.3.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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 === 0) this.mouseDownHandler(e);
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
- if (!isStylusTouch(e.evt)) return;
102
- e.evt.preventDefault();
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
- if (!isStylusTouch(e.evt)) return;
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
- const touch = e.evt.changedTouches?.[0];
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;
@@ -63,7 +63,7 @@ export class EditorSignature extends Editor {
63
63
  oldImage.destroy();
64
64
  ghostGroup.add(image);
65
65
  });
66
- this.getBgLayer(konvaStage).add(ghostGroup);
66
+ this.adoptHydratedGroup(konvaStage, ghostGroup);
67
67
  }
68
68
  mouseMoveHandler() {
69
69
  }
@@ -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.getBgLayer(konvaStage).add(ghostGroup);
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;
@@ -224,9 +224,6 @@ async function flattenPageAnnotations(pdfDoc, page, annotations) {
224
224
  drawX = 0;
225
225
  drawY = H_u;
226
226
  break;
227
- default:
228
- drawX = 0;
229
- drawY = 0;
230
227
  }
231
228
  page.drawImage(image, {
232
229
  x: drawX,
@@ -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 (!enabled) formFields.selectPlacedField(null);
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
- locale: KviewerLocale;
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. */