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.
Files changed (65) hide show
  1. package/README.md +29 -0
  2. package/dist/module.d.mts +9 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +25 -1
  5. package/dist/runtime/annotation/checkbox-styles.d.ts +19 -0
  6. package/dist/runtime/annotation/checkbox-styles.js +27 -0
  7. package/dist/runtime/annotation/engine/config.js +44 -0
  8. package/dist/runtime/annotation/engine/painter.d.ts +12 -2
  9. package/dist/runtime/annotation/engine/painter.js +36 -6
  10. package/dist/runtime/annotation/engine/tools/form-field.d.ts +30 -0
  11. package/dist/runtime/annotation/engine/tools/form-field.js +119 -0
  12. package/dist/runtime/annotation/engine/types.d.ts +118 -1
  13. package/dist/runtime/annotation/engine/types.js +4 -0
  14. package/dist/runtime/annotation/font-style.d.ts +23 -0
  15. package/dist/runtime/annotation/font-style.js +57 -0
  16. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +21 -0
  17. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +75 -0
  18. package/dist/runtime/annotation/parsers/parseFormFields.js +16 -3
  19. package/dist/runtime/annotation/pdf-export/export-form-fields.d.ts +8 -5
  20. package/dist/runtime/annotation/pdf-export/export-form-fields.js +295 -1
  21. package/dist/runtime/annotation/pdf-export/export.d.ts +3 -2
  22. package/dist/runtime/annotation/pdf-export/export.js +9 -2
  23. package/dist/runtime/assets/kviewer.css +1 -1
  24. package/dist/runtime/components/FormFieldLayer.d.vue.ts +5 -0
  25. package/dist/runtime/components/FormFieldLayer.vue +40 -3
  26. package/dist/runtime/components/FormFieldLayer.vue.d.ts +5 -0
  27. package/dist/runtime/components/PdfPage.vue +30 -1
  28. package/dist/runtime/components/Viewer.d.vue.ts +43 -4
  29. package/dist/runtime/components/Viewer.vue +106 -7
  30. package/dist/runtime/components/Viewer.vue.d.ts +43 -4
  31. package/dist/runtime/components/ViewerBar.vue +24 -2
  32. package/dist/runtime/components/ViewerTabs.d.vue.ts +15 -6
  33. package/dist/runtime/components/ViewerTabs.vue +7 -2
  34. package/dist/runtime/components/ViewerTabs.vue.d.ts +15 -6
  35. package/dist/runtime/components/form-fields/FormButton.vue +16 -4
  36. package/dist/runtime/components/form-fields/FormCheckbox.vue +31 -15
  37. package/dist/runtime/components/form-fields/FormDropdown.vue +3 -1
  38. package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +12 -1
  39. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +53 -10
  40. package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +12 -1
  41. package/dist/runtime/components/form-fields/FormRadioButton.vue +3 -1
  42. package/dist/runtime/components/form-fields/FormSignatureField.vue +2 -1
  43. package/dist/runtime/components/form-fields/FormTextField.vue +47 -7
  44. package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +16 -0
  45. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +131 -0
  46. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +16 -0
  47. package/dist/runtime/components/modals/SignatureDrawModal.vue +70 -15
  48. package/dist/runtime/components/panels/PlacedFieldSidebar.d.vue.ts +3 -0
  49. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +505 -0
  50. package/dist/runtime/components/panels/PlacedFieldSidebar.vue.d.ts +3 -0
  51. package/dist/runtime/components/tools/FormFieldTools.d.vue.ts +3 -0
  52. package/dist/runtime/components/tools/FormFieldTools.vue +35 -0
  53. package/dist/runtime/components/tools/FormFieldTools.vue.d.ts +3 -0
  54. package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
  55. package/dist/runtime/composables/useAnnotationEngine.js +3 -1
  56. package/dist/runtime/composables/useFormFields.d.ts +48 -2
  57. package/dist/runtime/composables/useFormFields.js +338 -6
  58. package/dist/runtime/composables/useInertiaPanzoom.js +4 -0
  59. package/dist/runtime/composables/usePageVirtualization.d.ts +6 -0
  60. package/dist/runtime/composables/usePageVirtualization.js +11 -3
  61. package/dist/runtime/composables/useViewerState.d.ts +3 -0
  62. package/dist/runtime/composables/useViewerState.js +22 -1
  63. package/dist/runtime/public-types.d.ts +1 -1
  64. package/dist/types.d.mts +1 -1
  65. package/package.json +2 -2
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <div class="flex items-center gap-1">
3
+ <ToolButton
4
+ :tool="getDef(AnnotationType.FORM_TEXT)"
5
+ :active="state.activeTool.value === AnnotationType.FORM_TEXT"
6
+ @click="state.selectTool(AnnotationType.FORM_TEXT)"
7
+ />
8
+ <ToolButton
9
+ :tool="getDef(AnnotationType.FORM_CHECKBOX)"
10
+ :active="state.activeTool.value === AnnotationType.FORM_CHECKBOX"
11
+ @click="state.selectTool(AnnotationType.FORM_CHECKBOX)"
12
+ />
13
+ <ToolButton
14
+ :tool="getDef(AnnotationType.FORM_RADIO)"
15
+ :active="state.activeTool.value === AnnotationType.FORM_RADIO"
16
+ @click="state.selectTool(AnnotationType.FORM_RADIO)"
17
+ />
18
+ <ToolButton
19
+ :tool="getDef(AnnotationType.FORM_SIGNATURE)"
20
+ :active="state.activeTool.value === AnnotationType.FORM_SIGNATURE"
21
+ @click="state.selectTool(AnnotationType.FORM_SIGNATURE)"
22
+ />
23
+ </div>
24
+ </template>
25
+
26
+ <script setup>
27
+ import { useViewerState } from "../../composables/useViewerState";
28
+ import { AnnotationType } from "../../annotation/engine/types";
29
+ import { annotationDefinitions } from "../../annotation/engine/config";
30
+ import ToolButton from "../ToolButton.vue";
31
+ const state = useViewerState();
32
+ function getDef(type) {
33
+ return annotationDefinitions.find((d) => d.type === type);
34
+ }
35
+ </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;
@@ -4,6 +4,8 @@ export declare function createAnnotationEngine(viewerState: ViewerState, options
4
4
  userName?: string;
5
5
  onRequestTextInput: PainterCallbacks['onRequestTextInput'];
6
6
  onRequestDeleteConfirm?: PainterCallbacks['onRequestDeleteConfirm'];
7
+ onPlaceField?: PainterCallbacks['onPlaceField'];
8
+ getFormFieldPreviewColor?: PainterCallbacks['getFormFieldPreviewColor'];
7
9
  freehandGroupingDelay?: number;
8
10
  setExternalGesturesEnabled?: (enabled: boolean) => void;
9
11
  }): Painter;
@@ -60,7 +60,9 @@ export function createAnnotationEngine(viewerState, options) {
60
60
  viewerState.history.commit();
61
61
  },
62
62
  onRequestTextInput: options.onRequestTextInput,
63
- onRequestDeleteConfirm: options.onRequestDeleteConfirm
63
+ onRequestDeleteConfirm: options.onRequestDeleteConfirm,
64
+ onPlaceField: options.onPlaceField,
65
+ getFormFieldPreviewColor: options.getFormFieldPreviewColor
64
66
  };
65
67
  const painter = new Painter({
66
68
  userName: options.userName ?? "User",
@@ -1,5 +1,5 @@
1
- import { type ShallowRef } from 'vue';
2
- import type { DetectedShape, FormFieldDefinition, FormFieldValue } from '../annotation/engine/types.js';
1
+ import { type Ref, type ShallowRef } from 'vue';
2
+ import type { AddFormFieldPayload, CheckboxStyle, DetectedShape, FormFieldDefinition, FormFieldValue, PlaceFieldPayload } from '../annotation/engine/types.js';
3
3
  export interface FormFieldsState {
4
4
  /** Field definitions grouped by page number */
5
5
  fieldDefinitions: ShallowRef<Map<number, FormFieldDefinition[]>>;
@@ -21,6 +21,52 @@ export interface FormFieldsState {
21
21
  resetValues: () => void;
22
22
  /** Create synthetic checkbox form fields from detected shapes */
23
23
  registerDetectedCheckboxes: (pageNumber: number, shapes: DetectedShape[], scale: number, pageHeight: number) => void;
24
+ /** Add a user-placed form field (from the placement tool). Returns the created def. */
25
+ addPlacedField: (payload: PlaceFieldPayload) => FormFieldDefinition;
26
+ /** Add a form field programmatically. Public API counterpart to
27
+ * `addPlacedField` with full control over the field's properties.
28
+ * Returns the created definition. */
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;
34
+ /** All form-field definitions, flattened across pages. */
35
+ getAllFields: () => FormFieldDefinition[];
36
+ /** All user-placed field definitions (used by export). */
37
+ getPlacedFields: () => FormFieldDefinition[];
38
+ /** Look up a field definition by id (any origin). */
39
+ getFieldById: (id: string) => FormFieldDefinition | undefined;
40
+ /** Id of the currently selected placed field (null if none). */
41
+ selectedPlacedFieldId: Ref<string | null>;
42
+ /** Select or deselect a placed field. Pass null to deselect. */
43
+ selectPlacedField: (id: string | null) => void;
44
+ /** Opaque role id new placements are auto-tagged with. The host owns
45
+ * what roles exist; the editor only carries the active id forward to
46
+ * newly placed fields and looks up its color via roleColors. */
47
+ activeRoleId: Ref<string | null>;
48
+ /** Set or clear the active role for upcoming placements. */
49
+ setActiveRole: (id: string | null) => void;
50
+ /** Map from roleId → CSS color, supplied by the host via the KViewer
51
+ * `roleColors` prop. Reading this drives the chrome and placement
52
+ * preview color. The editor never mutates role definitions itself. */
53
+ roleColors: ShallowRef<Record<string, string>>;
54
+ /** Replace the host-provided color map. Called by KViewer when the
55
+ * prop changes. */
56
+ setRoleColors: (colors: Record<string, string>) => void;
57
+ /** Apply checkbox-style hints (extracted from `/MK /CA` in the source PDF)
58
+ * to existing parsed checkbox defs and store the map for any defs
59
+ * parsed afterwards. Keyed by checkbox fieldName. */
60
+ applyCheckboxStyles: (styles: Map<string, CheckboxStyle>) => void;
61
+ /** Apply push-button captions (extracted from `/MK /CA` in the source
62
+ * PDF) to existing parsed button defs and store the map for any defs
63
+ * parsed afterwards. Keyed by button fieldName. */
64
+ applyButtonCaptions: (captions: Map<string, string>) => void;
65
+ /** Whether this field is locked by another (signed) signature field's
66
+ * lock rule. Reactive to both definitions and values, so a UI computed
67
+ * that calls this re-runs when any signature is signed or its lock
68
+ * rule changes. */
69
+ isFieldLocked: (fieldId: string) => boolean;
24
70
  /** Clear all values and definitions (for document change) */
25
71
  reset: () => void;
26
72
  }
@@ -1,18 +1,100 @@
1
1
  import {
2
2
  shallowRef,
3
+ ref,
3
4
  triggerRef,
4
5
  provide,
5
6
  inject
6
7
  } from "vue";
7
8
  import { parseFormFields } from "../annotation/parsers/parseFormFields.js";
9
+ import { generateUUID } from "../annotation/engine/utils.js";
8
10
  const FORM_FIELDS_KEY = Symbol("kviewer-form-fields");
9
11
  export function provideFormFields() {
10
12
  const fieldDefinitions = shallowRef(/* @__PURE__ */ new Map());
11
13
  const fieldValues = shallowRef(/* @__PURE__ */ new Map());
14
+ const selectedPlacedFieldId = ref(null);
15
+ const activeRoleId = ref(null);
16
+ const roleColors = shallowRef({});
17
+ function setActiveRole(id) {
18
+ activeRoleId.value = id;
19
+ }
20
+ function setRoleColors(colors) {
21
+ roleColors.value = colors;
22
+ }
23
+ function selectPlacedField(id) {
24
+ selectedPlacedFieldId.value = id;
25
+ }
26
+ function getFieldById(id) {
27
+ for (const defs of fieldDefinitions.value.values()) {
28
+ for (const d of defs) {
29
+ if (d.id === id) return d;
30
+ }
31
+ }
32
+ return void 0;
33
+ }
34
+ let checkboxStyleMap = null;
35
+ let buttonCaptionMap = null;
36
+ function applyCheckboxStyles(styles) {
37
+ checkboxStyleMap = styles;
38
+ let changed = false;
39
+ for (const [page, defs] of fieldDefinitions.value.entries()) {
40
+ let pageChanged = false;
41
+ const next = defs.map((d) => {
42
+ if (d.fieldType !== "checkbox" || d.origin !== "parsed") return d;
43
+ const style = styles.get(d.fieldName);
44
+ if (!style) return d;
45
+ if (d.originalCheckboxStyle) return d;
46
+ pageChanged = true;
47
+ return { ...d, checkboxStyle: d.checkboxStyle ?? style, originalCheckboxStyle: style };
48
+ });
49
+ if (pageChanged) {
50
+ fieldDefinitions.value.set(page, next);
51
+ changed = true;
52
+ }
53
+ }
54
+ if (changed) triggerRef(fieldDefinitions);
55
+ }
56
+ function applyButtonCaptions(captions) {
57
+ buttonCaptionMap = captions;
58
+ let changed = false;
59
+ for (const [page, defs] of fieldDefinitions.value.entries()) {
60
+ let pageChanged = false;
61
+ const next = defs.map((d) => {
62
+ if (d.fieldType !== "button" || d.origin !== "parsed") return d;
63
+ const caption = captions.get(d.fieldName);
64
+ if (!caption) return d;
65
+ if (d.buttonLabel === caption) return d;
66
+ pageChanged = true;
67
+ return { ...d, buttonLabel: caption };
68
+ });
69
+ if (pageChanged) {
70
+ fieldDefinitions.value.set(page, next);
71
+ changed = true;
72
+ }
73
+ }
74
+ if (changed) triggerRef(fieldDefinitions);
75
+ }
12
76
  function parsePageFields(pageNumber, rawAnnotations) {
13
77
  if (fieldDefinitions.value.has(pageNumber)) return;
14
78
  const defs = parseFormFields(rawAnnotations, pageNumber);
15
79
  if (defs.length === 0) return;
80
+ if (checkboxStyleMap) {
81
+ for (const def of defs) {
82
+ if (def.fieldType === "checkbox" && !def.checkboxStyle) {
83
+ const style = checkboxStyleMap.get(def.fieldName);
84
+ if (style) {
85
+ def.checkboxStyle = style;
86
+ def.originalCheckboxStyle = style;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ if (buttonCaptionMap) {
92
+ for (const def of defs) {
93
+ if (def.fieldType !== "button") continue;
94
+ const caption = buttonCaptionMap.get(def.fieldName);
95
+ if (caption) def.buttonLabel = caption;
96
+ }
97
+ }
16
98
  fieldDefinitions.value.set(pageNumber, defs);
17
99
  triggerRef(fieldDefinitions);
18
100
  for (const def of defs) {
@@ -30,11 +112,19 @@ export function provideFormFields() {
30
112
  const existing = fieldValues.value.get(fieldId);
31
113
  if (!existing) return;
32
114
  existing.value = value;
33
- if (existing.fieldType === "radio" && typeof value === "string" && value !== "") {
34
- const fieldName = existing.fieldName;
35
- for (const [id, fv] of fieldValues.value.entries()) {
36
- if (id !== fieldId && fv.fieldName === fieldName && fv.fieldType === "radio") {
37
- fv.value = "";
115
+ const { fieldName, fieldType } = existing;
116
+ if (fieldName) {
117
+ if (fieldType === "radio" && typeof value === "string" && value !== "") {
118
+ for (const [id, fv] of fieldValues.value.entries()) {
119
+ if (id !== fieldId && fv.fieldName === fieldName && fv.fieldType === "radio") {
120
+ fv.value = "";
121
+ }
122
+ }
123
+ } else if (fieldType !== "radio") {
124
+ for (const [id, fv] of fieldValues.value.entries()) {
125
+ if (id !== fieldId && fv.fieldName === fieldName && fv.fieldType === fieldType) {
126
+ fv.value = value;
127
+ }
38
128
  }
39
129
  }
40
130
  }
@@ -97,6 +187,7 @@ export function provideFormFields() {
97
187
  rect: [pdfX1, pdfY1, pdfX2, pdfY2],
98
188
  readOnly: false,
99
189
  required: false,
190
+ origin: "detected",
100
191
  exportValue: "Yes",
101
192
  defaultValue: "Off",
102
193
  shapeType: s.shapeType
@@ -115,6 +206,228 @@ export function provideFormFields() {
115
206
  }
116
207
  triggerRef(fieldValues);
117
208
  }
209
+ function nextPlacedFieldName(fieldType) {
210
+ const base = fieldType === "text" ? "Text" : fieldType === "checkbox" ? "Checkbox" : fieldType === "signature" ? "Signature" : fieldType === "dropdown" ? "Dropdown" : fieldType === "radio" ? "Radio" : "Field";
211
+ const existing = /* @__PURE__ */ new Set();
212
+ for (const defs of fieldDefinitions.value.values()) {
213
+ for (const d of defs) existing.add(d.fieldName);
214
+ }
215
+ let n = 1;
216
+ while (existing.has(`${base}_${n}`)) n += 1;
217
+ return `${base}_${n}`;
218
+ }
219
+ function placedDefaultValue(fieldType) {
220
+ if (fieldType === "checkbox") return false;
221
+ return "";
222
+ }
223
+ function addPlacedField(payload) {
224
+ const id = `placed-${generateUUID()}`;
225
+ let fieldName;
226
+ if (payload.fieldType === "radio" && selectedPlacedFieldId.value) {
227
+ const sel = getFieldById(selectedPlacedFieldId.value);
228
+ fieldName = sel && sel.fieldType === "radio" ? sel.fieldName : nextPlacedFieldName(payload.fieldType);
229
+ } else {
230
+ fieldName = nextPlacedFieldName(payload.fieldType);
231
+ }
232
+ const def = {
233
+ id,
234
+ pageNumber: payload.pageNumber,
235
+ fieldType: payload.fieldType,
236
+ fieldName,
237
+ rect: [...payload.rectPdf],
238
+ readOnly: false,
239
+ required: false,
240
+ origin: "placed"
241
+ };
242
+ if (activeRoleId.value) {
243
+ def.roleId = activeRoleId.value;
244
+ }
245
+ if (payload.fieldType === "radio") {
246
+ def.buttonValue = nextRadioOptionValue(fieldName);
247
+ }
248
+ if (payload.fieldType === "checkbox") {
249
+ def.checkboxStyle = "check";
250
+ }
251
+ if (payload.fieldType === "dropdown") {
252
+ def.combo = true;
253
+ def.options = [{ exportValue: "Option 1", displayValue: "Option 1" }];
254
+ }
255
+ const existingDefs = fieldDefinitions.value.get(payload.pageNumber) ?? [];
256
+ fieldDefinitions.value.set(payload.pageNumber, [...existingDefs, def]);
257
+ triggerRef(fieldDefinitions);
258
+ fieldValues.value.set(id, {
259
+ fieldId: id,
260
+ fieldName: def.fieldName,
261
+ fieldType: def.fieldType,
262
+ value: placedDefaultValue(def.fieldType)
263
+ });
264
+ triggerRef(fieldValues);
265
+ if (payload.fieldType === "radio") {
266
+ selectedPlacedFieldId.value = id;
267
+ }
268
+ return def;
269
+ }
270
+ function addFormField(payload) {
271
+ const id = `placed-${generateUUID()}`;
272
+ const fieldName = payload.fieldName ?? nextPlacedFieldName(payload.fieldType);
273
+ const def = {
274
+ id,
275
+ pageNumber: payload.pageNumber,
276
+ fieldType: payload.fieldType,
277
+ fieldName,
278
+ rect: [...payload.rect],
279
+ readOnly: payload.readOnly ?? false,
280
+ required: payload.required ?? false,
281
+ origin: "placed"
282
+ };
283
+ if (payload.roleId !== void 0) def.roleId = payload.roleId;
284
+ if (payload.defaultValue !== void 0) def.defaultValue = payload.defaultValue;
285
+ if (payload.fontSize !== void 0) def.fontSize = payload.fontSize;
286
+ if (payload.fontName !== void 0) def.fontName = payload.fontName;
287
+ if (payload.color !== void 0) def.color = payload.color;
288
+ if (payload.backgroundColor !== void 0) def.backgroundColor = payload.backgroundColor;
289
+ if (payload.textAlignment !== void 0) def.textAlignment = payload.textAlignment;
290
+ if (payload.fieldType === "radio") {
291
+ def.buttonValue = payload.buttonValue ?? nextRadioOptionValue(fieldName);
292
+ }
293
+ if (payload.fieldType === "checkbox") {
294
+ def.checkboxStyle = payload.checkboxStyle ?? "check";
295
+ }
296
+ if (payload.fieldType === "dropdown") {
297
+ def.combo = payload.combo ?? true;
298
+ def.options = payload.options ?? [{ exportValue: "Option 1", displayValue: "Option 1" }];
299
+ if (payload.multiSelect !== void 0) def.multiSelect = payload.multiSelect;
300
+ }
301
+ if (payload.fieldType === "text") {
302
+ if (payload.multiLine !== void 0) def.multiLine = payload.multiLine;
303
+ if (payload.password !== void 0) def.password = payload.password;
304
+ if (payload.comb !== void 0) def.comb = payload.comb;
305
+ if (payload.maxLen !== void 0) def.maxLen = payload.maxLen;
306
+ }
307
+ if (payload.fieldType === "signature") {
308
+ if (payload.promptText !== void 0) def.promptText = payload.promptText;
309
+ if (payload.lockAction !== void 0) def.lockAction = payload.lockAction;
310
+ if (payload.lockFieldNames !== void 0) def.lockFieldNames = payload.lockFieldNames;
311
+ }
312
+ const existingDefs = fieldDefinitions.value.get(payload.pageNumber) ?? [];
313
+ fieldDefinitions.value.set(payload.pageNumber, [...existingDefs, def]);
314
+ triggerRef(fieldDefinitions);
315
+ fieldValues.value.set(id, {
316
+ fieldId: id,
317
+ fieldName: def.fieldName,
318
+ fieldType: def.fieldType,
319
+ value: getDefaultValue(def)
320
+ });
321
+ triggerRef(fieldValues);
322
+ return def;
323
+ }
324
+ function getAllFields() {
325
+ const out = [];
326
+ for (const defs of fieldDefinitions.value.values()) {
327
+ for (const d of defs) out.push(d);
328
+ }
329
+ return out;
330
+ }
331
+ function nextRadioOptionValue(groupName) {
332
+ const used = /* @__PURE__ */ new Set();
333
+ for (const defs of fieldDefinitions.value.values()) {
334
+ for (const d of defs) {
335
+ if (d.fieldType === "radio" && d.fieldName === groupName && d.buttonValue) {
336
+ used.add(d.buttonValue);
337
+ }
338
+ }
339
+ }
340
+ let n = 1;
341
+ while (used.has(`Option_${n}`)) n += 1;
342
+ return `Option_${n}`;
343
+ }
344
+ function updatePlacedField(id, patch) {
345
+ let found;
346
+ for (const [page, defs] of fieldDefinitions.value.entries()) {
347
+ const idx = defs.findIndex((d) => d.id === id);
348
+ if (idx === -1) continue;
349
+ const existing = defs[idx];
350
+ const next = { ...existing, ...patch, id: existing.id };
351
+ const nextDefs = defs.slice();
352
+ nextDefs[idx] = next;
353
+ fieldDefinitions.value.set(page, nextDefs);
354
+ found = next;
355
+ break;
356
+ }
357
+ if (!found) return;
358
+ triggerRef(fieldDefinitions);
359
+ const fv = fieldValues.value.get(id);
360
+ if (fv && (fv.fieldName !== found.fieldName || fv.fieldType !== found.fieldType)) {
361
+ fv.fieldName = found.fieldName;
362
+ fv.fieldType = found.fieldType;
363
+ triggerRef(fieldValues);
364
+ }
365
+ }
366
+ function removePlacedField(id) {
367
+ let removed = false;
368
+ for (const [page, defs] of fieldDefinitions.value.entries()) {
369
+ const idx = defs.findIndex((d) => d.id === id);
370
+ if (idx === -1) continue;
371
+ const nextDefs = defs.slice();
372
+ nextDefs.splice(idx, 1);
373
+ if (nextDefs.length === 0) {
374
+ fieldDefinitions.value.delete(page);
375
+ } else {
376
+ fieldDefinitions.value.set(page, nextDefs);
377
+ }
378
+ removed = true;
379
+ break;
380
+ }
381
+ if (!removed) return;
382
+ triggerRef(fieldDefinitions);
383
+ if (fieldValues.value.delete(id)) {
384
+ triggerRef(fieldValues);
385
+ }
386
+ if (selectedPlacedFieldId.value === id) {
387
+ selectedPlacedFieldId.value = null;
388
+ }
389
+ }
390
+ function getPlacedFields() {
391
+ const out = [];
392
+ for (const defs of fieldDefinitions.value.values()) {
393
+ for (const d of defs) {
394
+ if (d.origin === "placed" || d.origin === "detected") {
395
+ out.push(d);
396
+ continue;
397
+ }
398
+ const rectMoved = d.originalRect && rectChanged(d.rect, d.originalRect);
399
+ const styleChanged = d.fieldType === "checkbox" && (d.checkboxStyle ?? "check") !== (d.originalCheckboxStyle ?? "check");
400
+ if (rectMoved || styleChanged) {
401
+ out.push(d);
402
+ }
403
+ }
404
+ }
405
+ return out;
406
+ }
407
+ function rectChanged(a, b) {
408
+ for (let i = 0; i < 4; i++) {
409
+ if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > 0.01) return true;
410
+ }
411
+ return false;
412
+ }
413
+ function isFieldLocked(fieldId) {
414
+ const target = getFieldById(fieldId);
415
+ if (!target) return false;
416
+ for (const defs of fieldDefinitions.value.values()) {
417
+ for (const sig of defs) {
418
+ if (sig.fieldType !== "signature") continue;
419
+ if (!sig.lockAction) continue;
420
+ if (sig.id === fieldId) continue;
421
+ const fv = fieldValues.value.get(sig.id);
422
+ if (!fv || typeof fv.value !== "string" || fv.value === "") continue;
423
+ if (sig.lockAction === "all") return true;
424
+ const list = sig.lockFieldNames ?? [];
425
+ if (sig.lockAction === "include" && list.includes(target.fieldName)) return true;
426
+ if (sig.lockAction === "exclude" && !list.includes(target.fieldName)) return true;
427
+ }
428
+ }
429
+ return false;
430
+ }
118
431
  function resetValues() {
119
432
  for (const [, defs] of fieldDefinitions.value.entries()) {
120
433
  for (const def of defs) {
@@ -129,6 +442,9 @@ export function provideFormFields() {
129
442
  function reset() {
130
443
  fieldDefinitions.value = /* @__PURE__ */ new Map();
131
444
  fieldValues.value = /* @__PURE__ */ new Map();
445
+ selectedPlacedFieldId.value = null;
446
+ checkboxStyleMap = null;
447
+ buttonCaptionMap = null;
132
448
  triggerRef(fieldDefinitions);
133
449
  triggerRef(fieldValues);
134
450
  }
@@ -142,8 +458,24 @@ export function provideFormFields() {
142
458
  getAllFieldValues,
143
459
  setFieldValueByName,
144
460
  registerDetectedCheckboxes,
461
+ addPlacedField,
462
+ addFormField,
463
+ updatePlacedField,
464
+ removePlacedField,
465
+ getAllFields,
466
+ getPlacedFields,
467
+ getFieldById,
468
+ selectedPlacedFieldId,
469
+ selectPlacedField,
470
+ applyCheckboxStyles,
471
+ applyButtonCaptions,
472
+ isFieldLocked,
145
473
  resetValues,
146
- reset
474
+ reset,
475
+ activeRoleId,
476
+ setActiveRole,
477
+ roleColors,
478
+ setRoleColors
147
479
  };
148
480
  provide(FORM_FIELDS_KEY, state);
149
481
  return state;
@@ -281,6 +281,7 @@ export function useInertiaPanzoom(options = {}) {
281
281
  }
282
282
  function onDown(e) {
283
283
  if (e.pointerType === "pen") return;
284
+ if (e.pointerType === "mouse") return;
284
285
  if (!gesturesEnabled) return;
285
286
  stopMomentum();
286
287
  setWillChangeActive(true);
@@ -425,6 +426,9 @@ export function useInertiaPanzoom(options = {}) {
425
426
  pinchStart = null;
426
427
  panStart = null;
427
428
  setWillChangeActive(false);
429
+ const b = getBounds();
430
+ tx = Math.max(b.minX, Math.min(b.maxX, tx));
431
+ ty = Math.max(b.minY, Math.min(b.maxY, ty));
428
432
  apply();
429
433
  },
430
434
  stop: () => {
@@ -4,6 +4,12 @@ export interface PageMeta {
4
4
  pageNumber: number;
5
5
  width: number;
6
6
  height: number;
7
+ /** PDF user-space origin of the page's visible area (`view[0]`, `view[1]`).
8
+ * Almost always (0, 0); non-zero for PDFs whose MediaBox has been
9
+ * shifted. Annotations in `/Rect` are in default user space, so any
10
+ * layer drawn at the canvas top-left must subtract this origin. */
11
+ viewOffsetX: number;
12
+ viewOffsetY: number;
7
13
  }
8
14
  export interface PageVirtualization {
9
15
  pageMetas: ShallowRef<PageMeta[]>;
@@ -120,10 +120,16 @@ export function createPageVirtualization() {
120
120
  async function fetchPageDimensions(doc, pageNumber) {
121
121
  const proxy = await doc.getPage(pageNumber);
122
122
  const view = proxy.view;
123
+ const vx = view[0] ?? 0;
124
+ const vy = view[1] ?? 0;
125
+ const vw = view[2] ?? 0;
126
+ const vh = view[3] ?? 0;
123
127
  return {
124
128
  pageNumber,
125
- width: view[2] - view[0],
126
- height: view[3] - view[1]
129
+ width: vw - vx,
130
+ height: vh - vy,
131
+ viewOffsetX: vx,
132
+ viewOffsetY: vy
127
133
  };
128
134
  }
129
135
  async function init(doc, root) {
@@ -139,7 +145,9 @@ export function createPageVirtualization() {
139
145
  metas.push({
140
146
  pageNumber: i,
141
147
  width: firstMeta.width,
142
- height: firstMeta.height
148
+ height: firstMeta.height,
149
+ viewOffsetX: firstMeta.viewOffsetX,
150
+ viewOffsetY: firstMeta.viewOffsetY
143
151
  });
144
152
  }
145
153
  }
@@ -57,6 +57,9 @@ export interface ViewerState {
57
57
  history: AnnotationHistory;
58
58
  readonly: Ref<boolean>;
59
59
  shapeDetection: Ref<boolean>;
60
+ visualZoom: Ref<number>;
61
+ formEditMode: Ref<boolean>;
62
+ setFormEditMode: (enabled: boolean) => void;
60
63
  }
61
64
  export interface ViewerStateProvider {
62
65
  state: ViewerState;
@@ -65,7 +65,22 @@ export function provideViewerState() {
65
65
  }
66
66
  const readonly = ref(false);
67
67
  const shapeDetectionEnabled = ref(false);
68
+ const visualZoom = ref(1);
68
69
  const stylusMode = ref(false);
70
+ const formEditMode = ref(false);
71
+ function isFormFieldTool(t) {
72
+ return typeof t === "number" && (t === AnnotationType.FORM_TEXT || t === AnnotationType.FORM_CHECKBOX || t === AnnotationType.FORM_RADIO || t === AnnotationType.FORM_SIGNATURE);
73
+ }
74
+ function setFormEditMode(enabled) {
75
+ if (formEditMode.value === enabled) return;
76
+ formEditMode.value = enabled;
77
+ const t = activeTool.value;
78
+ if (enabled) {
79
+ if (typeof t === "number" && !isFormFieldTool(t)) selectTool("hand");
80
+ } else {
81
+ if (isFormFieldTool(t)) selectTool("hand");
82
+ }
83
+ }
69
84
  function setStylusMode(enabled) {
70
85
  stylusMode.value = enabled;
71
86
  if (painter.value) {
@@ -95,6 +110,9 @@ export function provideViewerState() {
95
110
  }
96
111
  function selectTool(tool, dataTransfer) {
97
112
  if (readonly.value && tool !== "hand") return;
113
+ if (isFormFieldTool(tool) && !formEditMode.value) {
114
+ formEditMode.value = true;
115
+ }
98
116
  activeTool.value = tool;
99
117
  selectedAnnotation.value = null;
100
118
  selectedAnnotations.value = [];
@@ -181,7 +199,10 @@ export function provideViewerState() {
181
199
  downloadPdf,
182
200
  history,
183
201
  readonly,
184
- shapeDetection: shapeDetectionEnabled
202
+ shapeDetection: shapeDetectionEnabled,
203
+ visualZoom,
204
+ formEditMode,
205
+ setFormEditMode
185
206
  };
186
207
  provide(VIEWER_STATE_KEY, state);
187
208
  return {
@@ -1,4 +1,4 @@
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 { SignatureData, SignatureHandlers } from './annotation/engine/types.js';
3
+ export type { AddFormFieldPayload, CheckboxStyle, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, SignatureData, SignatureHandlers, } from './annotation/engine/types.js';
4
4
  export type { ViewMode } from './composables/viewMode.js';
package/dist/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { type ExportPdfOptions, type SignatureData, type SignatureHandlers, type ViewMode } from '../dist/runtime/public-types.js'
1
+ export { type AddFormFieldPayload, type CheckboxStyle, type ExportPdfOptions, type FormFieldDefinition, type FormFieldOrigin, type FormFieldType, type FormFieldValue, type SignatureData, type SignatureHandlers, type ViewMode } from '../dist/runtime/public-types.js'
2
2
 
3
3
  export { default } from './module.mjs'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kviewer",
3
- "version": "0.0.10",
3
+ "version": "0.1.0",
4
4
  "description": "Kabema PDF Editor",
5
5
  "repository": "kabema/kviewer",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "prepack": "nuxt-module-build build",
31
- "dev": "npm run dev:prepare && nuxt dev playground",
31
+ "dev": "npm run dev:prepare && portless kviewer nuxt dev playground",
32
32
  "dev:build": "nuxt build playground",
33
33
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
34
34
  "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",