kviewer 0.0.11 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +9 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +22 -2
- package/dist/runtime/annotation/engine/painter.d.ts +8 -1
- package/dist/runtime/annotation/engine/painter.js +11 -6
- package/dist/runtime/annotation/engine/tools/form-field.d.ts +6 -0
- package/dist/runtime/annotation/engine/tools/form-field.js +3 -1
- package/dist/runtime/annotation/engine/types.d.ts +64 -0
- package/dist/runtime/annotation/pdf-export/export-form-fields.js +102 -52
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/PdfPage.vue +6 -1
- package/dist/runtime/components/Viewer.d.vue.ts +41 -1
- package/dist/runtime/components/Viewer.vue +84 -3
- package/dist/runtime/components/Viewer.vue.d.ts +41 -1
- package/dist/runtime/components/ViewerBar.d.vue.ts +7 -1
- package/dist/runtime/components/ViewerBar.vue +36 -0
- package/dist/runtime/components/ViewerBar.vue.d.ts +7 -1
- package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -6
- package/dist/runtime/components/ViewerTabs.vue +11 -2
- package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -6
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +10 -2
- package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +1 -1
- package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
- package/dist/runtime/composables/useAnnotationEngine.js +2 -1
- package/dist/runtime/composables/useFormFields.d.ts +40 -3
- package/dist/runtime/composables/useFormFields.js +101 -1
- package/dist/runtime/composables/useInertiaPanzoom.js +1 -0
- package/dist/runtime/composables/usePageProxyCache.d.ts +4 -0
- package/dist/runtime/composables/usePageProxyCache.js +4 -1
- package/dist/runtime/composables/useScriptingBridge.d.ts +29 -0
- package/dist/runtime/composables/useScriptingBridge.js +74 -0
- package/dist/runtime/composables/useScriptingManager.d.ts +65 -0
- package/dist/runtime/composables/useScriptingManager.js +123 -0
- package/dist/runtime/embed/bridge-client.d.ts +58 -0
- package/dist/runtime/embed/bridge-client.js +143 -0
- package/dist/runtime/embed/bridge-host.d.ts +59 -0
- package/dist/runtime/embed/bridge-host.js +136 -0
- package/dist/runtime/embed/protocol.d.ts +105 -0
- package/dist/runtime/embed/protocol.js +13 -0
- package/dist/runtime/menu-items.d.ts +34 -0
- package/dist/runtime/menu-items.js +0 -0
- package/dist/runtime/public-types.d.ts +8 -1
- package/dist/runtime/public-types.js +3 -0
- package/dist/types.d.mts +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Ref, type ShallowRef } from 'vue';
|
|
2
|
-
import type { CheckboxStyle, DetectedShape, FormFieldDefinition, FormFieldValue, PlaceFieldPayload } from '../annotation/engine/types.js';
|
|
2
|
+
import type { AddFormFieldPayload, CheckboxStyle, DetectedShape, FormFieldDefinition, FormFieldValue, PlaceFieldPayload } from '../annotation/engine/types.js';
|
|
3
3
|
export interface FormFieldsState {
|
|
4
4
|
/** Field definitions grouped by page number */
|
|
5
5
|
fieldDefinitions: ShallowRef<Map<number, FormFieldDefinition[]>>;
|
|
@@ -23,10 +23,16 @@ export interface FormFieldsState {
|
|
|
23
23
|
registerDetectedCheckboxes: (pageNumber: number, shapes: DetectedShape[], scale: number, pageHeight: number) => void;
|
|
24
24
|
/** Add a user-placed form field (from the placement tool). Returns the created def. */
|
|
25
25
|
addPlacedField: (payload: PlaceFieldPayload) => FormFieldDefinition;
|
|
26
|
-
/**
|
|
26
|
+
/** Add a form field programmatically. Public API counterpart to
|
|
27
|
+
* `addPlacedField` with full control over the field's properties.
|
|
28
|
+
* Returns the created definition. */
|
|
29
|
+
addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
|
|
30
|
+
/** Update a form field's rect, name, or flags. */
|
|
27
31
|
updatePlacedField: (id: string, patch: Partial<FormFieldDefinition>) => void;
|
|
28
|
-
/** Remove a
|
|
32
|
+
/** Remove a form field and its value. */
|
|
29
33
|
removePlacedField: (id: string) => void;
|
|
34
|
+
/** All form-field definitions, flattened across pages. */
|
|
35
|
+
getAllFields: () => FormFieldDefinition[];
|
|
30
36
|
/** All user-placed field definitions (used by export). */
|
|
31
37
|
getPlacedFields: () => FormFieldDefinition[];
|
|
32
38
|
/** Look up a field definition by id (any origin). */
|
|
@@ -35,6 +41,19 @@ export interface FormFieldsState {
|
|
|
35
41
|
selectedPlacedFieldId: Ref<string | null>;
|
|
36
42
|
/** Select or deselect a placed field. Pass null to deselect. */
|
|
37
43
|
selectPlacedField: (id: string | null) => void;
|
|
44
|
+
/** Opaque role id new placements are auto-tagged with. The host owns
|
|
45
|
+
* what roles exist; the editor only carries the active id forward to
|
|
46
|
+
* newly placed fields and looks up its color via roleColors. */
|
|
47
|
+
activeRoleId: Ref<string | null>;
|
|
48
|
+
/** Set or clear the active role for upcoming placements. */
|
|
49
|
+
setActiveRole: (id: string | null) => void;
|
|
50
|
+
/** Map from roleId → CSS color, supplied by the host via the KViewer
|
|
51
|
+
* `roleColors` prop. Reading this drives the chrome and placement
|
|
52
|
+
* preview color. The editor never mutates role definitions itself. */
|
|
53
|
+
roleColors: ShallowRef<Record<string, string>>;
|
|
54
|
+
/** Replace the host-provided color map. Called by KViewer when the
|
|
55
|
+
* prop changes. */
|
|
56
|
+
setRoleColors: (colors: Record<string, string>) => void;
|
|
38
57
|
/** Apply checkbox-style hints (extracted from `/MK /CA` in the source PDF)
|
|
39
58
|
* to existing parsed checkbox defs and store the map for any defs
|
|
40
59
|
* parsed afterwards. Keyed by checkbox fieldName. */
|
|
@@ -48,8 +67,26 @@ export interface FormFieldsState {
|
|
|
48
67
|
* that calls this re-runs when any signature is signed or its lock
|
|
49
68
|
* rule changes. */
|
|
50
69
|
isFieldLocked: (fieldId: string) => boolean;
|
|
70
|
+
/** Install a sink that is invoked synchronously for every field-value
|
|
71
|
+
* write driven by user interaction. Used by the scripting bridge to
|
|
72
|
+
* forward changes into `pdfDoc.annotationStorage` and dispatch into
|
|
73
|
+
* the JS sandbox. The sink is NOT called for inbound updates from
|
|
74
|
+
* [[applyFieldValueFromExternal]] (those originate from the sandbox
|
|
75
|
+
* itself, so re-firing would create a ping-pong loop). */
|
|
76
|
+
setValueWriteSink: (sink: ValueWriteSink | null) => void;
|
|
77
|
+
/** Apply a value coming from outside Vue's reactive flow (e.g. the
|
|
78
|
+
* PDF scripting sandbox dispatching `updatefromsandbox`). Updates
|
|
79
|
+
* `fieldValues` and triggers rerender. Idempotent: a write that
|
|
80
|
+
* matches the current value is a no-op (prevents redundant renders
|
|
81
|
+
* from the sandbox's own sibling-mirror events). Skips non-parsed
|
|
82
|
+
* fields (placed/detected don't exist in the PDF object model so
|
|
83
|
+
* the sandbox can't legitimately address them). */
|
|
84
|
+
applyFieldValueFromExternal: (fieldId: string, value: string | boolean | string[]) => void;
|
|
51
85
|
/** Clear all values and definitions (for document change) */
|
|
52
86
|
reset: () => void;
|
|
53
87
|
}
|
|
88
|
+
/** Sink signature used by [[setValueWriteSink]]. Receives every user-driven
|
|
89
|
+
* field value write (the primary id AND each mirrored sibling). */
|
|
90
|
+
export type ValueWriteSink = (fieldId: string, value: string | boolean | string[]) => void;
|
|
54
91
|
export declare function provideFormFields(): FormFieldsState;
|
|
55
92
|
export declare function useFormFields(): FormFieldsState;
|
|
@@ -12,6 +12,14 @@ export function provideFormFields() {
|
|
|
12
12
|
const fieldDefinitions = shallowRef(/* @__PURE__ */ new Map());
|
|
13
13
|
const fieldValues = shallowRef(/* @__PURE__ */ new Map());
|
|
14
14
|
const selectedPlacedFieldId = ref(null);
|
|
15
|
+
const activeRoleId = ref(null);
|
|
16
|
+
const roleColors = shallowRef({});
|
|
17
|
+
function setActiveRole(id) {
|
|
18
|
+
activeRoleId.value = id;
|
|
19
|
+
}
|
|
20
|
+
function setRoleColors(colors) {
|
|
21
|
+
roleColors.value = colors;
|
|
22
|
+
}
|
|
15
23
|
function selectPlacedField(id) {
|
|
16
24
|
selectedPlacedFieldId.value = id;
|
|
17
25
|
}
|
|
@@ -23,6 +31,10 @@ export function provideFormFields() {
|
|
|
23
31
|
}
|
|
24
32
|
return void 0;
|
|
25
33
|
}
|
|
34
|
+
let valueWriteSink = null;
|
|
35
|
+
function setValueWriteSink(sink) {
|
|
36
|
+
valueWriteSink = sink;
|
|
37
|
+
}
|
|
26
38
|
let checkboxStyleMap = null;
|
|
27
39
|
let buttonCaptionMap = null;
|
|
28
40
|
function applyCheckboxStyles(styles) {
|
|
@@ -104,22 +116,37 @@ export function provideFormFields() {
|
|
|
104
116
|
const existing = fieldValues.value.get(fieldId);
|
|
105
117
|
if (!existing) return;
|
|
106
118
|
existing.value = value;
|
|
119
|
+
const sinkWrites = [[fieldId, value]];
|
|
107
120
|
const { fieldName, fieldType } = existing;
|
|
108
121
|
if (fieldName) {
|
|
109
122
|
if (fieldType === "radio" && typeof value === "string" && value !== "") {
|
|
110
123
|
for (const [id, fv] of fieldValues.value.entries()) {
|
|
111
124
|
if (id !== fieldId && fv.fieldName === fieldName && fv.fieldType === "radio") {
|
|
112
125
|
fv.value = "";
|
|
126
|
+
sinkWrites.push([id, ""]);
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
} else if (fieldType !== "radio") {
|
|
116
130
|
for (const [id, fv] of fieldValues.value.entries()) {
|
|
117
131
|
if (id !== fieldId && fv.fieldName === fieldName && fv.fieldType === fieldType) {
|
|
118
132
|
fv.value = value;
|
|
133
|
+
sinkWrites.push([id, value]);
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
}
|
|
122
137
|
}
|
|
138
|
+
if (valueWriteSink) {
|
|
139
|
+
for (const [id, v] of sinkWrites) valueWriteSink(id, v);
|
|
140
|
+
}
|
|
141
|
+
triggerRef(fieldValues);
|
|
142
|
+
}
|
|
143
|
+
function applyFieldValueFromExternal(fieldId, value) {
|
|
144
|
+
const existing = fieldValues.value.get(fieldId);
|
|
145
|
+
if (!existing) return;
|
|
146
|
+
const def = getFieldById(fieldId);
|
|
147
|
+
if (def && def.origin !== "parsed") return;
|
|
148
|
+
if (existing.value === value) return;
|
|
149
|
+
existing.value = value;
|
|
123
150
|
triggerRef(fieldValues);
|
|
124
151
|
}
|
|
125
152
|
function getFieldValue(fieldId) {
|
|
@@ -231,6 +258,9 @@ export function provideFormFields() {
|
|
|
231
258
|
required: false,
|
|
232
259
|
origin: "placed"
|
|
233
260
|
};
|
|
261
|
+
if (activeRoleId.value) {
|
|
262
|
+
def.roleId = activeRoleId.value;
|
|
263
|
+
}
|
|
234
264
|
if (payload.fieldType === "radio") {
|
|
235
265
|
def.buttonValue = nextRadioOptionValue(fieldName);
|
|
236
266
|
}
|
|
@@ -256,6 +286,67 @@ export function provideFormFields() {
|
|
|
256
286
|
}
|
|
257
287
|
return def;
|
|
258
288
|
}
|
|
289
|
+
function addFormField(payload) {
|
|
290
|
+
const id = `placed-${generateUUID()}`;
|
|
291
|
+
const fieldName = payload.fieldName ?? nextPlacedFieldName(payload.fieldType);
|
|
292
|
+
const def = {
|
|
293
|
+
id,
|
|
294
|
+
pageNumber: payload.pageNumber,
|
|
295
|
+
fieldType: payload.fieldType,
|
|
296
|
+
fieldName,
|
|
297
|
+
rect: [...payload.rect],
|
|
298
|
+
readOnly: payload.readOnly ?? false,
|
|
299
|
+
required: payload.required ?? false,
|
|
300
|
+
origin: "placed"
|
|
301
|
+
};
|
|
302
|
+
if (payload.roleId !== void 0) def.roleId = payload.roleId;
|
|
303
|
+
if (payload.defaultValue !== void 0) def.defaultValue = payload.defaultValue;
|
|
304
|
+
if (payload.fontSize !== void 0) def.fontSize = payload.fontSize;
|
|
305
|
+
if (payload.fontName !== void 0) def.fontName = payload.fontName;
|
|
306
|
+
if (payload.color !== void 0) def.color = payload.color;
|
|
307
|
+
if (payload.backgroundColor !== void 0) def.backgroundColor = payload.backgroundColor;
|
|
308
|
+
if (payload.textAlignment !== void 0) def.textAlignment = payload.textAlignment;
|
|
309
|
+
if (payload.fieldType === "radio") {
|
|
310
|
+
def.buttonValue = payload.buttonValue ?? nextRadioOptionValue(fieldName);
|
|
311
|
+
}
|
|
312
|
+
if (payload.fieldType === "checkbox") {
|
|
313
|
+
def.checkboxStyle = payload.checkboxStyle ?? "check";
|
|
314
|
+
}
|
|
315
|
+
if (payload.fieldType === "dropdown") {
|
|
316
|
+
def.combo = payload.combo ?? true;
|
|
317
|
+
def.options = payload.options ?? [{ exportValue: "Option 1", displayValue: "Option 1" }];
|
|
318
|
+
if (payload.multiSelect !== void 0) def.multiSelect = payload.multiSelect;
|
|
319
|
+
}
|
|
320
|
+
if (payload.fieldType === "text") {
|
|
321
|
+
if (payload.multiLine !== void 0) def.multiLine = payload.multiLine;
|
|
322
|
+
if (payload.password !== void 0) def.password = payload.password;
|
|
323
|
+
if (payload.comb !== void 0) def.comb = payload.comb;
|
|
324
|
+
if (payload.maxLen !== void 0) def.maxLen = payload.maxLen;
|
|
325
|
+
}
|
|
326
|
+
if (payload.fieldType === "signature") {
|
|
327
|
+
if (payload.promptText !== void 0) def.promptText = payload.promptText;
|
|
328
|
+
if (payload.lockAction !== void 0) def.lockAction = payload.lockAction;
|
|
329
|
+
if (payload.lockFieldNames !== void 0) def.lockFieldNames = payload.lockFieldNames;
|
|
330
|
+
}
|
|
331
|
+
const existingDefs = fieldDefinitions.value.get(payload.pageNumber) ?? [];
|
|
332
|
+
fieldDefinitions.value.set(payload.pageNumber, [...existingDefs, def]);
|
|
333
|
+
triggerRef(fieldDefinitions);
|
|
334
|
+
fieldValues.value.set(id, {
|
|
335
|
+
fieldId: id,
|
|
336
|
+
fieldName: def.fieldName,
|
|
337
|
+
fieldType: def.fieldType,
|
|
338
|
+
value: getDefaultValue(def)
|
|
339
|
+
});
|
|
340
|
+
triggerRef(fieldValues);
|
|
341
|
+
return def;
|
|
342
|
+
}
|
|
343
|
+
function getAllFields() {
|
|
344
|
+
const out = [];
|
|
345
|
+
for (const defs of fieldDefinitions.value.values()) {
|
|
346
|
+
for (const d of defs) out.push(d);
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
259
350
|
function nextRadioOptionValue(groupName) {
|
|
260
351
|
const used = /* @__PURE__ */ new Set();
|
|
261
352
|
for (const defs of fieldDefinitions.value.values()) {
|
|
@@ -373,6 +464,7 @@ export function provideFormFields() {
|
|
|
373
464
|
selectedPlacedFieldId.value = null;
|
|
374
465
|
checkboxStyleMap = null;
|
|
375
466
|
buttonCaptionMap = null;
|
|
467
|
+
valueWriteSink = null;
|
|
376
468
|
triggerRef(fieldDefinitions);
|
|
377
469
|
triggerRef(fieldValues);
|
|
378
470
|
}
|
|
@@ -387,8 +479,10 @@ export function provideFormFields() {
|
|
|
387
479
|
setFieldValueByName,
|
|
388
480
|
registerDetectedCheckboxes,
|
|
389
481
|
addPlacedField,
|
|
482
|
+
addFormField,
|
|
390
483
|
updatePlacedField,
|
|
391
484
|
removePlacedField,
|
|
485
|
+
getAllFields,
|
|
392
486
|
getPlacedFields,
|
|
393
487
|
getFieldById,
|
|
394
488
|
selectedPlacedFieldId,
|
|
@@ -397,7 +491,13 @@ export function provideFormFields() {
|
|
|
397
491
|
applyButtonCaptions,
|
|
398
492
|
isFieldLocked,
|
|
399
493
|
resetValues,
|
|
400
|
-
reset
|
|
494
|
+
reset,
|
|
495
|
+
setValueWriteSink,
|
|
496
|
+
applyFieldValueFromExternal,
|
|
497
|
+
activeRoleId,
|
|
498
|
+
setActiveRole,
|
|
499
|
+
roleColors,
|
|
500
|
+
setRoleColors
|
|
401
501
|
};
|
|
402
502
|
provide(FORM_FIELDS_KEY, state);
|
|
403
503
|
return state;
|
|
@@ -2,6 +2,10 @@ import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
|
|
|
2
2
|
export interface PageProxyCache {
|
|
3
3
|
setDocument: (doc: PDFDocumentProxy) => void;
|
|
4
4
|
getPage: (pageNumber: number) => Promise<PDFPageProxy>;
|
|
5
|
+
/** Return a cached page proxy synchronously, or undefined if not resolved yet.
|
|
6
|
+
* Used by the scripting viewer-adapter where PDFScriptingManager calls
|
|
7
|
+
* `getPageView(idx).pdfPage` synchronously during page-open dispatch. */
|
|
8
|
+
getPageSync: (pageNumber: number) => PDFPageProxy | undefined;
|
|
5
9
|
clear: () => void;
|
|
6
10
|
}
|
|
7
11
|
export declare function createPageProxyCache(maxSize?: number): PageProxyCache;
|
|
@@ -60,7 +60,10 @@ export function createPageProxyCache(maxSize = DEFAULT_MAX_SIZE) {
|
|
|
60
60
|
inflight.clear();
|
|
61
61
|
doc = null;
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
function getPageSync(pageNumber) {
|
|
64
|
+
return cache.get(pageNumber);
|
|
65
|
+
}
|
|
66
|
+
const cacheInstance = { setDocument, getPage, getPageSync, clear };
|
|
64
67
|
provide(PAGE_PROXY_CACHE_KEY, cacheInstance);
|
|
65
68
|
return cacheInstance;
|
|
66
69
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
2
|
+
import type { FormFieldsState } from './useFormFields.js';
|
|
3
|
+
import { type ScriptingManager } from './useScriptingManager.js';
|
|
4
|
+
export interface ScriptingBridge {
|
|
5
|
+
destroy: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ScriptingBridgeOptions {
|
|
8
|
+
formFields: FormFieldsState;
|
|
9
|
+
scripting: ScriptingManager;
|
|
10
|
+
pdfDoc: PDFDocumentProxy;
|
|
11
|
+
}
|
|
12
|
+
/** Bridge between kviewer's reactive form-state, the PDF document's
|
|
13
|
+
* AnnotationStorage (which the sandbox reads/writes), and the scripting
|
|
14
|
+
* manager's event bus. Ownership rules:
|
|
15
|
+
*
|
|
16
|
+
* - Outbound (user → sandbox): every `setFieldValue` write hits the
|
|
17
|
+
* `valueWriteSink` we install. We mirror into AnnotationStorage AND
|
|
18
|
+
* dispatch an `'Action'` (and `'Validate'` for text) event into the
|
|
19
|
+
* sandbox so any field-AA script runs.
|
|
20
|
+
*
|
|
21
|
+
* - Inbound (sandbox → UI): we subscribe to `updatefromsandbox` on the
|
|
22
|
+
* event bus, translate the per-field-type payload back into kviewer's
|
|
23
|
+
* primitive shape, and call `applyFieldValueFromExternal` (which is
|
|
24
|
+
* sink-blind, so no ping-pong).
|
|
25
|
+
*
|
|
26
|
+
* We also seed AnnotationStorage from current fieldValues at boot so
|
|
27
|
+
* any pre-bridge user input survives the sandbox handshake.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createScriptingBridge(opts: ScriptingBridgeOptions): ScriptingBridge;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getEventBus } from "./useScriptingManager.js";
|
|
2
|
+
export function createScriptingBridge(opts) {
|
|
3
|
+
const { formFields, scripting, pdfDoc } = opts;
|
|
4
|
+
const storage = pdfDoc.annotationStorage;
|
|
5
|
+
const eventBus = getEventBus(scripting);
|
|
6
|
+
for (const fv of formFields.getAllFieldValues()) {
|
|
7
|
+
const def = formFields.getFieldById(fv.fieldId);
|
|
8
|
+
if (!def || def.origin !== "parsed") continue;
|
|
9
|
+
storage.setValue(fv.fieldId, toStorageShape(def.fieldType, fv.value));
|
|
10
|
+
}
|
|
11
|
+
formFields.setValueWriteSink((fieldId, value) => {
|
|
12
|
+
const def = formFields.getFieldById(fieldId);
|
|
13
|
+
if (!def || def.origin !== "parsed") return;
|
|
14
|
+
storage.setValue(fieldId, toStorageShape(def.fieldType, value));
|
|
15
|
+
scripting.dispatchFieldEvent(fieldId, "Action", value);
|
|
16
|
+
if (def.fieldType === "checkbox" || def.fieldType === "radio" || def.fieldType === "button") {
|
|
17
|
+
scripting.dispatchFieldEvent(fieldId, "Mouse Up", value);
|
|
18
|
+
}
|
|
19
|
+
if (def.fieldType === "text") {
|
|
20
|
+
scripting.dispatchFieldEvent(fieldId, "Validate", value);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const onUpdate = (evt) => {
|
|
24
|
+
const detail = evt?.detail;
|
|
25
|
+
if (!detail) return;
|
|
26
|
+
const primaryId = detail.id ?? evt?.id;
|
|
27
|
+
if (!primaryId) return;
|
|
28
|
+
const siblings = Array.isArray(detail.siblings) ? detail.siblings : [];
|
|
29
|
+
const ids = [primaryId, ...siblings];
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
const def = formFields.getFieldById(id);
|
|
32
|
+
if (!def || def.origin !== "parsed") continue;
|
|
33
|
+
const kviewerValue = fromStorageShape(def.fieldType, detail);
|
|
34
|
+
if (kviewerValue === void 0) continue;
|
|
35
|
+
formFields.applyFieldValueFromExternal(id, kviewerValue);
|
|
36
|
+
storage.setValue(id, toStorageShape(def.fieldType, kviewerValue));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
eventBus.on("kviewer-updatefromsandbox", onUpdate);
|
|
40
|
+
return {
|
|
41
|
+
destroy() {
|
|
42
|
+
eventBus.off("kviewer-updatefromsandbox", onUpdate);
|
|
43
|
+
formFields.setValueWriteSink(null);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function toStorageShape(fieldType, value) {
|
|
48
|
+
if (fieldType === "checkbox") return { value: Boolean(value) };
|
|
49
|
+
if (fieldType === "radio") return { value: typeof value === "string" ? value : "" };
|
|
50
|
+
if (fieldType === "dropdown") {
|
|
51
|
+
return { value };
|
|
52
|
+
}
|
|
53
|
+
if (fieldType === "text") return { value: String(value ?? "") };
|
|
54
|
+
if (fieldType === "signature") return { value: String(value ?? "") };
|
|
55
|
+
return { value };
|
|
56
|
+
}
|
|
57
|
+
function fromStorageShape(fieldType, detail) {
|
|
58
|
+
const value = detail?.value;
|
|
59
|
+
if (value === void 0) return void 0;
|
|
60
|
+
if (fieldType === "checkbox") {
|
|
61
|
+
if (typeof value === "boolean") return value;
|
|
62
|
+
return value !== "Off" && value !== "" && value != null;
|
|
63
|
+
}
|
|
64
|
+
if (fieldType === "radio") {
|
|
65
|
+
if (typeof value === "string") return value;
|
|
66
|
+
if (value == null) return "";
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
if (fieldType === "dropdown") {
|
|
70
|
+
if (Array.isArray(value)) return value.map(String);
|
|
71
|
+
return value == null ? "" : String(value);
|
|
72
|
+
}
|
|
73
|
+
return value == null ? "" : String(value);
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
/** Minimal viewer surface PDFScriptingManager calls into. The official
|
|
4
|
+
* PDFViewer is large; we mock the slice the manager actually touches
|
|
5
|
+
* (verified in pdfjs-dist/web/pdf_viewer.mjs lines 7548-7691 and 7639). */
|
|
6
|
+
export interface ScriptingViewerAdapter {
|
|
7
|
+
currentPageNumber: number;
|
|
8
|
+
pagesCount: number;
|
|
9
|
+
pagesPromise: Promise<void>;
|
|
10
|
+
nextPage: () => void;
|
|
11
|
+
previousPage: () => void;
|
|
12
|
+
currentScaleValue: number | string;
|
|
13
|
+
increaseScale: () => void;
|
|
14
|
+
decreaseScale: () => void;
|
|
15
|
+
spreadMode: number;
|
|
16
|
+
readonly isInPresentationMode: boolean;
|
|
17
|
+
readonly isChangingPresentationMode: boolean;
|
|
18
|
+
getPageView: (idx: number) => {
|
|
19
|
+
renderingState: number;
|
|
20
|
+
pdfPage: PDFPageProxy;
|
|
21
|
+
} | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface ScriptingManagerDeps {
|
|
24
|
+
viewerState: {
|
|
25
|
+
currentPage: Ref<number>;
|
|
26
|
+
totalPages: Ref<number>;
|
|
27
|
+
scale: Ref<number>;
|
|
28
|
+
setScale: (v: number) => void;
|
|
29
|
+
scrollToPage: (n: number) => void;
|
|
30
|
+
};
|
|
31
|
+
virtualization: {
|
|
32
|
+
isPageRendered: (pageNumber: number) => boolean;
|
|
33
|
+
};
|
|
34
|
+
proxyCache: {
|
|
35
|
+
getPageSync: (pageNumber: number) => PDFPageProxy | undefined;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface ScriptingManager {
|
|
39
|
+
/** Pump a user-interaction event into the sandbox for a given field. */
|
|
40
|
+
dispatchFieldEvent: (fieldId: string, name: 'Action' | 'Validate' | 'Focus' | 'Blur' | 'Mouse Up' | 'Mouse Down' | 'Mouse Enter' | 'Mouse Exit' | 'Keystroke', value: string | boolean | string[], extra?: Record<string, unknown>) => void;
|
|
41
|
+
/** Notify the sandbox that a page finished rendering, so any deferred
|
|
42
|
+
* PageOpen action can fire (pdf_viewer.mjs:7441). */
|
|
43
|
+
notifyPageRendered: (pageNumber: number) => void;
|
|
44
|
+
/** Bind a document to the sandbox. Safe to call repeatedly — pass
|
|
45
|
+
* null to detach (e.g. before swapping documents). */
|
|
46
|
+
setDocument: (pdfDoc: PDFDocumentProxy | null) => Promise<void>;
|
|
47
|
+
/** Tear down the manager, sandbox worker, and event listeners.
|
|
48
|
+
* Idempotent. */
|
|
49
|
+
destroy: () => Promise<void>;
|
|
50
|
+
/** True once the sandbox has booted for the current document. */
|
|
51
|
+
isReady: () => boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Construct the scripting manager. ONE instance per `<Viewer>` lifetime —
|
|
54
|
+
* reuse `setDocument(null)` then `setDocument(newDoc)` across document
|
|
55
|
+
* swaps. PDFScriptingManagerComponents attaches a window-level
|
|
56
|
+
* `updatefromsandbox` listener in its constructor (pdf_viewer.mjs:7733)
|
|
57
|
+
* with no removal path; constructing it per-document would leak. */
|
|
58
|
+
export declare function createScriptingManager(deps: ScriptingManagerDeps): Promise<ScriptingManager>;
|
|
59
|
+
/** Cast helper used by the bridge — keeps the eventBus access narrowly
|
|
60
|
+
* typed without exporting an extra interface. */
|
|
61
|
+
export declare function getEventBus(manager: ScriptingManager): {
|
|
62
|
+
on: (name: string, handler: (...args: unknown[]) => void) => void;
|
|
63
|
+
off: (name: string, handler: (...args: unknown[]) => void) => void;
|
|
64
|
+
dispatch: (name: string, payload: unknown) => void;
|
|
65
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export async function createScriptingManager(deps) {
|
|
2
|
+
const pdfViewer = await import("pdfjs-dist/web/pdf_viewer.mjs");
|
|
3
|
+
const { PDFScriptingManager, EventBus, RenderingStates } = pdfViewer;
|
|
4
|
+
const eventBus = new EventBus();
|
|
5
|
+
const onSandboxWindowEvent = (event) => {
|
|
6
|
+
const d = event.detail ?? {};
|
|
7
|
+
eventBus.dispatch("kviewer-updatefromsandbox", {
|
|
8
|
+
source: window,
|
|
9
|
+
detail: {
|
|
10
|
+
...d,
|
|
11
|
+
// Re-attach id and siblings explicitly (spread preserves them now,
|
|
12
|
+
// but is defensive against future mutations between our snapshot
|
|
13
|
+
// and the bridge's handler).
|
|
14
|
+
id: d.id,
|
|
15
|
+
siblings: d.siblings
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
window.addEventListener("updatefromsandbox", onSandboxWindowEvent);
|
|
20
|
+
const sandboxBundleSrc = new URL("/_kviewer/pdfjs/pdf.sandbox.mjs", window.location.origin).href;
|
|
21
|
+
const adapter = {
|
|
22
|
+
get currentPageNumber() {
|
|
23
|
+
return deps.viewerState.currentPage.value;
|
|
24
|
+
},
|
|
25
|
+
set currentPageNumber(n) {
|
|
26
|
+
const clamped = Math.min(Math.max(1, n), deps.viewerState.totalPages.value);
|
|
27
|
+
deps.viewerState.scrollToPage(clamped);
|
|
28
|
+
},
|
|
29
|
+
get pagesCount() {
|
|
30
|
+
return deps.viewerState.totalPages.value;
|
|
31
|
+
},
|
|
32
|
+
// Pages are virtualized — there's no global "all pages loaded" gate
|
|
33
|
+
// analogous to the official viewer's `pagesPromise`. Resolve immediately;
|
|
34
|
+
// the manager only awaits it before dispatching 'print'.
|
|
35
|
+
pagesPromise: Promise.resolve(),
|
|
36
|
+
nextPage() {
|
|
37
|
+
const cur = deps.viewerState.currentPage.value;
|
|
38
|
+
if (cur < deps.viewerState.totalPages.value) deps.viewerState.scrollToPage(cur + 1);
|
|
39
|
+
},
|
|
40
|
+
previousPage() {
|
|
41
|
+
const cur = deps.viewerState.currentPage.value;
|
|
42
|
+
if (cur > 1) deps.viewerState.scrollToPage(cur - 1);
|
|
43
|
+
},
|
|
44
|
+
get currentScaleValue() {
|
|
45
|
+
return deps.viewerState.scale.value;
|
|
46
|
+
},
|
|
47
|
+
set currentScaleValue(v) {
|
|
48
|
+
const n = typeof v === "number" ? v : Number.parseFloat(v);
|
|
49
|
+
if (Number.isFinite(n)) deps.viewerState.setScale(n);
|
|
50
|
+
},
|
|
51
|
+
increaseScale() {
|
|
52
|
+
deps.viewerState.setScale(deps.viewerState.scale.value * 1.1);
|
|
53
|
+
},
|
|
54
|
+
decreaseScale() {
|
|
55
|
+
deps.viewerState.setScale(deps.viewerState.scale.value / 1.1);
|
|
56
|
+
},
|
|
57
|
+
spreadMode: 0,
|
|
58
|
+
isInPresentationMode: false,
|
|
59
|
+
isChangingPresentationMode: false,
|
|
60
|
+
getPageView(idx) {
|
|
61
|
+
const pageNumber = idx + 1;
|
|
62
|
+
const pdfPage = deps.proxyCache.getPageSync(pageNumber);
|
|
63
|
+
if (!pdfPage) return void 0;
|
|
64
|
+
return {
|
|
65
|
+
pdfPage,
|
|
66
|
+
renderingState: deps.virtualization.isPageRendered(pageNumber) ? RenderingStates.FINISHED : 0
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const manager = new PDFScriptingManager({
|
|
71
|
+
eventBus,
|
|
72
|
+
sandboxBundleSrc,
|
|
73
|
+
// Required: PDFScriptingManager passes the result into the sandbox's
|
|
74
|
+
// docInfo. The official viewer assembles a rich metadata blob; we
|
|
75
|
+
// provide the minimum the sandbox needs to boot. Fields are read by
|
|
76
|
+
// PDF scripts via `this.<key>` (e.g. `this.numPages`).
|
|
77
|
+
docProperties: async (pdfDoc) => ({
|
|
78
|
+
numPages: pdfDoc.numPages,
|
|
79
|
+
URL: "",
|
|
80
|
+
baseURL: "",
|
|
81
|
+
filesize: 0,
|
|
82
|
+
filename: "",
|
|
83
|
+
metadata: "",
|
|
84
|
+
authors: ""
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
manager.setViewer(adapter);
|
|
88
|
+
return {
|
|
89
|
+
dispatchFieldEvent(fieldId, name, value, extra) {
|
|
90
|
+
eventBus.dispatch("dispatcheventinsandbox", {
|
|
91
|
+
source: window,
|
|
92
|
+
detail: {
|
|
93
|
+
id: fieldId,
|
|
94
|
+
name,
|
|
95
|
+
value,
|
|
96
|
+
willCommit: true,
|
|
97
|
+
commitKey: 1,
|
|
98
|
+
...extra
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
notifyPageRendered(pageNumber) {
|
|
103
|
+
eventBus.dispatch("pagerendered", { source: window, pageNumber });
|
|
104
|
+
},
|
|
105
|
+
async setDocument(pdfDoc) {
|
|
106
|
+
await manager.setDocument(pdfDoc);
|
|
107
|
+
},
|
|
108
|
+
async destroy() {
|
|
109
|
+
window.removeEventListener("updatefromsandbox", onSandboxWindowEvent);
|
|
110
|
+
await manager.setDocument(null);
|
|
111
|
+
const dp = manager.destroyPromise;
|
|
112
|
+
if (dp) await dp;
|
|
113
|
+
},
|
|
114
|
+
isReady() {
|
|
115
|
+
return manager.ready;
|
|
116
|
+
},
|
|
117
|
+
// Expose for the bridge.
|
|
118
|
+
...{ _eventBus: eventBus }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function getEventBus(manager) {
|
|
122
|
+
return manager._eventBus;
|
|
123
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExportPdfOptions } from '../annotation/pdf-export/export.js';
|
|
2
|
+
import type { AddFormFieldPayload, FormFieldDefinition, FormFieldValue, IAnnotationStore } from '../annotation/engine/types.js';
|
|
3
|
+
import { type EventName, type EventPayloads, type ImportAnnotationsMode } from './protocol.js';
|
|
4
|
+
export interface KViewerEmbedClientOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Origin of the iframe — used for postMessage `targetOrigin` AND as
|
|
7
|
+
* the inbound origin allowlist (responses/events from any other origin
|
|
8
|
+
* are dropped). Pass `'*'` for development only; production should pin
|
|
9
|
+
* a single origin.
|
|
10
|
+
*/
|
|
11
|
+
iframeOrigin: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional override for the parent's window. Defaults to `window`.
|
|
14
|
+
* Useful for tests.
|
|
15
|
+
*/
|
|
16
|
+
windowRef?: Window;
|
|
17
|
+
}
|
|
18
|
+
type EventHandler<E extends EventName> = (payload: EventPayloads[E]) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Parent-page client that drives a `<KViewer>` instance running inside
|
|
21
|
+
* an iframe. Wraps `postMessage` into a Promise-based RPC mirroring the
|
|
22
|
+
* viewer's exposed methods, and surfaces iframe-emitted events
|
|
23
|
+
* (`ready`, `formEditMode-changed`) via `on()`.
|
|
24
|
+
*/
|
|
25
|
+
export declare class KViewerEmbedClient {
|
|
26
|
+
private readonly iframe;
|
|
27
|
+
private readonly options;
|
|
28
|
+
private readonly win;
|
|
29
|
+
private readonly allowAny;
|
|
30
|
+
private readonly pending;
|
|
31
|
+
private readonly handlers;
|
|
32
|
+
private disposed;
|
|
33
|
+
constructor(iframe: HTMLIFrameElement, options: KViewerEmbedClientOptions);
|
|
34
|
+
/** Tear down the message listener and reject any in-flight requests. */
|
|
35
|
+
dispose(): void;
|
|
36
|
+
/** Subscribe to an iframe-emitted event. Returns an unsubscribe fn. */
|
|
37
|
+
on<E extends EventName>(name: E, handler: EventHandler<E>): () => void;
|
|
38
|
+
getAnnotations(): Promise<IAnnotationStore[]>;
|
|
39
|
+
importAnnotations(annotations: IAnnotationStore[], options?: {
|
|
40
|
+
mode?: ImportAnnotationsMode;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
loaded: number;
|
|
43
|
+
skipped: number;
|
|
44
|
+
}>;
|
|
45
|
+
exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
|
|
46
|
+
getFormFieldValues(): Promise<FormFieldValue[]>;
|
|
47
|
+
setFormFieldValue(fieldName: string, value: string | boolean | string[]): Promise<void>;
|
|
48
|
+
addFormField(payload: AddFormFieldPayload): Promise<FormFieldDefinition>;
|
|
49
|
+
updateFormField(id: string, patch: Partial<FormFieldDefinition>): Promise<void>;
|
|
50
|
+
removeFormField(id: string): Promise<void>;
|
|
51
|
+
getFormFields(): Promise<FormFieldDefinition[]>;
|
|
52
|
+
getFormEditMode(): Promise<boolean>;
|
|
53
|
+
setFormEditMode(enabled: boolean): Promise<void>;
|
|
54
|
+
toggleFormEditMode(): Promise<boolean>;
|
|
55
|
+
private call;
|
|
56
|
+
private handleMessage;
|
|
57
|
+
}
|
|
58
|
+
export {};
|