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.
Files changed (45) hide show
  1. package/dist/module.d.mts +9 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +22 -2
  4. package/dist/runtime/annotation/engine/painter.d.ts +8 -1
  5. package/dist/runtime/annotation/engine/painter.js +11 -6
  6. package/dist/runtime/annotation/engine/tools/form-field.d.ts +6 -0
  7. package/dist/runtime/annotation/engine/tools/form-field.js +3 -1
  8. package/dist/runtime/annotation/engine/types.d.ts +64 -0
  9. package/dist/runtime/annotation/pdf-export/export-form-fields.js +102 -52
  10. package/dist/runtime/assets/kviewer.css +1 -1
  11. package/dist/runtime/components/PdfPage.vue +6 -1
  12. package/dist/runtime/components/Viewer.d.vue.ts +41 -1
  13. package/dist/runtime/components/Viewer.vue +84 -3
  14. package/dist/runtime/components/Viewer.vue.d.ts +41 -1
  15. package/dist/runtime/components/ViewerBar.d.vue.ts +7 -1
  16. package/dist/runtime/components/ViewerBar.vue +36 -0
  17. package/dist/runtime/components/ViewerBar.vue.d.ts +7 -1
  18. package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -6
  19. package/dist/runtime/components/ViewerTabs.vue +11 -2
  20. package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -6
  21. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +10 -2
  22. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +1 -1
  23. package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
  24. package/dist/runtime/composables/useAnnotationEngine.js +2 -1
  25. package/dist/runtime/composables/useFormFields.d.ts +40 -3
  26. package/dist/runtime/composables/useFormFields.js +101 -1
  27. package/dist/runtime/composables/useInertiaPanzoom.js +1 -0
  28. package/dist/runtime/composables/usePageProxyCache.d.ts +4 -0
  29. package/dist/runtime/composables/usePageProxyCache.js +4 -1
  30. package/dist/runtime/composables/useScriptingBridge.d.ts +29 -0
  31. package/dist/runtime/composables/useScriptingBridge.js +74 -0
  32. package/dist/runtime/composables/useScriptingManager.d.ts +65 -0
  33. package/dist/runtime/composables/useScriptingManager.js +123 -0
  34. package/dist/runtime/embed/bridge-client.d.ts +58 -0
  35. package/dist/runtime/embed/bridge-client.js +143 -0
  36. package/dist/runtime/embed/bridge-host.d.ts +59 -0
  37. package/dist/runtime/embed/bridge-host.js +136 -0
  38. package/dist/runtime/embed/protocol.d.ts +105 -0
  39. package/dist/runtime/embed/protocol.js +13 -0
  40. package/dist/runtime/menu-items.d.ts +34 -0
  41. package/dist/runtime/menu-items.js +0 -0
  42. package/dist/runtime/public-types.d.ts +8 -1
  43. package/dist/runtime/public-types.js +3 -0
  44. package/dist/types.d.mts +1 -1
  45. 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
- /** Update a placed field's rect, name, or flags. */
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 placed field and its value. */
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;
@@ -281,6 +281,7 @@ export function useInertiaPanzoom(options = {}) {
281
281
  }
282
282
  function onDown(e) {
283
283
  if (e.pointerType === "pen") return;
284
+ if (e.pointerType === "mouse") return;
284
285
  if (!gesturesEnabled) return;
285
286
  stopMomentum();
286
287
  setWillChangeActive(true);
@@ -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
- const cacheInstance = { setDocument, getPage, clear };
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 {};