kviewer 0.3.3 → 0.3.5

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 (47) hide show
  1. package/dist/module.d.mts +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/annotation/engine/editor/selector.d.ts +1 -0
  4. package/dist/runtime/annotation/engine/editor/selector.js +17 -5
  5. package/dist/runtime/annotation/engine/painter.d.ts +4 -2
  6. package/dist/runtime/annotation/engine/painter.js +4 -0
  7. package/dist/runtime/annotation/engine/tools/form-field.d.ts +5 -5
  8. package/dist/runtime/annotation/engine/tools/form-field.js +1 -1
  9. package/dist/runtime/annotation/engine/types.d.ts +18 -9
  10. package/dist/runtime/annotation/pdf-export/export-form-fields.js +6 -29
  11. package/dist/runtime/assets/kviewer.css +1 -1
  12. package/dist/runtime/components/AnnotationToolbar.vue +12 -1
  13. package/dist/runtime/components/FormFieldLayer.vue +7 -2
  14. package/dist/runtime/components/PdfPage.vue +24 -1
  15. package/dist/runtime/components/SelectionToolbarItems.d.vue.ts +8 -0
  16. package/dist/runtime/components/SelectionToolbarItems.vue +43 -0
  17. package/dist/runtime/components/SelectionToolbarItems.vue.d.ts +8 -0
  18. package/dist/runtime/components/Viewer.d.vue.ts +24 -17
  19. package/dist/runtime/components/Viewer.vue +60 -37
  20. package/dist/runtime/components/Viewer.vue.d.ts +24 -17
  21. package/dist/runtime/components/ViewerBar.d.vue.ts +4 -4
  22. package/dist/runtime/components/ViewerBar.vue +2 -1
  23. package/dist/runtime/components/ViewerBar.vue.d.ts +4 -4
  24. package/dist/runtime/components/ViewerTabs.d.vue.ts +16 -17
  25. package/dist/runtime/components/ViewerTabs.vue +5 -6
  26. package/dist/runtime/components/ViewerTabs.vue.d.ts +16 -17
  27. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +35 -10
  28. package/dist/runtime/components/form-fields/FormSignatureField.vue +10 -2
  29. package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +4 -0
  30. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +35 -18
  31. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +4 -0
  32. package/dist/runtime/components/form-fields/PlacedFieldToolbar.d.vue.ts +7 -0
  33. package/dist/runtime/components/form-fields/PlacedFieldToolbar.vue +51 -0
  34. package/dist/runtime/components/form-fields/PlacedFieldToolbar.vue.d.ts +7 -0
  35. package/dist/runtime/composables/useFormFields.d.ts +28 -17
  36. package/dist/runtime/composables/useFormFields.js +20 -24
  37. package/dist/runtime/embed/bridge-client.d.ts +4 -2
  38. package/dist/runtime/embed/bridge-client.js +2 -1
  39. package/dist/runtime/embed/bridge-host.d.ts +2 -0
  40. package/dist/runtime/embed/bridge-host.js +16 -0
  41. package/dist/runtime/embed/protocol.d.ts +12 -0
  42. package/dist/runtime/public-types.d.ts +2 -1
  43. package/dist/runtime/selection-items.d.ts +61 -0
  44. package/dist/runtime/selection-items.js +1 -0
  45. package/dist/runtime/toolbar-tools.d.ts +4 -1
  46. package/dist/types.d.mts +1 -1
  47. package/package.json +1 -1
@@ -4,14 +4,19 @@
4
4
  :class="{
5
5
  'kviewer-form-field--editable': showChrome,
6
6
  'kviewer-form-field--placed': isPlaced,
7
- 'kviewer-form-field--selected': props.selected
7
+ 'kviewer-form-field--selected': props.selected,
8
+ 'kviewer-form-field--readonly': isReadonlyOutsideAuthoring
8
9
  }"
9
- :style="[positionStyle, { '--kvw-field-color': chromeColor }]"
10
+ :style="[positionStyle, {
11
+ '--kvw-field-color': chromeColor,
12
+ '--kvw-handle-zoom': handleZoom
13
+ }]"
10
14
  :data-element-id="props.field.id"
15
+ @contextmenu="onContextMenu"
11
16
  >
12
17
  <div
13
18
  class="kviewer-form-field__body"
14
- :class="{ 'kviewer-form-field__body--locked': showChrome }"
19
+ :class="{ 'kviewer-form-field__body--locked': chromeMode === 'authoring' }"
15
20
  >
16
21
  <FormTextField v-if="props.field.fieldType === 'text'" :field="props.field" />
17
22
  <FormCheckbox v-else-if="props.field.fieldType === 'checkbox'" :field="props.field" />
@@ -27,8 +32,14 @@
27
32
  :selected="props.selected"
28
33
  :scale="props.scale"
29
34
  :page-height="props.pageTransform.height"
35
+ :mode="chromeMode ?? 'authoring'"
30
36
  @select="emit('select')"
31
37
  />
38
+
39
+ <PlacedFieldToolbar
40
+ v-if="showChrome && props.selected && !state.readonly.value"
41
+ :field="props.field"
42
+ />
32
43
  </div>
33
44
  </template>
34
45
 
@@ -44,6 +55,7 @@ import FormDropdown from "./FormDropdown.vue";
44
55
  import FormSignatureField from "./FormSignatureField.vue";
45
56
  import FormButton from "./FormButton.vue";
46
57
  import PlacedFieldChrome from "./PlacedFieldChrome.vue";
58
+ import PlacedFieldToolbar from "./PlacedFieldToolbar.vue";
47
59
  const props = defineProps({
48
60
  field: { type: Object, required: true },
49
61
  pageTransform: { type: Object, required: true },
@@ -55,14 +67,27 @@ const state = useViewerState();
55
67
  const formFields = useFormFields();
56
68
  const isPlaced = computed(() => props.field.origin === "placed");
57
69
  const isFormEditMode = computed(() => state.formEditMode.value);
58
- const showChrome = computed(
59
- () => isFormEditMode.value || formFields.editablePlacedFields.value && isPlaced.value
70
+ const chromeMode = computed(() => {
71
+ if (isFormEditMode.value) return "authoring";
72
+ if (formFields.editablePlacedFields.value && isPlaced.value && !props.field.readOnly) {
73
+ return "live";
74
+ }
75
+ return null;
76
+ });
77
+ const showChrome = computed(() => chromeMode.value !== null);
78
+ const isReadonlyOutsideAuthoring = computed(
79
+ () => props.field.readOnly && !isFormEditMode.value
60
80
  );
61
- const chromeColor = computed(() => {
62
- const id = props.field.roleId;
63
- if (!id) return "#1677ff";
64
- return formFields.roleColors.value[id] ?? "#1677ff";
81
+ function onContextMenu(e) {
82
+ if (chromeMode.value !== "live") return;
83
+ e.preventDefault();
84
+ emit("select");
85
+ }
86
+ const handleZoom = computed(() => {
87
+ const totalZoom = (props.scale || 1) * (state.visualZoom.value || 1);
88
+ return (1 / totalZoom).toFixed(3);
65
89
  });
90
+ const chromeColor = computed(() => props.field.fieldColor ?? "#1677ff");
66
91
  const positionStyle = computed(() => {
67
92
  const rect = props.field.rect;
68
93
  const r = convertPdfRectToCanvas(props.pageTransform, [rect[0], rect[1], rect[2], rect[3]]);
@@ -77,5 +102,5 @@ const positionStyle = computed(() => {
77
102
  </script>
78
103
 
79
104
  <style>
80
- .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}.kviewer-form-field--editable{outline:1px dashed color-mix(in srgb,var(--kvw-field-color,#1677ff) 45%,transparent);outline-offset:0}.kviewer-form-field--editable.kviewer-form-field--placed{outline-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 75%,transparent)}.kviewer-form-field--editable.kviewer-form-field--selected{outline:none}
105
+ .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}
81
106
  </style>
@@ -55,10 +55,18 @@ const signatureUrl = computed(() => {
55
55
  });
56
56
  const hasSigned = computed(() => !!signatureUrl.value);
57
57
  const clickToSignEnabled = computed(() => state.clickToSign.value);
58
- async function onClickSign() {
59
- if (!clickToSignEnabled.value) return;
58
+ const liveEditing = computed(
59
+ () => formFields.editablePlacedFields.value && props.field.origin === "placed" && !state.formEditMode.value
60
+ );
61
+ async function onClickSign(e) {
60
62
  if (props.field.readOnly || state.readonly.value) return;
61
63
  if (formFields.isFieldLocked(props.field.id)) return;
64
+ if (liveEditing.value && (hasSigned.value || !clickToSignEnabled.value)) {
65
+ e.stopPropagation();
66
+ formFields.selectPlacedField(props.field.id);
67
+ return;
68
+ }
69
+ if (!clickToSignEnabled.value) return;
62
70
  if (state.signatures.value.length > 0) {
63
71
  const sig = state.signatures.value[0];
64
72
  if (sig) {
@@ -6,6 +6,10 @@ type __VLS_Props = {
6
6
  scale: number;
7
7
  /** PDF-space page height (kept for parity; unused here). */
8
8
  pageHeight: number;
9
+ /** 'authoring' = form-edit mode (chrome is the whole interactive
10
+ * surface). 'live' = editablePlacedFields outside form-edit mode
11
+ * (chrome is transparent except grip and handles). */
12
+ mode: 'authoring' | 'live';
9
13
  };
10
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
11
15
  select: () => any;
@@ -1,19 +1,32 @@
1
1
  <template>
2
- <!-- The whole chrome is the interactive surface in form-edit mode:
3
- click to select, drag anywhere to move, plus a label bar above and
4
- eight resize handles when selected. -->
2
+ <!-- Two interaction modes. Authoring (form-edit mode): the whole chrome
3
+ is the interactive surface — click to select, drag anywhere to move.
4
+ Live (editablePlacedFields): the surface is transparent so the field
5
+ body stays usable (click-to-sign, text input); management happens on
6
+ the always-visible grip ("handle = manage, body = use"). -->
5
7
  <div
6
8
  ref="rootEl"
7
9
  class="kviewer-placed-chrome"
8
- :class="{ 'kviewer-placed-chrome--selected': selected }"
9
- :style="{ '--kvw-handle-zoom': handleZoom }"
10
+ :class="{
11
+ 'kviewer-placed-chrome--selected': selected,
12
+ 'kviewer-placed-chrome--live': mode === 'live'
13
+ }"
10
14
  @pointerdown.stop="onChromePointerDown"
11
15
  @click.stop
12
16
  >
13
17
  <div
14
- v-if="selected"
18
+ class="kviewer-placed-chrome__outline"
19
+ :class="{
20
+ 'kviewer-placed-chrome__outline--placed': field.origin === 'placed',
21
+ 'kviewer-placed-chrome__outline--selected': selected
22
+ }"
23
+ />
24
+ <div
25
+ v-if="selected || mode === 'live'"
15
26
  class="kviewer-placed-chrome__grip"
16
27
  :title="field.fieldName"
28
+ @pointerdown.stop="onChromePointerDown"
29
+ @click.stop
17
30
  >
18
31
  <UIcon name="i-lucide-grip-vertical" class="kviewer-placed-chrome__grip-dots" />
19
32
  <span class="kviewer-placed-chrome__grip-label">{{ field.fieldName }}</span>
@@ -33,20 +46,15 @@
33
46
  </template>
34
47
 
35
48
  <script setup>
36
- import { computed, ref } from "vue";
49
+ import { ref } from "vue";
37
50
  import { useFormFields } from "../../composables/useFormFields";
38
- import { useViewerState } from "../../composables/useViewerState";
39
51
  const rootEl = ref(null);
40
- const viewerState = useViewerState();
41
- const handleZoom = computed(() => {
42
- const totalZoom = (props.scale || 1) * (viewerState.visualZoom.value || 1);
43
- return Math.max(0.4, 1 / totalZoom).toFixed(3);
44
- });
45
52
  const props = defineProps({
46
53
  field: { type: Object, required: true },
47
54
  selected: { type: Boolean, required: true },
48
55
  scale: { type: Number, required: true },
49
- pageHeight: { type: Number, required: true }
56
+ pageHeight: { type: Number, required: true },
57
+ mode: { type: String, required: true }
50
58
  });
51
59
  const emit = defineEmits(["select"]);
52
60
  const formFields = useFormFields();
@@ -91,12 +99,16 @@ function onChromePointerDown(e) {
91
99
  startRect[2] + dx,
92
100
  startRect[3] + pdfDy
93
101
  ];
94
- formFields.updatePlacedField(props.field.id, { rect: next });
102
+ formFields.updatePlacedField(props.field.id, { rect: next }, { silent: true });
95
103
  };
96
104
  const onUp = () => {
97
105
  window.removeEventListener("pointermove", onMove);
98
106
  window.removeEventListener("pointerup", onUp);
99
- if (!dragging) emit("select");
107
+ if (!dragging) {
108
+ emit("select");
109
+ return;
110
+ }
111
+ formFields.updatePlacedField(props.field.id, { rect: [...props.field.rect] });
100
112
  };
101
113
  window.addEventListener("pointermove", onMove);
102
114
  window.addEventListener("pointerup", onUp);
@@ -106,6 +118,7 @@ function onHandlePointerDown(e, name) {
106
118
  const startY = e.clientY;
107
119
  const [sx1, sy1, sx2, sy2] = props.field.rect;
108
120
  const s = effectiveScale();
121
+ let resized = false;
109
122
  const onMove = (ev) => {
110
123
  const dx = (ev.clientX - startX) / s;
111
124
  const dy = (ev.clientY - startY) / s;
@@ -115,11 +128,15 @@ function onHandlePointerDown(e, name) {
115
128
  if (name.includes("s")) y1 = Math.min(y2 - 4, sy1 + pdfDy);
116
129
  if (name.includes("e")) x2 = Math.max(x1 + 4, sx2 + dx);
117
130
  if (name.includes("w")) x1 = Math.min(x2 - 4, sx1 + dx);
118
- formFields.updatePlacedField(props.field.id, { rect: [x1, y1, x2, y2] });
131
+ resized = true;
132
+ formFields.updatePlacedField(props.field.id, { rect: [x1, y1, x2, y2] }, { silent: true });
119
133
  };
120
134
  const onUp = () => {
121
135
  window.removeEventListener("pointermove", onMove);
122
136
  window.removeEventListener("pointerup", onUp);
137
+ if (resized) {
138
+ formFields.updatePlacedField(props.field.id, { rect: [...props.field.rect] });
139
+ }
123
140
  };
124
141
  window.addEventListener("pointermove", onMove);
125
142
  window.addEventListener("pointerup", onUp);
@@ -127,5 +144,5 @@ function onHandlePointerDown(e, name) {
127
144
  </script>
128
145
 
129
146
  <style>
130
- .kviewer-placed-chrome{cursor:move;inset:0;pointer-events:auto;position:absolute;touch-action:none}.kviewer-placed-chrome--selected{outline:1.5px dashed var(--kvw-field-color,#1677ff);outline-offset:0}.kviewer-placed-chrome__grip{align-items:center;background:color-mix(in srgb,var(--kvw-field-color,#1677ff) 15%,transparent);border:1px solid color-mix(in srgb,var(--kvw-field-color,#1677ff) 40%,transparent);border-radius:calc(3px*var(--kvw-handle-zoom, 1));bottom:calc(100% + 3px*var(--kvw-handle-zoom, 1));color:var(--kvw-field-color,#1677ff);cursor:move;display:flex;font-size:calc(9px*var(--kvw-handle-zoom, 1));gap:calc(3px*var(--kvw-handle-zoom, 1));height:calc(16px*var(--kvw-handle-zoom, 1));left:0;line-height:1;overflow:hidden;padding:0 calc(6px*var(--kvw-handle-zoom, 1)) 0 calc(2px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;right:0;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kviewer-placed-chrome__grip-dots{color:var(--kvw-field-color,#1677ff);flex:0 0 auto;height:calc(10px*var(--kvw-handle-zoom, 1));width:calc(10px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__grip-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kviewer-placed-chrome__handle{background:#fff;border:1px solid var(--kvw-field-color,#1677ff);border-radius:2px;height:calc(8px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;touch-action:none;width:calc(8px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle:before{content:"";inset:calc(-8px*var(--kvw-handle-zoom, 1));position:absolute}.kviewer-placed-chrome__handle--n{left:50%;top:calc(-4px*var(--kvw-handle-zoom, 1));transform:translateX(-50%)}.kviewer-placed-chrome__handle--s{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:50%;transform:translateX(-50%)}.kviewer-placed-chrome__handle--e{right:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--w{left:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--ne{right:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--nw{left:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--se{bottom:calc(-4px*var(--kvw-handle-zoom, 1));right:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--sw{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:calc(-4px*var(--kvw-handle-zoom, 1))}
147
+ .kviewer-placed-chrome{cursor:move;inset:0;pointer-events:auto;position:absolute;touch-action:none}.kviewer-placed-chrome--live{cursor:default;pointer-events:none}.kviewer-placed-chrome__outline{border:1px dashed color-mix(in srgb,var(--kvw-field-color,#1677ff) 45%,transparent);box-sizing:border-box;height:calc(100%/var(--kvw-handle-zoom, 1));left:0;pointer-events:none;position:absolute;top:0;transform:scale(var(--kvw-handle-zoom,1));transform-origin:0 0;width:calc(100%/var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__outline--placed{border-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 75%,transparent)}.kviewer-placed-chrome__outline--selected{border:1.5px dashed var(--kvw-field-color,#1677ff)}.kviewer-placed-chrome__grip{align-items:center;background:color-mix(in srgb,var(--kvw-field-color,#1677ff) 15%,transparent);border:1px solid color-mix(in srgb,var(--kvw-field-color,#1677ff) 40%,transparent);border-radius:4px;bottom:calc(100% + 4px*var(--kvw-handle-zoom, 1));box-sizing:border-box;color:var(--kvw-field-color,#1677ff);cursor:move;display:flex;font-size:12px;gap:4px;height:22px;left:0;line-height:1;overflow:hidden;padding:0 8px 0 3px;pointer-events:auto;position:absolute;touch-action:none;transform:scale(var(--kvw-handle-zoom,1));transform-origin:0 100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:calc(100%/var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__grip-dots{color:var(--kvw-field-color,#1677ff);flex:0 0 auto;height:13px;width:13px}.kviewer-placed-chrome__grip-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kviewer-placed-chrome__handle{background:#fff;border:1px solid var(--kvw-field-color,#1677ff);border-radius:2px;height:8px;pointer-events:auto;position:absolute;touch-action:none;transform:scale(var(--kvw-handle-zoom,1));width:8px}.kviewer-placed-chrome__handle:before{content:"";inset:-8px;position:absolute}.kviewer-placed-chrome__handle--n{left:50%;top:-4px;transform:translateX(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--s{bottom:-4px;left:50%;transform:translateX(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--e{right:-4px;top:50%;transform:translateY(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--w{left:-4px;top:50%;transform:translateY(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--ne{right:-4px;top:-4px}.kviewer-placed-chrome__handle--nw{left:-4px;top:-4px}.kviewer-placed-chrome__handle--se{bottom:-4px;right:-4px}.kviewer-placed-chrome__handle--sw{bottom:-4px;left:-4px}
131
148
  </style>
@@ -6,6 +6,10 @@ type __VLS_Props = {
6
6
  scale: number;
7
7
  /** PDF-space page height (kept for parity; unused here). */
8
8
  pageHeight: number;
9
+ /** 'authoring' = form-edit mode (chrome is the whole interactive
10
+ * surface). 'live' = editablePlacedFields outside form-edit mode
11
+ * (chrome is transparent except grip and handles). */
12
+ mode: 'authoring' | 'live';
9
13
  };
10
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
11
15
  select: () => any;
@@ -0,0 +1,7 @@
1
+ import type { FormFieldDefinition } from '../../annotation/engine/types.js';
2
+ type __VLS_Props = {
3
+ field: FormFieldDefinition;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <!-- Same pill as AnnotationToolbar, but anchored in the page DOM below
3
+ the selected field so it follows pan/zoom for free (the Konva
4
+ selection popover is viewport-fixed because the stage has no DOM
5
+ anchor to attach to). Stops pointer events so a click here never
6
+ reaches page-level deselect handlers. -->
7
+ <div
8
+ class="kviewer-placed-toolbar"
9
+ @pointerdown.stop
10
+ @click.stop
11
+ >
12
+ <div class="flex items-center gap-0.5 bg-elevated rounded-lg shadow-lg border border-default p-1">
13
+ <SelectionToolbarItems :items="customItems" :ctx="selectionCtx" />
14
+
15
+ <button
16
+ class="flex items-center justify-center w-7 h-7 rounded-md hover:bg-default cursor-pointer"
17
+ :title="t('deleteField')"
18
+ data-testid="placed-field-delete"
19
+ @click="onDelete"
20
+ >
21
+ <UIcon name="i-lucide-trash-2" class="text-sm text-error" />
22
+ </button>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { computed, inject } from "vue";
29
+ import { useFormFields } from "../../composables/useFormFields";
30
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
31
+ import { SELECTION_ITEMS_KEY } from "../../selection-items";
32
+ import SelectionToolbarItems from "../SelectionToolbarItems.vue";
33
+ const props = defineProps({
34
+ field: { type: Object, required: true }
35
+ });
36
+ const formFields = useFormFields();
37
+ const { t } = useKviewerI18n();
38
+ const customItems = inject(SELECTION_ITEMS_KEY, void 0);
39
+ const selectionCtx = computed(() => ({
40
+ kind: "field",
41
+ annotations: [],
42
+ field: props.field
43
+ }));
44
+ function onDelete() {
45
+ formFields.removePlacedField(props.field.id);
46
+ }
47
+ </script>
48
+
49
+ <style>
50
+ .kviewer-placed-toolbar{left:50%;pointer-events:auto;position:absolute;top:calc(100% + 8px*var(--kvw-handle-zoom, 1));transform:translateX(-50%) scale(var(--kvw-handle-zoom,1));transform-origin:50% 0;width:-moz-max-content;width:max-content;z-index:1}
51
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { FormFieldDefinition } from '../../annotation/engine/types.js';
2
+ type __VLS_Props = {
3
+ field: FormFieldDefinition;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -27,10 +27,22 @@ export interface FormFieldsState {
27
27
  * `addPlacedField` with full control over the field's properties.
28
28
  * Returns the created definition. */
29
29
  addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
30
- /** Update a form field's rect, name, or flags. */
31
- updatePlacedField: (id: string, patch: Partial<FormFieldDefinition>) => void;
32
- /** Remove a form field and its value. */
33
- removePlacedField: (id: string) => void;
30
+ /** Update a form field's rect, name, or flags. Notifies the field-event
31
+ * sink unless `opts.silent` pass silent from the public API (the host
32
+ * knows about its own calls) and from high-frequency gesture updates
33
+ * (commit one non-silent update on gesture end instead). */
34
+ updatePlacedField: (id: string, patch: Partial<FormFieldDefinition>, opts?: {
35
+ silent?: boolean;
36
+ }) => void;
37
+ /** Remove a form field and its value. Same `silent` contract as
38
+ * `updatePlacedField`. */
39
+ removePlacedField: (id: string, opts?: {
40
+ silent?: boolean;
41
+ }) => void;
42
+ /** Install the sink that receives user-driven field mutations. Viewer.vue
43
+ * owns it and fans out to the `field-updated` / `field-removed` emits and
44
+ * the embed bridge. Survives document changes (unlike the value sink). */
45
+ setFieldEventSink: (sink: FieldEventSink | null) => void;
34
46
  /** All form-field definitions, flattened across pages. */
35
47
  getAllFields: () => FormFieldDefinition[];
36
48
  /** All user-placed field definitions (used by export). */
@@ -45,19 +57,6 @@ export interface FormFieldsState {
45
57
  selectedPlacedFieldId: Ref<string | null>;
46
58
  /** Select or deselect a placed field. Pass null to deselect. */
47
59
  selectPlacedField: (id: string | null) => void;
48
- /** Opaque role id new placements are auto-tagged with. The host owns
49
- * what roles exist; the editor only carries the active id forward to
50
- * newly placed fields and looks up its color via roleColors. */
51
- activeRoleId: Ref<string | null>;
52
- /** Set or clear the active role for upcoming placements. */
53
- setActiveRole: (id: string | null) => void;
54
- /** Map from roleId → CSS color, supplied by the host via the KViewer
55
- * `roleColors` prop. Reading this drives the chrome and placement
56
- * preview color. The editor never mutates role definitions itself. */
57
- roleColors: ShallowRef<Record<string, string>>;
58
- /** Replace the host-provided color map. Called by KViewer when the
59
- * prop changes. */
60
- setRoleColors: (colors: Record<string, string>) => void;
61
60
  /** Per-field-type presets merged into every tool placement. Host-owned
62
61
  * via the KViewer `fieldDefaults` prop or the bridge; programmatic
63
62
  * `addFormField` calls are NOT affected (the payload is explicit). */
@@ -110,5 +109,17 @@ export interface FormFieldsState {
110
109
  /** Sink signature used by [[setValueWriteSink]]. Receives every user-driven
111
110
  * field value write (the primary id AND each mirrored sibling). */
112
111
  export type ValueWriteSink = (fieldId: string, value: string | boolean | string[]) => void;
112
+ /** User-driven field mutation, delivered to the sink installed via
113
+ * [[setFieldEventSink]]. `field` is the post-mutation definition for
114
+ * updates and the last-known definition for removals. */
115
+ export type FieldEvent = {
116
+ type: 'updated';
117
+ field: FormFieldDefinition;
118
+ patch: Partial<FormFieldDefinition>;
119
+ } | {
120
+ type: 'removed';
121
+ field: FormFieldDefinition;
122
+ };
123
+ export type FieldEventSink = (event: FieldEvent) => void;
113
124
  export declare function provideFormFields(): FormFieldsState;
114
125
  export declare function useFormFields(): FormFieldsState;
@@ -12,16 +12,8 @@ export function provideFormFields() {
12
12
  const fieldDefinitions = shallowRef(/* @__PURE__ */ new Map());
13
13
  const fieldValues = shallowRef(/* @__PURE__ */ new Map());
14
14
  const selectedPlacedFieldId = ref(null);
15
- const activeRoleId = ref(null);
16
- const roleColors = shallowRef({});
17
15
  const placedFieldDefaults = shallowRef({});
18
16
  const editablePlacedFields = ref(false);
19
- function setActiveRole(id) {
20
- activeRoleId.value = id;
21
- }
22
- function setRoleColors(colors) {
23
- roleColors.value = colors;
24
- }
25
17
  function setPlacedFieldDefaults(defaults) {
26
18
  placedFieldDefaults.value = defaults;
27
19
  }
@@ -43,6 +35,10 @@ export function provideFormFields() {
43
35
  function setValueWriteSink(sink) {
44
36
  valueWriteSink = sink;
45
37
  }
38
+ let fieldEventSink = null;
39
+ function setFieldEventSink(sink) {
40
+ fieldEventSink = sink;
41
+ }
46
42
  let checkboxStyleMap = null;
47
43
  let buttonCaptionMap = null;
48
44
  let signedSignatureNames = null;
@@ -297,9 +293,7 @@ export function provideFormFields() {
297
293
  )
298
294
  );
299
295
  }
300
- if (activeRoleId.value) {
301
- def.roleId = activeRoleId.value;
302
- }
296
+ if (def.meta) def.meta = { ...def.meta };
303
297
  if (payload.fieldType === "radio") {
304
298
  def.buttonValue = nextRadioOptionValue(fieldName);
305
299
  }
@@ -340,7 +334,8 @@ export function provideFormFields() {
340
334
  required: payload.required ?? false,
341
335
  origin: "placed"
342
336
  };
343
- if (payload.roleId !== void 0) def.roleId = payload.roleId;
337
+ if (payload.fieldColor !== void 0) def.fieldColor = payload.fieldColor;
338
+ if (payload.meta !== void 0) def.meta = { ...payload.meta };
344
339
  if (payload.defaultValue !== void 0) def.defaultValue = payload.defaultValue;
345
340
  if (payload.fontSize !== void 0) def.fontSize = payload.fontSize;
346
341
  if (payload.fontName !== void 0) def.fontName = payload.fontName;
@@ -401,7 +396,7 @@ export function provideFormFields() {
401
396
  while (used.has(`Option_${n}`)) n += 1;
402
397
  return `Option_${n}`;
403
398
  }
404
- function updatePlacedField(id, patch) {
399
+ function updatePlacedField(id, patch, opts) {
405
400
  let found;
406
401
  for (const [page, defs] of fieldDefinitions.value.entries()) {
407
402
  const idx = defs.findIndex((d) => d.id === id);
@@ -422,12 +417,16 @@ export function provideFormFields() {
422
417
  fv.fieldType = found.fieldType;
423
418
  triggerRef(fieldValues);
424
419
  }
420
+ if (!opts?.silent && fieldEventSink) {
421
+ fieldEventSink({ type: "updated", field: found, patch });
422
+ }
425
423
  }
426
- function removePlacedField(id) {
427
- let removed = false;
424
+ function removePlacedField(id, opts) {
425
+ let removed;
428
426
  for (const [page, defs] of fieldDefinitions.value.entries()) {
429
427
  const idx = defs.findIndex((d) => d.id === id);
430
428
  if (idx === -1) continue;
429
+ removed = defs[idx];
431
430
  const nextDefs = defs.slice();
432
431
  nextDefs.splice(idx, 1);
433
432
  if (nextDefs.length === 0) {
@@ -435,7 +434,6 @@ export function provideFormFields() {
435
434
  } else {
436
435
  fieldDefinitions.value.set(page, nextDefs);
437
436
  }
438
- removed = true;
439
437
  break;
440
438
  }
441
439
  if (!removed) return;
@@ -446,6 +444,9 @@ export function provideFormFields() {
446
444
  if (selectedPlacedFieldId.value === id) {
447
445
  selectedPlacedFieldId.value = null;
448
446
  }
447
+ if (!opts?.silent && fieldEventSink) {
448
+ fieldEventSink({ type: "removed", field: removed });
449
+ }
449
450
  }
450
451
  function getPlacedFields() {
451
452
  const out = [];
@@ -477,14 +478,12 @@ export function provideFormFields() {
477
478
  if (d.fieldType !== "signature") continue;
478
479
  const fv = fieldValues.value.get(d.id);
479
480
  const signed = typeof fv?.value === "string" && fv.value !== "" || d.signedInSource === true;
480
- const status = {
481
+ out.push({
481
482
  id: d.id,
482
483
  fieldName: d.fieldName,
483
484
  pageNumber: d.pageNumber,
484
485
  signed
485
- };
486
- if (d.roleId) status.roleId = d.roleId;
487
- out.push(status);
486
+ });
488
487
  }
489
488
  }
490
489
  return out;
@@ -556,11 +555,8 @@ export function provideFormFields() {
556
555
  resetValues,
557
556
  reset,
558
557
  setValueWriteSink,
558
+ setFieldEventSink,
559
559
  applyFieldValueFromExternal,
560
- activeRoleId,
561
- setActiveRole,
562
- roleColors,
563
- setRoleColors,
564
560
  placedFieldDefaults,
565
561
  setPlacedFieldDefaults,
566
562
  editablePlacedFields,
@@ -21,7 +21,8 @@ type EventHandler<E extends EventName> = (payload: EventPayloads[E]) => void;
21
21
  * an iframe. Wraps `postMessage` into a Promise-based RPC mirroring the
22
22
  * viewer's exposed methods, and surfaces iframe-emitted events
23
23
  * (`ready`, `formEditMode-changed`, `viewedPages-changed`, `all-pages-read`,
24
- * `field-placed`, `signedStatus-changed`) via `on()`.
24
+ * `field-placed`, `field-updated`, `field-removed`, `signedStatus-changed`)
25
+ * via `on()`.
25
26
  */
26
27
  export declare class KViewerEmbedClient {
27
28
  private readonly iframe;
@@ -68,7 +69,8 @@ export declare class KViewerEmbedClient {
68
69
  * a push-style signal subscribe via `on('signedStatus-changed', …)`. */
69
70
  getSignedStatus(): Promise<boolean>;
70
71
  /** Per-widget signed-state snapshot (id, fieldName, pageNumber,
71
- * signed, roleId). */
72
+ * signed). Correlate to host data by `id` (see the `meta` bag on
73
+ * `getFormFields()` for grouping, e.g. by signer). */
72
74
  getSignatureFieldStatus(): Promise<SignatureFieldStatus[]>;
73
75
  getFormEditMode(): Promise<boolean>;
74
76
  setFormEditMode(enabled: boolean): Promise<void>;
@@ -104,7 +104,8 @@ export class KViewerEmbedClient {
104
104
  return this.call("getSignedStatus");
105
105
  }
106
106
  /** Per-widget signed-state snapshot (id, fieldName, pageNumber,
107
- * signed, roleId). */
107
+ * signed). Correlate to host data by `id` (see the `meta` bag on
108
+ * `getFormFields()` for grouping, e.g. by signer). */
108
109
  getSignatureFieldStatus() {
109
110
  return this.call("getSignatureFieldStatus");
110
111
  }
@@ -27,6 +27,8 @@ export interface KViewerApi {
27
27
  setFieldDefaults: (defaults: PlacedFieldDefaults) => void;
28
28
  setClickToSign: (enabled: boolean) => void;
29
29
  onFieldPlaced: (cb: (field: FormFieldDefinition) => void) => () => void;
30
+ onFieldUpdated: (cb: (field: FormFieldDefinition, patch: Partial<FormFieldDefinition>) => void) => () => void;
31
+ onFieldRemoved: (cb: (field: FormFieldDefinition) => void) => () => void;
30
32
  getSignedStatus: () => boolean;
31
33
  getSignatureFieldStatus: () => SignatureFieldStatus[];
32
34
  onSignedStatusChanged: (cb: (payload: {
@@ -202,6 +202,22 @@ export function useKViewerEmbedBridge(viewerRef, options) {
202
202
  )
203
203
  );
204
204
  }
205
+ if (typeof viewer.onFieldUpdated === "function") {
206
+ events.push("field-updated");
207
+ stopViewerStateWatches.push(
208
+ viewer.onFieldUpdated(
209
+ (field, patch) => postEvent("field-updated", { field, patch })
210
+ )
211
+ );
212
+ }
213
+ if (typeof viewer.onFieldRemoved === "function") {
214
+ events.push("field-removed");
215
+ stopViewerStateWatches.push(
216
+ viewer.onFieldRemoved(
217
+ (field) => postEvent("field-removed", { field })
218
+ )
219
+ );
220
+ }
205
221
  if (typeof viewer.onSignedStatusChanged === "function") {
206
222
  events.push("signedStatus-changed");
207
223
  stopViewerStateWatches.push(
@@ -150,6 +150,18 @@ export interface EventPayloads {
150
150
  'field-placed': {
151
151
  field: FormFieldDefinition;
152
152
  };
153
+ /** The user changed a field through the viewer UI (moved/resized it or
154
+ * edited its properties). Not fired for `updateFormField()` calls.
155
+ * Continuous edits may fire repeatedly — treat as an upsert. */
156
+ 'field-updated': {
157
+ field: FormFieldDefinition;
158
+ patch: Partial<FormFieldDefinition>;
159
+ };
160
+ /** The user removed a field through the viewer UI. Not fired for
161
+ * `removeFormField()` calls. `field` is the last-known definition. */
162
+ 'field-removed': {
163
+ field: FormFieldDefinition;
164
+ };
153
165
  /** A signature field's signed state changed (signed, cleared, added,
154
166
  * removed, or renamed). Push-style counterpart to `getSignedStatus()`. */
155
167
  'signedStatus-changed': {
@@ -1,6 +1,6 @@
1
1
  export type { ExportPdfOptions } from './annotation/pdf-export/export.js';
2
2
  export type { ViewerTabItem, AddTabOptions } from './components/ViewerTabs.vue.js';
3
- export type { AddFormFieldPayload, CheckboxStyle, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, PlacedFieldDefaultProps, PlacedFieldDefaults, SignatureData, SignatureFieldStatus, SignatureHandlers, } from './annotation/engine/types.js';
3
+ export type { AddFormFieldPayload, CheckboxStyle, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, IAnnotationStore, PlacedFieldDefaultProps, PlacedFieldDefaults, SignatureData, SignatureFieldStatus, SignatureHandlers, } from './annotation/engine/types.js';
4
4
  export type { ViewMode } from './composables/viewMode.js';
5
5
  export type { ViewerState, ActiveTool } from './composables/useViewerState.js';
6
6
  export type { KViewerApi, KViewerHostApi, KViewerEmbedBridgeOptions, } from './embed/bridge-host.js';
@@ -10,4 +10,5 @@ export { KViewerEmbedClient } from './embed/bridge-client.js';
10
10
  export type { EventName as KViewerEmbedEventName, EventPayloads as KViewerEmbedEventPayloads, MethodName as KViewerEmbedMethodName, } from './embed/protocol.js';
11
11
  export { KVIEWER_EMBED_PROTOCOL_VERSION, UNSUPPORTED_METHOD_ERROR_NAME as KVIEWER_EMBED_UNSUPPORTED_METHOD_ERROR_NAME, } from './embed/protocol.js';
12
12
  export type { ViewerMenuItem, ViewerMenuButtonItem, ViewerMenuCheckboxItem, ViewerMenuSeparatorItem, } from './menu-items.js';
13
+ export type { ViewerSelectionItem, ViewerSelectionButtonItem, ViewerSelectionSelectItem, ViewerSelectionContext, } from './selection-items.js';
13
14
  export type { ViewerToolId, ViewerTopToolId, ViewerBottomToolId, ViewerLayoutToolId, ViewerToolEntry, ViewerToolSlot, } from './toolbar-tools.js';