kviewer 0.0.9 → 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 (74) 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/editor/editor.d.ts +6 -3
  8. package/dist/runtime/annotation/engine/editor/editor.js +22 -9
  9. package/dist/runtime/annotation/engine/editor/selector.d.ts +9 -1
  10. package/dist/runtime/annotation/engine/editor/selector.js +72 -30
  11. package/dist/runtime/annotation/engine/painter.d.ts +6 -2
  12. package/dist/runtime/annotation/engine/painter.js +42 -5
  13. package/dist/runtime/annotation/engine/tools/form-field.d.ts +24 -0
  14. package/dist/runtime/annotation/engine/tools/form-field.js +117 -0
  15. package/dist/runtime/annotation/engine/types.d.ts +54 -1
  16. package/dist/runtime/annotation/engine/types.js +4 -0
  17. package/dist/runtime/annotation/font-style.d.ts +23 -0
  18. package/dist/runtime/annotation/font-style.js +57 -0
  19. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +21 -0
  20. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +75 -0
  21. package/dist/runtime/annotation/parsers/parseFormFields.js +16 -3
  22. package/dist/runtime/annotation/pdf-export/export-form-fields.d.ts +8 -5
  23. package/dist/runtime/annotation/pdf-export/export-form-fields.js +245 -1
  24. package/dist/runtime/annotation/pdf-export/export.d.ts +3 -2
  25. package/dist/runtime/annotation/pdf-export/export.js +9 -2
  26. package/dist/runtime/assets/kviewer.css +1 -1
  27. package/dist/runtime/components/FormFieldLayer.d.vue.ts +5 -0
  28. package/dist/runtime/components/FormFieldLayer.vue +40 -3
  29. package/dist/runtime/components/FormFieldLayer.vue.d.ts +5 -0
  30. package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
  31. package/dist/runtime/components/PdfPage.vue +75 -18
  32. package/dist/runtime/components/PdfPage.vue.d.ts +6 -1
  33. package/dist/runtime/components/ToolButton.d.vue.ts +4 -2
  34. package/dist/runtime/components/ToolButton.vue.d.ts +4 -2
  35. package/dist/runtime/components/Viewer.d.vue.ts +18 -3
  36. package/dist/runtime/components/Viewer.vue +278 -170
  37. package/dist/runtime/components/Viewer.vue.d.ts +18 -3
  38. package/dist/runtime/components/ViewerBar.vue +24 -2
  39. package/dist/runtime/components/form-fields/FormButton.vue +16 -4
  40. package/dist/runtime/components/form-fields/FormCheckbox.vue +31 -15
  41. package/dist/runtime/components/form-fields/FormDropdown.vue +3 -1
  42. package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +12 -1
  43. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +45 -9
  44. package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +12 -1
  45. package/dist/runtime/components/form-fields/FormRadioButton.vue +3 -1
  46. package/dist/runtime/components/form-fields/FormSignatureField.vue +2 -1
  47. package/dist/runtime/components/form-fields/FormTextField.vue +47 -7
  48. package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +16 -0
  49. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +131 -0
  50. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +16 -0
  51. package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
  52. package/dist/runtime/components/modals/SignatureDrawModal.vue +70 -15
  53. package/dist/runtime/components/panels/PlacedFieldSidebar.d.vue.ts +3 -0
  54. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +505 -0
  55. package/dist/runtime/components/panels/PlacedFieldSidebar.vue.d.ts +3 -0
  56. package/dist/runtime/components/tools/ActionTools.vue +4 -5
  57. package/dist/runtime/components/tools/FormFieldTools.d.vue.ts +3 -0
  58. package/dist/runtime/components/tools/FormFieldTools.vue +35 -0
  59. package/dist/runtime/components/tools/FormFieldTools.vue.d.ts +3 -0
  60. package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
  61. package/dist/runtime/composables/useAnnotationEngine.js +6 -2
  62. package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
  63. package/dist/runtime/composables/useAnnotationHistory.js +10 -3
  64. package/dist/runtime/composables/useFormFields.d.ts +29 -2
  65. package/dist/runtime/composables/useFormFields.js +259 -5
  66. package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
  67. package/dist/runtime/composables/useInertiaPanzoom.js +475 -0
  68. package/dist/runtime/composables/usePageVirtualization.d.ts +9 -0
  69. package/dist/runtime/composables/usePageVirtualization.js +34 -3
  70. package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
  71. package/dist/runtime/composables/useViewerSearch.js +15 -5
  72. package/dist/runtime/composables/useViewerState.d.ts +3 -0
  73. package/dist/runtime/composables/useViewerState.js +22 -1
  74. package/package.json +2 -2
@@ -5,11 +5,12 @@
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
- class="absolute inset-0 overflow-auto bg-muted/50"
12
- :class="{ 'overflow-hidden': isSinglePageMode }"
12
+ class="absolute inset-0 overflow-hidden bg-[#F0F3F5] dark:bg-muted/50"
13
+ style="overscroll-behavior: none"
13
14
  data-testid="viewer-scroll"
14
15
  >
15
16
  <!-- Error state -->
@@ -39,24 +40,29 @@
39
40
  </div>
40
41
 
41
42
  <ClientOnly v-else>
42
- <!-- Single-page-at-a-time mode -->
43
+ <!-- Single-page mode with panzoom for pinch-to-zoom -->
43
44
  <div
44
45
  v-if="isSinglePageMode && singlePageMeta"
45
- class="flex flex-col items-center py-4"
46
- :style="{ minHeight: '100%' }"
46
+ class="panzoom-viewport"
47
47
  >
48
48
  <div
49
- :ref="(el) => onPageRef(singlePageMeta.pageNumber, el)"
50
- :style="pageSlotStyle(singlePageMeta)"
51
- class="kviewer-page-slot"
49
+ ref="zoomWrapper"
50
+ class="flex flex-col items-center justify-center p-4"
51
+ :style="{ minHeight: '100%' }"
52
52
  >
53
- <div :style="pageRotationStyle(singlePageMeta)">
54
- <PdfPage
55
- :key="singlePageMeta.pageNumber"
56
- :page-number="singlePageMeta.pageNumber"
57
- :page-meta="singlePageMeta"
58
- :scale="pageScale"
59
- />
53
+ <div
54
+ :ref="(el) => onPageRef(singlePageMeta.pageNumber, el)"
55
+ :style="pageSlotStyle(singlePageMeta)"
56
+ class="kviewer-page-slot"
57
+ >
58
+ <div :style="pageRotationStyle(singlePageMeta)">
59
+ <PdfPage
60
+ :key="singlePageMeta.pageNumber"
61
+ :page-number="singlePageMeta.pageNumber"
62
+ :page-meta="singlePageMeta"
63
+ :scale="pageScale"
64
+ />
65
+ </div>
60
66
  </div>
61
67
  </div>
62
68
  </div>
@@ -64,38 +70,14 @@
64
70
  <!-- Continuous scroll — single-page layout -->
65
71
  <div
66
72
  v-else-if="pageSettings.layoutMode.value === 'single'"
67
- class="flex flex-col items-center gap-4 py-4"
73
+ class="panzoom-viewport"
68
74
  >
69
75
  <div
70
- v-for="meta in pageMetas"
71
- :key="meta.pageNumber"
72
- :ref="(el) => onPageRef(meta.pageNumber, el)"
73
- :style="pageSlotStyle(meta)"
74
- class="kviewer-page-slot"
75
- >
76
- <div :style="pageRotationStyle(meta)">
77
- <PdfPage
78
- v-if="isPageRendered(meta.pageNumber)"
79
- :page-number="meta.pageNumber"
80
- :page-meta="meta"
81
- :scale="pageScale"
82
- />
83
- </div>
84
- </div>
85
- </div>
86
-
87
- <!-- Continuous scroll — two-page (side-by-side) layout -->
88
- <div
89
- v-else
90
- class="flex flex-col items-center gap-4 py-4"
91
- >
92
- <div
93
- v-for="(pair, index) in pagedPairs"
94
- :key="index"
95
- class="flex gap-4"
76
+ ref="zoomWrapper"
77
+ class="flex flex-col gap-4 p-4"
96
78
  >
97
79
  <div
98
- v-for="meta in pair"
80
+ v-for="meta in pageMetas"
99
81
  :key="meta.pageNumber"
100
82
  :ref="(el) => onPageRef(meta.pageNumber, el)"
101
83
  :style="pageSlotStyle(meta)"
@@ -112,12 +94,51 @@
112
94
  </div>
113
95
  </div>
114
96
  </div>
97
+
98
+ <!-- Continuous scroll — two-page (side-by-side) layout -->
99
+ <div
100
+ v-else
101
+ class="panzoom-viewport"
102
+ >
103
+ <div
104
+ ref="zoomWrapper"
105
+ class="flex flex-col gap-4 p-4"
106
+ >
107
+ <div
108
+ v-for="(pair, index) in pagedPairs"
109
+ :key="index"
110
+ class="flex gap-4"
111
+ >
112
+ <div
113
+ v-for="meta in pair"
114
+ :key="meta.pageNumber"
115
+ :ref="(el) => onPageRef(meta.pageNumber, el)"
116
+ :style="pageSlotStyle(meta)"
117
+ class="kviewer-page-slot"
118
+ >
119
+ <div :style="pageRotationStyle(meta)">
120
+ <PdfPage
121
+ v-if="isPageRendered(meta.pageNumber)"
122
+ :page-number="meta.pageNumber"
123
+ :page-meta="meta"
124
+ :scale="pageScale"
125
+ />
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
115
131
  </ClientOnly>
116
132
  </div>
117
133
 
118
134
  <ClientOnly>
119
135
  <FloatingPageIndicator :scroll-container="scrollContainer" />
120
136
  </ClientOnly>
137
+ </div>
138
+
139
+ <ClientOnly v-if="!viewerState.readonly.value">
140
+ <PlacedFieldSidebar />
141
+ </ClientOnly>
121
142
  </div>
122
143
 
123
144
  <!-- Footer slot: empty by default -->
@@ -136,6 +157,16 @@
136
157
  @submit="onFreeTextSubmit"
137
158
  @cancel="onFreeTextCancel"
138
159
  />
160
+ <!-- iOS focus bridge: focused synchronously in the tap handler so the
161
+ keyboard pops up; modal textarea takes over on mount without
162
+ closing the keyboard. -->
163
+ <input
164
+ ref="iosFocusBridge"
165
+ type="text"
166
+ aria-hidden="true"
167
+ tabindex="-1"
168
+ class="ios-focus-bridge"
169
+ >
139
170
  </ClientOnly>
140
171
  </div>
141
172
  </template>
@@ -155,6 +186,7 @@ import {
155
186
  import ViewerBar from "./ViewerBar.vue";
156
187
  import PdfPage from "./PdfPage.vue";
157
188
  import FreeTextModal from "./modals/FreeTextModal.vue";
189
+ import PlacedFieldSidebar from "./panels/PlacedFieldSidebar.vue";
158
190
  import AnnotationToolbar from "./AnnotationToolbar.vue";
159
191
  import FloatingPageIndicator from "./FloatingPageIndicator.vue";
160
192
  import { providePageSettings } from "../composables/usePageSettings";
@@ -174,7 +206,9 @@ import { createPageProxyCache } from "../composables/usePageProxyCache";
174
206
  import { createSearchIndex } from "../composables/useSearchIndex";
175
207
  import { createShapeDetection } from "../composables/useShapeDetection";
176
208
  import { provideFormFields } from "../composables/useFormFields";
177
- import { isIPad, getTouchDistance, getTouchCenter, clampScale } from "../annotation/engine/input-device";
209
+ import { extractCheckboxStyles, extractButtonCaptions } from "../annotation/parsers/extractCheckboxStyles";
210
+ import { isIPad } from "../annotation/engine/input-device";
211
+ import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
178
212
  const props = defineProps({
179
213
  source: { type: null, required: true },
180
214
  textLayer: { type: Boolean, required: false },
@@ -186,8 +220,10 @@ const props = defineProps({
186
220
  readonly: { type: Boolean, required: false, default: false },
187
221
  shapeDetection: { type: Boolean, required: false, default: false },
188
222
  active: { type: Boolean, required: false, default: true },
189
- freehandGroupingDelay: { type: Number, required: false, default: 1e3 }
223
+ freehandGroupingDelay: { type: Number, required: false, default: 1e3 },
224
+ formEditMode: { type: Boolean, required: false, default: void 0 }
190
225
  });
226
+ const emit = defineEmits(["update:formEditMode"]);
191
227
  const viewerRoot = ref(null);
192
228
  const scrollContainer = ref(null);
193
229
  const { state: viewerState, setScrollToPageFn, setDownloadPdfFn } = provideViewerState();
@@ -197,6 +233,20 @@ watchEffect(() => {
197
233
  watchEffect(() => {
198
234
  viewerState.shapeDetection.value = props.shapeDetection ?? false;
199
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
+ );
200
250
  const viewerSearch = provideViewerSearch();
201
251
  const formFieldsState = provideFormFields();
202
252
  const pageSettings = providePageSettings(viewerRoot);
@@ -204,8 +254,12 @@ const virtualization = createPageVirtualization();
204
254
  setScrollToPageFn((pageNumber) => {
205
255
  if (isSinglePageMode.value) {
206
256
  viewerState.currentPage.value = pageNumber;
207
- } else {
208
- virtualization.scrollToPage(pageNumber);
257
+ return;
258
+ }
259
+ const el = virtualization.getPageElement(pageNumber);
260
+ if (el) {
261
+ inertiaPanzoom.scrollToElement(el, { block: "start" });
262
+ viewerState.currentPage.value = pageNumber;
209
263
  }
210
264
  });
211
265
  const proxyCache = createPageProxyCache();
@@ -241,6 +295,8 @@ const isRotatedSideways = computed(() => {
241
295
  const r = pageSettings.rotation.value;
242
296
  return r === 90 || r === 270;
243
297
  });
298
+ const zoomWrapper = ref(null);
299
+ const inertiaPanzoom = useInertiaPanzoom({ minScale: 0.5, maxScale: 4 });
244
300
  function pageSlotStyle(meta) {
245
301
  const s = pageScale.value;
246
302
  if (isRotatedSideways.value) {
@@ -297,8 +353,10 @@ const pagedPairs = computed(() => {
297
353
  });
298
354
  const freeTextModalOpen = ref(false);
299
355
  const freeTextDefaults = ref({ color: "#ff0000", fontSize: 18 });
356
+ const iosFocusBridge = ref(null);
300
357
  let freeTextResolve = null;
301
358
  function onRequestTextInput(opts) {
359
+ iosFocusBridge.value?.focus({ preventScroll: true });
302
360
  return new Promise((resolve) => {
303
361
  freeTextDefaults.value = {
304
362
  color: opts.defaultColor,
@@ -334,14 +392,34 @@ async function loadDocument() {
334
392
  pdfjs.GlobalWorkerOptions.workerSrc = workerModule.default;
335
393
  }
336
394
  const source = props.source;
337
- const loadingTask = pdfjs.getDocument(
338
- source
339
- );
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);
340
404
  const pdfDoc = await loadingTask.promise;
341
405
  doc.value = pdfDoc;
342
406
  viewerState.doc.value = pdfDoc;
343
407
  viewerState.totalPages.value = pdfDoc.numPages;
344
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));
417
+ if (!scrollContainer.value) {
418
+ await nextTick();
419
+ }
420
+ if (!scrollContainer.value) {
421
+ throw new Error("Scroll container not available");
422
+ }
345
423
  await virtualization.init(pdfDoc, scrollContainer.value);
346
424
  const metas = virtualization.pageMetas.value;
347
425
  if (metas.length > 0) {
@@ -378,7 +456,17 @@ async function loadDocument() {
378
456
  );
379
457
  }
380
458
  viewerSearch.setScrollToPage((pageNumber) => {
381
- virtualization.scrollToPage(pageNumber);
459
+ if (isSinglePageMode.value) {
460
+ viewerState.currentPage.value = pageNumber;
461
+ return;
462
+ }
463
+ const el = virtualization.getPageElement(pageNumber);
464
+ if (el) inertiaPanzoom.scrollToElement(el, { block: "start" });
465
+ });
466
+ viewerSearch.setScrollToElement((el) => {
467
+ if (!isSinglePageMode.value) {
468
+ inertiaPanzoom.scrollToElement(el, { block: "center" });
469
+ }
382
470
  });
383
471
  searchIndex.buildIndex(pdfDoc).then(() => {
384
472
  viewerSearch.setSearchIndex({
@@ -437,11 +525,13 @@ async function exportPdf(options = {}) {
437
525
  const pdfData = await doc.value.getData();
438
526
  const exportContext = painter.getExportContext();
439
527
  const formFieldValues = formFieldsState.getAllFieldValues();
528
+ const placedFormFieldDefinitions = formFieldsState.getPlacedFields();
440
529
  const bytes = await exportAnnotationsToPdf({
441
530
  pdfData,
442
531
  annotations: exportContext.annotations,
443
532
  options: mergedOptions,
444
533
  formFieldValues,
534
+ placedFormFieldDefinitions,
445
535
  originalAnnotationIds: exportContext.originalIds,
446
536
  modifiedAnnotationIds: exportContext.modifiedIds,
447
537
  deletedAnnotationIds: exportContext.deletedIds
@@ -481,116 +571,97 @@ async function importAnnotations(annotations, options) {
481
571
  function getKonvaCanvasState() {
482
572
  return painter?.getCanvasState() ?? {};
483
573
  }
484
- let pinchActive = false;
485
- let pinchStartDistance = 0;
486
- let pinchStartScale = 1;
487
- let pinchCurrentRatio = 1;
488
- let pinchOriginX = 0;
489
- let pinchOriginY = 0;
490
- let pinchStartViewportX = 0;
491
- let pinchStartViewportY = 0;
492
- let pinchCurrentViewportX = 0;
493
- let pinchCurrentViewportY = 0;
494
- function getPinchTarget() {
495
- return scrollContainer.value?.firstElementChild;
496
- }
497
- function onPinchTouchStart(e) {
498
- if (!viewerState.stylusMode.value) return;
499
- if (e.touches.length === 2) {
500
- const sc = scrollContainer.value;
501
- const scRect = sc.getBoundingClientRect();
502
- const center = getTouchCenter(e.touches[0], e.touches[1]);
503
- pinchActive = true;
504
- pinchStartDistance = getTouchDistance(e.touches[0], e.touches[1]);
505
- pinchStartScale = viewerState.scale.value;
506
- pinchCurrentRatio = 1;
507
- pinchStartViewportX = center.x - scRect.left;
508
- pinchStartViewportY = center.y - scRect.top;
509
- pinchCurrentViewportX = pinchStartViewportX;
510
- pinchCurrentViewportY = pinchStartViewportY;
511
- pinchOriginX = pinchStartViewportX + sc.scrollLeft;
512
- pinchOriginY = pinchStartViewportY + sc.scrollTop;
513
- const target = getPinchTarget();
514
- if (target) {
515
- target.style.transformOrigin = `${pinchOriginX}px ${pinchOriginY}px`;
516
- target.style.willChange = "transform";
517
- }
518
- }
519
- }
520
- function onPinchTouchMove(e) {
521
- if (!viewerState.stylusMode.value || !pinchActive || e.touches.length < 2) return;
522
- e.preventDefault();
523
- const sc = scrollContainer.value;
524
- if (!sc) return;
525
- const scRect = sc.getBoundingClientRect();
526
- const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
527
- pinchCurrentRatio = currentDistance / pinchStartDistance;
528
- const clamped = clampScale(pinchStartScale * pinchCurrentRatio);
529
- pinchCurrentRatio = clamped / pinchStartScale;
530
- const center = getTouchCenter(e.touches[0], e.touches[1]);
531
- pinchCurrentViewportX = center.x - scRect.left;
532
- pinchCurrentViewportY = center.y - scRect.top;
533
- const target = getPinchTarget();
534
- if (target) target.style.transform = `scale(${pinchCurrentRatio})`;
535
- sc.scrollLeft = pinchOriginX * pinchCurrentRatio - pinchCurrentViewportX;
536
- sc.scrollTop = pinchOriginY * pinchCurrentRatio - pinchCurrentViewportY;
537
- }
538
- function onPinchTouchEnd(e) {
539
- if (e.touches.length < 2 && pinchActive) {
540
- pinchActive = false;
541
- const target = getPinchTarget();
542
- if (target) {
543
- target.style.transform = "";
544
- target.style.willChange = "";
545
- }
546
- const finalScale = clampScale(pinchStartScale * pinchCurrentRatio);
547
- if (Math.abs(finalScale - pinchStartScale) > 0.01) {
548
- const layoutRatio = finalScale / pinchStartScale;
549
- const newScrollLeft = pinchOriginX * layoutRatio - pinchCurrentViewportX;
550
- const newScrollTop = pinchOriginY * layoutRatio - pinchCurrentViewportY;
551
- viewerState.setScale(finalScale);
552
- nextTick(() => {
553
- const sc = scrollContainer.value;
554
- if (sc) {
555
- sc.scrollLeft = newScrollLeft;
556
- sc.scrollTop = newScrollTop;
557
- }
558
- });
559
- }
560
- pinchCurrentRatio = 1;
561
- }
574
+ function initPanzoom() {
575
+ inertiaPanzoom.detach();
576
+ if (!zoomWrapper.value) return;
577
+ inertiaPanzoom.attach(zoomWrapper.value);
562
578
  }
563
579
  let wheelCooldown = false;
564
- function onSinglePageWheel(event) {
565
- if (!isSinglePageMode.value) return;
566
- event.preventDefault();
567
- if (wheelCooldown) return;
568
- wheelCooldown = true;
569
- setTimeout(() => {
570
- wheelCooldown = false;
571
- }, 300);
572
- const total = viewerState.totalPages.value;
573
- const cur = viewerState.currentPage.value;
574
- if (event.deltaY > 0 && cur < total) {
575
- viewerState.currentPage.value = cur + 1;
576
- } else if (event.deltaY < 0 && cur > 1) {
577
- viewerState.currentPage.value = cur - 1;
578
- }
579
- }
580
- function onGlobalKeydown(event) {
581
- if (!props.active) return;
580
+ function onWheel(event) {
581
+ if (event.ctrlKey || event.metaKey) return;
582
582
  if (isSinglePageMode.value) {
583
+ event.preventDefault();
584
+ if (wheelCooldown) return;
585
+ wheelCooldown = true;
586
+ setTimeout(() => {
587
+ wheelCooldown = false;
588
+ }, 300);
583
589
  const total = viewerState.totalPages.value;
584
590
  const cur = viewerState.currentPage.value;
585
- if ((event.key === "ArrowDown" || event.key === "PageDown") && cur < total) {
586
- event.preventDefault();
591
+ if (event.deltaY > 0 && cur < total) {
587
592
  viewerState.currentPage.value = cur + 1;
588
- return;
589
- }
590
- if ((event.key === "ArrowUp" || event.key === "PageUp") && cur > 1) {
591
- event.preventDefault();
593
+ } else if (event.deltaY < 0 && cur > 1) {
592
594
  viewerState.currentPage.value = cur - 1;
593
- return;
595
+ }
596
+ return;
597
+ }
598
+ event.preventDefault();
599
+ const dx = event.shiftKey ? event.deltaY : event.deltaX;
600
+ const dy = event.shiftKey ? 0 : event.deltaY;
601
+ inertiaPanzoom.scrollBy(dx, dy);
602
+ }
603
+ function isTextInputTarget(target) {
604
+ if (!(target instanceof HTMLElement)) return false;
605
+ const tag = target.tagName;
606
+ return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
607
+ }
608
+ function onGlobalKeydown(event) {
609
+ if (!props.active) return;
610
+ const typing = isTextInputTarget(event.target);
611
+ if (!typing) {
612
+ if (isSinglePageMode.value) {
613
+ const total = viewerState.totalPages.value;
614
+ const cur = viewerState.currentPage.value;
615
+ if ((event.key === "ArrowDown" || event.key === "PageDown") && cur < total) {
616
+ event.preventDefault();
617
+ viewerState.currentPage.value = cur + 1;
618
+ return;
619
+ }
620
+ if ((event.key === "ArrowUp" || event.key === "PageUp") && cur > 1) {
621
+ event.preventDefault();
622
+ viewerState.currentPage.value = cur - 1;
623
+ return;
624
+ }
625
+ } else {
626
+ const vh = scrollContainer.value?.clientHeight ?? 600;
627
+ const arrowStep = 40;
628
+ const pageStep = Math.max(100, vh * 0.9);
629
+ const BIG = 1e9;
630
+ switch (event.key) {
631
+ case "ArrowDown":
632
+ event.preventDefault();
633
+ inertiaPanzoom.scrollBy(0, arrowStep);
634
+ return;
635
+ case "ArrowUp":
636
+ event.preventDefault();
637
+ inertiaPanzoom.scrollBy(0, -arrowStep);
638
+ return;
639
+ case "ArrowRight":
640
+ event.preventDefault();
641
+ inertiaPanzoom.scrollBy(arrowStep, 0);
642
+ return;
643
+ case "ArrowLeft":
644
+ event.preventDefault();
645
+ inertiaPanzoom.scrollBy(-arrowStep, 0);
646
+ return;
647
+ case "PageDown":
648
+ case " ":
649
+ event.preventDefault();
650
+ inertiaPanzoom.scrollBy(0, pageStep);
651
+ return;
652
+ case "PageUp":
653
+ event.preventDefault();
654
+ inertiaPanzoom.scrollBy(0, -pageStep);
655
+ return;
656
+ case "Home":
657
+ event.preventDefault();
658
+ inertiaPanzoom.scrollBy(0, -BIG);
659
+ return;
660
+ case "End":
661
+ event.preventDefault();
662
+ inertiaPanzoom.scrollBy(0, BIG);
663
+ return;
664
+ }
594
665
  }
595
666
  }
596
667
  const isFindShortcut = (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "f";
@@ -611,7 +682,16 @@ onMounted(() => {
611
682
  painter = createAnnotationEngine(viewerState, {
612
683
  userName: props.userName,
613
684
  onRequestTextInput,
614
- freehandGroupingDelay: props.freehandGroupingDelay
685
+ onPlaceField: (payload) => {
686
+ formFieldsState.addPlacedField(payload);
687
+ viewerState.selectTool("hand");
688
+ },
689
+ freehandGroupingDelay: props.freehandGroupingDelay,
690
+ // Suspend panzoom gestures while an annotation is being dragged or
691
+ // transformed. Otherwise single-finger pan runs in parallel with the
692
+ // Konva shape drag on mobile and the two CSS transforms fight every
693
+ // frame, making text drag feel laggy.
694
+ setExternalGesturesEnabled: (enabled) => inertiaPanzoom.setGesturesEnabled(enabled)
615
695
  });
616
696
  viewerState.selectTool(viewerState.activeTool.value);
617
697
  if (props.stamps) viewerState.stamps.value = props.stamps;
@@ -630,14 +710,31 @@ onMounted(() => {
630
710
  }
631
711
  });
632
712
  resizeObserver.observe(scrollContainer.value);
633
- scrollContainer.value.addEventListener("scroll", () => {
634
- viewerState.selectionRect.value = null;
635
- }, { passive: true });
636
- scrollContainer.value.addEventListener("wheel", onSinglePageWheel, { passive: false });
637
- scrollContainer.value.addEventListener("touchstart", onPinchTouchStart, { passive: true });
638
- scrollContainer.value.addEventListener("touchmove", onPinchTouchMove, { passive: false });
639
- scrollContainer.value.addEventListener("touchend", onPinchTouchEnd, { passive: true });
640
- scrollContainer.value.addEventListener("touchcancel", onPinchTouchEnd, { passive: true });
713
+ scrollContainer.value.addEventListener("wheel", onWheel, { passive: false });
714
+ inertiaPanzoom.onChange(() => {
715
+ if (viewerState.selectionRect.value) viewerState.selectionRect.value = null;
716
+ if (!isSinglePageMode.value) virtualization.updateCurrentPageByRect();
717
+ const z = inertiaPanzoom.getScale();
718
+ if (z !== viewerState.visualZoom.value) viewerState.visualZoom.value = z;
719
+ });
720
+ nextTick(() => initPanzoom());
721
+ watch(
722
+ [isSinglePageMode, () => pageSettings.layoutMode.value],
723
+ () => nextTick(() => initPanzoom())
724
+ );
725
+ watch(
726
+ [() => viewerState.activeTool.value, () => viewerState.stylusMode.value],
727
+ ([tool, stylusMode]) => {
728
+ const enable = stylusMode || tool === "hand";
729
+ inertiaPanzoom.setGesturesEnabled(enable);
730
+ },
731
+ { immediate: true }
732
+ );
733
+ watch(() => viewerState.scale.value, async () => {
734
+ await nextTick();
735
+ inertiaPanzoom.reset();
736
+ });
737
+ watch(() => viewerState.currentPage.value, () => inertiaPanzoom.reset());
641
738
  }
642
739
  if (isIPad()) {
643
740
  viewerState.setStylusMode(true);
@@ -687,11 +784,8 @@ watch(
687
784
  );
688
785
  onBeforeUnmount(() => {
689
786
  window.removeEventListener("keydown", onGlobalKeydown);
690
- scrollContainer.value?.removeEventListener("wheel", onSinglePageWheel);
691
- scrollContainer.value?.removeEventListener("touchstart", onPinchTouchStart);
692
- scrollContainer.value?.removeEventListener("touchmove", onPinchTouchMove);
693
- scrollContainer.value?.removeEventListener("touchend", onPinchTouchEnd);
694
- scrollContainer.value?.removeEventListener("touchcancel", onPinchTouchEnd);
787
+ scrollContainer.value?.removeEventListener("wheel", onWheel);
788
+ inertiaPanzoom.detach();
695
789
  resizeObserver?.disconnect();
696
790
  viewerSearch.reset();
697
791
  shapeDetectionCache.clearAllShapeCache();
@@ -707,6 +801,20 @@ defineExpose({
707
801
  exportPdf,
708
802
  getKonvaCanvasState,
709
803
  getFormFieldValues: () => formFieldsState.getAllFieldValues(),
710
- 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
+ }
711
815
  });
712
816
  </script>
817
+
818
+ <style scoped>
819
+ .kviewer-page-slot{margin-left:auto;margin-right:auto}.panzoom-viewport{height:100%;overflow:hidden;position:relative;width:100%}.ios-focus-bridge{border:0;font-size:16px;height:1px;left:0;opacity:0;padding:0;pointer-events:none;position:fixed;top:0;width:1px;z-index:-1}
820
+ </style>
@@ -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;