kviewer 0.0.10 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/module.d.mts +9 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -1
- package/dist/runtime/annotation/checkbox-styles.d.ts +19 -0
- package/dist/runtime/annotation/checkbox-styles.js +27 -0
- package/dist/runtime/annotation/engine/config.js +44 -0
- package/dist/runtime/annotation/engine/painter.d.ts +12 -2
- package/dist/runtime/annotation/engine/painter.js +36 -6
- package/dist/runtime/annotation/engine/tools/form-field.d.ts +30 -0
- package/dist/runtime/annotation/engine/tools/form-field.js +119 -0
- package/dist/runtime/annotation/engine/types.d.ts +118 -1
- package/dist/runtime/annotation/engine/types.js +4 -0
- package/dist/runtime/annotation/font-style.d.ts +23 -0
- package/dist/runtime/annotation/font-style.js +57 -0
- package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +21 -0
- package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +75 -0
- package/dist/runtime/annotation/parsers/parseFormFields.js +16 -3
- package/dist/runtime/annotation/pdf-export/export-form-fields.d.ts +8 -5
- package/dist/runtime/annotation/pdf-export/export-form-fields.js +295 -1
- package/dist/runtime/annotation/pdf-export/export.d.ts +3 -2
- package/dist/runtime/annotation/pdf-export/export.js +9 -2
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/FormFieldLayer.d.vue.ts +5 -0
- package/dist/runtime/components/FormFieldLayer.vue +40 -3
- package/dist/runtime/components/FormFieldLayer.vue.d.ts +5 -0
- package/dist/runtime/components/PdfPage.vue +30 -1
- package/dist/runtime/components/Viewer.d.vue.ts +43 -4
- package/dist/runtime/components/Viewer.vue +106 -7
- package/dist/runtime/components/Viewer.vue.d.ts +43 -4
- package/dist/runtime/components/ViewerBar.vue +24 -2
- package/dist/runtime/components/ViewerTabs.d.vue.ts +15 -6
- package/dist/runtime/components/ViewerTabs.vue +7 -2
- package/dist/runtime/components/ViewerTabs.vue.d.ts +15 -6
- package/dist/runtime/components/form-fields/FormButton.vue +16 -4
- package/dist/runtime/components/form-fields/FormCheckbox.vue +31 -15
- package/dist/runtime/components/form-fields/FormDropdown.vue +3 -1
- package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +12 -1
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +53 -10
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +12 -1
- package/dist/runtime/components/form-fields/FormRadioButton.vue +3 -1
- package/dist/runtime/components/form-fields/FormSignatureField.vue +2 -1
- package/dist/runtime/components/form-fields/FormTextField.vue +47 -7
- package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +16 -0
- package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +131 -0
- package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +16 -0
- package/dist/runtime/components/modals/SignatureDrawModal.vue +70 -15
- package/dist/runtime/components/panels/PlacedFieldSidebar.d.vue.ts +3 -0
- package/dist/runtime/components/panels/PlacedFieldSidebar.vue +505 -0
- package/dist/runtime/components/panels/PlacedFieldSidebar.vue.d.ts +3 -0
- package/dist/runtime/components/tools/FormFieldTools.d.vue.ts +3 -0
- package/dist/runtime/components/tools/FormFieldTools.vue +35 -0
- package/dist/runtime/components/tools/FormFieldTools.vue.d.ts +3 -0
- package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
- package/dist/runtime/composables/useAnnotationEngine.js +3 -1
- package/dist/runtime/composables/useFormFields.d.ts +48 -2
- package/dist/runtime/composables/useFormFields.js +338 -6
- package/dist/runtime/composables/useInertiaPanzoom.js +4 -0
- package/dist/runtime/composables/usePageVirtualization.d.ts +6 -0
- package/dist/runtime/composables/usePageVirtualization.js +11 -3
- package/dist/runtime/composables/useViewerState.d.ts +3 -0
- package/dist/runtime/composables/useViewerState.js +22 -1
- package/dist/runtime/public-types.d.ts +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +2 -2
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<UModal v-model:open="isOpen" title="Draw Signature" :ui="{ content: 'sm:max-w-
|
|
2
|
+
<UModal v-model:open="isOpen" title="Draw Signature" :ui="{ content: 'sm:max-w-2xl' }">
|
|
3
3
|
<template #body>
|
|
4
4
|
<div class="flex flex-col gap-3">
|
|
5
5
|
<div
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
ref="canvasWrapEl"
|
|
7
|
+
class="border border-default rounded bg-white relative w-full"
|
|
8
|
+
:style="{ aspectRatio: `${canvasAspect}` }"
|
|
8
9
|
>
|
|
9
10
|
<canvas
|
|
10
11
|
ref="canvasRef"
|
|
11
|
-
|
|
12
|
-
:height="canvasHeight"
|
|
13
|
-
class="cursor-crosshair"
|
|
12
|
+
class="cursor-crosshair block w-full h-full"
|
|
14
13
|
@pointerdown="onPointerDown"
|
|
15
14
|
@pointermove="onPointerMove"
|
|
16
15
|
@pointerup="onPointerUp"
|
|
@@ -50,7 +49,7 @@
|
|
|
50
49
|
</template>
|
|
51
50
|
|
|
52
51
|
<script setup>
|
|
53
|
-
import { ref, watch, nextTick } from "vue";
|
|
52
|
+
import { ref, watch, nextTick, onBeforeUnmount } from "vue";
|
|
54
53
|
import { defaultOptions } from "../../annotation/engine/config";
|
|
55
54
|
const props = defineProps({
|
|
56
55
|
open: { type: Boolean, required: true }
|
|
@@ -64,21 +63,51 @@ watch(
|
|
|
64
63
|
if (v) {
|
|
65
64
|
hasDrawn.value = false;
|
|
66
65
|
await nextTick();
|
|
66
|
+
syncCanvasSize();
|
|
67
67
|
clearCanvas();
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
);
|
|
71
71
|
watch(isOpen, (v) => emit("update:open", v));
|
|
72
|
-
const
|
|
73
|
-
const canvasHeight = defaultOptions.signature.HEIGHT;
|
|
72
|
+
const canvasAspect = `${defaultOptions.signature.WIDTH} / ${defaultOptions.signature.HEIGHT}`;
|
|
74
73
|
const signatureColors = defaultOptions.signature.COLORS;
|
|
75
74
|
const canvasRef = ref(null);
|
|
75
|
+
const canvasWrapEl = ref(null);
|
|
76
76
|
const strokeColor = ref(signatureColors[0]);
|
|
77
77
|
const hasDrawn = ref(false);
|
|
78
78
|
let isDrawing = false;
|
|
79
|
+
function syncCanvasSize() {
|
|
80
|
+
const canvas = canvasRef.value;
|
|
81
|
+
const wrap = canvasWrapEl.value;
|
|
82
|
+
if (!canvas || !wrap) return;
|
|
83
|
+
const cssW = Math.round(wrap.clientWidth);
|
|
84
|
+
const cssH = Math.round(wrap.clientHeight);
|
|
85
|
+
if (cssW <= 0 || cssH <= 0) return;
|
|
86
|
+
const dpr = window.devicePixelRatio || 1;
|
|
87
|
+
const targetW = Math.round(cssW * dpr);
|
|
88
|
+
const targetH = Math.round(cssH * dpr);
|
|
89
|
+
if (canvas.width === targetW && canvas.height === targetH) return;
|
|
90
|
+
canvas.width = targetW;
|
|
91
|
+
canvas.height = targetH;
|
|
92
|
+
const ctx = canvas.getContext("2d");
|
|
93
|
+
ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
94
|
+
hasDrawn.value = false;
|
|
95
|
+
}
|
|
96
|
+
let resizeObserver = null;
|
|
97
|
+
watch(canvasWrapEl, (el) => {
|
|
98
|
+
resizeObserver?.disconnect();
|
|
99
|
+
resizeObserver = null;
|
|
100
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
101
|
+
resizeObserver = new ResizeObserver(() => syncCanvasSize());
|
|
102
|
+
resizeObserver.observe(el);
|
|
103
|
+
});
|
|
104
|
+
onBeforeUnmount(() => resizeObserver?.disconnect());
|
|
79
105
|
function getCtx() {
|
|
80
106
|
return canvasRef.value?.getContext("2d") ?? null;
|
|
81
107
|
}
|
|
108
|
+
const STROKE_WIDTH = 2.5;
|
|
109
|
+
let lastPoint = null;
|
|
110
|
+
let lastMid = null;
|
|
82
111
|
function onPointerDown(e) {
|
|
83
112
|
isDrawing = true;
|
|
84
113
|
hasDrawn.value = true;
|
|
@@ -86,26 +115,52 @@ function onPointerDown(e) {
|
|
|
86
115
|
if (!ctx) return;
|
|
87
116
|
e.target.setPointerCapture(e.pointerId);
|
|
88
117
|
ctx.strokeStyle = strokeColor.value;
|
|
89
|
-
ctx.
|
|
118
|
+
ctx.fillStyle = strokeColor.value;
|
|
119
|
+
ctx.lineWidth = STROKE_WIDTH;
|
|
90
120
|
ctx.lineCap = "round";
|
|
91
121
|
ctx.lineJoin = "round";
|
|
122
|
+
const p = { x: e.offsetX, y: e.offsetY };
|
|
123
|
+
lastPoint = p;
|
|
124
|
+
lastMid = p;
|
|
92
125
|
ctx.beginPath();
|
|
93
|
-
ctx.
|
|
126
|
+
ctx.arc(p.x, p.y, STROKE_WIDTH / 2, 0, Math.PI * 2);
|
|
127
|
+
ctx.fill();
|
|
94
128
|
}
|
|
95
129
|
function onPointerMove(e) {
|
|
96
|
-
if (!isDrawing) return;
|
|
130
|
+
if (!isDrawing || !lastPoint || !lastMid) return;
|
|
97
131
|
const ctx = getCtx();
|
|
98
132
|
if (!ctx) return;
|
|
99
|
-
|
|
133
|
+
const p = { x: e.offsetX, y: e.offsetY };
|
|
134
|
+
const mid = { x: (lastPoint.x + p.x) / 2, y: (lastPoint.y + p.y) / 2 };
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.moveTo(lastMid.x, lastMid.y);
|
|
137
|
+
ctx.quadraticCurveTo(lastPoint.x, lastPoint.y, mid.x, mid.y);
|
|
100
138
|
ctx.stroke();
|
|
139
|
+
lastPoint = p;
|
|
140
|
+
lastMid = mid;
|
|
101
141
|
}
|
|
102
142
|
function onPointerUp() {
|
|
143
|
+
if (isDrawing && lastPoint && lastMid) {
|
|
144
|
+
const ctx = getCtx();
|
|
145
|
+
if (ctx) {
|
|
146
|
+
ctx.beginPath();
|
|
147
|
+
ctx.moveTo(lastMid.x, lastMid.y);
|
|
148
|
+
ctx.lineTo(lastPoint.x, lastPoint.y);
|
|
149
|
+
ctx.stroke();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
103
152
|
isDrawing = false;
|
|
153
|
+
lastPoint = null;
|
|
154
|
+
lastMid = null;
|
|
104
155
|
}
|
|
105
156
|
function clearCanvas() {
|
|
157
|
+
const canvas = canvasRef.value;
|
|
106
158
|
const ctx = getCtx();
|
|
107
|
-
if (!ctx) return;
|
|
108
|
-
ctx.
|
|
159
|
+
if (!canvas || !ctx) return;
|
|
160
|
+
ctx.save();
|
|
161
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
162
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
163
|
+
ctx.restore();
|
|
109
164
|
hasDrawn.value = false;
|
|
110
165
|
}
|
|
111
166
|
function submit() {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Inline sidebar that takes its place in the viewer's flex row.
|
|
3
|
+
Block-level positioning means it lives strictly inside the page-
|
|
4
|
+
view section — no fixed-position overlay, so it can never spill
|
|
5
|
+
under the ViewerBar at the top, and z-index battles with the
|
|
6
|
+
AnnotationToolbar are moot.
|
|
7
|
+
|
|
8
|
+
Always shown while form-edit mode is active so the user has a
|
|
9
|
+
persistent surface for field properties (and a placeholder hint
|
|
10
|
+
when nothing is selected yet). -->
|
|
11
|
+
<aside
|
|
12
|
+
v-show="state.formEditMode.value"
|
|
13
|
+
class="kviewer-placed-sidebar relative flex flex-col h-full w-80 shrink-0 border-s border-default bg-elevated overflow-hidden"
|
|
14
|
+
>
|
|
15
|
+
<header class="px-4 py-3 border-b border-default shrink-0">
|
|
16
|
+
<p class="text-highlighted font-semibold truncate">{{ title }}</p>
|
|
17
|
+
<p v-if="description" class="text-muted text-sm truncate">{{ description }}</p>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<div class="flex-1 min-h-0 overflow-y-auto">
|
|
21
|
+
<div v-if="field" class="flex flex-col gap-4 p-4">
|
|
22
|
+
<div class="flex flex-col gap-1">
|
|
23
|
+
<label class="text-xs font-medium text-muted">Field name</label>
|
|
24
|
+
<UInput
|
|
25
|
+
v-model="nameDraft"
|
|
26
|
+
size="sm"
|
|
27
|
+
spellcheck="false"
|
|
28
|
+
@blur="commitName"
|
|
29
|
+
@keydown.enter.prevent="commitName"
|
|
30
|
+
/>
|
|
31
|
+
<p class="text-xs text-muted">
|
|
32
|
+
Shared names group radio buttons and bind multiple widgets to the same value.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="flex flex-col gap-2">
|
|
37
|
+
<UCheckbox
|
|
38
|
+
:model-value="field.required"
|
|
39
|
+
label="Required"
|
|
40
|
+
@update:model-value="onRequired"
|
|
41
|
+
/>
|
|
42
|
+
<UCheckbox
|
|
43
|
+
:model-value="field.readOnly"
|
|
44
|
+
label="Read-only"
|
|
45
|
+
@update:model-value="onReadOnly"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<template v-if="field.fieldType === 'text'">
|
|
50
|
+
<div class="flex flex-col gap-2">
|
|
51
|
+
<UCheckbox
|
|
52
|
+
:model-value="!!field.multiLine"
|
|
53
|
+
label="Multi-line"
|
|
54
|
+
@update:model-value="onMultiLine"
|
|
55
|
+
/>
|
|
56
|
+
<div class="flex flex-col gap-1">
|
|
57
|
+
<label class="text-xs font-medium text-muted">Max length</label>
|
|
58
|
+
<UInput
|
|
59
|
+
type="number"
|
|
60
|
+
:model-value="field.maxLen != null ? String(field.maxLen) : ''"
|
|
61
|
+
placeholder="No limit"
|
|
62
|
+
size="sm"
|
|
63
|
+
min="0"
|
|
64
|
+
@update:model-value="onMaxLen"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<template v-if="field.fieldType === 'checkbox'">
|
|
71
|
+
<div class="flex flex-col gap-1">
|
|
72
|
+
<label class="text-xs font-medium text-muted">Style</label>
|
|
73
|
+
<USelect
|
|
74
|
+
:model-value="field.checkboxStyle ?? 'check'"
|
|
75
|
+
:items="checkboxStyleItems"
|
|
76
|
+
size="sm"
|
|
77
|
+
@update:model-value="onCheckboxStyle"
|
|
78
|
+
/>
|
|
79
|
+
<p class="text-xs text-muted">
|
|
80
|
+
Glyph drawn when checked. Saved as <code>/MK /CA</code> on export.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<template v-if="field.fieldType === 'dropdown'">
|
|
86
|
+
<div class="flex flex-col gap-1">
|
|
87
|
+
<label class="text-xs font-medium text-muted">Options</label>
|
|
88
|
+
<div class="flex flex-col gap-1">
|
|
89
|
+
<div
|
|
90
|
+
v-for="(opt, i) in field.options ?? []"
|
|
91
|
+
:key="i"
|
|
92
|
+
class="flex items-center gap-1"
|
|
93
|
+
>
|
|
94
|
+
<UInput
|
|
95
|
+
:model-value="opt.displayValue"
|
|
96
|
+
size="sm"
|
|
97
|
+
spellcheck="false"
|
|
98
|
+
placeholder="Label"
|
|
99
|
+
class="flex-1"
|
|
100
|
+
@update:model-value="(v) => updateOption(i, v)"
|
|
101
|
+
/>
|
|
102
|
+
<UButton
|
|
103
|
+
icon="i-lucide-x"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
color="neutral"
|
|
106
|
+
size="xs"
|
|
107
|
+
square
|
|
108
|
+
@click="removeOption(i)"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<UButton
|
|
113
|
+
icon="i-lucide-plus"
|
|
114
|
+
label="Add option"
|
|
115
|
+
variant="soft"
|
|
116
|
+
color="neutral"
|
|
117
|
+
size="xs"
|
|
118
|
+
class="mt-1 w-full justify-center"
|
|
119
|
+
@click="addOption"
|
|
120
|
+
/>
|
|
121
|
+
<UCheckbox
|
|
122
|
+
:model-value="field.combo !== false"
|
|
123
|
+
label="Combo box (collapsible)"
|
|
124
|
+
class="mt-2"
|
|
125
|
+
@update:model-value="onCombo"
|
|
126
|
+
/>
|
|
127
|
+
<UCheckbox
|
|
128
|
+
:model-value="!!field.editable"
|
|
129
|
+
label="Allow custom text"
|
|
130
|
+
@update:model-value="onEditable"
|
|
131
|
+
/>
|
|
132
|
+
<UCheckbox
|
|
133
|
+
:model-value="!!field.multiSelect"
|
|
134
|
+
label="Multi-select"
|
|
135
|
+
@update:model-value="onMultiSelect"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<template v-if="field.fieldType === 'signature'">
|
|
141
|
+
<div class="flex flex-col gap-1">
|
|
142
|
+
<label class="text-xs font-medium text-muted">Prompt text</label>
|
|
143
|
+
<UInput
|
|
144
|
+
v-model="promptDraft"
|
|
145
|
+
size="sm"
|
|
146
|
+
spellcheck="false"
|
|
147
|
+
placeholder="Click to sign"
|
|
148
|
+
@blur="commitPrompt"
|
|
149
|
+
@keydown.enter.prevent="commitPrompt"
|
|
150
|
+
/>
|
|
151
|
+
<p class="text-xs text-muted">
|
|
152
|
+
Shown on the unsigned widget. Defaults to "Click to sign".
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="flex flex-col gap-2">
|
|
157
|
+
<div class="flex flex-col gap-1">
|
|
158
|
+
<label class="text-xs font-medium text-muted">Lock fields after signing</label>
|
|
159
|
+
<USelect
|
|
160
|
+
:model-value="field.lockAction ?? 'none'"
|
|
161
|
+
:items="lockActionItems"
|
|
162
|
+
size="sm"
|
|
163
|
+
@update:model-value="onLockAction"
|
|
164
|
+
/>
|
|
165
|
+
<p class="text-xs text-muted">
|
|
166
|
+
Once this signature is filled, listed fields become read-only
|
|
167
|
+
inside the viewer. Not preserved on PDF export.
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div
|
|
172
|
+
v-if="field.lockAction === 'include' || field.lockAction === 'exclude'"
|
|
173
|
+
class="flex flex-col gap-1"
|
|
174
|
+
>
|
|
175
|
+
<label class="text-xs font-medium text-muted">
|
|
176
|
+
{{ field.lockAction === "include" ? "Lock these fields" : "Lock all fields except" }}
|
|
177
|
+
</label>
|
|
178
|
+
<div
|
|
179
|
+
v-if="otherFieldNames.length === 0"
|
|
180
|
+
class="text-xs text-muted italic"
|
|
181
|
+
>
|
|
182
|
+
No other named fields in the document.
|
|
183
|
+
</div>
|
|
184
|
+
<div v-else class="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
|
185
|
+
<UCheckbox
|
|
186
|
+
v-for="name in otherFieldNames"
|
|
187
|
+
:key="name"
|
|
188
|
+
:model-value="(field.lockFieldNames ?? []).includes(name)"
|
|
189
|
+
:label="name"
|
|
190
|
+
@update:model-value="(v) => toggleLockField(name, v === true)"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
|
|
197
|
+
<template v-if="field.fieldType === 'radio'">
|
|
198
|
+
<div class="flex flex-col gap-1">
|
|
199
|
+
<label class="text-xs font-medium text-muted">Group</label>
|
|
200
|
+
<USelect
|
|
201
|
+
:model-value="field.fieldName"
|
|
202
|
+
:items="radioGroupItems"
|
|
203
|
+
size="sm"
|
|
204
|
+
@update:model-value="onRadioGroup"
|
|
205
|
+
/>
|
|
206
|
+
<p class="text-xs text-muted">
|
|
207
|
+
Radios sharing a group act as one — picking one clears the others.
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="flex flex-col gap-1">
|
|
212
|
+
<label class="text-xs font-medium text-muted">Option value</label>
|
|
213
|
+
<UInput
|
|
214
|
+
v-model="buttonValueDraft"
|
|
215
|
+
size="sm"
|
|
216
|
+
spellcheck="false"
|
|
217
|
+
@blur="commitButtonValue"
|
|
218
|
+
@keydown.enter.prevent="commitButtonValue"
|
|
219
|
+
/>
|
|
220
|
+
<p class="text-xs text-muted">
|
|
221
|
+
Value stored when this option is selected. Must be unique inside the group.
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</template>
|
|
225
|
+
|
|
226
|
+
<div class="flex flex-col gap-1 text-xs text-muted">
|
|
227
|
+
<div>Type: <span class="font-medium">{{ field.fieldType }}</span></div>
|
|
228
|
+
<div>Page: <span class="font-medium">{{ field.pageNumber }}</span></div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<UButton
|
|
232
|
+
color="error"
|
|
233
|
+
variant="soft"
|
|
234
|
+
icon="i-lucide-trash-2"
|
|
235
|
+
block
|
|
236
|
+
@click="onDelete"
|
|
237
|
+
>
|
|
238
|
+
Delete field
|
|
239
|
+
</UButton>
|
|
240
|
+
</div>
|
|
241
|
+
<div v-else class="text-sm text-muted p-4">
|
|
242
|
+
Select a placed field to edit its properties.
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</aside>
|
|
246
|
+
</template>
|
|
247
|
+
|
|
248
|
+
<script setup>
|
|
249
|
+
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
250
|
+
import { CHECKBOX_STYLE_OPTIONS } from "../../annotation/checkbox-styles";
|
|
251
|
+
import { useFormFields } from "../../composables/useFormFields";
|
|
252
|
+
import { useViewerState } from "../../composables/useViewerState";
|
|
253
|
+
const checkboxStyleItems = CHECKBOX_STYLE_OPTIONS;
|
|
254
|
+
const formFields = useFormFields();
|
|
255
|
+
const state = useViewerState();
|
|
256
|
+
const field = computed(() => {
|
|
257
|
+
const id = formFields.selectedPlacedFieldId.value;
|
|
258
|
+
if (!id) return null;
|
|
259
|
+
return formFields.getFieldById(id) ?? null;
|
|
260
|
+
});
|
|
261
|
+
const title = computed(() => {
|
|
262
|
+
if (!field.value) return "Field properties";
|
|
263
|
+
return field.value.fieldName;
|
|
264
|
+
});
|
|
265
|
+
const description = computed(() => {
|
|
266
|
+
if (!field.value) return void 0;
|
|
267
|
+
return `${labelForType(field.value.fieldType)} on page ${field.value.pageNumber}`;
|
|
268
|
+
});
|
|
269
|
+
function labelForType(t) {
|
|
270
|
+
switch (t) {
|
|
271
|
+
case "text":
|
|
272
|
+
return "Text field";
|
|
273
|
+
case "checkbox":
|
|
274
|
+
return "Checkbox";
|
|
275
|
+
case "radio":
|
|
276
|
+
return "Radio button";
|
|
277
|
+
case "signature":
|
|
278
|
+
return "Signature field";
|
|
279
|
+
case "dropdown":
|
|
280
|
+
return "Dropdown";
|
|
281
|
+
case "button":
|
|
282
|
+
return "Button";
|
|
283
|
+
default:
|
|
284
|
+
return "Field";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const nameDraft = ref("");
|
|
288
|
+
const buttonValueDraft = ref("");
|
|
289
|
+
const promptDraft = ref("");
|
|
290
|
+
watch(field, (f) => {
|
|
291
|
+
nameDraft.value = f?.fieldName ?? "";
|
|
292
|
+
buttonValueDraft.value = f?.buttonValue ?? "";
|
|
293
|
+
promptDraft.value = f?.promptText ?? "";
|
|
294
|
+
}, { immediate: true });
|
|
295
|
+
const lockActionItems = [
|
|
296
|
+
{ label: "None", value: "none" },
|
|
297
|
+
{ label: "All other fields", value: "all" },
|
|
298
|
+
{ label: "Selected fields only", value: "include" },
|
|
299
|
+
{ label: "All except selected", value: "exclude" }
|
|
300
|
+
];
|
|
301
|
+
const NEW_GROUP_SENTINEL = "__new_radio_group__";
|
|
302
|
+
const radioGroupItems = computed(() => {
|
|
303
|
+
const names = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const defs of formFields.fieldDefinitions.value.values()) {
|
|
305
|
+
for (const d of defs) {
|
|
306
|
+
if (d.fieldType === "radio" && d.fieldName) names.add(d.fieldName);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const sorted = Array.from(names).sort();
|
|
310
|
+
return [
|
|
311
|
+
...sorted.map((n) => ({ label: n, value: n })),
|
|
312
|
+
{ label: "+ New group", value: NEW_GROUP_SENTINEL }
|
|
313
|
+
];
|
|
314
|
+
});
|
|
315
|
+
const otherFieldNames = computed(() => {
|
|
316
|
+
const current = field.value;
|
|
317
|
+
if (!current) return [];
|
|
318
|
+
const names = /* @__PURE__ */ new Set();
|
|
319
|
+
for (const defs of formFields.fieldDefinitions.value.values()) {
|
|
320
|
+
for (const d of defs) {
|
|
321
|
+
if (d.id === current.id) continue;
|
|
322
|
+
if (!d.fieldName) continue;
|
|
323
|
+
names.add(d.fieldName);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return Array.from(names).sort();
|
|
327
|
+
});
|
|
328
|
+
function commitName() {
|
|
329
|
+
if (!field.value) return;
|
|
330
|
+
const v = nameDraft.value.trim();
|
|
331
|
+
if (!v || v === field.value.fieldName) {
|
|
332
|
+
nameDraft.value = field.value.fieldName;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
formFields.updatePlacedField(field.value.id, { fieldName: v });
|
|
336
|
+
}
|
|
337
|
+
function commitButtonValue() {
|
|
338
|
+
if (!field.value) return;
|
|
339
|
+
const v = buttonValueDraft.value.trim();
|
|
340
|
+
if (!v || v === field.value.buttonValue) {
|
|
341
|
+
buttonValueDraft.value = field.value.buttonValue ?? "";
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
formFields.updatePlacedField(field.value.id, { buttonValue: v });
|
|
345
|
+
}
|
|
346
|
+
function commitPrompt() {
|
|
347
|
+
if (!field.value) return;
|
|
348
|
+
const v = promptDraft.value.trim();
|
|
349
|
+
const next = v === "" ? void 0 : v;
|
|
350
|
+
if (next === field.value.promptText) return;
|
|
351
|
+
formFields.updatePlacedField(field.value.id, { promptText: next });
|
|
352
|
+
}
|
|
353
|
+
function onLockAction(v) {
|
|
354
|
+
if (!field.value) return;
|
|
355
|
+
if (v === "none") {
|
|
356
|
+
formFields.updatePlacedField(field.value.id, {
|
|
357
|
+
lockAction: void 0,
|
|
358
|
+
lockFieldNames: void 0
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
formFields.updatePlacedField(field.value.id, {
|
|
363
|
+
lockAction: v,
|
|
364
|
+
// Initialize an empty list when switching to include/exclude so the
|
|
365
|
+
// checkbox UI binds against an array immediately.
|
|
366
|
+
lockFieldNames: field.value.lockFieldNames ?? []
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function nextFreeRadioGroupName() {
|
|
370
|
+
const used = /* @__PURE__ */ new Set();
|
|
371
|
+
for (const defs of formFields.fieldDefinitions.value.values()) {
|
|
372
|
+
for (const d of defs) used.add(d.fieldName);
|
|
373
|
+
}
|
|
374
|
+
let n = 1;
|
|
375
|
+
while (used.has(`Radio_${n}`)) n += 1;
|
|
376
|
+
return `Radio_${n}`;
|
|
377
|
+
}
|
|
378
|
+
function nextFreeOptionValue(groupName, ignoreId) {
|
|
379
|
+
const used = /* @__PURE__ */ new Set();
|
|
380
|
+
for (const defs of formFields.fieldDefinitions.value.values()) {
|
|
381
|
+
for (const d of defs) {
|
|
382
|
+
if (d.id === ignoreId) continue;
|
|
383
|
+
if (d.fieldType === "radio" && d.fieldName === groupName && d.buttonValue) {
|
|
384
|
+
used.add(d.buttonValue);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let n = 1;
|
|
389
|
+
while (used.has(`Option_${n}`)) n += 1;
|
|
390
|
+
return `Option_${n}`;
|
|
391
|
+
}
|
|
392
|
+
function onRadioGroup(v) {
|
|
393
|
+
if (!field.value) return;
|
|
394
|
+
if (v === field.value.fieldName) return;
|
|
395
|
+
if (v === NEW_GROUP_SENTINEL) {
|
|
396
|
+
const name = nextFreeRadioGroupName();
|
|
397
|
+
formFields.updatePlacedField(field.value.id, {
|
|
398
|
+
fieldName: name,
|
|
399
|
+
buttonValue: "Option_1"
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const current = field.value.buttonValue ?? "";
|
|
404
|
+
const collides = (() => {
|
|
405
|
+
if (!current) return true;
|
|
406
|
+
for (const defs of formFields.fieldDefinitions.value.values()) {
|
|
407
|
+
for (const d of defs) {
|
|
408
|
+
if (d.id === field.value.id) continue;
|
|
409
|
+
if (d.fieldType === "radio" && d.fieldName === v && d.buttonValue === current) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
})();
|
|
416
|
+
const buttonValue = collides ? nextFreeOptionValue(v, field.value.id) : current;
|
|
417
|
+
formFields.updatePlacedField(field.value.id, {
|
|
418
|
+
fieldName: v,
|
|
419
|
+
buttonValue
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function toggleLockField(name, checked) {
|
|
423
|
+
if (!field.value) return;
|
|
424
|
+
const current = new Set(field.value.lockFieldNames ?? []);
|
|
425
|
+
if (checked) current.add(name);
|
|
426
|
+
else current.delete(name);
|
|
427
|
+
formFields.updatePlacedField(field.value.id, {
|
|
428
|
+
lockFieldNames: Array.from(current)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function onRequired(v) {
|
|
432
|
+
if (!field.value) return;
|
|
433
|
+
formFields.updatePlacedField(field.value.id, { required: v === true });
|
|
434
|
+
}
|
|
435
|
+
function onReadOnly(v) {
|
|
436
|
+
if (!field.value) return;
|
|
437
|
+
formFields.updatePlacedField(field.value.id, { readOnly: v === true });
|
|
438
|
+
}
|
|
439
|
+
function onMultiLine(v) {
|
|
440
|
+
if (!field.value) return;
|
|
441
|
+
formFields.updatePlacedField(field.value.id, { multiLine: v === true });
|
|
442
|
+
}
|
|
443
|
+
function onMaxLen(v) {
|
|
444
|
+
if (!field.value) return;
|
|
445
|
+
const n = typeof v === "number" ? v : Number.parseInt(v, 10);
|
|
446
|
+
const next = Number.isFinite(n) && n > 0 ? n : void 0;
|
|
447
|
+
formFields.updatePlacedField(field.value.id, { maxLen: next });
|
|
448
|
+
}
|
|
449
|
+
function onDelete() {
|
|
450
|
+
if (!field.value) return;
|
|
451
|
+
formFields.removePlacedField(field.value.id);
|
|
452
|
+
}
|
|
453
|
+
function onCheckboxStyle(v) {
|
|
454
|
+
if (!field.value) return;
|
|
455
|
+
formFields.updatePlacedField(field.value.id, { checkboxStyle: v });
|
|
456
|
+
}
|
|
457
|
+
function updateOption(i, displayValue) {
|
|
458
|
+
if (!field.value) return;
|
|
459
|
+
const next = (field.value.options ?? []).slice();
|
|
460
|
+
if (!next[i]) return;
|
|
461
|
+
next[i] = { exportValue: displayValue, displayValue };
|
|
462
|
+
formFields.updatePlacedField(field.value.id, { options: next });
|
|
463
|
+
}
|
|
464
|
+
function addOption() {
|
|
465
|
+
if (!field.value) return;
|
|
466
|
+
const existing = field.value.options ?? [];
|
|
467
|
+
const label = `Option ${existing.length + 1}`;
|
|
468
|
+
const next = [...existing, { exportValue: label, displayValue: label }];
|
|
469
|
+
formFields.updatePlacedField(field.value.id, { options: next });
|
|
470
|
+
}
|
|
471
|
+
function removeOption(i) {
|
|
472
|
+
if (!field.value) return;
|
|
473
|
+
const next = (field.value.options ?? []).slice();
|
|
474
|
+
next.splice(i, 1);
|
|
475
|
+
formFields.updatePlacedField(field.value.id, { options: next });
|
|
476
|
+
}
|
|
477
|
+
function onCombo(v) {
|
|
478
|
+
if (!field.value) return;
|
|
479
|
+
formFields.updatePlacedField(field.value.id, { combo: v === true });
|
|
480
|
+
}
|
|
481
|
+
function onEditable(v) {
|
|
482
|
+
if (!field.value) return;
|
|
483
|
+
formFields.updatePlacedField(field.value.id, { editable: v === true });
|
|
484
|
+
}
|
|
485
|
+
function onMultiSelect(v) {
|
|
486
|
+
if (!field.value) return;
|
|
487
|
+
formFields.updatePlacedField(field.value.id, { multiSelect: v === true });
|
|
488
|
+
}
|
|
489
|
+
function onDocPointerDown(e) {
|
|
490
|
+
if (!formFields.selectedPlacedFieldId.value) return;
|
|
491
|
+
const target = e.target;
|
|
492
|
+
if (!target) return;
|
|
493
|
+
if (target.closest(".kviewer-form-field")) return;
|
|
494
|
+
if (target.closest(".kviewer-placed-sidebar")) return;
|
|
495
|
+
if (target.closest('[role="dialog"]')) return;
|
|
496
|
+
if (target.closest("[data-reka-popper-content-wrapper]")) return;
|
|
497
|
+
formFields.selectPlacedField(null);
|
|
498
|
+
}
|
|
499
|
+
if (typeof window !== "undefined") {
|
|
500
|
+
window.addEventListener("pointerdown", onDocPointerDown, true);
|
|
501
|
+
onBeforeUnmount(() => {
|
|
502
|
+
window.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|