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
@@ -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>;
@@ -1,6 +1,6 @@
1
1
  import { type ComponentPublicInstance } from 'vue';
2
2
  import type { ViewMode } from '../composables/useViewerState.js';
3
- import type { StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
+ import type { FormFieldDefinition, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
5
  import type { ViewerToolEntry } from '../toolbar-tools.js';
6
6
  export interface ViewerTabItem {
@@ -65,6 +65,13 @@ type __VLS_Props = {
65
65
  menuItems?: ViewerMenuItem[];
66
66
  /** Forwarded to every Viewer instance — see KViewer's `tools`. */
67
67
  tools?: ViewerToolEntry[];
68
+ /** Forwarded to every Viewer instance — see KViewer's `clickToSign`. */
69
+ clickToSign?: boolean;
70
+ /** Forwarded to every Viewer instance — see KViewer's
71
+ * `editablePlacedFields`. */
72
+ editablePlacedFields?: boolean;
73
+ /** Forwarded to every Viewer instance — see KViewer's `fieldDefaults`. */
74
+ fieldDefaults?: PlacedFieldDefaults;
68
75
  };
69
76
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
70
77
  id?: string;
@@ -72,7 +79,7 @@ declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
72
79
  declare function removeTab(id: string): boolean;
73
80
  declare function activateTab(id: string): void;
74
81
  declare function getViewer(id: string): ComponentPublicInstance | null;
75
- declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
82
+ declare var __VLS_7: {}, __VLS_21: {}, __VLS_34: {
76
83
  tab: {
77
84
  id: string;
78
85
  label: string;
@@ -172,7 +179,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
172
179
  shapeDetection?: boolean | undefined;
173
180
  scripting?: boolean | undefined;
174
181
  };
175
- }, __VLS_35: {
182
+ }, __VLS_37: {
176
183
  tab: {
177
184
  id: string;
178
185
  label: string;
@@ -272,21 +279,21 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
272
279
  shapeDetection?: boolean | undefined;
273
280
  scripting?: boolean | undefined;
274
281
  };
275
- }, __VLS_39: string, __VLS_40: {
282
+ }, __VLS_41: string, __VLS_42: {
276
283
  state: import("../composables/useViewerState.js").ViewerState;
277
- }, __VLS_42: {};
284
+ }, __VLS_44: {};
278
285
  type __VLS_Slots = {} & {
279
- [K in NonNullable<typeof __VLS_39>]?: (props: typeof __VLS_40) => any;
286
+ [K in NonNullable<typeof __VLS_41>]?: (props: typeof __VLS_42) => any;
280
287
  } & {
281
288
  'tabs-leading'?: (props: typeof __VLS_7) => any;
282
289
  } & {
283
290
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
284
291
  } & {
285
- header?: (props: typeof __VLS_32) => any;
292
+ header?: (props: typeof __VLS_34) => any;
286
293
  } & {
287
- footer?: (props: typeof __VLS_35) => any;
294
+ footer?: (props: typeof __VLS_37) => any;
288
295
  } & {
289
- empty?: (props: typeof __VLS_42) => any;
296
+ empty?: (props: typeof __VLS_44) => any;
290
297
  };
291
298
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
292
299
  addTab: typeof addTab;
@@ -394,13 +401,23 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
394
401
  scripting?: boolean | undefined;
395
402
  }[];
396
403
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
404
+ "field-placed": (field: FormFieldDefinition) => any;
397
405
  "update:activeRoleId": (value: string | null) => any;
406
+ "signed-status-changed": (payload: {
407
+ allSigned: boolean;
408
+ fields: SignatureFieldStatus[];
409
+ }) => any;
398
410
  "update:activeTab": (id: string) => any;
399
411
  "tab-added": (tab: ViewerTabItem) => any;
400
412
  "tab-close": (id: string) => any;
401
413
  "tab-removed": (id: string) => any;
402
414
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
415
+ "onField-placed"?: ((field: FormFieldDefinition) => any) | undefined;
403
416
  "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
417
+ "onSigned-status-changed"?: ((payload: {
418
+ allSigned: boolean;
419
+ fields: SignatureFieldStatus[];
420
+ }) => any) | undefined;
404
421
  "onUpdate:activeTab"?: ((id: string) => any) | undefined;
405
422
  "onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
406
423
  "onTab-close"?: ((id: string) => any) | undefined;
@@ -408,6 +425,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
408
425
  }>, {
409
426
  userName: string;
410
427
  zoom: number;
428
+ clickToSign: boolean;
411
429
  menuItems: ViewerMenuItem[];
412
430
  tools: ViewerToolEntry[];
413
431
  scripting: boolean;
@@ -416,6 +434,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
416
434
  viewMode: ViewMode;
417
435
  roleColors: Record<string, string>;
418
436
  activeRoleId: string | null;
437
+ editablePlacedFields: boolean;
438
+ fieldDefaults: PlacedFieldDefaults;
419
439
  defaultActiveTab: string;
420
440
  minTabs: number;
421
441
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -64,7 +64,12 @@
64
64
  :tools="tools"
65
65
  :role-colors="roleColors"
66
66
  :active-role-id="activeRoleId"
67
+ :click-to-sign="clickToSign"
68
+ :editable-placed-fields="editablePlacedFields"
69
+ :field-defaults="fieldDefaults"
67
70
  @update:active-role-id="(v) => emit('update:activeRoleId', v)"
71
+ @field-placed="(f) => emit('field-placed', f)"
72
+ @signed-status-changed="(p) => emit('signed-status-changed', p)"
68
73
  >
69
74
  <template v-if="$slots.header" #header>
70
75
  <slot name="header" :tab="activeTab" />
@@ -107,13 +112,16 @@ const props = defineProps({
107
112
  activeRoleId: { type: [String, null], required: false, default: void 0 },
108
113
  scripting: { type: Boolean, required: false, default: false },
109
114
  menuItems: { type: Array, required: false, default: void 0 },
110
- tools: { type: Array, required: false, default: void 0 }
115
+ tools: { type: Array, required: false, default: void 0 },
116
+ clickToSign: { type: Boolean, required: false, default: false },
117
+ editablePlacedFields: { type: Boolean, required: false, default: false },
118
+ fieldDefaults: { type: Object, required: false, default: void 0 }
111
119
  });
112
120
  const slots = useSlots();
113
121
  const toolSlotNames = computed(
114
122
  () => Object.keys(slots).filter((name) => name.startsWith("tool-"))
115
123
  );
116
- const emit = defineEmits(["update:activeTab", "tab-added", "tab-close", "tab-removed", "update:activeRoleId"]);
124
+ const emit = defineEmits(["update:activeTab", "tab-added", "tab-close", "tab-removed", "update:activeRoleId", "field-placed", "signed-status-changed"]);
117
125
  let idCounter = 0;
118
126
  function generateId() {
119
127
  return `tab-${Date.now()}-${++idCounter}`;
@@ -1,6 +1,6 @@
1
1
  import { type ComponentPublicInstance } from 'vue';
2
2
  import type { ViewMode } from '../composables/useViewerState.js';
3
- import type { StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
+ import type { FormFieldDefinition, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
5
  import type { ViewerToolEntry } from '../toolbar-tools.js';
6
6
  export interface ViewerTabItem {
@@ -65,6 +65,13 @@ type __VLS_Props = {
65
65
  menuItems?: ViewerMenuItem[];
66
66
  /** Forwarded to every Viewer instance — see KViewer's `tools`. */
67
67
  tools?: ViewerToolEntry[];
68
+ /** Forwarded to every Viewer instance — see KViewer's `clickToSign`. */
69
+ clickToSign?: boolean;
70
+ /** Forwarded to every Viewer instance — see KViewer's
71
+ * `editablePlacedFields`. */
72
+ editablePlacedFields?: boolean;
73
+ /** Forwarded to every Viewer instance — see KViewer's `fieldDefaults`. */
74
+ fieldDefaults?: PlacedFieldDefaults;
68
75
  };
69
76
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
70
77
  id?: string;
@@ -72,7 +79,7 @@ declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
72
79
  declare function removeTab(id: string): boolean;
73
80
  declare function activateTab(id: string): void;
74
81
  declare function getViewer(id: string): ComponentPublicInstance | null;
75
- declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
82
+ declare var __VLS_7: {}, __VLS_21: {}, __VLS_34: {
76
83
  tab: {
77
84
  id: string;
78
85
  label: string;
@@ -172,7 +179,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
172
179
  shapeDetection?: boolean | undefined;
173
180
  scripting?: boolean | undefined;
174
181
  };
175
- }, __VLS_35: {
182
+ }, __VLS_37: {
176
183
  tab: {
177
184
  id: string;
178
185
  label: string;
@@ -272,21 +279,21 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
272
279
  shapeDetection?: boolean | undefined;
273
280
  scripting?: boolean | undefined;
274
281
  };
275
- }, __VLS_39: string, __VLS_40: {
282
+ }, __VLS_41: string, __VLS_42: {
276
283
  state: import("../composables/useViewerState.js").ViewerState;
277
- }, __VLS_42: {};
284
+ }, __VLS_44: {};
278
285
  type __VLS_Slots = {} & {
279
- [K in NonNullable<typeof __VLS_39>]?: (props: typeof __VLS_40) => any;
286
+ [K in NonNullable<typeof __VLS_41>]?: (props: typeof __VLS_42) => any;
280
287
  } & {
281
288
  'tabs-leading'?: (props: typeof __VLS_7) => any;
282
289
  } & {
283
290
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
284
291
  } & {
285
- header?: (props: typeof __VLS_32) => any;
292
+ header?: (props: typeof __VLS_34) => any;
286
293
  } & {
287
- footer?: (props: typeof __VLS_35) => any;
294
+ footer?: (props: typeof __VLS_37) => any;
288
295
  } & {
289
- empty?: (props: typeof __VLS_42) => any;
296
+ empty?: (props: typeof __VLS_44) => any;
290
297
  };
291
298
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
292
299
  addTab: typeof addTab;
@@ -394,13 +401,23 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
394
401
  scripting?: boolean | undefined;
395
402
  }[];
396
403
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
404
+ "field-placed": (field: FormFieldDefinition) => any;
397
405
  "update:activeRoleId": (value: string | null) => any;
406
+ "signed-status-changed": (payload: {
407
+ allSigned: boolean;
408
+ fields: SignatureFieldStatus[];
409
+ }) => any;
398
410
  "update:activeTab": (id: string) => any;
399
411
  "tab-added": (tab: ViewerTabItem) => any;
400
412
  "tab-close": (id: string) => any;
401
413
  "tab-removed": (id: string) => any;
402
414
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
415
+ "onField-placed"?: ((field: FormFieldDefinition) => any) | undefined;
403
416
  "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
417
+ "onSigned-status-changed"?: ((payload: {
418
+ allSigned: boolean;
419
+ fields: SignatureFieldStatus[];
420
+ }) => any) | undefined;
404
421
  "onUpdate:activeTab"?: ((id: string) => any) | undefined;
405
422
  "onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
406
423
  "onTab-close"?: ((id: string) => any) | undefined;
@@ -408,6 +425,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
408
425
  }>, {
409
426
  userName: string;
410
427
  zoom: number;
428
+ clickToSign: boolean;
411
429
  menuItems: ViewerMenuItem[];
412
430
  tools: ViewerToolEntry[];
413
431
  scripting: boolean;
@@ -416,6 +434,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
416
434
  viewMode: ViewMode;
417
435
  roleColors: Record<string, string>;
418
436
  activeRoleId: string | null;
437
+ editablePlacedFields: boolean;
438
+ fieldDefaults: PlacedFieldDefaults;
419
439
  defaultActiveTab: string;
420
440
  minTabs: number;
421
441
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -11,7 +11,7 @@
11
11
  >
12
12
  <div
13
13
  class="kviewer-form-field__body"
14
- :class="{ 'kviewer-form-field__body--locked': isFormEditMode }"
14
+ :class="{ 'kviewer-form-field__body--locked': showChrome }"
15
15
  >
16
16
  <FormTextField v-if="props.field.fieldType === 'text'" :field="props.field" />
17
17
  <FormCheckbox v-else-if="props.field.fieldType === 'checkbox'" :field="props.field" />
@@ -55,7 +55,9 @@ const state = useViewerState();
55
55
  const formFields = useFormFields();
56
56
  const isPlaced = computed(() => props.field.origin === "placed");
57
57
  const isFormEditMode = computed(() => state.formEditMode.value);
58
- const showChrome = computed(() => isFormEditMode.value);
58
+ const showChrome = computed(
59
+ () => isFormEditMode.value || formFields.editablePlacedFields.value && isPlaced.value
60
+ );
59
61
  const chromeColor = computed(() => {
60
62
  const id = props.field.roleId;
61
63
  if (!id) return "#1677ff";
@@ -1,7 +1,10 @@
1
1
  <template>
2
2
  <div
3
3
  class="kviewer-form-signature"
4
- :class="{ 'kviewer-form-signature--filled': hasSigned }"
4
+ :class="{
5
+ 'kviewer-form-signature--filled': hasSigned,
6
+ 'kviewer-form-signature--static': !clickToSignEnabled
7
+ }"
5
8
  @click="onClickSign"
6
9
  >
7
10
  <img
@@ -10,13 +13,21 @@
10
13
  class="kviewer-form-signature__preview"
11
14
  :alt="t('signature')"
12
15
  >
13
- <span v-else class="kviewer-form-signature__placeholder">
16
+ <span v-else-if="clickToSignEnabled" class="kviewer-form-signature__placeholder">
14
17
  {{ props.field.promptText || t("clickToSign") }}
15
18
  </span>
19
+ <!-- click-to-sign off: passive marker — the host owns the signing flow.
20
+ promptText is a call to action ("Sign here"), so it only applies to
21
+ the click-to-sign placeholder; the marker stays a neutral label. -->
22
+ <span v-else class="kviewer-form-signature__placeholder kviewer-form-signature__marker">
23
+ <UIcon name="i-lucide-signature" class="kviewer-form-signature__marker-icon" />
24
+ {{ t("signature") }}
25
+ </span>
16
26
  </div>
17
27
 
18
28
  <ClientOnly>
19
29
  <SignatureDrawModal
30
+ v-if="clickToSignEnabled"
20
31
  v-model:open="drawModalOpen"
21
32
  @submit="onDrawSubmit"
22
33
  @cancel="drawModalOpen = false"
@@ -43,7 +54,9 @@ const signatureUrl = computed(() => {
43
54
  return fv.value;
44
55
  });
45
56
  const hasSigned = computed(() => !!signatureUrl.value);
57
+ const clickToSignEnabled = computed(() => state.clickToSign.value);
46
58
  async function onClickSign() {
59
+ if (!clickToSignEnabled.value) return;
47
60
  if (props.field.readOnly || state.readonly.value) return;
48
61
  if (formFields.isFieldLocked(props.field.id)) return;
49
62
  if (state.signatures.value.length > 0) {
@@ -2,7 +2,7 @@
2
2
  <UModal
3
3
  v-model:open="isOpen"
4
4
  :title="props.initialText !== void 0 ? t('editText') : t('freeText')"
5
- :ui="{ width: 'sm:max-w-md' }"
5
+ :ui="{ content: 'sm:max-w-md' }"
6
6
  >
7
7
  <template #body>
8
8
  <div class="flex flex-col gap-3">
@@ -1,8 +1,25 @@
1
1
  <template>
2
- <UModal v-model:open="isOpen" :title="t('drawSignature')" :ui="{ content: 'sm:max-w-2xl' }">
2
+ <UModal v-model:open="isOpen" :title="t('addSignature')" :ui="{ content: 'sm:max-w-2xl' }">
3
3
  <template #body>
4
4
  <div class="flex flex-col gap-3">
5
+ <div class="flex gap-1" role="tablist">
6
+ <button
7
+ v-for="tab in tabs"
8
+ :key="tab"
9
+ role="tab"
10
+ :aria-selected="mode === tab"
11
+ class="px-3 py-1.5 text-sm rounded cursor-pointer"
12
+ :class="
13
+ mode === tab ? 'bg-elevated text-highlighted font-medium' : 'text-muted hover:text-default'
14
+ "
15
+ @click="mode = tab"
16
+ >
17
+ {{ tab === "draw" ? t("drawSignature") : t("uploadSignature") }}
18
+ </button>
19
+ </div>
20
+
5
21
  <div
22
+ v-show="mode === 'draw'"
6
23
  ref="canvasWrapEl"
7
24
  class="border border-default rounded bg-white relative w-full"
8
25
  :style="{ aspectRatio: `${canvasAspect}` }"
@@ -16,7 +33,8 @@
16
33
  @pointerleave="onPointerUp"
17
34
  />
18
35
  </div>
19
- <div class="flex items-center gap-3">
36
+
37
+ <div v-if="mode === 'draw'" class="flex items-center gap-3">
20
38
  <div class="flex gap-1">
21
39
  <button
22
40
  v-for="c in signatureColors"
@@ -33,6 +51,50 @@
33
51
  {{ t("clear") }}
34
52
  </UButton>
35
53
  </div>
54
+
55
+ <template v-if="mode === 'upload'">
56
+ <!-- The checkerboard makes transparency visible, so users can
57
+ tell whether background removal actually worked before
58
+ placing the signature on the page. -->
59
+ <div
60
+ class="border border-dashed border-default rounded relative w-full flex items-center justify-center bg-checkerboard"
61
+ :style="{ aspectRatio: `${canvasAspect}` }"
62
+ @dragover.prevent
63
+ @drop.prevent="onDrop"
64
+ >
65
+ <img
66
+ v-if="uploadedUrl"
67
+ :src="uploadedUrl"
68
+ class="max-w-full max-h-full object-contain"
69
+ :alt="t('uploadSignature')"
70
+ >
71
+ <button
72
+ v-else
73
+ class="w-full h-full flex flex-col items-center justify-center gap-1 text-muted text-sm cursor-pointer"
74
+ @click="fileInputEl?.click()"
75
+ >
76
+ <span>{{ t("dropSignatureImage") }}</span>
77
+ <span class="text-xs">{{ acceptHint }}</span>
78
+ </button>
79
+ </div>
80
+
81
+ <input
82
+ ref="fileInputEl"
83
+ type="file"
84
+ :accept="accept"
85
+ class="hidden"
86
+ @change="onFileChange"
87
+ >
88
+
89
+ <p v-if="uploadError" class="text-sm text-error">{{ uploadError }}</p>
90
+
91
+ <div class="flex items-center gap-3">
92
+ <UButton variant="ghost" color="neutral" size="xs" @click="fileInputEl?.click()">
93
+ {{ t("chooseImage") }}
94
+ </UButton>
95
+ <UCheckbox v-model="removeBackground" size="sm" :label="t('removeWhiteBackground')" />
96
+ </div>
97
+ </template>
36
98
  </div>
37
99
  </template>
38
100
  <template #footer>
@@ -40,7 +102,7 @@
40
102
  <UButton variant="ghost" color="neutral" @click="cancel">
41
103
  {{ t("cancel") }}
42
104
  </UButton>
43
- <UButton :disabled="!hasDrawn" @click="submit">
105
+ <UButton :disabled="!hasContent" @click="submit">
44
106
  {{ t("save") }}
45
107
  </UButton>
46
108
  </div>
@@ -49,8 +111,9 @@
49
111
  </template>
50
112
 
51
113
  <script setup>
52
- import { ref, watch, nextTick, onBeforeUnmount } from "vue";
114
+ import { ref, computed, watch, nextTick, onBeforeUnmount } from "vue";
53
115
  import { defaultOptions } from "../../annotation/engine/config";
116
+ import { fitWithin, knockOutWhite } from "../../annotation/utils/signature_image";
54
117
  import { useKviewerI18n } from "../../composables/useKviewerI18n";
55
118
  const { t } = useKviewerI18n();
56
119
  const props = defineProps({
@@ -64,6 +127,8 @@ watch(
64
127
  isOpen.value = v;
65
128
  if (v) {
66
129
  hasDrawn.value = false;
130
+ mode.value = "draw";
131
+ resetUpload();
67
132
  await nextTick();
68
133
  syncCanvasSize();
69
134
  clearCanvas();
@@ -71,6 +136,8 @@ watch(
71
136
  }
72
137
  );
73
138
  watch(isOpen, (v) => emit("update:open", v));
139
+ const tabs = ["draw", "upload"];
140
+ const mode = ref("draw");
74
141
  const canvasAspect = `${defaultOptions.signature.WIDTH} / ${defaultOptions.signature.HEIGHT}`;
75
142
  const signatureColors = defaultOptions.signature.COLORS;
76
143
  const canvasRef = ref(null);
@@ -165,7 +232,102 @@ function clearCanvas() {
165
232
  ctx.restore();
166
233
  hasDrawn.value = false;
167
234
  }
235
+ const accept = defaultOptions.signature.ACCEPT;
236
+ const maxSize = defaultOptions.signature.MAX_SIZE;
237
+ const fileInputEl = ref(null);
238
+ const uploadedUrl = ref(null);
239
+ const uploadError = ref(null);
240
+ const removeBackground = ref(true);
241
+ let sourceImage = null;
242
+ const acceptHint = accept.replace(/\./g, "").toUpperCase().replace(/,/g, ", ");
243
+ const maxSizeMb = Math.round(maxSize / (1024 * 1024));
244
+ const hasContent = computed(
245
+ () => mode.value === "draw" ? hasDrawn.value : !!uploadedUrl.value
246
+ );
247
+ function resetUpload() {
248
+ uploadedUrl.value = null;
249
+ uploadError.value = null;
250
+ sourceImage = null;
251
+ if (fileInputEl.value) fileInputEl.value.value = "";
252
+ }
253
+ function onDrop(e) {
254
+ const file = e.dataTransfer?.files?.[0];
255
+ if (file) void handleFile(file);
256
+ }
257
+ function onFileChange(e) {
258
+ const file = e.target.files?.[0];
259
+ if (file) void handleFile(file);
260
+ }
261
+ async function handleFile(file) {
262
+ uploadError.value = null;
263
+ const extension = `.${file.name.split(".").pop()?.toLowerCase() ?? ""}`;
264
+ if (!accept.split(",").includes(extension)) {
265
+ uploadError.value = t("unsupportedImageType", { accept: acceptHint });
266
+ return;
267
+ }
268
+ if (file.size > maxSize) {
269
+ uploadError.value = t("imageTooLarge", { size: maxSizeMb });
270
+ return;
271
+ }
272
+ try {
273
+ sourceImage = await loadImage(file);
274
+ renderUpload();
275
+ } catch {
276
+ sourceImage = null;
277
+ uploadError.value = t("imageReadFailed");
278
+ }
279
+ }
280
+ function loadImage(file) {
281
+ return new Promise((resolve, reject) => {
282
+ const reader = new FileReader();
283
+ reader.onerror = () => reject(new Error("read failed"));
284
+ reader.onload = () => {
285
+ const img = new Image();
286
+ img.onload = () => resolve(img);
287
+ img.onerror = () => reject(new Error("decode failed"));
288
+ img.src = reader.result;
289
+ };
290
+ reader.readAsDataURL(file);
291
+ });
292
+ }
293
+ const MAX_OUTPUT_SCALE = 3;
294
+ function renderUpload() {
295
+ const img = sourceImage;
296
+ if (!img) return;
297
+ const { width, height } = fitWithin(
298
+ img.naturalWidth,
299
+ img.naturalHeight,
300
+ defaultOptions.signature.WIDTH * MAX_OUTPUT_SCALE,
301
+ defaultOptions.signature.HEIGHT * MAX_OUTPUT_SCALE
302
+ );
303
+ const canvas = document.createElement("canvas");
304
+ canvas.width = width;
305
+ canvas.height = height;
306
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
307
+ if (!ctx) {
308
+ uploadError.value = t("imageReadFailed");
309
+ return;
310
+ }
311
+ ctx.drawImage(img, 0, 0, width, height);
312
+ if (removeBackground.value) {
313
+ try {
314
+ const imageData = ctx.getImageData(0, 0, width, height);
315
+ knockOutWhite(imageData.data);
316
+ ctx.putImageData(imageData, 0, 0);
317
+ } catch {
318
+ uploadError.value = t("backgroundRemovalFailed");
319
+ }
320
+ }
321
+ uploadedUrl.value = canvas.toDataURL("image/png");
322
+ }
323
+ watch(removeBackground, () => {
324
+ if (sourceImage) renderUpload();
325
+ });
168
326
  function submit() {
327
+ if (mode.value === "upload") {
328
+ if (uploadedUrl.value) emit("submit", uploadedUrl.value);
329
+ return;
330
+ }
169
331
  const canvas = canvasRef.value;
170
332
  if (!canvas) return;
171
333
  const imageUrl = canvas.toDataURL("image/png");
@@ -175,3 +337,7 @@ function cancel() {
175
337
  emit("cancel");
176
338
  }
177
339
  </script>
340
+
341
+ <style scoped>
342
+ .bg-checkerboard{background-color:#fff;background-image:linear-gradient(45deg,#e5e5e5 25%,transparent 0),linear-gradient(-45deg,#e5e5e5 25%,transparent 0),linear-gradient(45deg,transparent 75%,#e5e5e5 0),linear-gradient(-45deg,transparent 75%,#e5e5e5 0);background-position:0 0,0 8px,8px -8px,-8px 0;background-size:16px 16px}
343
+ </style>