kviewer 0.0.8 → 0.0.10

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 (34) hide show
  1. package/dist/module.d.mts +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/annotation/engine/editor/editor.d.ts +11 -5
  4. package/dist/runtime/annotation/engine/editor/editor.js +27 -11
  5. package/dist/runtime/annotation/engine/editor/selector.d.ts +20 -1
  6. package/dist/runtime/annotation/engine/editor/selector.js +132 -33
  7. package/dist/runtime/annotation/engine/painter.d.ts +4 -1
  8. package/dist/runtime/annotation/engine/painter.js +21 -5
  9. package/dist/runtime/annotation/engine/tools/freehand.js +2 -2
  10. package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
  11. package/dist/runtime/components/PdfPage.vue +51 -18
  12. package/dist/runtime/components/PdfPage.vue.d.ts +6 -1
  13. package/dist/runtime/components/ToolButton.d.vue.ts +4 -2
  14. package/dist/runtime/components/ToolButton.vue.d.ts +4 -2
  15. package/dist/runtime/components/Viewer.d.vue.ts +3 -0
  16. package/dist/runtime/components/Viewer.vue +225 -165
  17. package/dist/runtime/components/Viewer.vue.d.ts +3 -0
  18. package/dist/runtime/components/form-fields/FormSignatureField.vue +23 -1
  19. package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
  20. package/dist/runtime/components/tools/ActionTools.vue +4 -5
  21. package/dist/runtime/components/tools/DrawingTools.vue +1 -1
  22. package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
  23. package/dist/runtime/composables/useAnnotationEngine.js +5 -1
  24. package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
  25. package/dist/runtime/composables/useAnnotationHistory.js +10 -3
  26. package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
  27. package/dist/runtime/composables/useInertiaPanzoom.js +472 -0
  28. package/dist/runtime/composables/usePageVirtualization.d.ts +3 -0
  29. package/dist/runtime/composables/usePageVirtualization.js +23 -0
  30. package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
  31. package/dist/runtime/composables/useViewerSearch.js +15 -5
  32. package/dist/runtime/public-types.d.ts +2 -0
  33. package/dist/types.d.mts +1 -1
  34. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
- import type { IAnnotationType } from '../annotation/engine/types.js';
2
1
  type __VLS_Props = {
3
- tool: IAnnotationType | undefined;
2
+ tool: {
3
+ name: string;
4
+ icon?: string;
5
+ } | undefined;
4
6
  active: boolean;
5
7
  indicatorColor?: string;
6
8
  };
@@ -1,6 +1,8 @@
1
- import type { IAnnotationType } from '../annotation/engine/types.js';
2
1
  type __VLS_Props = {
3
- tool: IAnnotationType | undefined;
2
+ tool: {
3
+ name: string;
4
+ icon?: string;
5
+ } | undefined;
4
6
  active: boolean;
5
7
  indicatorColor?: string;
6
8
  };
@@ -15,6 +15,8 @@ type __VLS_Props = {
15
15
  shapeDetection?: boolean;
16
16
  /** When false, global keyboard shortcuts (e.g. Cmd+F) are suppressed. Used by ViewerTabs to prevent hidden viewers from capturing input. */
17
17
  active?: boolean;
18
+ /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
+ freehandGroupingDelay?: number;
18
20
  };
19
21
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
20
22
  type ImportMode = 'replace' | 'merge';
@@ -40,6 +42,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
42
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
41
43
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
42
44
  userName: string;
45
+ freehandGroupingDelay: number;
43
46
  zoom: number;
44
47
  readonly: boolean;
45
48
  active: boolean;
@@ -8,8 +8,8 @@
8
8
  <div class="relative flex-1 min-h-0">
9
9
  <div
10
10
  ref="scrollContainer"
11
- class="absolute inset-0 overflow-auto bg-muted/50"
12
- :class="{ 'overflow-hidden': isSinglePageMode }"
11
+ class="absolute inset-0 overflow-hidden bg-[#F0F3F5] dark:bg-muted/50"
12
+ style="overscroll-behavior: none"
13
13
  data-testid="viewer-scroll"
14
14
  >
15
15
  <!-- Error state -->
@@ -39,24 +39,29 @@
39
39
  </div>
40
40
 
41
41
  <ClientOnly v-else>
42
- <!-- Single-page-at-a-time mode -->
42
+ <!-- Single-page mode with panzoom for pinch-to-zoom -->
43
43
  <div
44
44
  v-if="isSinglePageMode && singlePageMeta"
45
- class="flex flex-col items-center py-4"
46
- :style="{ minHeight: '100%' }"
45
+ class="panzoom-viewport"
47
46
  >
48
47
  <div
49
- :ref="(el) => onPageRef(singlePageMeta.pageNumber, el)"
50
- :style="pageSlotStyle(singlePageMeta)"
51
- class="kviewer-page-slot"
48
+ ref="zoomWrapper"
49
+ class="flex flex-col items-center justify-center p-4"
50
+ :style="{ minHeight: '100%' }"
52
51
  >
53
- <div :style="pageRotationStyle(singlePageMeta)">
54
- <PdfPage
55
- :key="singlePageMeta.pageNumber"
56
- :page-number="singlePageMeta.pageNumber"
57
- :page-meta="singlePageMeta"
58
- :scale="pageScale"
59
- />
52
+ <div
53
+ :ref="(el) => onPageRef(singlePageMeta.pageNumber, el)"
54
+ :style="pageSlotStyle(singlePageMeta)"
55
+ class="kviewer-page-slot"
56
+ >
57
+ <div :style="pageRotationStyle(singlePageMeta)">
58
+ <PdfPage
59
+ :key="singlePageMeta.pageNumber"
60
+ :page-number="singlePageMeta.pageNumber"
61
+ :page-meta="singlePageMeta"
62
+ :scale="pageScale"
63
+ />
64
+ </div>
60
65
  </div>
61
66
  </div>
62
67
  </div>
@@ -64,38 +69,14 @@
64
69
  <!-- Continuous scroll — single-page layout -->
65
70
  <div
66
71
  v-else-if="pageSettings.layoutMode.value === 'single'"
67
- class="flex flex-col items-center gap-4 py-4"
68
- >
69
- <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"
72
+ class="panzoom-viewport"
91
73
  >
92
74
  <div
93
- v-for="(pair, index) in pagedPairs"
94
- :key="index"
95
- class="flex gap-4"
75
+ ref="zoomWrapper"
76
+ class="flex flex-col gap-4 p-4"
96
77
  >
97
78
  <div
98
- v-for="meta in pair"
79
+ v-for="meta in pageMetas"
99
80
  :key="meta.pageNumber"
100
81
  :ref="(el) => onPageRef(meta.pageNumber, el)"
101
82
  :style="pageSlotStyle(meta)"
@@ -112,6 +93,40 @@
112
93
  </div>
113
94
  </div>
114
95
  </div>
96
+
97
+ <!-- Continuous scroll — two-page (side-by-side) layout -->
98
+ <div
99
+ v-else
100
+ class="panzoom-viewport"
101
+ >
102
+ <div
103
+ ref="zoomWrapper"
104
+ class="flex flex-col gap-4 p-4"
105
+ >
106
+ <div
107
+ v-for="(pair, index) in pagedPairs"
108
+ :key="index"
109
+ class="flex gap-4"
110
+ >
111
+ <div
112
+ v-for="meta in pair"
113
+ :key="meta.pageNumber"
114
+ :ref="(el) => onPageRef(meta.pageNumber, el)"
115
+ :style="pageSlotStyle(meta)"
116
+ class="kviewer-page-slot"
117
+ >
118
+ <div :style="pageRotationStyle(meta)">
119
+ <PdfPage
120
+ v-if="isPageRendered(meta.pageNumber)"
121
+ :page-number="meta.pageNumber"
122
+ :page-meta="meta"
123
+ :scale="pageScale"
124
+ />
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
115
130
  </ClientOnly>
116
131
  </div>
117
132
 
@@ -136,6 +151,16 @@
136
151
  @submit="onFreeTextSubmit"
137
152
  @cancel="onFreeTextCancel"
138
153
  />
154
+ <!-- iOS focus bridge: focused synchronously in the tap handler so the
155
+ keyboard pops up; modal textarea takes over on mount without
156
+ closing the keyboard. -->
157
+ <input
158
+ ref="iosFocusBridge"
159
+ type="text"
160
+ aria-hidden="true"
161
+ tabindex="-1"
162
+ class="ios-focus-bridge"
163
+ >
139
164
  </ClientOnly>
140
165
  </div>
141
166
  </template>
@@ -174,7 +199,8 @@ import { createPageProxyCache } from "../composables/usePageProxyCache";
174
199
  import { createSearchIndex } from "../composables/useSearchIndex";
175
200
  import { createShapeDetection } from "../composables/useShapeDetection";
176
201
  import { provideFormFields } from "../composables/useFormFields";
177
- import { isIPad, getTouchDistance, getTouchCenter, clampScale } from "../annotation/engine/input-device";
202
+ import { isIPad } from "../annotation/engine/input-device";
203
+ import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
178
204
  const props = defineProps({
179
205
  source: { type: null, required: true },
180
206
  textLayer: { type: Boolean, required: false },
@@ -185,7 +211,8 @@ const props = defineProps({
185
211
  zoom: { type: Number, required: false, default: 1 },
186
212
  readonly: { type: Boolean, required: false, default: false },
187
213
  shapeDetection: { type: Boolean, required: false, default: false },
188
- active: { type: Boolean, required: false, default: true }
214
+ active: { type: Boolean, required: false, default: true },
215
+ freehandGroupingDelay: { type: Number, required: false, default: 1e3 }
189
216
  });
190
217
  const viewerRoot = ref(null);
191
218
  const scrollContainer = ref(null);
@@ -203,8 +230,12 @@ const virtualization = createPageVirtualization();
203
230
  setScrollToPageFn((pageNumber) => {
204
231
  if (isSinglePageMode.value) {
205
232
  viewerState.currentPage.value = pageNumber;
206
- } else {
207
- virtualization.scrollToPage(pageNumber);
233
+ return;
234
+ }
235
+ const el = virtualization.getPageElement(pageNumber);
236
+ if (el) {
237
+ inertiaPanzoom.scrollToElement(el, { block: "start" });
238
+ viewerState.currentPage.value = pageNumber;
208
239
  }
209
240
  });
210
241
  const proxyCache = createPageProxyCache();
@@ -240,6 +271,8 @@ const isRotatedSideways = computed(() => {
240
271
  const r = pageSettings.rotation.value;
241
272
  return r === 90 || r === 270;
242
273
  });
274
+ const zoomWrapper = ref(null);
275
+ const inertiaPanzoom = useInertiaPanzoom({ minScale: 0.5, maxScale: 4 });
243
276
  function pageSlotStyle(meta) {
244
277
  const s = pageScale.value;
245
278
  if (isRotatedSideways.value) {
@@ -296,8 +329,10 @@ const pagedPairs = computed(() => {
296
329
  });
297
330
  const freeTextModalOpen = ref(false);
298
331
  const freeTextDefaults = ref({ color: "#ff0000", fontSize: 18 });
332
+ const iosFocusBridge = ref(null);
299
333
  let freeTextResolve = null;
300
334
  function onRequestTextInput(opts) {
335
+ iosFocusBridge.value?.focus({ preventScroll: true });
301
336
  return new Promise((resolve) => {
302
337
  freeTextDefaults.value = {
303
338
  color: opts.defaultColor,
@@ -341,6 +376,12 @@ async function loadDocument() {
341
376
  viewerState.doc.value = pdfDoc;
342
377
  viewerState.totalPages.value = pdfDoc.numPages;
343
378
  proxyCache.setDocument(pdfDoc);
379
+ if (!scrollContainer.value) {
380
+ await nextTick();
381
+ }
382
+ if (!scrollContainer.value) {
383
+ throw new Error("Scroll container not available");
384
+ }
344
385
  await virtualization.init(pdfDoc, scrollContainer.value);
345
386
  const metas = virtualization.pageMetas.value;
346
387
  if (metas.length > 0) {
@@ -377,7 +418,17 @@ async function loadDocument() {
377
418
  );
378
419
  }
379
420
  viewerSearch.setScrollToPage((pageNumber) => {
380
- virtualization.scrollToPage(pageNumber);
421
+ if (isSinglePageMode.value) {
422
+ viewerState.currentPage.value = pageNumber;
423
+ return;
424
+ }
425
+ const el = virtualization.getPageElement(pageNumber);
426
+ if (el) inertiaPanzoom.scrollToElement(el, { block: "start" });
427
+ });
428
+ viewerSearch.setScrollToElement((el) => {
429
+ if (!isSinglePageMode.value) {
430
+ inertiaPanzoom.scrollToElement(el, { block: "center" });
431
+ }
381
432
  });
382
433
  searchIndex.buildIndex(pdfDoc).then(() => {
383
434
  viewerSearch.setSearchIndex({
@@ -480,116 +531,97 @@ async function importAnnotations(annotations, options) {
480
531
  function getKonvaCanvasState() {
481
532
  return painter?.getCanvasState() ?? {};
482
533
  }
483
- let pinchActive = false;
484
- let pinchStartDistance = 0;
485
- let pinchStartScale = 1;
486
- let pinchCurrentRatio = 1;
487
- let pinchOriginX = 0;
488
- let pinchOriginY = 0;
489
- let pinchStartViewportX = 0;
490
- let pinchStartViewportY = 0;
491
- let pinchCurrentViewportX = 0;
492
- let pinchCurrentViewportY = 0;
493
- function getPinchTarget() {
494
- return scrollContainer.value?.firstElementChild;
495
- }
496
- function onPinchTouchStart(e) {
497
- if (!viewerState.stylusMode.value) return;
498
- if (e.touches.length === 2) {
499
- const sc = scrollContainer.value;
500
- const scRect = sc.getBoundingClientRect();
501
- const center = getTouchCenter(e.touches[0], e.touches[1]);
502
- pinchActive = true;
503
- pinchStartDistance = getTouchDistance(e.touches[0], e.touches[1]);
504
- pinchStartScale = viewerState.scale.value;
505
- pinchCurrentRatio = 1;
506
- pinchStartViewportX = center.x - scRect.left;
507
- pinchStartViewportY = center.y - scRect.top;
508
- pinchCurrentViewportX = pinchStartViewportX;
509
- pinchCurrentViewportY = pinchStartViewportY;
510
- pinchOriginX = pinchStartViewportX + sc.scrollLeft;
511
- pinchOriginY = pinchStartViewportY + sc.scrollTop;
512
- const target = getPinchTarget();
513
- if (target) {
514
- target.style.transformOrigin = `${pinchOriginX}px ${pinchOriginY}px`;
515
- target.style.willChange = "transform";
516
- }
517
- }
518
- }
519
- function onPinchTouchMove(e) {
520
- if (!viewerState.stylusMode.value || !pinchActive || e.touches.length < 2) return;
521
- e.preventDefault();
522
- const sc = scrollContainer.value;
523
- if (!sc) return;
524
- const scRect = sc.getBoundingClientRect();
525
- const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
526
- pinchCurrentRatio = currentDistance / pinchStartDistance;
527
- const clamped = clampScale(pinchStartScale * pinchCurrentRatio);
528
- pinchCurrentRatio = clamped / pinchStartScale;
529
- const center = getTouchCenter(e.touches[0], e.touches[1]);
530
- pinchCurrentViewportX = center.x - scRect.left;
531
- pinchCurrentViewportY = center.y - scRect.top;
532
- const target = getPinchTarget();
533
- if (target) target.style.transform = `scale(${pinchCurrentRatio})`;
534
- sc.scrollLeft = pinchOriginX * pinchCurrentRatio - pinchCurrentViewportX;
535
- sc.scrollTop = pinchOriginY * pinchCurrentRatio - pinchCurrentViewportY;
536
- }
537
- function onPinchTouchEnd(e) {
538
- if (e.touches.length < 2 && pinchActive) {
539
- pinchActive = false;
540
- const target = getPinchTarget();
541
- if (target) {
542
- target.style.transform = "";
543
- target.style.willChange = "";
544
- }
545
- const finalScale = clampScale(pinchStartScale * pinchCurrentRatio);
546
- if (Math.abs(finalScale - pinchStartScale) > 0.01) {
547
- const layoutRatio = finalScale / pinchStartScale;
548
- const newScrollLeft = pinchOriginX * layoutRatio - pinchCurrentViewportX;
549
- const newScrollTop = pinchOriginY * layoutRatio - pinchCurrentViewportY;
550
- viewerState.setScale(finalScale);
551
- nextTick(() => {
552
- const sc = scrollContainer.value;
553
- if (sc) {
554
- sc.scrollLeft = newScrollLeft;
555
- sc.scrollTop = newScrollTop;
556
- }
557
- });
558
- }
559
- pinchCurrentRatio = 1;
560
- }
534
+ function initPanzoom() {
535
+ inertiaPanzoom.detach();
536
+ if (!zoomWrapper.value) return;
537
+ inertiaPanzoom.attach(zoomWrapper.value);
561
538
  }
562
539
  let wheelCooldown = false;
563
- function onSinglePageWheel(event) {
564
- if (!isSinglePageMode.value) return;
565
- event.preventDefault();
566
- if (wheelCooldown) return;
567
- wheelCooldown = true;
568
- setTimeout(() => {
569
- wheelCooldown = false;
570
- }, 300);
571
- const total = viewerState.totalPages.value;
572
- const cur = viewerState.currentPage.value;
573
- if (event.deltaY > 0 && cur < total) {
574
- viewerState.currentPage.value = cur + 1;
575
- } else if (event.deltaY < 0 && cur > 1) {
576
- viewerState.currentPage.value = cur - 1;
577
- }
578
- }
579
- function onGlobalKeydown(event) {
580
- if (!props.active) return;
540
+ function onWheel(event) {
541
+ if (event.ctrlKey || event.metaKey) return;
581
542
  if (isSinglePageMode.value) {
543
+ event.preventDefault();
544
+ if (wheelCooldown) return;
545
+ wheelCooldown = true;
546
+ setTimeout(() => {
547
+ wheelCooldown = false;
548
+ }, 300);
582
549
  const total = viewerState.totalPages.value;
583
550
  const cur = viewerState.currentPage.value;
584
- if ((event.key === "ArrowDown" || event.key === "PageDown") && cur < total) {
585
- event.preventDefault();
551
+ if (event.deltaY > 0 && cur < total) {
586
552
  viewerState.currentPage.value = cur + 1;
587
- return;
588
- }
589
- if ((event.key === "ArrowUp" || event.key === "PageUp") && cur > 1) {
590
- event.preventDefault();
553
+ } else if (event.deltaY < 0 && cur > 1) {
591
554
  viewerState.currentPage.value = cur - 1;
592
- return;
555
+ }
556
+ return;
557
+ }
558
+ event.preventDefault();
559
+ const dx = event.shiftKey ? event.deltaY : event.deltaX;
560
+ const dy = event.shiftKey ? 0 : event.deltaY;
561
+ inertiaPanzoom.scrollBy(dx, dy);
562
+ }
563
+ function isTextInputTarget(target) {
564
+ if (!(target instanceof HTMLElement)) return false;
565
+ const tag = target.tagName;
566
+ return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
567
+ }
568
+ function onGlobalKeydown(event) {
569
+ if (!props.active) return;
570
+ const typing = isTextInputTarget(event.target);
571
+ if (!typing) {
572
+ if (isSinglePageMode.value) {
573
+ const total = viewerState.totalPages.value;
574
+ const cur = viewerState.currentPage.value;
575
+ if ((event.key === "ArrowDown" || event.key === "PageDown") && cur < total) {
576
+ event.preventDefault();
577
+ viewerState.currentPage.value = cur + 1;
578
+ return;
579
+ }
580
+ if ((event.key === "ArrowUp" || event.key === "PageUp") && cur > 1) {
581
+ event.preventDefault();
582
+ viewerState.currentPage.value = cur - 1;
583
+ return;
584
+ }
585
+ } else {
586
+ const vh = scrollContainer.value?.clientHeight ?? 600;
587
+ const arrowStep = 40;
588
+ const pageStep = Math.max(100, vh * 0.9);
589
+ const BIG = 1e9;
590
+ switch (event.key) {
591
+ case "ArrowDown":
592
+ event.preventDefault();
593
+ inertiaPanzoom.scrollBy(0, arrowStep);
594
+ return;
595
+ case "ArrowUp":
596
+ event.preventDefault();
597
+ inertiaPanzoom.scrollBy(0, -arrowStep);
598
+ return;
599
+ case "ArrowRight":
600
+ event.preventDefault();
601
+ inertiaPanzoom.scrollBy(arrowStep, 0);
602
+ return;
603
+ case "ArrowLeft":
604
+ event.preventDefault();
605
+ inertiaPanzoom.scrollBy(-arrowStep, 0);
606
+ return;
607
+ case "PageDown":
608
+ case " ":
609
+ event.preventDefault();
610
+ inertiaPanzoom.scrollBy(0, pageStep);
611
+ return;
612
+ case "PageUp":
613
+ event.preventDefault();
614
+ inertiaPanzoom.scrollBy(0, -pageStep);
615
+ return;
616
+ case "Home":
617
+ event.preventDefault();
618
+ inertiaPanzoom.scrollBy(0, -BIG);
619
+ return;
620
+ case "End":
621
+ event.preventDefault();
622
+ inertiaPanzoom.scrollBy(0, BIG);
623
+ return;
624
+ }
593
625
  }
594
626
  }
595
627
  const isFindShortcut = (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "f";
@@ -609,7 +641,13 @@ function onGlobalKeydown(event) {
609
641
  onMounted(() => {
610
642
  painter = createAnnotationEngine(viewerState, {
611
643
  userName: props.userName,
612
- onRequestTextInput
644
+ onRequestTextInput,
645
+ freehandGroupingDelay: props.freehandGroupingDelay,
646
+ // Suspend panzoom gestures while an annotation is being dragged or
647
+ // transformed. Otherwise single-finger pan runs in parallel with the
648
+ // Konva shape drag on mobile and the two CSS transforms fight every
649
+ // frame, making text drag feel laggy.
650
+ setExternalGesturesEnabled: (enabled) => inertiaPanzoom.setGesturesEnabled(enabled)
613
651
  });
614
652
  viewerState.selectTool(viewerState.activeTool.value);
615
653
  if (props.stamps) viewerState.stamps.value = props.stamps;
@@ -628,14 +666,26 @@ onMounted(() => {
628
666
  }
629
667
  });
630
668
  resizeObserver.observe(scrollContainer.value);
631
- scrollContainer.value.addEventListener("scroll", () => {
632
- viewerState.selectionRect.value = null;
633
- }, { passive: true });
634
- scrollContainer.value.addEventListener("wheel", onSinglePageWheel, { passive: false });
635
- scrollContainer.value.addEventListener("touchstart", onPinchTouchStart, { passive: true });
636
- scrollContainer.value.addEventListener("touchmove", onPinchTouchMove, { passive: false });
637
- scrollContainer.value.addEventListener("touchend", onPinchTouchEnd, { passive: true });
638
- scrollContainer.value.addEventListener("touchcancel", onPinchTouchEnd, { passive: true });
669
+ scrollContainer.value.addEventListener("wheel", onWheel, { passive: false });
670
+ inertiaPanzoom.onChange(() => {
671
+ if (viewerState.selectionRect.value) viewerState.selectionRect.value = null;
672
+ if (!isSinglePageMode.value) virtualization.updateCurrentPageByRect();
673
+ });
674
+ nextTick(() => initPanzoom());
675
+ watch(
676
+ [isSinglePageMode, () => pageSettings.layoutMode.value],
677
+ () => nextTick(() => initPanzoom())
678
+ );
679
+ watch(
680
+ [() => viewerState.activeTool.value, () => viewerState.stylusMode.value],
681
+ ([tool, stylusMode]) => {
682
+ const enable = stylusMode || tool === "hand";
683
+ inertiaPanzoom.setGesturesEnabled(enable);
684
+ },
685
+ { immediate: true }
686
+ );
687
+ watch(() => viewerState.scale.value, () => inertiaPanzoom.reset());
688
+ watch(() => viewerState.currentPage.value, () => inertiaPanzoom.reset());
639
689
  }
640
690
  if (isIPad()) {
641
691
  viewerState.setStylusMode(true);
@@ -674,13 +724,19 @@ watch(
674
724
  viewerState.stamps.value = newStamps ?? [];
675
725
  }
676
726
  );
727
+ watch(
728
+ () => props.signatureHandlers,
729
+ (newHandlers) => {
730
+ viewerState.signatureHandlers = newHandlers ?? null;
731
+ if (newHandlers) {
732
+ viewerState.loadSignatures();
733
+ }
734
+ }
735
+ );
677
736
  onBeforeUnmount(() => {
678
737
  window.removeEventListener("keydown", onGlobalKeydown);
679
- scrollContainer.value?.removeEventListener("wheel", onSinglePageWheel);
680
- scrollContainer.value?.removeEventListener("touchstart", onPinchTouchStart);
681
- scrollContainer.value?.removeEventListener("touchmove", onPinchTouchMove);
682
- scrollContainer.value?.removeEventListener("touchend", onPinchTouchEnd);
683
- scrollContainer.value?.removeEventListener("touchcancel", onPinchTouchEnd);
738
+ scrollContainer.value?.removeEventListener("wheel", onWheel);
739
+ inertiaPanzoom.detach();
684
740
  resizeObserver?.disconnect();
685
741
  viewerSearch.reset();
686
742
  shapeDetectionCache.clearAllShapeCache();
@@ -699,3 +755,7 @@ defineExpose({
699
755
  setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value)
700
756
  });
701
757
  </script>
758
+
759
+ <style scoped>
760
+ .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}
761
+ </style>
@@ -15,6 +15,8 @@ type __VLS_Props = {
15
15
  shapeDetection?: boolean;
16
16
  /** When false, global keyboard shortcuts (e.g. Cmd+F) are suppressed. Used by ViewerTabs to prevent hidden viewers from capturing input. */
17
17
  active?: boolean;
18
+ /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
+ freehandGroupingDelay?: number;
18
20
  };
19
21
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
20
22
  type ImportMode = 'replace' | 'merge';
@@ -40,6 +42,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
42
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
41
43
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
42
44
  userName: string;
45
+ freehandGroupingDelay: number;
43
46
  zoom: number;
44
47
  readonly: boolean;
45
48
  active: boolean;
@@ -14,17 +14,27 @@
14
14
  Click to sign
15
15
  </span>
16
16
  </div>
17
+
18
+ <ClientOnly>
19
+ <SignatureDrawModal
20
+ v-model:open="drawModalOpen"
21
+ @submit="onDrawSubmit"
22
+ @cancel="drawModalOpen = false"
23
+ />
24
+ </ClientOnly>
17
25
  </template>
18
26
 
19
27
  <script setup>
20
- import { computed } from "vue";
28
+ import { ref, computed } from "vue";
21
29
  import { useFormFields } from "../../composables/useFormFields";
22
30
  import { useViewerState } from "../../composables/useViewerState";
31
+ import SignatureDrawModal from "../modals/SignatureDrawModal.vue";
23
32
  const props = defineProps({
24
33
  field: { type: Object, required: true }
25
34
  });
26
35
  const formFields = useFormFields();
27
36
  const state = useViewerState();
37
+ const drawModalOpen = ref(false);
28
38
  const signatureUrl = computed(() => {
29
39
  const fv = formFields.getFieldValue(props.field.id);
30
40
  if (!fv || typeof fv.value !== "string" || fv.value === "") return null;
@@ -50,5 +60,17 @@ async function onClickSign() {
50
60
  }
51
61
  }
52
62
  }
63
+ drawModalOpen.value = true;
64
+ }
65
+ async function onDrawSubmit(imageUrl) {
66
+ drawModalOpen.value = false;
67
+ if (state.signatureHandlers) {
68
+ const saved = await state.saveSignature(imageUrl);
69
+ if (saved) {
70
+ formFields.setFieldValue(props.field.id, saved.imageUrl);
71
+ return;
72
+ }
73
+ }
74
+ formFields.setFieldValue(props.field.id, imageUrl);
53
75
  }
54
76
  </script>