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.
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/runtime/annotation/engine/editor/editor.d.ts +11 -5
- package/dist/runtime/annotation/engine/editor/editor.js +27 -11
- package/dist/runtime/annotation/engine/editor/selector.d.ts +20 -1
- package/dist/runtime/annotation/engine/editor/selector.js +132 -33
- package/dist/runtime/annotation/engine/painter.d.ts +4 -1
- package/dist/runtime/annotation/engine/painter.js +21 -5
- package/dist/runtime/annotation/engine/tools/freehand.js +2 -2
- package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
- package/dist/runtime/components/PdfPage.vue +51 -18
- package/dist/runtime/components/PdfPage.vue.d.ts +6 -1
- package/dist/runtime/components/ToolButton.d.vue.ts +4 -2
- package/dist/runtime/components/ToolButton.vue.d.ts +4 -2
- package/dist/runtime/components/Viewer.d.vue.ts +3 -0
- package/dist/runtime/components/Viewer.vue +225 -165
- package/dist/runtime/components/Viewer.vue.d.ts +3 -0
- package/dist/runtime/components/form-fields/FormSignatureField.vue +23 -1
- package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
- package/dist/runtime/components/tools/ActionTools.vue +4 -5
- package/dist/runtime/components/tools/DrawingTools.vue +1 -1
- package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
- package/dist/runtime/composables/useAnnotationEngine.js +5 -1
- package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
- package/dist/runtime/composables/useAnnotationHistory.js +10 -3
- package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
- package/dist/runtime/composables/useInertiaPanzoom.js +472 -0
- package/dist/runtime/composables/usePageVirtualization.d.ts +3 -0
- package/dist/runtime/composables/usePageVirtualization.js +23 -0
- package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
- package/dist/runtime/composables/useViewerSearch.js +15 -5
- package/dist/runtime/public-types.d.ts +2 -0
- package/dist/types.d.mts +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
12
|
-
|
|
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-
|
|
42
|
+
<!-- Single-page mode with panzoom for pinch-to-zoom -->
|
|
43
43
|
<div
|
|
44
44
|
v-if="isSinglePageMode && singlePageMeta"
|
|
45
|
-
class="
|
|
46
|
-
:style="{ minHeight: '100%' }"
|
|
45
|
+
class="panzoom-viewport"
|
|
47
46
|
>
|
|
48
47
|
<div
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
ref="zoomWrapper"
|
|
49
|
+
class="flex flex-col items-center justify-center p-4"
|
|
50
|
+
:style="{ minHeight: '100%' }"
|
|
52
51
|
>
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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="
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
564
|
-
if (
|
|
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 (
|
|
585
|
-
event.preventDefault();
|
|
551
|
+
if (event.deltaY > 0 && cur < total) {
|
|
586
552
|
viewerState.currentPage.value = cur + 1;
|
|
587
|
-
|
|
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
|
-
|
|
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("
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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",
|
|
680
|
-
|
|
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>
|