kviewer 0.0.10 → 0.1.0
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.d.mts +9 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -1
- 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 +12 -2
- package/dist/runtime/annotation/engine/painter.js +36 -6
- package/dist/runtime/annotation/engine/tools/form-field.d.ts +30 -0
- package/dist/runtime/annotation/engine/tools/form-field.js +119 -0
- package/dist/runtime/annotation/engine/types.d.ts +118 -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 +295 -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 +30 -1
- package/dist/runtime/components/Viewer.d.vue.ts +43 -4
- package/dist/runtime/components/Viewer.vue +106 -7
- package/dist/runtime/components/Viewer.vue.d.ts +43 -4
- package/dist/runtime/components/ViewerBar.vue +24 -2
- package/dist/runtime/components/ViewerTabs.d.vue.ts +15 -6
- package/dist/runtime/components/ViewerTabs.vue +7 -2
- package/dist/runtime/components/ViewerTabs.vue.d.ts +15 -6
- 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 +53 -10
- 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 +2 -0
- package/dist/runtime/composables/useAnnotationEngine.js +3 -1
- package/dist/runtime/composables/useFormFields.d.ts +48 -2
- package/dist/runtime/composables/useFormFields.js +338 -6
- package/dist/runtime/composables/useInertiaPanzoom.js +4 -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/dist/runtime/public-types.d.ts +1 -1
- package/dist/types.d.mts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -81,6 +81,11 @@ await viewerRef.value?.importAnnotations(draft, { mode: 'replace' })
|
|
|
81
81
|
// Read / write form field values
|
|
82
82
|
const fields = viewerRef.value?.getFormFieldValues()
|
|
83
83
|
viewerRef.value?.setFormFieldValue('email', 'user@example.com')
|
|
84
|
+
|
|
85
|
+
// Toggle form-edit mode programmatically
|
|
86
|
+
viewerRef.value?.setFormEditMode(true)
|
|
87
|
+
viewerRef.value?.toggleFormEditMode()
|
|
88
|
+
const isEditing = viewerRef.value?.formEditMode.value
|
|
84
89
|
</script>
|
|
85
90
|
```
|
|
86
91
|
|
|
@@ -92,9 +97,33 @@ viewerRef.value?.setFormFieldValue('email', 'user@example.com')
|
|
|
92
97
|
| `getFormFieldValues()` | `FormFieldValue[]` |
|
|
93
98
|
| `setFormFieldValue(fieldName, value)` | `void` |
|
|
94
99
|
| `getKonvaCanvasState()` | `Record<number, string>` |
|
|
100
|
+
| `formEditMode` | `Ref<boolean>` |
|
|
101
|
+
| `setFormEditMode(enabled)` | `void` |
|
|
102
|
+
| `toggleFormEditMode()` | `boolean` (the new state) |
|
|
95
103
|
|
|
96
104
|
`exportPdf` options default to `{ flatten: false, download: false, preserveOriginalAnnotations: false }`.
|
|
97
105
|
|
|
106
|
+
### Form-edit mode
|
|
107
|
+
|
|
108
|
+
`KViewer` exposes a **form-edit mode** that turns every form field into a movable/resizable widget with a property sidebar. It's also reachable from the burger menu in the toolbar, but consumers can drive it externally via prop binding or the imperative API.
|
|
109
|
+
|
|
110
|
+
Two-way binding via `v-model:form-edit-mode`:
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<template>
|
|
114
|
+
<button @click="formEditMode = !formEditMode">
|
|
115
|
+
{{ formEditMode ? 'Exit edit mode' : 'Edit form' }}
|
|
116
|
+
</button>
|
|
117
|
+
<KViewer v-model:form-edit-mode="formEditMode" :source="pdfUrl" />
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<script setup lang="ts">
|
|
121
|
+
const formEditMode = ref(false)
|
|
122
|
+
</script>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or imperatively from the ref API (`setFormEditMode`, `toggleFormEditMode`, reactive `formEditMode` ref).
|
|
126
|
+
|
|
98
127
|
When native PDF annotations are auto-imported into Konva, exporting with `preserveOriginalAnnotations: true` may duplicate annotations. Prefer `preserveOriginalAnnotations: false` in that workflow.
|
|
99
128
|
|
|
100
129
|
## Development
|
package/dist/module.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
export { ExportPdfOptions, SignatureData, SignatureHandlers, ViewMode } from '../dist/runtime/public-types.js';
|
|
2
|
+
export { AddFormFieldPayload, CheckboxStyle, ExportPdfOptions, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, SignatureData, SignatureHandlers, ViewMode } from '../dist/runtime/public-types.js';
|
|
3
3
|
|
|
4
4
|
interface ModuleOptions {
|
|
5
5
|
/**
|
|
@@ -7,6 +7,14 @@ interface ModuleOptions {
|
|
|
7
7
|
* @defaultValue `K`
|
|
8
8
|
*/
|
|
9
9
|
prefix?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Minimum pixel ratio used to rasterize PDF pages. The canvas is rendered
|
|
12
|
+
* at `cssSize * max(devicePixelRatio, minRenderPixelRatio)` and downscaled
|
|
13
|
+
* by CSS, producing a supersampled, sharper image on non-HiDPI displays.
|
|
14
|
+
* Higher values cost ~quadratically more memory and CPU.
|
|
15
|
+
* @defaultValue `2`
|
|
16
|
+
*/
|
|
17
|
+
minRenderPixelRatio?: number;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
1
3
|
import { defineNuxtModule, createResolver, addPlugin, addComponentsDir } from '@nuxt/kit';
|
|
2
4
|
|
|
3
5
|
const module$1 = defineNuxtModule({
|
|
@@ -7,10 +9,15 @@ const module$1 = defineNuxtModule({
|
|
|
7
9
|
},
|
|
8
10
|
// Default configuration options of the Nuxt module
|
|
9
11
|
defaults: {
|
|
10
|
-
prefix: "K"
|
|
12
|
+
prefix: "K",
|
|
13
|
+
minRenderPixelRatio: 2
|
|
11
14
|
},
|
|
12
15
|
setup(options, nuxt) {
|
|
13
16
|
const { resolve } = createResolver(import.meta.url);
|
|
17
|
+
nuxt.options.runtimeConfig.public.kviewer = {
|
|
18
|
+
...nuxt.options.runtimeConfig.public.kviewer,
|
|
19
|
+
minRenderPixelRatio: options.minRenderPixelRatio ?? 2
|
|
20
|
+
};
|
|
14
21
|
nuxt.options.css.push("pdfjs-dist/web/pdf_viewer.css");
|
|
15
22
|
nuxt.options.css.push(resolve("./runtime/assets/kviewer.css"));
|
|
16
23
|
addPlugin(resolve("./runtime/plugin"));
|
|
@@ -20,6 +27,23 @@ const module$1 = defineNuxtModule({
|
|
|
20
27
|
prefix: options.prefix,
|
|
21
28
|
ignore: ["color-mode/**", "content/**", "prose/**"]
|
|
22
29
|
});
|
|
30
|
+
const require_ = createRequire(import.meta.url);
|
|
31
|
+
const pdfjsDir = dirname(require_.resolve("pdfjs-dist/package.json"));
|
|
32
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
33
|
+
nitroConfig.publicAssets ||= [];
|
|
34
|
+
nitroConfig.publicAssets.push(
|
|
35
|
+
{
|
|
36
|
+
dir: join(pdfjsDir, "standard_fonts"),
|
|
37
|
+
baseURL: "/_kviewer/standard_fonts",
|
|
38
|
+
maxAge: 60 * 60 * 24 * 365
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
dir: join(pdfjsDir, "cmaps"),
|
|
42
|
+
baseURL: "/_kviewer/cmaps",
|
|
43
|
+
maxAge: 60 * 60 * 24 * 365
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
});
|
|
23
47
|
}
|
|
24
48
|
});
|
|
25
49
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CheckboxStyle } from './engine/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Maps each standard checkbox style to:
|
|
4
|
+
* - `caption`: the single ZapfDingbats character written to `/MK /CA` on
|
|
5
|
+
* the widget when the PDF is exported (per ISO 32000-1 §12.5.6.19).
|
|
6
|
+
* - `glyph`: a Unicode character with the same visual meaning, used by
|
|
7
|
+
* the HTML viewer to render the checkbox in the user's browser without
|
|
8
|
+
* needing to embed the ZapfDingbats font.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CHECKBOX_STYLE_TABLE: Record<CheckboxStyle, {
|
|
11
|
+
caption: string;
|
|
12
|
+
glyph: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** Reverse lookup: ZapfDingbats `/MK /CA` character → CheckboxStyle. */
|
|
15
|
+
export declare function captionToStyle(caption: string): CheckboxStyle | undefined;
|
|
16
|
+
export declare const CHECKBOX_STYLE_OPTIONS: {
|
|
17
|
+
label: string;
|
|
18
|
+
value: CheckboxStyle;
|
|
19
|
+
}[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const CHECKBOX_STYLE_TABLE = {
|
|
2
|
+
// Heavy check / cross (U+2714 / U+2718) match Apple Preview's chunkier
|
|
3
|
+
// ZapfDingbats-style rendering — the thin U+2713/U+2717 glyphs feel
|
|
4
|
+
// anemic next to the field border. The trailing U+FE0E (variation
|
|
5
|
+
// selector-15) forces the text presentation, otherwise macOS swaps
|
|
6
|
+
// them for colored emoji.
|
|
7
|
+
check: { caption: "4", glyph: "\u2714\uFE0E" },
|
|
8
|
+
cross: { caption: "8", glyph: "\u2718\uFE0E" },
|
|
9
|
+
diamond: { caption: "u", glyph: "\u25C6" },
|
|
10
|
+
circle: { caption: "l", glyph: "\u25CF" },
|
|
11
|
+
star: { caption: "H", glyph: "\u2605" },
|
|
12
|
+
square: { caption: "n", glyph: "\u25A0" }
|
|
13
|
+
};
|
|
14
|
+
export function captionToStyle(caption) {
|
|
15
|
+
for (const [style, info] of Object.entries(CHECKBOX_STYLE_TABLE)) {
|
|
16
|
+
if (info.caption === caption) return style;
|
|
17
|
+
}
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
export const CHECKBOX_STYLE_OPTIONS = [
|
|
21
|
+
{ label: "Check", value: "check" },
|
|
22
|
+
{ label: "Cross", value: "cross" },
|
|
23
|
+
{ label: "Diamond", value: "diamond" },
|
|
24
|
+
{ label: "Circle", value: "circle" },
|
|
25
|
+
{ label: "Star", value: "star" },
|
|
26
|
+
{ label: "Square", value: "square" }
|
|
27
|
+
];
|
|
@@ -279,5 +279,49 @@ export const annotationDefinitions = [
|
|
|
279
279
|
resizable: true,
|
|
280
280
|
draggable: true,
|
|
281
281
|
icon: "i-lucide-stamp"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "formText",
|
|
285
|
+
type: AnnotationType.FORM_TEXT,
|
|
286
|
+
pdfjsEditorType: PdfjsAnnotationEditorType.NONE,
|
|
287
|
+
pdfjsAnnotationType: PdfjsAnnotationType.WIDGET,
|
|
288
|
+
subtype: "Widget",
|
|
289
|
+
isOnce: true,
|
|
290
|
+
resizable: false,
|
|
291
|
+
draggable: false,
|
|
292
|
+
icon: "i-lucide-text-cursor-input"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "formCheckbox",
|
|
296
|
+
type: AnnotationType.FORM_CHECKBOX,
|
|
297
|
+
pdfjsEditorType: PdfjsAnnotationEditorType.NONE,
|
|
298
|
+
pdfjsAnnotationType: PdfjsAnnotationType.WIDGET,
|
|
299
|
+
subtype: "Widget",
|
|
300
|
+
isOnce: true,
|
|
301
|
+
resizable: false,
|
|
302
|
+
draggable: false,
|
|
303
|
+
icon: "i-lucide-square-check"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "formSignature",
|
|
307
|
+
type: AnnotationType.FORM_SIGNATURE,
|
|
308
|
+
pdfjsEditorType: PdfjsAnnotationEditorType.NONE,
|
|
309
|
+
pdfjsAnnotationType: PdfjsAnnotationType.WIDGET,
|
|
310
|
+
subtype: "Widget",
|
|
311
|
+
isOnce: true,
|
|
312
|
+
resizable: false,
|
|
313
|
+
draggable: false,
|
|
314
|
+
icon: "i-lucide-signature"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "formRadio",
|
|
318
|
+
type: AnnotationType.FORM_RADIO,
|
|
319
|
+
pdfjsEditorType: PdfjsAnnotationEditorType.NONE,
|
|
320
|
+
pdfjsAnnotationType: PdfjsAnnotationType.WIDGET,
|
|
321
|
+
subtype: "Widget",
|
|
322
|
+
isOnce: true,
|
|
323
|
+
resizable: false,
|
|
324
|
+
draggable: false,
|
|
325
|
+
icon: "i-lucide-circle-dot"
|
|
282
326
|
}
|
|
283
327
|
];
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import Konva from 'konva';
|
|
2
2
|
import type { IRect } from 'konva/lib/types';
|
|
3
|
-
import { type IAnnotationStore, type IAnnotationStyle, type IAnnotationType } from './types.js';
|
|
3
|
+
import { type IAnnotationStore, type IAnnotationStyle, type IAnnotationType, type PlaceFieldPayload } from './types.js';
|
|
4
4
|
import { type IEditorFreeTextOptions } from './tools/free-text.js';
|
|
5
5
|
export interface KonvaCanvas {
|
|
6
6
|
pageNumber: number;
|
|
7
7
|
konvaStage: Konva.Stage;
|
|
8
8
|
wrapper: HTMLDivElement;
|
|
9
9
|
isActive: boolean;
|
|
10
|
+
/** Base PDF-space page height — needed for coord flipping in placement tools. */
|
|
11
|
+
pageHeight: number;
|
|
10
12
|
}
|
|
11
13
|
export interface PainterCallbacks {
|
|
12
14
|
setDefaultMode: () => void;
|
|
@@ -20,6 +22,11 @@ export interface PainterCallbacks {
|
|
|
20
22
|
onAnnotationChanged: (annotationStore: IAnnotationStore, selectorRect: IRect) => void;
|
|
21
23
|
onRequestTextInput: IEditorFreeTextOptions['onRequestTextInput'];
|
|
22
24
|
onRequestDeleteConfirm?: (id: string) => Promise<boolean>;
|
|
25
|
+
onPlaceField?: (payload: PlaceFieldPayload) => void;
|
|
26
|
+
/** Optional accessor returning the color to use for the form-field
|
|
27
|
+
* placement preview. Lets the active role color override the default
|
|
28
|
+
* blue without rebuilding the editor between placements. */
|
|
29
|
+
getFormFieldPreviewColor?: () => string | undefined;
|
|
23
30
|
}
|
|
24
31
|
export declare class Painter {
|
|
25
32
|
private userName;
|
|
@@ -51,7 +58,10 @@ export declare class Painter {
|
|
|
51
58
|
private updateStore;
|
|
52
59
|
/**
|
|
53
60
|
* Convert a Konva stage-relative rect to viewport-relative coordinates
|
|
54
|
-
* by combining with the stage container's DOM position.
|
|
61
|
+
* by combining with the stage container's DOM position. `getBoundingClientRect`
|
|
62
|
+
* already includes ancestor CSS transforms (panzoom), but `stageRect` is in
|
|
63
|
+
* pre-transform canvas pixels — scale it by the same factor so the offset
|
|
64
|
+
* matches the on-screen size.
|
|
55
65
|
*/
|
|
56
66
|
private toViewportRect;
|
|
57
67
|
private parseStampData;
|
|
@@ -22,6 +22,7 @@ import { EditorStamp } from "./tools/stamp.js";
|
|
|
22
22
|
import { EditorNote } from "./tools/note.js";
|
|
23
23
|
import { EditorArrow } from "./tools/arrow.js";
|
|
24
24
|
import { EditorCloud } from "./tools/cloud.js";
|
|
25
|
+
import { EditorFormField } from "./tools/form-field.js";
|
|
25
26
|
import { Selector } from "./editor/selector.js";
|
|
26
27
|
import { Store } from "./store.js";
|
|
27
28
|
import { isIPad } from "./input-device.js";
|
|
@@ -181,17 +182,21 @@ export class Painter {
|
|
|
181
182
|
}
|
|
182
183
|
/**
|
|
183
184
|
* Convert a Konva stage-relative rect to viewport-relative coordinates
|
|
184
|
-
* by combining with the stage container's DOM position.
|
|
185
|
+
* by combining with the stage container's DOM position. `getBoundingClientRect`
|
|
186
|
+
* already includes ancestor CSS transforms (panzoom), but `stageRect` is in
|
|
187
|
+
* pre-transform canvas pixels — scale it by the same factor so the offset
|
|
188
|
+
* matches the on-screen size.
|
|
185
189
|
*/
|
|
186
190
|
toViewportRect(pageNumber, stageRect) {
|
|
187
191
|
const canvas = this.konvaCanvasStore.get(pageNumber);
|
|
188
192
|
if (!canvas) return stageRect;
|
|
189
193
|
const containerBounds = canvas.wrapper.getBoundingClientRect();
|
|
194
|
+
const visualScale = canvas.wrapper.offsetWidth > 0 ? containerBounds.width / canvas.wrapper.offsetWidth : 1;
|
|
190
195
|
return {
|
|
191
|
-
x: containerBounds.left + stageRect.x,
|
|
192
|
-
y: containerBounds.top + stageRect.y,
|
|
193
|
-
width: stageRect.width,
|
|
194
|
-
height: stageRect.height
|
|
196
|
+
x: containerBounds.left + stageRect.x * visualScale,
|
|
197
|
+
y: containerBounds.top + stageRect.y * visualScale,
|
|
198
|
+
width: stageRect.width * visualScale,
|
|
199
|
+
height: stageRect.height * visualScale
|
|
195
200
|
};
|
|
196
201
|
}
|
|
197
202
|
parseStampData() {
|
|
@@ -244,6 +249,11 @@ export class Painter {
|
|
|
244
249
|
);
|
|
245
250
|
return;
|
|
246
251
|
}
|
|
252
|
+
if (storeEditor instanceof EditorFormField) {
|
|
253
|
+
storeEditor.setPageHeight(
|
|
254
|
+
this.konvaCanvasStore.get(pageNumber)?.pageHeight ?? 0
|
|
255
|
+
);
|
|
256
|
+
}
|
|
247
257
|
storeEditor.activate(konvaStage, annotation);
|
|
248
258
|
return;
|
|
249
259
|
}
|
|
@@ -312,6 +322,25 @@ export class Painter {
|
|
|
312
322
|
case AnnotationType.STRIKEOUT:
|
|
313
323
|
editor = new EditorHighLight(editorOpts, annotation.type);
|
|
314
324
|
break;
|
|
325
|
+
case AnnotationType.FORM_TEXT:
|
|
326
|
+
case AnnotationType.FORM_CHECKBOX:
|
|
327
|
+
case AnnotationType.FORM_RADIO:
|
|
328
|
+
case AnnotationType.FORM_SIGNATURE: {
|
|
329
|
+
const onPlaceField = this.callbacks.onPlaceField;
|
|
330
|
+
if (!onPlaceField) {
|
|
331
|
+
console.warn("Form-field placement requires an onPlaceField callback.");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const pageHeight = this.konvaCanvasStore.get(pageNumber)?.pageHeight ?? 0;
|
|
335
|
+
const fieldType = annotation.type === AnnotationType.FORM_TEXT ? "text" : annotation.type === AnnotationType.FORM_CHECKBOX ? "checkbox" : annotation.type === AnnotationType.FORM_RADIO ? "radio" : "signature";
|
|
336
|
+
editor = new EditorFormField(editorOpts, {
|
|
337
|
+
fieldType,
|
|
338
|
+
pageHeight,
|
|
339
|
+
onPlaceField,
|
|
340
|
+
getPreviewColor: this.callbacks.getFormFieldPreviewColor
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
315
344
|
case AnnotationType.SELECT:
|
|
316
345
|
this.selector.activate(pageNumber);
|
|
317
346
|
break;
|
|
@@ -451,7 +480,8 @@ export class Painter {
|
|
|
451
480
|
pageNumber,
|
|
452
481
|
konvaStage,
|
|
453
482
|
wrapper: container,
|
|
454
|
-
isActive: false
|
|
483
|
+
isActive: false,
|
|
484
|
+
pageHeight: height
|
|
455
485
|
});
|
|
456
486
|
this.reDrawAnnotation(pageNumber);
|
|
457
487
|
this.enablePainting();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { KonvaEventObject } from 'konva/lib/Node';
|
|
2
|
+
import { type FormFieldType, type IAnnotationStore, type IAnnotationStyle, type PlaceFieldPayload } from '../types.js';
|
|
3
|
+
import { Editor, type IEditorOptions } from '../editor/editor.js';
|
|
4
|
+
export interface IEditorFormFieldOptions {
|
|
5
|
+
fieldType: FormFieldType;
|
|
6
|
+
/** PDF-space page height, needed to flip Konva page-local Y → PDF Y. */
|
|
7
|
+
pageHeight: number;
|
|
8
|
+
onPlaceField: (payload: PlaceFieldPayload) => void;
|
|
9
|
+
/** Resolves to the active role color so the placement preview matches
|
|
10
|
+
* the color the field will be tagged with. Re-read on every drag start
|
|
11
|
+
* so the user can switch roles between placements without rebuilding
|
|
12
|
+
* the editor. */
|
|
13
|
+
getPreviewColor?: () => string | undefined;
|
|
14
|
+
}
|
|
15
|
+
export declare class EditorFormField extends Editor {
|
|
16
|
+
private fieldType;
|
|
17
|
+
private pageHeight;
|
|
18
|
+
private onPlaceField;
|
|
19
|
+
private getPreviewColor?;
|
|
20
|
+
private preview;
|
|
21
|
+
private vertex;
|
|
22
|
+
constructor(editorOptions: IEditorOptions, opts: IEditorFormFieldOptions);
|
|
23
|
+
setPageHeight(pageHeight: number): void;
|
|
24
|
+
protected mouseDownHandler(e: KonvaEventObject<MouseEvent | TouchEvent>): void;
|
|
25
|
+
protected mouseMoveHandler(e: KonvaEventObject<MouseEvent | TouchEvent>): void;
|
|
26
|
+
protected mouseUpHandler(): void;
|
|
27
|
+
private globalPointerUpHandler;
|
|
28
|
+
protected changeStyle(_a: IAnnotationStore, _s: IAnnotationStyle): void;
|
|
29
|
+
addSerializedGroupToLayer(): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import Konva from "konva";
|
|
2
|
+
import {
|
|
3
|
+
AnnotationType
|
|
4
|
+
} from "../types.js";
|
|
5
|
+
import { Editor } from "../editor/editor.js";
|
|
6
|
+
const TOO_SMALL = 6;
|
|
7
|
+
const DEFAULT_CHECKBOX = 18;
|
|
8
|
+
const DEFAULT_RADIO = 18;
|
|
9
|
+
const DEFAULT_SIGNATURE = { width: 200, height: 60 };
|
|
10
|
+
export class EditorFormField extends Editor {
|
|
11
|
+
fieldType;
|
|
12
|
+
pageHeight;
|
|
13
|
+
onPlaceField;
|
|
14
|
+
getPreviewColor;
|
|
15
|
+
preview = null;
|
|
16
|
+
vertex = { x: 0, y: 0 };
|
|
17
|
+
constructor(editorOptions, opts) {
|
|
18
|
+
super({
|
|
19
|
+
...editorOptions,
|
|
20
|
+
editorType: opts.fieldType === "text" ? AnnotationType.FORM_TEXT : opts.fieldType === "checkbox" ? AnnotationType.FORM_CHECKBOX : opts.fieldType === "radio" ? AnnotationType.FORM_RADIO : AnnotationType.FORM_SIGNATURE
|
|
21
|
+
});
|
|
22
|
+
this.fieldType = opts.fieldType;
|
|
23
|
+
this.pageHeight = opts.pageHeight;
|
|
24
|
+
this.onPlaceField = opts.onPlaceField;
|
|
25
|
+
this.getPreviewColor = opts.getPreviewColor;
|
|
26
|
+
}
|
|
27
|
+
setPageHeight(pageHeight) {
|
|
28
|
+
this.pageHeight = pageHeight;
|
|
29
|
+
}
|
|
30
|
+
mouseDownHandler(e) {
|
|
31
|
+
if (e.currentTarget !== this.konvaStage) return;
|
|
32
|
+
const pos = this.konvaStage.getRelativePointerPosition();
|
|
33
|
+
if (!pos) return;
|
|
34
|
+
this.isPainting = true;
|
|
35
|
+
this.vertex = { x: pos.x, y: pos.y };
|
|
36
|
+
this.preview = new Konva.Rect({
|
|
37
|
+
x: pos.x,
|
|
38
|
+
y: pos.y,
|
|
39
|
+
width: 0,
|
|
40
|
+
height: 0,
|
|
41
|
+
stroke: this.getPreviewColor?.() ?? "#1677ff",
|
|
42
|
+
strokeWidth: 1,
|
|
43
|
+
dash: [4, 4],
|
|
44
|
+
strokeScaleEnabled: false,
|
|
45
|
+
visible: false,
|
|
46
|
+
listening: false
|
|
47
|
+
});
|
|
48
|
+
this.getBgLayer().add(this.preview);
|
|
49
|
+
window.addEventListener("mouseup", this.globalPointerUpHandler);
|
|
50
|
+
}
|
|
51
|
+
mouseMoveHandler(e) {
|
|
52
|
+
if (!this.isPainting || !this.preview) return;
|
|
53
|
+
e.evt.preventDefault();
|
|
54
|
+
const pos = this.konvaStage.getRelativePointerPosition();
|
|
55
|
+
if (!pos) return;
|
|
56
|
+
this.preview.show();
|
|
57
|
+
this.preview.setAttrs({
|
|
58
|
+
x: Math.min(this.vertex.x, pos.x),
|
|
59
|
+
y: Math.min(this.vertex.y, pos.y),
|
|
60
|
+
width: Math.abs(pos.x - this.vertex.x),
|
|
61
|
+
height: Math.abs(pos.y - this.vertex.y)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
mouseUpHandler() {
|
|
65
|
+
if (!this.isPainting) return;
|
|
66
|
+
this.isPainting = false;
|
|
67
|
+
window.removeEventListener("mouseup", this.globalPointerUpHandler);
|
|
68
|
+
const preview = this.preview;
|
|
69
|
+
if (!preview) return;
|
|
70
|
+
let x = preview.x();
|
|
71
|
+
let y = preview.y();
|
|
72
|
+
let w = preview.width();
|
|
73
|
+
let h = preview.height();
|
|
74
|
+
const dragged = preview.isVisible() && Math.max(w, h) >= TOO_SMALL;
|
|
75
|
+
if (!dragged) {
|
|
76
|
+
if (this.fieldType === "checkbox") {
|
|
77
|
+
w = DEFAULT_CHECKBOX;
|
|
78
|
+
h = DEFAULT_CHECKBOX;
|
|
79
|
+
x = this.vertex.x - w / 2;
|
|
80
|
+
y = this.vertex.y - h / 2;
|
|
81
|
+
} else if (this.fieldType === "radio") {
|
|
82
|
+
w = DEFAULT_RADIO;
|
|
83
|
+
h = DEFAULT_RADIO;
|
|
84
|
+
x = this.vertex.x - w / 2;
|
|
85
|
+
y = this.vertex.y - h / 2;
|
|
86
|
+
} else if (this.fieldType === "signature") {
|
|
87
|
+
w = DEFAULT_SIGNATURE.width;
|
|
88
|
+
h = DEFAULT_SIGNATURE.height;
|
|
89
|
+
x = this.vertex.x - w / 2;
|
|
90
|
+
y = this.vertex.y - h / 2;
|
|
91
|
+
} else {
|
|
92
|
+
preview.destroy();
|
|
93
|
+
this.preview = null;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
preview.destroy();
|
|
98
|
+
this.preview = null;
|
|
99
|
+
const x1 = x;
|
|
100
|
+
const x2 = x + w;
|
|
101
|
+
const y2 = this.pageHeight - y;
|
|
102
|
+
const y1 = this.pageHeight - (y + h);
|
|
103
|
+
this.onPlaceField({
|
|
104
|
+
pageNumber: this.pageNumber,
|
|
105
|
+
fieldType: this.fieldType,
|
|
106
|
+
rectPdf: [x1, y1, x2, y2]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
globalPointerUpHandler = (e) => {
|
|
110
|
+
if (e.button !== 0) return;
|
|
111
|
+
this.mouseUpHandler();
|
|
112
|
+
};
|
|
113
|
+
changeStyle(_a, _s) {
|
|
114
|
+
}
|
|
115
|
+
// Placed fields are never re-drawn from a konva string; the form-field
|
|
116
|
+
// HTML layer owns their visual representation.
|
|
117
|
+
addSerializedGroupToLayer() {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -53,7 +53,11 @@ export declare enum AnnotationType {
|
|
|
53
53
|
STAMP = 10,
|
|
54
54
|
NOTE = 11,
|
|
55
55
|
ARROW = 12,
|
|
56
|
-
CLOUD = 13
|
|
56
|
+
CLOUD = 13,
|
|
57
|
+
FORM_TEXT = 14,
|
|
58
|
+
FORM_CHECKBOX = 15,
|
|
59
|
+
FORM_SIGNATURE = 16,
|
|
60
|
+
FORM_RADIO = 17
|
|
57
61
|
}
|
|
58
62
|
export interface IAnnotationType {
|
|
59
63
|
name: string;
|
|
@@ -134,16 +138,42 @@ export interface IAnnotationStore {
|
|
|
134
138
|
draggable: boolean;
|
|
135
139
|
}
|
|
136
140
|
export type FormFieldType = 'text' | 'checkbox' | 'radio' | 'dropdown' | 'signature' | 'button';
|
|
141
|
+
/**
|
|
142
|
+
* Standard PDF checkbox glyph styles, per Acrobat's "Check Box Style"
|
|
143
|
+
* options. Each maps to a single ZapfDingbats character written to the
|
|
144
|
+
* widget's `/MK /CA` entry — see ISO 32000-1 §12.5.6.19.
|
|
145
|
+
*/
|
|
146
|
+
export type CheckboxStyle = 'check' | 'cross' | 'diamond' | 'circle' | 'star' | 'square';
|
|
147
|
+
/**
|
|
148
|
+
* Where a form field came from. Export uses this to decide whether to
|
|
149
|
+
* update an existing PDF widget ('parsed') or create a new one ('placed',
|
|
150
|
+
* 'detected'). Missing = 'parsed' for backward compat.
|
|
151
|
+
*/
|
|
152
|
+
export type FormFieldOrigin = 'parsed' | 'detected' | 'placed';
|
|
137
153
|
export interface FormFieldDefinition {
|
|
138
154
|
id: string;
|
|
139
155
|
pageNumber: number;
|
|
140
156
|
fieldType: FormFieldType;
|
|
141
157
|
fieldName: string;
|
|
142
158
|
rect: [number, number, number, number];
|
|
159
|
+
/** The rect this field had when first parsed from the source PDF. Used
|
|
160
|
+
* on export to detect that a parsed field has been moved/resized so
|
|
161
|
+
* its existing widget can be updated in place. Unset for placed and
|
|
162
|
+
* detected fields (their `rect` IS their original rect). */
|
|
163
|
+
originalRect?: [number, number, number, number];
|
|
143
164
|
readOnly: boolean;
|
|
144
165
|
required: boolean;
|
|
166
|
+
origin?: FormFieldOrigin;
|
|
145
167
|
/** Visual shape hint for detected checkboxes (circle renders round, rectangle renders square). */
|
|
146
168
|
shapeType?: DetectedShapeType;
|
|
169
|
+
/** Checkbox glyph style. Only used when fieldType === 'checkbox'.
|
|
170
|
+
* Defaults to 'check' for placed checkboxes; populated from `/MK /CA`
|
|
171
|
+
* for parsed checkboxes when the source PDF specifies a non-default. */
|
|
172
|
+
checkboxStyle?: CheckboxStyle;
|
|
173
|
+
/** The checkbox style this field had when first parsed from the source
|
|
174
|
+
* PDF. Used on export to detect that a parsed checkbox's style was
|
|
175
|
+
* changed in form-edit mode, so we know to overwrite its `/MK /CA`. */
|
|
176
|
+
originalCheckboxStyle?: CheckboxStyle;
|
|
147
177
|
defaultValue?: string;
|
|
148
178
|
maxLen?: number;
|
|
149
179
|
multiLine?: boolean;
|
|
@@ -159,10 +189,31 @@ export interface FormFieldDefinition {
|
|
|
159
189
|
multiSelect?: boolean;
|
|
160
190
|
editable?: boolean;
|
|
161
191
|
buttonLabel?: string;
|
|
192
|
+
/** Signature-only: placeholder text shown on the unsigned widget.
|
|
193
|
+
* Defaults to 'Click to sign' when unset. */
|
|
194
|
+
promptText?: string;
|
|
195
|
+
/** Signature-only: when this signature is filled, lock other fields in
|
|
196
|
+
* the form so they can no longer be edited. Mirrors PDF spec
|
|
197
|
+
* /Lock /Action: 'all' locks every other field; 'include' locks only
|
|
198
|
+
* fields listed in lockFieldNames; 'exclude' locks every field NOT
|
|
199
|
+
* listed. Enforced inside the viewer (the viewer flattens signatures
|
|
200
|
+
* to images on export, so this doesn't round-trip as a real /Sig
|
|
201
|
+
* /Lock dict). */
|
|
202
|
+
lockAction?: 'all' | 'include' | 'exclude';
|
|
203
|
+
/** Field names referenced by lockAction = 'include' or 'exclude'. */
|
|
204
|
+
lockFieldNames?: string[];
|
|
162
205
|
fontSize?: number;
|
|
206
|
+
/** PDF font name as parsed from `/DA` (e.g. "Helv", "HelvB", "Times-Bold").
|
|
207
|
+
* Used to derive CSS font-family / font-weight / font-style. */
|
|
208
|
+
fontName?: string;
|
|
163
209
|
color?: number[];
|
|
164
210
|
backgroundColor?: number[];
|
|
165
211
|
textAlignment?: number;
|
|
212
|
+
/** Opaque role tag for color-coding in form-edit mode. The editor
|
|
213
|
+
* doesn't own role definitions — the host (integrating app) maps
|
|
214
|
+
* roleId → color via the `roleColors` prop on KViewer. Not exported
|
|
215
|
+
* to the PDF. */
|
|
216
|
+
roleId?: string;
|
|
166
217
|
}
|
|
167
218
|
export interface FormFieldValue {
|
|
168
219
|
fieldId: string;
|
|
@@ -170,6 +221,72 @@ export interface FormFieldValue {
|
|
|
170
221
|
fieldType: FormFieldType;
|
|
171
222
|
value: string | boolean | string[];
|
|
172
223
|
}
|
|
224
|
+
/** Payload for placing a new form field via the drawing tool. */
|
|
225
|
+
export interface PlaceFieldPayload {
|
|
226
|
+
pageNumber: number;
|
|
227
|
+
fieldType: FormFieldType;
|
|
228
|
+
/** PDF-space rect [x1, y1, x2, y2], bottom-left origin. */
|
|
229
|
+
rectPdf: [number, number, number, number];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Public payload for adding a form field programmatically. All optional
|
|
233
|
+
* fields fall back to sensible defaults — only `pageNumber`, `fieldType`,
|
|
234
|
+
* and `rect` are required. The created field has `origin: 'placed'` so it
|
|
235
|
+
* is exported into the PDF as a new widget.
|
|
236
|
+
*/
|
|
237
|
+
export interface AddFormFieldPayload {
|
|
238
|
+
pageNumber: number;
|
|
239
|
+
fieldType: FormFieldType;
|
|
240
|
+
/** PDF-space rect [x1, y1, x2, y2], bottom-left origin. */
|
|
241
|
+
rect: [number, number, number, number];
|
|
242
|
+
/** Field name. Auto-generated when omitted. Multiple widgets that share
|
|
243
|
+
* a `fieldName` (and `fieldType`) act as one PDF field — values are
|
|
244
|
+
* mirrored across them on edit. Radios with the same `fieldName` form
|
|
245
|
+
* one option group. */
|
|
246
|
+
fieldName?: string;
|
|
247
|
+
/** Opaque role tag for color-coding in form-edit mode. */
|
|
248
|
+
roleId?: string;
|
|
249
|
+
/** Default value for the field. Becomes the initial value too. */
|
|
250
|
+
defaultValue?: string;
|
|
251
|
+
readOnly?: boolean;
|
|
252
|
+
required?: boolean;
|
|
253
|
+
/** checkbox: glyph style. Defaults to 'check'. */
|
|
254
|
+
checkboxStyle?: CheckboxStyle;
|
|
255
|
+
/** signature: placeholder text on the unsigned widget. */
|
|
256
|
+
promptText?: string;
|
|
257
|
+
/** signature: lock other fields once signed. */
|
|
258
|
+
lockAction?: 'all' | 'include' | 'exclude';
|
|
259
|
+
/** signature: field names referenced by lockAction = 'include' / 'exclude'. */
|
|
260
|
+
lockFieldNames?: string[];
|
|
261
|
+
/** dropdown: editable combo box (true) vs list box (false). Defaults to true. */
|
|
262
|
+
combo?: boolean;
|
|
263
|
+
/** dropdown / listbox: allow multiple selections. */
|
|
264
|
+
multiSelect?: boolean;
|
|
265
|
+
/** dropdown / listbox: choices. */
|
|
266
|
+
options?: {
|
|
267
|
+
displayValue: string;
|
|
268
|
+
exportValue: string;
|
|
269
|
+
}[];
|
|
270
|
+
/** text: render as multiple lines. */
|
|
271
|
+
multiLine?: boolean;
|
|
272
|
+
/** text: render as a password field. */
|
|
273
|
+
password?: boolean;
|
|
274
|
+
/** text: render as evenly-spaced character cells (requires `maxLen`). */
|
|
275
|
+
comb?: boolean;
|
|
276
|
+
/** text: maximum input length. */
|
|
277
|
+
maxLen?: number;
|
|
278
|
+
/** radio: this widget's option value within its group. Auto-generated
|
|
279
|
+
* when omitted (e.g. "Option_1", "Option_2", …). */
|
|
280
|
+
buttonValue?: string;
|
|
281
|
+
fontSize?: number;
|
|
282
|
+
fontName?: string;
|
|
283
|
+
/** RGB color tuple in 0–1 (PDF native). */
|
|
284
|
+
color?: number[];
|
|
285
|
+
/** RGB background color tuple in 0–1 (PDF native). */
|
|
286
|
+
backgroundColor?: number[];
|
|
287
|
+
/** 0 = left, 1 = center, 2 = right. */
|
|
288
|
+
textAlignment?: number;
|
|
289
|
+
}
|
|
173
290
|
export interface DetectedShapeRect {
|
|
174
291
|
x: number;
|
|
175
292
|
y: number;
|
|
@@ -54,6 +54,10 @@ export var AnnotationType = /* @__PURE__ */ ((AnnotationType2) => {
|
|
|
54
54
|
AnnotationType2[AnnotationType2["NOTE"] = 11] = "NOTE";
|
|
55
55
|
AnnotationType2[AnnotationType2["ARROW"] = 12] = "ARROW";
|
|
56
56
|
AnnotationType2[AnnotationType2["CLOUD"] = 13] = "CLOUD";
|
|
57
|
+
AnnotationType2[AnnotationType2["FORM_TEXT"] = 14] = "FORM_TEXT";
|
|
58
|
+
AnnotationType2[AnnotationType2["FORM_CHECKBOX"] = 15] = "FORM_CHECKBOX";
|
|
59
|
+
AnnotationType2[AnnotationType2["FORM_SIGNATURE"] = 16] = "FORM_SIGNATURE";
|
|
60
|
+
AnnotationType2[AnnotationType2["FORM_RADIO"] = 17] = "FORM_RADIO";
|
|
57
61
|
return AnnotationType2;
|
|
58
62
|
})(AnnotationType || {});
|
|
59
63
|
export var CommentStatus = /* @__PURE__ */ ((CommentStatus2) => {
|