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