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
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.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({
|
|
@@ -20,6 +22,23 @@ const module$1 = defineNuxtModule({
|
|
|
20
22
|
prefix: options.prefix,
|
|
21
23
|
ignore: ["color-mode/**", "content/**", "prose/**"]
|
|
22
24
|
});
|
|
25
|
+
const require_ = createRequire(import.meta.url);
|
|
26
|
+
const pdfjsDir = dirname(require_.resolve("pdfjs-dist/package.json"));
|
|
27
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
28
|
+
nitroConfig.publicAssets ||= [];
|
|
29
|
+
nitroConfig.publicAssets.push(
|
|
30
|
+
{
|
|
31
|
+
dir: join(pdfjsDir, "standard_fonts"),
|
|
32
|
+
baseURL: "/_kviewer/standard_fonts",
|
|
33
|
+
maxAge: 60 * 60 * 24 * 365
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
dir: join(pdfjsDir, "cmaps"),
|
|
37
|
+
baseURL: "/_kviewer/cmaps",
|
|
38
|
+
maxAge: 60 * 60 * 24 * 365
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
});
|
|
23
42
|
}
|
|
24
43
|
});
|
|
25
44
|
|
|
@@ -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
|
];
|
|
@@ -64,9 +64,12 @@ export declare abstract class Editor {
|
|
|
64
64
|
addSerializedGroupToLayer(konvaStage: Konva.Stage, konvaString: string): void;
|
|
65
65
|
deleteGroup(id: string, konvaStage: Konva.Stage): void;
|
|
66
66
|
updateStyle(annotationStore: IAnnotationStore, style: IAnnotationStyle): void;
|
|
67
|
-
static Timer: {
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
static Timer: Map<number, {
|
|
68
|
+
id: number;
|
|
69
|
+
callback: (pageNumber: number) => void;
|
|
70
|
+
}>;
|
|
70
71
|
static TimerClear(pageNumber: number): void;
|
|
71
72
|
static TimerStart(pageNumber: number, callback: (pageNumber: number) => void, delay?: number): void;
|
|
73
|
+
/** Immediately execute and clear all pending timers (e.g. before export). */
|
|
74
|
+
static flushAllTimers(): void;
|
|
72
75
|
}
|
|
@@ -231,18 +231,31 @@ export class Editor {
|
|
|
231
231
|
updateStyle(annotationStore, style) {
|
|
232
232
|
this.changeStyle(annotationStore, style);
|
|
233
233
|
}
|
|
234
|
-
static Timer =
|
|
234
|
+
static Timer = /* @__PURE__ */ new Map();
|
|
235
235
|
static TimerClear(pageNumber) {
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
238
|
-
window.clearTimeout(
|
|
236
|
+
const entry = Editor.Timer.get(pageNumber);
|
|
237
|
+
if (entry) {
|
|
238
|
+
window.clearTimeout(entry.id);
|
|
239
|
+
Editor.Timer.delete(pageNumber);
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
static TimerStart(pageNumber, callback, delay = 1e3) {
|
|
242
|
-
Editor.Timer
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
Editor.Timer.set(pageNumber, {
|
|
244
|
+
callback,
|
|
245
|
+
id: window.setTimeout(() => {
|
|
246
|
+
Editor.Timer.delete(pageNumber);
|
|
247
|
+
if (typeof callback === "function") {
|
|
248
|
+
callback(pageNumber);
|
|
249
|
+
}
|
|
250
|
+
}, delay)
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/** Immediately execute and clear all pending timers (e.g. before export). */
|
|
254
|
+
static flushAllTimers() {
|
|
255
|
+
for (const [pageNumber, entry] of Editor.Timer) {
|
|
256
|
+
window.clearTimeout(entry.id);
|
|
257
|
+
Editor.Timer.delete(pageNumber);
|
|
258
|
+
entry.callback(pageNumber);
|
|
259
|
+
}
|
|
247
260
|
}
|
|
248
261
|
}
|
|
@@ -13,6 +13,13 @@ export interface ISelectorOptions {
|
|
|
13
13
|
onFreeTextDoubleClick?: (id: string) => void;
|
|
14
14
|
/** When provided and returns true, only pen/mouse input can interact. */
|
|
15
15
|
getStylusModeEnabled?: () => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Toggle external gesture handlers (e.g. panzoom) off while an annotation
|
|
18
|
+
* is being dragged/transformed. Without this, on mobile the panzoom
|
|
19
|
+
* single-finger pan runs in parallel with Konva's shape drag and both
|
|
20
|
+
* CSS transforms compete every frame, causing visible lag.
|
|
21
|
+
*/
|
|
22
|
+
setExternalGesturesEnabled?: (enabled: boolean) => void;
|
|
16
23
|
}
|
|
17
24
|
export declare class Selector {
|
|
18
25
|
readonly onSelected: (id: string, isClick: boolean, clientRect: IRect) => void;
|
|
@@ -23,6 +30,7 @@ export declare class Selector {
|
|
|
23
30
|
private onRequestDeleteConfirm?;
|
|
24
31
|
private onFreeTextDoubleClick?;
|
|
25
32
|
private getStylusModeEnabled?;
|
|
33
|
+
private setExternalGesturesEnabled?;
|
|
26
34
|
private transformerStore;
|
|
27
35
|
private getAnnotationStore;
|
|
28
36
|
private konvaCanvasStore;
|
|
@@ -32,7 +40,7 @@ export declare class Selector {
|
|
|
32
40
|
private marqueeRect;
|
|
33
41
|
private marqueeStartPos;
|
|
34
42
|
private marqueeStage;
|
|
35
|
-
constructor({ konvaCanvasStore, getAnnotationStore, onDelete, onSelected, onMultiSelected, onCancel, onChanged, onRequestDeleteConfirm, onFreeTextDoubleClick, getStylusModeEnabled, }: ISelectorOptions);
|
|
43
|
+
constructor({ konvaCanvasStore, getAnnotationStore, onDelete, onSelected, onMultiSelected, onCancel, onChanged, onRequestDeleteConfirm, onFreeTextDoubleClick, getStylusModeEnabled, setExternalGesturesEnabled, }: ISelectorOptions);
|
|
36
44
|
/** Returns true if this event should be rejected (finger touch in stylus mode). */
|
|
37
45
|
private isStylusRejected;
|
|
38
46
|
private handleKeyDown;
|
|
@@ -12,6 +12,7 @@ export class Selector {
|
|
|
12
12
|
onRequestDeleteConfirm;
|
|
13
13
|
onFreeTextDoubleClick;
|
|
14
14
|
getStylusModeEnabled;
|
|
15
|
+
setExternalGesturesEnabled;
|
|
15
16
|
transformerStore = /* @__PURE__ */ new Map();
|
|
16
17
|
getAnnotationStore;
|
|
17
18
|
konvaCanvasStore;
|
|
@@ -31,7 +32,8 @@ export class Selector {
|
|
|
31
32
|
onChanged,
|
|
32
33
|
onRequestDeleteConfirm,
|
|
33
34
|
onFreeTextDoubleClick,
|
|
34
|
-
getStylusModeEnabled
|
|
35
|
+
getStylusModeEnabled,
|
|
36
|
+
setExternalGesturesEnabled
|
|
35
37
|
}) {
|
|
36
38
|
this.konvaCanvasStore = konvaCanvasStore;
|
|
37
39
|
this.getAnnotationStore = getAnnotationStore;
|
|
@@ -43,6 +45,7 @@ export class Selector {
|
|
|
43
45
|
this.onRequestDeleteConfirm = onRequestDeleteConfirm;
|
|
44
46
|
this.onFreeTextDoubleClick = onFreeTextDoubleClick;
|
|
45
47
|
this.getStylusModeEnabled = getStylusModeEnabled;
|
|
48
|
+
this.setExternalGesturesEnabled = setExternalGesturesEnabled;
|
|
46
49
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
47
50
|
window.addEventListener("keydown", this.handleKeyDown);
|
|
48
51
|
}
|
|
@@ -188,6 +191,7 @@ export class Selector {
|
|
|
188
191
|
const transformer = new Konva.Transformer({
|
|
189
192
|
resizeEnabled: rawAnnotationStore.resizable,
|
|
190
193
|
rotateEnabled: false,
|
|
194
|
+
enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
|
|
191
195
|
borderStrokeWidth: defaultOptions.chooseSetting.STROKEWIDTH,
|
|
192
196
|
borderStroke: defaultOptions.chooseSetting.COLOR,
|
|
193
197
|
anchorFill: defaultOptions.chooseSetting.COLOR,
|
|
@@ -223,7 +227,34 @@ export class Selector {
|
|
|
223
227
|
}
|
|
224
228
|
transformer.off("transformend");
|
|
225
229
|
transformer.off("transformstart");
|
|
230
|
+
let dragLayer = null;
|
|
231
|
+
const moveToDragLayer = () => {
|
|
232
|
+
if (dragLayer) return;
|
|
233
|
+
const bg = this.getBackgroundLayer(konvaStage);
|
|
234
|
+
dragLayer = new Konva.Layer();
|
|
235
|
+
konvaStage.add(dragLayer);
|
|
236
|
+
group.moveTo(dragLayer);
|
|
237
|
+
transformer.moveTo(dragLayer);
|
|
238
|
+
bg.batchDraw();
|
|
239
|
+
dragLayer.batchDraw();
|
|
240
|
+
};
|
|
241
|
+
const restoreFromDragLayer = () => {
|
|
242
|
+
if (!dragLayer) return;
|
|
243
|
+
const bg = this.getBackgroundLayer(konvaStage);
|
|
244
|
+
group.moveTo(bg);
|
|
245
|
+
transformer.moveTo(bg);
|
|
246
|
+
dragLayer.destroy();
|
|
247
|
+
dragLayer = null;
|
|
248
|
+
bg.batchDraw();
|
|
249
|
+
};
|
|
250
|
+
transformer.on("transformstart", () => {
|
|
251
|
+
this.setExternalGesturesEnabled?.(false);
|
|
252
|
+
moveToDragLayer();
|
|
253
|
+
this.onCancel();
|
|
254
|
+
});
|
|
226
255
|
transformer.on("transformend", () => {
|
|
256
|
+
this.setExternalGesturesEnabled?.(true);
|
|
257
|
+
restoreFromDragLayer();
|
|
227
258
|
this.onChanged(
|
|
228
259
|
group.id(),
|
|
229
260
|
group.toJSON(),
|
|
@@ -232,13 +263,22 @@ export class Selector {
|
|
|
232
263
|
transformer.getClientRect()
|
|
233
264
|
);
|
|
234
265
|
});
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
266
|
+
let dragStartRect = null;
|
|
267
|
+
let dragAnchorPos = null;
|
|
238
268
|
transformer.on("dragstart", () => {
|
|
269
|
+
this.setExternalGesturesEnabled?.(false);
|
|
270
|
+
moveToDragLayer();
|
|
239
271
|
this.onCancel();
|
|
272
|
+
const boxes = transformer.nodes().map((node) => node.getClientRect());
|
|
273
|
+
dragStartRect = this.getTotalBox(boxes);
|
|
274
|
+
const firstNode = transformer.nodes()[0];
|
|
275
|
+
dragAnchorPos = firstNode ? firstNode.getAbsolutePosition() : null;
|
|
240
276
|
});
|
|
241
277
|
transformer.on("dragend", () => {
|
|
278
|
+
this.setExternalGesturesEnabled?.(true);
|
|
279
|
+
restoreFromDragLayer();
|
|
280
|
+
dragStartRect = null;
|
|
281
|
+
dragAnchorPos = null;
|
|
242
282
|
this.onChanged(
|
|
243
283
|
group.id(),
|
|
244
284
|
group.toJSON(),
|
|
@@ -248,29 +288,32 @@ export class Selector {
|
|
|
248
288
|
);
|
|
249
289
|
});
|
|
250
290
|
transformer.on("dragmove", () => {
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
if (!dragStartRect || !dragAnchorPos) return;
|
|
292
|
+
const nodes = transformer.nodes();
|
|
293
|
+
const firstNode = nodes[0];
|
|
294
|
+
if (!firstNode) return;
|
|
295
|
+
const currentPos = firstNode.getAbsolutePosition();
|
|
296
|
+
const dx = currentPos.x - dragAnchorPos.x;
|
|
297
|
+
const dy = currentPos.y - dragAnchorPos.y;
|
|
298
|
+
const boxX = dragStartRect.x + dx;
|
|
299
|
+
const boxY = dragStartRect.y + dy;
|
|
300
|
+
const halfWidth = dragStartRect.width / 2;
|
|
301
|
+
const halfHeight = dragStartRect.height / 2;
|
|
302
|
+
const stageW = konvaStage.width();
|
|
303
|
+
const stageH = konvaStage.height();
|
|
304
|
+
let clampDx = 0;
|
|
305
|
+
let clampDy = 0;
|
|
306
|
+
if (boxX + halfWidth < 0) clampDx = -(boxX + halfWidth);
|
|
307
|
+
else if (boxX + halfWidth > stageW) clampDx = stageW - (boxX + halfWidth);
|
|
308
|
+
if (boxY + halfHeight < 0) clampDy = -(boxY + halfHeight);
|
|
309
|
+
else if (boxY + halfHeight > stageH) clampDy = stageH - (boxY + halfHeight);
|
|
310
|
+
if (clampDx !== 0 || clampDy !== 0) {
|
|
311
|
+
nodes.forEach((shape) => {
|
|
312
|
+
const pos = shape.getAbsolutePosition();
|
|
313
|
+
shape.setAbsolutePosition({ x: pos.x + clampDx, y: pos.y + clampDy });
|
|
314
|
+
});
|
|
315
|
+
dragAnchorPos = firstNode.getAbsolutePosition();
|
|
316
|
+
}
|
|
274
317
|
});
|
|
275
318
|
transformer.nodes([group]);
|
|
276
319
|
this.getBackgroundLayer(konvaStage).add(transformer);
|
|
@@ -507,12 +550,11 @@ export class Selector {
|
|
|
507
550
|
this.transformerStore.set("__multi__", transformer);
|
|
508
551
|
nodes.forEach((node) => {
|
|
509
552
|
node.on("dragstart.marquee", () => {
|
|
553
|
+
this.setExternalGesturesEnabled?.(false);
|
|
510
554
|
this.onCancel();
|
|
511
555
|
});
|
|
512
|
-
node.on("dragmove.marquee", () => {
|
|
513
|
-
this.onMultiSelected(matchedIds, transformer.getClientRect());
|
|
514
|
-
});
|
|
515
556
|
node.on("dragend.marquee", () => {
|
|
557
|
+
this.setExternalGesturesEnabled?.(true);
|
|
516
558
|
this.onMultiSelected(matchedIds, transformer.getClientRect());
|
|
517
559
|
});
|
|
518
560
|
});
|
|
@@ -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,7 @@ 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;
|
|
23
26
|
}
|
|
24
27
|
export declare class Painter {
|
|
25
28
|
private userName;
|
|
@@ -33,12 +36,13 @@ export declare class Painter {
|
|
|
33
36
|
private getStylusModeEnabled?;
|
|
34
37
|
private getReadonly?;
|
|
35
38
|
private freehandGroupingDelay;
|
|
36
|
-
constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, freehandGroupingDelay, }: {
|
|
39
|
+
constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, freehandGroupingDelay, setExternalGesturesEnabled, }: {
|
|
37
40
|
userName: string;
|
|
38
41
|
callbacks: PainterCallbacks;
|
|
39
42
|
getStylusModeEnabled?: () => boolean;
|
|
40
43
|
getReadonly?: () => boolean;
|
|
41
44
|
freehandGroupingDelay?: number;
|
|
45
|
+
setExternalGesturesEnabled?: (enabled: boolean) => void;
|
|
42
46
|
});
|
|
43
47
|
private editFreeText;
|
|
44
48
|
private bindGlobalEvents;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PAINTER_PAINTING_TYPE
|
|
11
11
|
} from "./const.js";
|
|
12
12
|
import { hideCursorPreview } from "./cursor-preview.js";
|
|
13
|
+
import { Editor } from "./editor/editor.js";
|
|
13
14
|
import { EditorCircle } from "./tools/circle.js";
|
|
14
15
|
import { EditorFreeHand } from "./tools/freehand.js";
|
|
15
16
|
import { EditorFreeHighlight } from "./tools/free-highlight.js";
|
|
@@ -21,8 +22,13 @@ import { EditorStamp } from "./tools/stamp.js";
|
|
|
21
22
|
import { EditorNote } from "./tools/note.js";
|
|
22
23
|
import { EditorArrow } from "./tools/arrow.js";
|
|
23
24
|
import { EditorCloud } from "./tools/cloud.js";
|
|
25
|
+
import { EditorFormField } from "./tools/form-field.js";
|
|
24
26
|
import { Selector } from "./editor/selector.js";
|
|
25
27
|
import { Store } from "./store.js";
|
|
28
|
+
import { isIPad } from "./input-device.js";
|
|
29
|
+
if (typeof window !== "undefined" && isIPad()) {
|
|
30
|
+
Konva.pixelRatio = 1;
|
|
31
|
+
}
|
|
26
32
|
export class Painter {
|
|
27
33
|
userName;
|
|
28
34
|
konvaCanvasStore = /* @__PURE__ */ new Map();
|
|
@@ -40,7 +46,8 @@ export class Painter {
|
|
|
40
46
|
callbacks,
|
|
41
47
|
getStylusModeEnabled,
|
|
42
48
|
getReadonly,
|
|
43
|
-
freehandGroupingDelay
|
|
49
|
+
freehandGroupingDelay,
|
|
50
|
+
setExternalGesturesEnabled
|
|
44
51
|
}) {
|
|
45
52
|
this.userName = userName;
|
|
46
53
|
this.callbacks = callbacks;
|
|
@@ -86,7 +93,8 @@ export class Painter {
|
|
|
86
93
|
onFreeTextDoubleClick: (id) => {
|
|
87
94
|
void this.editFreeText(id);
|
|
88
95
|
},
|
|
89
|
-
getStylusModeEnabled: this.getStylusModeEnabled
|
|
96
|
+
getStylusModeEnabled: this.getStylusModeEnabled,
|
|
97
|
+
setExternalGesturesEnabled
|
|
90
98
|
});
|
|
91
99
|
this.bindGlobalEvents();
|
|
92
100
|
}
|
|
@@ -138,7 +146,9 @@ export class Painter {
|
|
|
138
146
|
}
|
|
139
147
|
applyStylusTouchAction(stage) {
|
|
140
148
|
stage.preventDefault(false);
|
|
141
|
-
|
|
149
|
+
if (!isIPad()) {
|
|
150
|
+
Konva.pixelRatio = window.devicePixelRatio || 1;
|
|
151
|
+
}
|
|
142
152
|
const content = stage.content;
|
|
143
153
|
if (content) content.style.touchAction = "pan-x pan-y";
|
|
144
154
|
stage.container().querySelectorAll("canvas").forEach((c) => {
|
|
@@ -147,7 +157,9 @@ export class Painter {
|
|
|
147
157
|
}
|
|
148
158
|
removeStylusTouchAction(stage) {
|
|
149
159
|
stage.preventDefault(true);
|
|
150
|
-
|
|
160
|
+
if (!isIPad()) {
|
|
161
|
+
Konva.pixelRatio = window.devicePixelRatio || 1;
|
|
162
|
+
}
|
|
151
163
|
const content = stage.content;
|
|
152
164
|
if (content) content.style.touchAction = "";
|
|
153
165
|
stage.container().querySelectorAll("canvas").forEach((c) => {
|
|
@@ -233,6 +245,11 @@ export class Painter {
|
|
|
233
245
|
);
|
|
234
246
|
return;
|
|
235
247
|
}
|
|
248
|
+
if (storeEditor instanceof EditorFormField) {
|
|
249
|
+
storeEditor.setPageHeight(
|
|
250
|
+
this.konvaCanvasStore.get(pageNumber)?.pageHeight ?? 0
|
|
251
|
+
);
|
|
252
|
+
}
|
|
236
253
|
storeEditor.activate(konvaStage, annotation);
|
|
237
254
|
return;
|
|
238
255
|
}
|
|
@@ -301,6 +318,24 @@ export class Painter {
|
|
|
301
318
|
case AnnotationType.STRIKEOUT:
|
|
302
319
|
editor = new EditorHighLight(editorOpts, annotation.type);
|
|
303
320
|
break;
|
|
321
|
+
case AnnotationType.FORM_TEXT:
|
|
322
|
+
case AnnotationType.FORM_CHECKBOX:
|
|
323
|
+
case AnnotationType.FORM_RADIO:
|
|
324
|
+
case AnnotationType.FORM_SIGNATURE: {
|
|
325
|
+
const onPlaceField = this.callbacks.onPlaceField;
|
|
326
|
+
if (!onPlaceField) {
|
|
327
|
+
console.warn("Form-field placement requires an onPlaceField callback.");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const pageHeight = this.konvaCanvasStore.get(pageNumber)?.pageHeight ?? 0;
|
|
331
|
+
const fieldType = annotation.type === AnnotationType.FORM_TEXT ? "text" : annotation.type === AnnotationType.FORM_CHECKBOX ? "checkbox" : annotation.type === AnnotationType.FORM_RADIO ? "radio" : "signature";
|
|
332
|
+
editor = new EditorFormField(editorOpts, {
|
|
333
|
+
fieldType,
|
|
334
|
+
pageHeight,
|
|
335
|
+
onPlaceField
|
|
336
|
+
});
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
304
339
|
case AnnotationType.SELECT:
|
|
305
340
|
this.selector.activate(pageNumber);
|
|
306
341
|
break;
|
|
@@ -440,7 +475,8 @@ export class Painter {
|
|
|
440
475
|
pageNumber,
|
|
441
476
|
konvaStage,
|
|
442
477
|
wrapper: container,
|
|
443
|
-
isActive: false
|
|
478
|
+
isActive: false,
|
|
479
|
+
pageHeight: height
|
|
444
480
|
});
|
|
445
481
|
this.reDrawAnnotation(pageNumber);
|
|
446
482
|
this.enablePainting();
|
|
@@ -551,6 +587,7 @@ export class Painter {
|
|
|
551
587
|
return this.store.annotations;
|
|
552
588
|
}
|
|
553
589
|
getExportContext() {
|
|
590
|
+
Editor.flushAllTimers();
|
|
554
591
|
const originalIds = this.store.originalIds;
|
|
555
592
|
const modifiedIds = this.store.modifiedOriginals;
|
|
556
593
|
const changedAnnotations = this.store.annotations.filter(
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
10
|
+
export declare class EditorFormField extends Editor {
|
|
11
|
+
private fieldType;
|
|
12
|
+
private pageHeight;
|
|
13
|
+
private onPlaceField;
|
|
14
|
+
private preview;
|
|
15
|
+
private vertex;
|
|
16
|
+
constructor(editorOptions: IEditorOptions, opts: IEditorFormFieldOptions);
|
|
17
|
+
setPageHeight(pageHeight: number): void;
|
|
18
|
+
protected mouseDownHandler(e: KonvaEventObject<MouseEvent | TouchEvent>): void;
|
|
19
|
+
protected mouseMoveHandler(e: KonvaEventObject<MouseEvent | TouchEvent>): void;
|
|
20
|
+
protected mouseUpHandler(): void;
|
|
21
|
+
private globalPointerUpHandler;
|
|
22
|
+
protected changeStyle(_a: IAnnotationStore, _s: IAnnotationStyle): void;
|
|
23
|
+
addSerializedGroupToLayer(): void;
|
|
24
|
+
}
|