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.
- package/README.md +29 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -0
- package/dist/runtime/annotation/checkbox-styles.d.ts +19 -0
- package/dist/runtime/annotation/checkbox-styles.js +27 -0
- package/dist/runtime/annotation/engine/config.js +44 -0
- package/dist/runtime/annotation/engine/editor/editor.d.ts +6 -3
- package/dist/runtime/annotation/engine/editor/editor.js +22 -9
- package/dist/runtime/annotation/engine/editor/selector.d.ts +9 -1
- package/dist/runtime/annotation/engine/editor/selector.js +72 -30
- package/dist/runtime/annotation/engine/painter.d.ts +6 -2
- package/dist/runtime/annotation/engine/painter.js +42 -5
- package/dist/runtime/annotation/engine/tools/form-field.d.ts +24 -0
- package/dist/runtime/annotation/engine/tools/form-field.js +117 -0
- package/dist/runtime/annotation/engine/types.d.ts +54 -1
- package/dist/runtime/annotation/engine/types.js +4 -0
- package/dist/runtime/annotation/font-style.d.ts +23 -0
- package/dist/runtime/annotation/font-style.js +57 -0
- package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +21 -0
- package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +75 -0
- package/dist/runtime/annotation/parsers/parseFormFields.js +16 -3
- package/dist/runtime/annotation/pdf-export/export-form-fields.d.ts +8 -5
- package/dist/runtime/annotation/pdf-export/export-form-fields.js +245 -1
- package/dist/runtime/annotation/pdf-export/export.d.ts +3 -2
- package/dist/runtime/annotation/pdf-export/export.js +9 -2
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/FormFieldLayer.d.vue.ts +5 -0
- package/dist/runtime/components/FormFieldLayer.vue +40 -3
- package/dist/runtime/components/FormFieldLayer.vue.d.ts +5 -0
- package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
- package/dist/runtime/components/PdfPage.vue +75 -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 +18 -3
- package/dist/runtime/components/Viewer.vue +278 -170
- package/dist/runtime/components/Viewer.vue.d.ts +18 -3
- package/dist/runtime/components/ViewerBar.vue +24 -2
- package/dist/runtime/components/form-fields/FormButton.vue +16 -4
- package/dist/runtime/components/form-fields/FormCheckbox.vue +31 -15
- package/dist/runtime/components/form-fields/FormDropdown.vue +3 -1
- package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +12 -1
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +45 -9
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +12 -1
- package/dist/runtime/components/form-fields/FormRadioButton.vue +3 -1
- package/dist/runtime/components/form-fields/FormSignatureField.vue +2 -1
- package/dist/runtime/components/form-fields/FormTextField.vue +47 -7
- package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +16 -0
- package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +131 -0
- package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +16 -0
- package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
- package/dist/runtime/components/modals/SignatureDrawModal.vue +70 -15
- package/dist/runtime/components/panels/PlacedFieldSidebar.d.vue.ts +3 -0
- package/dist/runtime/components/panels/PlacedFieldSidebar.vue +505 -0
- package/dist/runtime/components/panels/PlacedFieldSidebar.vue.d.ts +3 -0
- package/dist/runtime/components/tools/ActionTools.vue +4 -5
- package/dist/runtime/components/tools/FormFieldTools.d.vue.ts +3 -0
- package/dist/runtime/components/tools/FormFieldTools.vue +35 -0
- package/dist/runtime/components/tools/FormFieldTools.vue.d.ts +3 -0
- package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
- package/dist/runtime/composables/useAnnotationEngine.js +6 -2
- package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
- package/dist/runtime/composables/useAnnotationHistory.js +10 -3
- package/dist/runtime/composables/useFormFields.d.ts +29 -2
- package/dist/runtime/composables/useFormFields.js +259 -5
- package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
- package/dist/runtime/composables/useInertiaPanzoom.js +475 -0
- package/dist/runtime/composables/usePageVirtualization.d.ts +9 -0
- package/dist/runtime/composables/usePageVirtualization.js +34 -3
- package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
- package/dist/runtime/composables/useViewerSearch.js +15 -5
- package/dist/runtime/composables/useViewerState.d.ts +3 -0
- package/dist/runtime/composables/useViewerState.js +22 -1
- package/package.json +2 -2
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
<ViewerBar />
|
|
6
6
|
</slot>
|
|
7
7
|
|
|
8
|
-
<div class="
|
|
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-
|
|
12
|
-
|
|
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-
|
|
43
|
+
<!-- Single-page mode with panzoom for pinch-to-zoom -->
|
|
43
44
|
<div
|
|
44
45
|
v-if="isSinglePageMode && singlePageMeta"
|
|
45
|
-
class="
|
|
46
|
-
:style="{ minHeight: '100%' }"
|
|
46
|
+
class="panzoom-viewport"
|
|
47
47
|
>
|
|
48
48
|
<div
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
ref="zoomWrapper"
|
|
50
|
+
class="flex flex-col items-center justify-center p-4"
|
|
51
|
+
:style="{ minHeight: '100%' }"
|
|
52
52
|
>
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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="
|
|
73
|
+
class="panzoom-viewport"
|
|
68
74
|
>
|
|
69
75
|
<div
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
565
|
-
if (
|
|
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 (
|
|
586
|
-
event.preventDefault();
|
|
591
|
+
if (event.deltaY > 0 && cur < total) {
|
|
587
592
|
viewerState.currentPage.value = cur + 1;
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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",
|
|
691
|
-
|
|
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: {},
|
|
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
|
|
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
|
-
|
|
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;
|