kviewer 0.0.10 → 0.0.11

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 (59) hide show
  1. package/README.md +29 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +19 -0
  4. package/dist/runtime/annotation/checkbox-styles.d.ts +19 -0
  5. package/dist/runtime/annotation/checkbox-styles.js +27 -0
  6. package/dist/runtime/annotation/engine/config.js +44 -0
  7. package/dist/runtime/annotation/engine/painter.d.ts +4 -1
  8. package/dist/runtime/annotation/engine/painter.js +26 -1
  9. package/dist/runtime/annotation/engine/tools/form-field.d.ts +24 -0
  10. package/dist/runtime/annotation/engine/tools/form-field.js +117 -0
  11. package/dist/runtime/annotation/engine/types.d.ts +54 -1
  12. package/dist/runtime/annotation/engine/types.js +4 -0
  13. package/dist/runtime/annotation/font-style.d.ts +23 -0
  14. package/dist/runtime/annotation/font-style.js +57 -0
  15. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +21 -0
  16. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +75 -0
  17. package/dist/runtime/annotation/parsers/parseFormFields.js +16 -3
  18. package/dist/runtime/annotation/pdf-export/export-form-fields.d.ts +8 -5
  19. package/dist/runtime/annotation/pdf-export/export-form-fields.js +245 -1
  20. package/dist/runtime/annotation/pdf-export/export.d.ts +3 -2
  21. package/dist/runtime/annotation/pdf-export/export.js +9 -2
  22. package/dist/runtime/assets/kviewer.css +1 -1
  23. package/dist/runtime/components/FormFieldLayer.d.vue.ts +5 -0
  24. package/dist/runtime/components/FormFieldLayer.vue +40 -3
  25. package/dist/runtime/components/FormFieldLayer.vue.d.ts +5 -0
  26. package/dist/runtime/components/PdfPage.vue +24 -0
  27. package/dist/runtime/components/Viewer.d.vue.ts +18 -3
  28. package/dist/runtime/components/Viewer.vue +66 -7
  29. package/dist/runtime/components/Viewer.vue.d.ts +18 -3
  30. package/dist/runtime/components/ViewerBar.vue +24 -2
  31. package/dist/runtime/components/form-fields/FormButton.vue +16 -4
  32. package/dist/runtime/components/form-fields/FormCheckbox.vue +31 -15
  33. package/dist/runtime/components/form-fields/FormDropdown.vue +3 -1
  34. package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +12 -1
  35. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +45 -9
  36. package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +12 -1
  37. package/dist/runtime/components/form-fields/FormRadioButton.vue +3 -1
  38. package/dist/runtime/components/form-fields/FormSignatureField.vue +2 -1
  39. package/dist/runtime/components/form-fields/FormTextField.vue +47 -7
  40. package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +16 -0
  41. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +131 -0
  42. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +16 -0
  43. package/dist/runtime/components/modals/SignatureDrawModal.vue +70 -15
  44. package/dist/runtime/components/panels/PlacedFieldSidebar.d.vue.ts +3 -0
  45. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +505 -0
  46. package/dist/runtime/components/panels/PlacedFieldSidebar.vue.d.ts +3 -0
  47. package/dist/runtime/components/tools/FormFieldTools.d.vue.ts +3 -0
  48. package/dist/runtime/components/tools/FormFieldTools.vue +35 -0
  49. package/dist/runtime/components/tools/FormFieldTools.vue.d.ts +3 -0
  50. package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
  51. package/dist/runtime/composables/useAnnotationEngine.js +2 -1
  52. package/dist/runtime/composables/useFormFields.d.ts +29 -2
  53. package/dist/runtime/composables/useFormFields.js +259 -5
  54. package/dist/runtime/composables/useInertiaPanzoom.js +3 -0
  55. package/dist/runtime/composables/usePageVirtualization.d.ts +6 -0
  56. package/dist/runtime/composables/usePageVirtualization.js +11 -3
  57. package/dist/runtime/composables/useViewerState.d.ts +3 -0
  58. package/dist/runtime/composables/useViewerState.js +22 -1
  59. package/package.json +2 -2
@@ -5,7 +5,8 @@
5
5
  <ViewerBar />
6
6
  </slot>
7
7
 
8
- <div class="relative flex-1 min-h-0">
8
+ <div class="flex flex-1 min-h-0">
9
+ <div class="relative flex-1 min-w-0">
9
10
  <div
10
11
  ref="scrollContainer"
11
12
  class="absolute inset-0 overflow-hidden bg-[#F0F3F5] dark:bg-muted/50"
@@ -133,6 +134,11 @@
133
134
  <ClientOnly>
134
135
  <FloatingPageIndicator :scroll-container="scrollContainer" />
135
136
  </ClientOnly>
137
+ </div>
138
+
139
+ <ClientOnly v-if="!viewerState.readonly.value">
140
+ <PlacedFieldSidebar />
141
+ </ClientOnly>
136
142
  </div>
137
143
 
138
144
  <!-- Footer slot: empty by default -->
@@ -180,6 +186,7 @@ import {
180
186
  import ViewerBar from "./ViewerBar.vue";
181
187
  import PdfPage from "./PdfPage.vue";
182
188
  import FreeTextModal from "./modals/FreeTextModal.vue";
189
+ import PlacedFieldSidebar from "./panels/PlacedFieldSidebar.vue";
183
190
  import AnnotationToolbar from "./AnnotationToolbar.vue";
184
191
  import FloatingPageIndicator from "./FloatingPageIndicator.vue";
185
192
  import { providePageSettings } from "../composables/usePageSettings";
@@ -199,6 +206,7 @@ import { createPageProxyCache } from "../composables/usePageProxyCache";
199
206
  import { createSearchIndex } from "../composables/useSearchIndex";
200
207
  import { createShapeDetection } from "../composables/useShapeDetection";
201
208
  import { provideFormFields } from "../composables/useFormFields";
209
+ import { extractCheckboxStyles, extractButtonCaptions } from "../annotation/parsers/extractCheckboxStyles";
202
210
  import { isIPad } from "../annotation/engine/input-device";
203
211
  import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
204
212
  const props = defineProps({
@@ -212,8 +220,10 @@ const props = defineProps({
212
220
  readonly: { type: Boolean, required: false, default: false },
213
221
  shapeDetection: { type: Boolean, required: false, default: false },
214
222
  active: { type: Boolean, required: false, default: true },
215
- freehandGroupingDelay: { type: Number, required: false, default: 1e3 }
223
+ freehandGroupingDelay: { type: Number, required: false, default: 1e3 },
224
+ formEditMode: { type: Boolean, required: false, default: void 0 }
216
225
  });
226
+ const emit = defineEmits(["update:formEditMode"]);
217
227
  const viewerRoot = ref(null);
218
228
  const scrollContainer = ref(null);
219
229
  const { state: viewerState, setScrollToPageFn, setDownloadPdfFn } = provideViewerState();
@@ -223,6 +233,20 @@ watchEffect(() => {
223
233
  watchEffect(() => {
224
234
  viewerState.shapeDetection.value = props.shapeDetection ?? false;
225
235
  });
236
+ watch(
237
+ () => props.formEditMode,
238
+ (v) => {
239
+ if (v === void 0) return;
240
+ if (viewerState.formEditMode.value !== v) viewerState.formEditMode.value = v;
241
+ },
242
+ { immediate: true }
243
+ );
244
+ watch(
245
+ () => viewerState.formEditMode.value,
246
+ (v) => {
247
+ if (props.formEditMode !== v) emit("update:formEditMode", v);
248
+ }
249
+ );
226
250
  const viewerSearch = provideViewerSearch();
227
251
  const formFieldsState = provideFormFields();
228
252
  const pageSettings = providePageSettings(viewerRoot);
@@ -368,14 +392,28 @@ async function loadDocument() {
368
392
  pdfjs.GlobalWorkerOptions.workerSrc = workerModule.default;
369
393
  }
370
394
  const source = props.source;
371
- const loadingTask = pdfjs.getDocument(
372
- source
373
- );
395
+ const baseParams = source instanceof Uint8Array || typeof source === "string" ? { url: typeof source === "string" ? source : void 0, data: source instanceof Uint8Array ? source : void 0 } : { ...source };
396
+ const docParams = {
397
+ ...baseParams,
398
+ standardFontDataUrl: "/_kviewer/standard_fonts/",
399
+ cMapUrl: "/_kviewer/cmaps/",
400
+ cMapPacked: true,
401
+ useSystemFonts: true
402
+ };
403
+ const loadingTask = pdfjs.getDocument(docParams);
374
404
  const pdfDoc = await loadingTask.promise;
375
405
  doc.value = pdfDoc;
376
406
  viewerState.doc.value = pdfDoc;
377
407
  viewerState.totalPages.value = pdfDoc.numPages;
378
408
  proxyCache.setDocument(pdfDoc);
409
+ pdfDoc.getData().then(async (bytes) => {
410
+ const [styles, captions] = await Promise.all([
411
+ extractCheckboxStyles(bytes),
412
+ extractButtonCaptions(bytes)
413
+ ]);
414
+ if (styles.size > 0) formFieldsState.applyCheckboxStyles(styles);
415
+ if (captions.size > 0) formFieldsState.applyButtonCaptions(captions);
416
+ }).catch((err) => console.warn("[KViewer] Failed to read MK data from source PDF:", err));
379
417
  if (!scrollContainer.value) {
380
418
  await nextTick();
381
419
  }
@@ -487,11 +525,13 @@ async function exportPdf(options = {}) {
487
525
  const pdfData = await doc.value.getData();
488
526
  const exportContext = painter.getExportContext();
489
527
  const formFieldValues = formFieldsState.getAllFieldValues();
528
+ const placedFormFieldDefinitions = formFieldsState.getPlacedFields();
490
529
  const bytes = await exportAnnotationsToPdf({
491
530
  pdfData,
492
531
  annotations: exportContext.annotations,
493
532
  options: mergedOptions,
494
533
  formFieldValues,
534
+ placedFormFieldDefinitions,
495
535
  originalAnnotationIds: exportContext.originalIds,
496
536
  modifiedAnnotationIds: exportContext.modifiedIds,
497
537
  deletedAnnotationIds: exportContext.deletedIds
@@ -642,6 +682,10 @@ onMounted(() => {
642
682
  painter = createAnnotationEngine(viewerState, {
643
683
  userName: props.userName,
644
684
  onRequestTextInput,
685
+ onPlaceField: (payload) => {
686
+ formFieldsState.addPlacedField(payload);
687
+ viewerState.selectTool("hand");
688
+ },
645
689
  freehandGroupingDelay: props.freehandGroupingDelay,
646
690
  // Suspend panzoom gestures while an annotation is being dragged or
647
691
  // transformed. Otherwise single-finger pan runs in parallel with the
@@ -670,6 +714,8 @@ onMounted(() => {
670
714
  inertiaPanzoom.onChange(() => {
671
715
  if (viewerState.selectionRect.value) viewerState.selectionRect.value = null;
672
716
  if (!isSinglePageMode.value) virtualization.updateCurrentPageByRect();
717
+ const z = inertiaPanzoom.getScale();
718
+ if (z !== viewerState.visualZoom.value) viewerState.visualZoom.value = z;
673
719
  });
674
720
  nextTick(() => initPanzoom());
675
721
  watch(
@@ -684,7 +730,10 @@ onMounted(() => {
684
730
  },
685
731
  { immediate: true }
686
732
  );
687
- watch(() => viewerState.scale.value, () => inertiaPanzoom.reset());
733
+ watch(() => viewerState.scale.value, async () => {
734
+ await nextTick();
735
+ inertiaPanzoom.reset();
736
+ });
688
737
  watch(() => viewerState.currentPage.value, () => inertiaPanzoom.reset());
689
738
  }
690
739
  if (isIPad()) {
@@ -752,7 +801,17 @@ defineExpose({
752
801
  exportPdf,
753
802
  getKonvaCanvasState,
754
803
  getFormFieldValues: () => formFieldsState.getAllFieldValues(),
755
- setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value)
804
+ setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value),
805
+ /** Reactive form-edit-mode flag — read with `.value`. */
806
+ formEditMode: viewerState.formEditMode,
807
+ /** Programmatically enter or leave form-edit mode. */
808
+ setFormEditMode: (enabled) => viewerState.setFormEditMode(enabled),
809
+ /** Flip form-edit mode. Returns the new state. */
810
+ toggleFormEditMode: () => {
811
+ const next = !viewerState.formEditMode.value;
812
+ viewerState.setFormEditMode(next);
813
+ return next;
814
+ }
756
815
  });
757
816
  </script>
758
817
 
@@ -17,6 +17,10 @@ type __VLS_Props = {
17
17
  active?: boolean;
18
18
  /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
19
  freehandGroupingDelay?: number;
20
+ /** Whether the viewer is in form-edit mode: every form field renders
21
+ * with selection chrome, can be moved/resized, and the property panel
22
+ * is shown. Supports v-model via `v-model:form-edit-mode`. */
23
+ formEditMode?: boolean;
20
24
  };
21
25
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
22
26
  type ImportMode = 'replace' | 'merge';
@@ -27,11 +31,11 @@ declare function importAnnotations(annotations: IAnnotationStore[], options?: {
27
31
  skipped: number;
28
32
  }>;
29
33
  declare function getKonvaCanvasState(): Record<number, string>;
30
- declare var __VLS_1: {}, __VLS_40: {};
34
+ declare var __VLS_1: {}, __VLS_51: {};
31
35
  type __VLS_Slots = {} & {
32
36
  header?: (props: typeof __VLS_1) => any;
33
37
  } & {
34
- footer?: (props: typeof __VLS_40) => any;
38
+ footer?: (props: typeof __VLS_51) => any;
35
39
  };
36
40
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
37
41
  getAnnotations: () => IAnnotationStore[];
@@ -40,7 +44,17 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
44
  getKonvaCanvasState: typeof getKonvaCanvasState;
41
45
  getFormFieldValues: () => import("../annotation/engine/types.js").FormFieldValue[];
42
46
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
43
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
47
+ /** Reactive form-edit-mode flag read with `.value`. */
48
+ formEditMode: import("vue").Ref<boolean, boolean>;
49
+ /** Programmatically enter or leave form-edit mode. */
50
+ setFormEditMode: (enabled: boolean) => void;
51
+ /** Flip form-edit mode. Returns the new state. */
52
+ toggleFormEditMode: () => boolean;
53
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
54
+ "update:formEditMode": (value: boolean) => any;
55
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
56
+ "onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
57
+ }>, {
44
58
  userName: string;
45
59
  freehandGroupingDelay: number;
46
60
  zoom: number;
@@ -50,6 +64,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
50
64
  signatureHandlers: SignatureHandlers;
51
65
  viewMode: ViewMode;
52
66
  shapeDetection: boolean;
67
+ formEditMode: boolean;
53
68
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
54
69
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
55
70
  declare const _default: typeof __VLS_export;
@@ -5,7 +5,7 @@
5
5
  <UPopover>
6
6
  <UButton icon="i-lucide-menu" variant="ghost" color="neutral" size="xs" data-testid="viewer-menu" />
7
7
  <template #content>
8
- <div class="p-1">
8
+ <div class="p-1 flex flex-col gap-0.5">
9
9
  <UButton
10
10
  icon="i-lucide-download"
11
11
  label="Download"
@@ -16,6 +16,16 @@
16
16
  data-testid="viewer-download"
17
17
  @click="onDownload"
18
18
  />
19
+ <UButton
20
+ :icon="state.shapeDetection.value ? 'i-lucide-square-check-big' : 'i-lucide-square-dashed'"
21
+ :label="state.shapeDetection.value ? 'Disable form field detection' : 'Detect form fields'"
22
+ :variant="state.shapeDetection.value ? 'soft' : 'ghost'"
23
+ :color="state.shapeDetection.value ? 'primary' : 'neutral'"
24
+ size="xs"
25
+ class="w-full justify-start"
26
+ data-testid="viewer-toggle-shape-detection"
27
+ @click="state.shapeDetection.value = !state.shapeDetection.value"
28
+ />
19
29
  </div>
20
30
  </template>
21
31
  </UPopover>
@@ -30,6 +40,16 @@
30
40
 
31
41
  <HandTool />
32
42
  <MarqueeTool />
43
+
44
+ <UButton
45
+ icon="i-lucide-form-input"
46
+ :variant="state.formEditMode.value ? 'soft' : 'ghost'"
47
+ :color="state.formEditMode.value ? 'primary' : 'neutral'"
48
+ size="xs"
49
+ :title="state.formEditMode.value ? 'Exit form-edit mode' : 'Enter form-edit mode'"
50
+ data-testid="form-edit-toggle"
51
+ @click="state.setFormEditMode(!state.formEditMode.value)"
52
+ />
33
53
  </template>
34
54
 
35
55
  <div class="ml-auto flex items-center gap-2">
@@ -45,7 +65,8 @@
45
65
  </div>
46
66
 
47
67
  <div class="justify-self-center">
48
- <DrawingTools :style-version="styleVersion" />
68
+ <FormFieldTools v-if="state.formEditMode.value" />
69
+ <DrawingTools v-else :style-version="styleVersion" />
49
70
  </div>
50
71
 
51
72
  <div class="justify-self-end flex items-center gap-1">
@@ -63,6 +84,7 @@ import ZoomControls from "./tools/ZoomControls.vue";
63
84
  import HandTool from "./tools/HandTool.vue";
64
85
  import MarqueeTool from "./tools/MarqueeTool.vue";
65
86
  import DrawingTools from "./tools/DrawingTools.vue";
87
+ import FormFieldTools from "./tools/FormFieldTools.vue";
66
88
  import ActionTools from "./tools/ActionTools.vue";
67
89
  import PageInfo from "./tools/PageInfo.vue";
68
90
  import SearchTool from "./tools/SearchTool.vue";
@@ -12,6 +12,7 @@
12
12
 
13
13
  <script setup>
14
14
  import { computed } from "vue";
15
+ import { decodeFontName } from "../../annotation/font-style";
15
16
  import { useFormFields } from "../../composables/useFormFields";
16
17
  import { useViewerState } from "../../composables/useViewerState";
17
18
  const props = defineProps({
@@ -19,12 +20,23 @@ const props = defineProps({
19
20
  });
20
21
  const formFields = useFormFields();
21
22
  const state = useViewerState();
22
- const isReadOnly = computed(() => state.readonly.value || props.field.readOnly);
23
+ const isReadOnly = computed(
24
+ () => state.readonly.value || props.field.readOnly || formFields.isFieldLocked(props.field.id)
25
+ );
26
+ const FONT_BORDER_SIZE = 2;
27
+ const LINE_HEIGHT_FACTOR = 1.35;
28
+ const DEFAULT_FIELD_FONT_SIZE = 9;
23
29
  const buttonStyle = computed(() => {
24
30
  const style = {};
25
- if (props.field.fontSize && props.field.fontSize > 0) {
26
- style.fontSize = `${props.field.fontSize}px`;
27
- }
31
+ const declared = props.field.fontSize && props.field.fontSize > 0 ? props.field.fontSize : DEFAULT_FIELD_FONT_SIZE;
32
+ const rect = props.field.rect;
33
+ const rectHeight = Math.abs(rect[3] - rect[1] - FONT_BORDER_SIZE);
34
+ const computed2 = Math.min(declared, rectHeight / LINE_HEIGHT_FACTOR);
35
+ style.fontSize = `${Math.round(10 * computed2) / 10}px`;
36
+ const fontStyle = decodeFontName(props.field.fontName);
37
+ style.fontFamily = fontStyle.fontFamily;
38
+ if (fontStyle.fontWeight !== "normal") style.fontWeight = fontStyle.fontWeight;
39
+ if (fontStyle.fontStyle !== "normal") style.fontStyle = fontStyle.fontStyle;
28
40
  if (props.field.color && props.field.color.length >= 3) {
29
41
  style.color = `rgb(${props.field.color[0]}, ${props.field.color[1]}, ${props.field.color[2]})`;
30
42
  }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <!-- Detected shape: custom × icon on transparent background -->
2
+ <!-- Detected shape: lightweight × icon on transparent background -->
3
3
  <div
4
4
  v-if="isDetected"
5
5
  class="kviewer-form-checkbox kviewer-form-checkbox--detected"
@@ -29,20 +29,30 @@
29
29
  </svg>
30
30
  </div>
31
31
 
32
- <!-- Real PDF form field: native checkbox -->
33
- <div v-else class="kviewer-form-checkbox">
34
- <input
35
- type="checkbox"
36
- :checked="isChecked"
37
- :disabled="isReadOnly"
38
- :required="props.field.required"
39
- @change="onChange"
40
- >
32
+ <!-- Real PDF form field: render a custom box that draws the matching
33
+ Unicode glyph for whichever `/MK /CA` style is set (default 'check'
34
+ included). Native HTML checkboxes can't render cross/diamond/star/etc,
35
+ and rendering the default check via the same path keeps the look
36
+ consistent across all styles. -->
37
+ <div
38
+ v-else
39
+ class="kviewer-form-checkbox kviewer-form-checkbox--styled"
40
+ :class="{ 'kviewer-form-checkbox--checked': isChecked }"
41
+ role="checkbox"
42
+ :aria-checked="isChecked"
43
+ :aria-disabled="isReadOnly"
44
+ tabindex="0"
45
+ @click="toggle"
46
+ @keydown.space.prevent="toggle"
47
+ @keydown.enter.prevent="toggle"
48
+ >
49
+ <span v-if="isChecked" class="kviewer-form-checkbox__glyph">{{ styledGlyph }}</span>
41
50
  </div>
42
51
  </template>
43
52
 
44
53
  <script setup>
45
54
  import { computed } from "vue";
55
+ import { CHECKBOX_STYLE_TABLE } from "../../annotation/checkbox-styles";
46
56
  import { useFormFields } from "../../composables/useFormFields";
47
57
  import { useViewerState } from "../../composables/useViewerState";
48
58
  const props = defineProps({
@@ -51,17 +61,23 @@ const props = defineProps({
51
61
  const formFields = useFormFields();
52
62
  const state = useViewerState();
53
63
  const isDetected = computed(() => props.field.shapeType != null);
54
- const isReadOnly = computed(() => state.readonly.value || props.field.readOnly);
64
+ const isReadOnly = computed(
65
+ () => state.readonly.value || props.field.readOnly || formFields.isFieldLocked(props.field.id)
66
+ );
55
67
  const isChecked = computed(() => {
56
68
  const fv = formFields.getFieldValue(props.field.id);
57
69
  return fv?.value === true;
58
70
  });
71
+ const styledGlyph = computed(() => {
72
+ const style = props.field.checkboxStyle ?? "check";
73
+ return CHECKBOX_STYLE_TABLE[style]?.glyph ?? CHECKBOX_STYLE_TABLE.check.glyph;
74
+ });
59
75
  function toggle() {
60
76
  if (isReadOnly.value) return;
61
77
  formFields.setFieldValue(props.field.id, !isChecked.value);
62
78
  }
63
- function onChange(event) {
64
- const target = event.target;
65
- formFields.setFieldValue(props.field.id, target.checked);
66
- }
67
79
  </script>
80
+
81
+ <style>
82
+ .kviewer-form-checkbox--styled{align-items:center;background:#fff;border:1px solid #444;cursor:pointer;display:flex;height:100%;justify-content:center;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.kviewer-form-checkbox--styled[aria-disabled=true]{cursor:not-allowed;opacity:.6}.kviewer-form-checkbox__glyph{color:#000;font-size:clamp(8px,88cqh,64px);font-variant-emoji:text;line-height:1;pointer-events:none}.kviewer-form-checkbox--styled{container-type:size}
83
+ </style>
@@ -74,7 +74,9 @@ const props = defineProps({
74
74
  });
75
75
  const formFields = useFormFields();
76
76
  const state = useViewerState();
77
- const isReadOnly = computed(() => state.readonly.value || props.field.readOnly);
77
+ const isReadOnly = computed(
78
+ () => state.readonly.value || props.field.readOnly || formFields.isFieldLocked(props.field.id)
79
+ );
78
80
  const datalistId = computed(() => `kviewer-datalist-${props.field.id}`);
79
81
  const listBoxSize = computed(() => {
80
82
  const optCount = props.field.options?.length ?? 0;
@@ -2,7 +2,18 @@ import type { FormFieldDefinition } from '../../annotation/engine/types.js';
2
2
  type __VLS_Props = {
3
3
  field: FormFieldDefinition;
4
4
  pageHeight: number;
5
+ scale: number;
6
+ /** PDF user-space origin (view[0], view[1]). /Rect coords are in
7
+ * default user space; the canvas top-left corresponds to PDF
8
+ * (viewOffsetX, viewOffsetY + pageHeight), so we have to subtract. */
9
+ viewOffsetX?: number;
10
+ viewOffsetY?: number;
11
+ selected: boolean;
5
12
  };
6
- 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>;
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
14
+ select: () => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: (() => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
18
  declare const _default: typeof __VLS_export;
8
19
  export default _default;
@@ -1,33 +1,65 @@
1
1
  <template>
2
2
  <div
3
3
  class="kviewer-form-field"
4
+ :class="{
5
+ 'kviewer-form-field--editable': showChrome,
6
+ 'kviewer-form-field--placed': isPlaced,
7
+ 'kviewer-form-field--selected': props.selected
8
+ }"
4
9
  :style="positionStyle"
5
10
  >
6
- <FormTextField v-if="props.field.fieldType === 'text'" :field="props.field" />
7
- <FormCheckbox v-else-if="props.field.fieldType === 'checkbox'" :field="props.field" />
8
- <FormRadioButton v-else-if="props.field.fieldType === 'radio'" :field="props.field" />
9
- <FormDropdown v-else-if="props.field.fieldType === 'dropdown'" :field="props.field" />
10
- <FormSignatureField v-else-if="props.field.fieldType === 'signature'" :field="props.field" />
11
- <FormButton v-else-if="props.field.fieldType === 'button'" :field="props.field" />
11
+ <div
12
+ class="kviewer-form-field__body"
13
+ :class="{ 'kviewer-form-field__body--locked': isFormEditMode }"
14
+ >
15
+ <FormTextField v-if="props.field.fieldType === 'text'" :field="props.field" />
16
+ <FormCheckbox v-else-if="props.field.fieldType === 'checkbox'" :field="props.field" />
17
+ <FormRadioButton v-else-if="props.field.fieldType === 'radio'" :field="props.field" />
18
+ <FormDropdown v-else-if="props.field.fieldType === 'dropdown'" :field="props.field" />
19
+ <FormSignatureField v-else-if="props.field.fieldType === 'signature'" :field="props.field" />
20
+ <FormButton v-else-if="props.field.fieldType === 'button'" :field="props.field" />
21
+ </div>
22
+
23
+ <PlacedFieldChrome
24
+ v-if="showChrome"
25
+ :field="props.field"
26
+ :selected="props.selected"
27
+ :scale="props.scale"
28
+ :page-height="props.pageHeight"
29
+ @select="emit('select')"
30
+ />
12
31
  </div>
13
32
  </template>
14
33
 
15
34
  <script setup>
16
35
  import { computed } from "vue";
36
+ import { useViewerState } from "../../composables/useViewerState";
17
37
  import FormTextField from "./FormTextField.vue";
18
38
  import FormCheckbox from "./FormCheckbox.vue";
19
39
  import FormRadioButton from "./FormRadioButton.vue";
20
40
  import FormDropdown from "./FormDropdown.vue";
21
41
  import FormSignatureField from "./FormSignatureField.vue";
22
42
  import FormButton from "./FormButton.vue";
43
+ import PlacedFieldChrome from "./PlacedFieldChrome.vue";
23
44
  const props = defineProps({
24
45
  field: { type: Object, required: true },
25
- pageHeight: { type: Number, required: true }
46
+ pageHeight: { type: Number, required: true },
47
+ scale: { type: Number, required: true },
48
+ viewOffsetX: { type: Number, required: false },
49
+ viewOffsetY: { type: Number, required: false },
50
+ selected: { type: Boolean, required: true }
26
51
  });
52
+ const emit = defineEmits(["select"]);
53
+ const state = useViewerState();
54
+ const isPlaced = computed(() => props.field.origin === "placed");
55
+ const isFormEditMode = computed(() => state.formEditMode.value);
56
+ const showChrome = computed(() => isFormEditMode.value);
27
57
  const positionStyle = computed(() => {
28
58
  const rect = props.field.rect;
29
- const left = rect[0];
30
- const top = props.pageHeight - rect[3];
59
+ const vx = props.viewOffsetX ?? 0;
60
+ const vy = props.viewOffsetY ?? 0;
61
+ const left = rect[0] - vx;
62
+ const top = props.pageHeight + vy - rect[3];
31
63
  const width = rect[2] - rect[0];
32
64
  const height = rect[3] - rect[1];
33
65
  return {
@@ -39,3 +71,7 @@ const positionStyle = computed(() => {
39
71
  };
40
72
  });
41
73
  </script>
74
+
75
+ <style>
76
+ .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}.kviewer-form-field--editable{outline:1px dashed rgba(22,119,255,.45);outline-offset:0}.kviewer-form-field--editable.kviewer-form-field--placed{outline-color:rgba(22,119,255,.75)}.kviewer-form-field--editable.kviewer-form-field--selected{outline:none}
77
+ </style>
@@ -2,7 +2,18 @@ import type { FormFieldDefinition } from '../../annotation/engine/types.js';
2
2
  type __VLS_Props = {
3
3
  field: FormFieldDefinition;
4
4
  pageHeight: number;
5
+ scale: number;
6
+ /** PDF user-space origin (view[0], view[1]). /Rect coords are in
7
+ * default user space; the canvas top-left corresponds to PDF
8
+ * (viewOffsetX, viewOffsetY + pageHeight), so we have to subtract. */
9
+ viewOffsetX?: number;
10
+ viewOffsetY?: number;
11
+ selected: boolean;
5
12
  };
6
- 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>;
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
14
+ select: () => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onSelect?: (() => any) | undefined;
17
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
18
  declare const _default: typeof __VLS_export;
8
19
  export default _default;
@@ -21,7 +21,9 @@ const props = defineProps({
21
21
  });
22
22
  const formFields = useFormFields();
23
23
  const state = useViewerState();
24
- const isReadOnly = computed(() => state.readonly.value || props.field.readOnly);
24
+ const isReadOnly = computed(
25
+ () => state.readonly.value || props.field.readOnly || formFields.isFieldLocked(props.field.id)
26
+ );
25
27
  const isSelected = computed(() => {
26
28
  const fv = formFields.getFieldValue(props.field.id);
27
29
  if (!fv) return false;
@@ -11,7 +11,7 @@
11
11
  alt="Signature"
12
12
  >
13
13
  <span v-else class="kviewer-form-signature__placeholder">
14
- Click to sign
14
+ {{ props.field.promptText || "Click to sign" }}
15
15
  </span>
16
16
  </div>
17
17
 
@@ -43,6 +43,7 @@ const signatureUrl = computed(() => {
43
43
  const hasSigned = computed(() => !!signatureUrl.value);
44
44
  async function onClickSign() {
45
45
  if (props.field.readOnly || state.readonly.value) return;
46
+ if (formFields.isFieldLocked(props.field.id)) return;
46
47
  if (state.signatures.value.length > 0) {
47
48
  const sig = state.signatures.value[0];
48
49
  if (sig) {
@@ -19,11 +19,13 @@
19
19
  :style="fieldStyle"
20
20
  class="kviewer-form-input"
21
21
  @input="onInput"
22
+ @scroll="onScroll"
22
23
  >
23
24
  </template>
24
25
 
25
26
  <script setup>
26
27
  import { computed } from "vue";
28
+ import { decodeFontName } from "../../annotation/font-style";
27
29
  import { useFormFields } from "../../composables/useFormFields";
28
30
  import { useViewerState } from "../../composables/useViewerState";
29
31
  const props = defineProps({
@@ -31,18 +33,37 @@ const props = defineProps({
31
33
  });
32
34
  const formFields = useFormFields();
33
35
  const state = useViewerState();
34
- const isReadOnly = computed(() => state.readonly.value || props.field.readOnly);
36
+ const isReadOnly = computed(
37
+ () => state.readonly.value || props.field.readOnly || formFields.isFieldLocked(props.field.id)
38
+ );
35
39
  const currentValue = computed(() => {
36
40
  const fv = formFields.getFieldValue(props.field.id);
37
41
  return typeof fv?.value === "string" ? fv.value : "";
38
42
  });
43
+ const FONT_BORDER_SIZE = 2;
44
+ const LINE_HEIGHT_FACTOR = 1.35;
45
+ const DEFAULT_FIELD_FONT_SIZE = 9;
39
46
  const fieldStyle = computed(() => {
40
47
  const style = {};
41
- if (props.field.fontSize && props.field.fontSize > 0) {
42
- style.fontSize = `${props.field.fontSize}px`;
48
+ const declared = props.field.fontSize && props.field.fontSize > 0 ? props.field.fontSize : DEFAULT_FIELD_FONT_SIZE;
49
+ const rect = props.field.rect;
50
+ const rectHeight = Math.abs(rect[3] - rect[1] - FONT_BORDER_SIZE);
51
+ let computed2;
52
+ if (props.field.multiLine) {
53
+ const numberOfLines = Math.max(
54
+ 1,
55
+ Math.round(rectHeight / (LINE_HEIGHT_FACTOR * declared))
56
+ );
57
+ const lineHeight = rectHeight / numberOfLines;
58
+ computed2 = Math.min(declared, lineHeight / LINE_HEIGHT_FACTOR);
43
59
  } else {
44
- style.fontSize = "inherit";
60
+ computed2 = Math.min(declared, rectHeight / LINE_HEIGHT_FACTOR);
45
61
  }
62
+ style.fontSize = `${Math.round(10 * computed2) / 10}px`;
63
+ const fontStyle = decodeFontName(props.field.fontName);
64
+ style.fontFamily = fontStyle.fontFamily;
65
+ if (fontStyle.fontWeight !== "normal") style.fontWeight = fontStyle.fontWeight;
66
+ if (fontStyle.fontStyle !== "normal") style.fontStyle = fontStyle.fontStyle;
46
67
  if (props.field.color && props.field.color.length >= 3) {
47
68
  style.color = `rgb(${props.field.color[0]}, ${props.field.color[1]}, ${props.field.color[2]})`;
48
69
  }
@@ -54,16 +75,35 @@ const fieldStyle = computed(() => {
54
75
  style.textAlign = alignMap[props.field.textAlignment] ?? "left";
55
76
  }
56
77
  if (props.field.comb && props.field.maxLen && props.field.maxLen > 0) {
57
- const rect = props.field.rect;
58
78
  const fieldWidth = rect[2] - rect[0];
59
79
  const cellWidth = fieldWidth / props.field.maxLen;
60
- style.letterSpacing = `${cellWidth - (props.field.fontSize ?? 12) * 0.6}px`;
61
- style.fontFamily = "monospace";
80
+ const charWidth = computed2 * 0.6;
81
+ style.fontFamily = '"Courier New", Courier, monospace';
82
+ style.fontWeight = "700";
83
+ style.textAlign = "left";
84
+ style.letterSpacing = `${cellWidth - charWidth}px`;
85
+ style.paddingLeft = `${Math.max(0, (cellWidth - charWidth) / 2)}px`;
86
+ style.paddingRight = "0";
87
+ style.paddingTop = "0";
88
+ style.paddingBottom = "0";
89
+ style.borderLeftWidth = "0";
90
+ style.backgroundImage = "linear-gradient(to right, rgba(0, 0, 0, 0.55) 1px, transparent 1px)";
91
+ style.backgroundSize = `${100 / props.field.maxLen}% 100%`;
92
+ style.backgroundRepeat = "repeat-x";
93
+ style.borderTop = "1px solid rgba(0, 0, 0, 0.55)";
94
+ style.borderBottom = "1px solid rgba(0, 0, 0, 0.55)";
95
+ style.borderRight = "1px solid rgba(0, 0, 0, 0.55)";
62
96
  }
63
97
  return style;
64
98
  });
65
99
  function onInput(event) {
66
100
  const target = event.target;
67
101
  formFields.setFieldValue(props.field.id, target.value);
102
+ if (props.field.comb) target.scrollLeft = 0;
103
+ }
104
+ function onScroll(event) {
105
+ if (!props.field.comb) return;
106
+ const target = event.target;
107
+ if (target.scrollLeft !== 0) target.scrollLeft = 0;
68
108
  }
69
109
  </script>