kviewer 0.1.0 → 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 +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -1
- package/dist/runtime/components/Viewer.d.vue.ts +16 -0
- package/dist/runtime/components/Viewer.vue +43 -2
- package/dist/runtime/components/Viewer.vue.d.ts +16 -0
- 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 +14 -0
- package/dist/runtime/components/ViewerTabs.vue +5 -1
- package/dist/runtime/components/ViewerTabs.vue.d.ts +14 -0
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +1 -0
- package/dist/runtime/composables/useFormFields.d.ts +18 -0
- package/dist/runtime/composables/useFormFields.js +22 -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 +7 -0
- package/dist/runtime/public-types.js +3 -0
- package/dist/types.d.mts +1 -1
- package/package.json +1 -1
|
@@ -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 {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KVIEWER_EMBED_PROTOCOL_VERSION,
|
|
3
|
+
REQUEST_KIND,
|
|
4
|
+
isEvent,
|
|
5
|
+
isResponse
|
|
6
|
+
} from "./protocol.js";
|
|
7
|
+
let nextRequestId = 0;
|
|
8
|
+
function makeRequestId() {
|
|
9
|
+
nextRequestId += 1;
|
|
10
|
+
return `kvr-${Date.now().toString(36)}-${nextRequestId.toString(36)}`;
|
|
11
|
+
}
|
|
12
|
+
export class KViewerEmbedClient {
|
|
13
|
+
iframe;
|
|
14
|
+
options;
|
|
15
|
+
win;
|
|
16
|
+
allowAny;
|
|
17
|
+
pending = /* @__PURE__ */ new Map();
|
|
18
|
+
handlers = /* @__PURE__ */ new Map();
|
|
19
|
+
disposed = false;
|
|
20
|
+
constructor(iframe, options) {
|
|
21
|
+
this.iframe = iframe;
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.allowAny = options.iframeOrigin === "*";
|
|
24
|
+
const win = options.windowRef ?? (typeof window !== "undefined" ? window : void 0);
|
|
25
|
+
if (!win) {
|
|
26
|
+
throw new Error("KViewerEmbedClient requires a window (browser environment)");
|
|
27
|
+
}
|
|
28
|
+
this.win = win;
|
|
29
|
+
this.win.addEventListener("message", this.handleMessage);
|
|
30
|
+
}
|
|
31
|
+
/** Tear down the message listener and reject any in-flight requests. */
|
|
32
|
+
dispose() {
|
|
33
|
+
if (this.disposed) return;
|
|
34
|
+
this.disposed = true;
|
|
35
|
+
this.win.removeEventListener("message", this.handleMessage);
|
|
36
|
+
const err = new Error("KViewerEmbedClient was disposed");
|
|
37
|
+
for (const { reject } of this.pending.values()) reject(err);
|
|
38
|
+
this.pending.clear();
|
|
39
|
+
this.handlers.clear();
|
|
40
|
+
}
|
|
41
|
+
/** Subscribe to an iframe-emitted event. Returns an unsubscribe fn. */
|
|
42
|
+
on(name, handler) {
|
|
43
|
+
let set = this.handlers.get(name);
|
|
44
|
+
if (!set) {
|
|
45
|
+
set = /* @__PURE__ */ new Set();
|
|
46
|
+
this.handlers.set(name, set);
|
|
47
|
+
}
|
|
48
|
+
set.add(handler);
|
|
49
|
+
return () => {
|
|
50
|
+
set?.delete(handler);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// ── method mirrors ─────────────────────────────────────────────────────
|
|
54
|
+
getAnnotations() {
|
|
55
|
+
return this.call("getAnnotations");
|
|
56
|
+
}
|
|
57
|
+
importAnnotations(annotations, options) {
|
|
58
|
+
return this.call("importAnnotations", annotations, options);
|
|
59
|
+
}
|
|
60
|
+
exportPdf(options) {
|
|
61
|
+
return this.call("exportPdf", options);
|
|
62
|
+
}
|
|
63
|
+
getFormFieldValues() {
|
|
64
|
+
return this.call("getFormFieldValues");
|
|
65
|
+
}
|
|
66
|
+
setFormFieldValue(fieldName, value) {
|
|
67
|
+
return this.call("setFormFieldValue", fieldName, value);
|
|
68
|
+
}
|
|
69
|
+
addFormField(payload) {
|
|
70
|
+
return this.call("addFormField", payload);
|
|
71
|
+
}
|
|
72
|
+
updateFormField(id, patch) {
|
|
73
|
+
return this.call("updateFormField", id, patch);
|
|
74
|
+
}
|
|
75
|
+
removeFormField(id) {
|
|
76
|
+
return this.call("removeFormField", id);
|
|
77
|
+
}
|
|
78
|
+
getFormFields() {
|
|
79
|
+
return this.call("getFormFields");
|
|
80
|
+
}
|
|
81
|
+
getFormEditMode() {
|
|
82
|
+
return this.call("getFormEditMode");
|
|
83
|
+
}
|
|
84
|
+
setFormEditMode(enabled) {
|
|
85
|
+
return this.call("setFormEditMode", enabled);
|
|
86
|
+
}
|
|
87
|
+
toggleFormEditMode() {
|
|
88
|
+
return this.call("toggleFormEditMode");
|
|
89
|
+
}
|
|
90
|
+
// ── internals ──────────────────────────────────────────────────────────
|
|
91
|
+
call(method, ...args) {
|
|
92
|
+
if (this.disposed) {
|
|
93
|
+
return Promise.reject(new Error("KViewerEmbedClient was disposed"));
|
|
94
|
+
}
|
|
95
|
+
const target = this.iframe.contentWindow;
|
|
96
|
+
if (!target) {
|
|
97
|
+
return Promise.reject(new Error("iframe has no contentWindow"));
|
|
98
|
+
}
|
|
99
|
+
const id = makeRequestId();
|
|
100
|
+
const message = {
|
|
101
|
+
kind: REQUEST_KIND,
|
|
102
|
+
v: KVIEWER_EMBED_PROTOCOL_VERSION,
|
|
103
|
+
id,
|
|
104
|
+
method,
|
|
105
|
+
args
|
|
106
|
+
};
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
this.pending.set(id, {
|
|
109
|
+
resolve,
|
|
110
|
+
reject
|
|
111
|
+
});
|
|
112
|
+
target.postMessage(message, this.options.iframeOrigin);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
handleMessage = (event) => {
|
|
116
|
+
if (!this.allowAny && event.origin !== this.options.iframeOrigin) return;
|
|
117
|
+
if (event.source !== this.iframe.contentWindow) return;
|
|
118
|
+
if (isResponse(event.data)) {
|
|
119
|
+
const pending = this.pending.get(event.data.id);
|
|
120
|
+
if (!pending) return;
|
|
121
|
+
this.pending.delete(event.data.id);
|
|
122
|
+
if (event.data.ok) {
|
|
123
|
+
pending.resolve(event.data.result);
|
|
124
|
+
} else {
|
|
125
|
+
const err = new Error(event.data.error.message);
|
|
126
|
+
if (event.data.error.name) err.name = event.data.error.name;
|
|
127
|
+
pending.reject(err);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (isEvent(event.data)) {
|
|
132
|
+
const set = this.handlers.get(event.data.name);
|
|
133
|
+
if (!set) return;
|
|
134
|
+
for (const handler of set) {
|
|
135
|
+
try {
|
|
136
|
+
handler(event.data.payload);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error("[KViewerEmbedClient] event handler threw:", err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
import type { ExportPdfOptions } from '../annotation/pdf-export/export.js';
|
|
3
|
+
import type { AddFormFieldPayload, FormFieldDefinition, FormFieldValue, IAnnotationStore } from '../annotation/engine/types.js';
|
|
4
|
+
import { type MethodName } from './protocol.js';
|
|
5
|
+
/**
|
|
6
|
+
* Surface the bridge calls on the KViewer template ref. Mirrors the
|
|
7
|
+
* methods defined in `defineExpose` inside Viewer.vue. Kept as a
|
|
8
|
+
* structural type so a real `<KViewer>` ref satisfies it without any
|
|
9
|
+
* extra wiring at the call site.
|
|
10
|
+
*/
|
|
11
|
+
export interface KViewerApi {
|
|
12
|
+
getAnnotations: () => IAnnotationStore[];
|
|
13
|
+
importAnnotations: (annotations: IAnnotationStore[], options?: {
|
|
14
|
+
mode?: 'replace' | 'merge';
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
loaded: number;
|
|
17
|
+
skipped: number;
|
|
18
|
+
}>;
|
|
19
|
+
exportPdf: (options?: ExportPdfOptions) => Promise<Uint8Array>;
|
|
20
|
+
getFormFieldValues: () => FormFieldValue[];
|
|
21
|
+
setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
|
|
22
|
+
addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
|
|
23
|
+
updateFormField: (id: string, patch: Partial<FormFieldDefinition>) => void;
|
|
24
|
+
removeFormField: (id: string) => void;
|
|
25
|
+
getFormFields: () => FormFieldDefinition[];
|
|
26
|
+
formEditMode: Ref<boolean>;
|
|
27
|
+
setFormEditMode: (enabled: boolean) => void;
|
|
28
|
+
toggleFormEditMode: () => boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface KViewerEmbedBridgeOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Origin of the parent window that hosts this iframe. Used both as
|
|
33
|
+
* the inbound origin allowlist (anything else is dropped) and as the
|
|
34
|
+
* `targetOrigin` for outbound messages.
|
|
35
|
+
*
|
|
36
|
+
* Pass `'*'` to allow any origin. Demo / development only — there is
|
|
37
|
+
* no origin verification in that mode and `postMessage` will broadcast
|
|
38
|
+
* to whatever window happens to be the parent.
|
|
39
|
+
*/
|
|
40
|
+
parentOrigin: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional override for the window the host listens on. Defaults to
|
|
43
|
+
* `window`. Useful for tests.
|
|
44
|
+
*/
|
|
45
|
+
windowRef?: Window;
|
|
46
|
+
/**
|
|
47
|
+
* Optional override for the parent window receiver. Defaults to
|
|
48
|
+
* `window.parent`. The bridge falls back to the request's
|
|
49
|
+
* `event.source` for replies, so this is only needed for the unsolicited
|
|
50
|
+
* `ready` and `formEditMode-changed` events.
|
|
51
|
+
*/
|
|
52
|
+
parentWindow?: Window;
|
|
53
|
+
}
|
|
54
|
+
interface BridgeHandle {
|
|
55
|
+
/** Manually tear down listeners. Called automatically on scope dispose. */
|
|
56
|
+
unmount: () => void;
|
|
57
|
+
}
|
|
58
|
+
export declare function useKViewerEmbedBridge(viewerRef: Ref<KViewerApi | null | undefined>, options: KViewerEmbedBridgeOptions): BridgeHandle;
|
|
59
|
+
export type { MethodName };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { onScopeDispose, watch } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
EVENT_KIND,
|
|
4
|
+
KVIEWER_EMBED_PROTOCOL_VERSION,
|
|
5
|
+
RESPONSE_KIND,
|
|
6
|
+
isRequest
|
|
7
|
+
} from "./protocol.js";
|
|
8
|
+
export function useKViewerEmbedBridge(viewerRef, options) {
|
|
9
|
+
const allowAny = options.parentOrigin === "*";
|
|
10
|
+
const winMaybe = options.windowRef ?? (typeof window !== "undefined" ? window : void 0);
|
|
11
|
+
if (!winMaybe) {
|
|
12
|
+
return { unmount: () => {
|
|
13
|
+
} };
|
|
14
|
+
}
|
|
15
|
+
const win = winMaybe;
|
|
16
|
+
const parent = options.parentWindow ?? win.parent;
|
|
17
|
+
function originAllowed(origin) {
|
|
18
|
+
return allowAny || origin === options.parentOrigin;
|
|
19
|
+
}
|
|
20
|
+
function postEvent(name, payload) {
|
|
21
|
+
if (!parent || parent === win) return;
|
|
22
|
+
const message = {
|
|
23
|
+
kind: EVENT_KIND,
|
|
24
|
+
v: KVIEWER_EMBED_PROTOCOL_VERSION,
|
|
25
|
+
name,
|
|
26
|
+
payload
|
|
27
|
+
};
|
|
28
|
+
parent.postMessage(message, options.parentOrigin);
|
|
29
|
+
}
|
|
30
|
+
async function dispatch(req) {
|
|
31
|
+
const viewer = viewerRef.value;
|
|
32
|
+
if (!viewer) {
|
|
33
|
+
throw new Error("KViewer ref is not yet available");
|
|
34
|
+
}
|
|
35
|
+
switch (req.method) {
|
|
36
|
+
case "getAnnotations":
|
|
37
|
+
return viewer.getAnnotations();
|
|
38
|
+
case "importAnnotations": {
|
|
39
|
+
const [annotations, opts] = req.args;
|
|
40
|
+
return viewer.importAnnotations(annotations, opts);
|
|
41
|
+
}
|
|
42
|
+
case "exportPdf": {
|
|
43
|
+
const [opts] = req.args;
|
|
44
|
+
return viewer.exportPdf(opts);
|
|
45
|
+
}
|
|
46
|
+
case "getFormFieldValues":
|
|
47
|
+
return viewer.getFormFieldValues();
|
|
48
|
+
case "setFormFieldValue": {
|
|
49
|
+
const [name, value] = req.args;
|
|
50
|
+
viewer.setFormFieldValue(name, value);
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
case "addFormField": {
|
|
54
|
+
const [payload] = req.args;
|
|
55
|
+
return viewer.addFormField(payload);
|
|
56
|
+
}
|
|
57
|
+
case "updateFormField": {
|
|
58
|
+
const [id, patch] = req.args;
|
|
59
|
+
viewer.updateFormField(id, patch);
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
case "removeFormField": {
|
|
63
|
+
const [id] = req.args;
|
|
64
|
+
viewer.removeFormField(id);
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
case "getFormFields":
|
|
68
|
+
return viewer.getFormFields();
|
|
69
|
+
case "getFormEditMode":
|
|
70
|
+
return viewer.formEditMode.value;
|
|
71
|
+
case "setFormEditMode": {
|
|
72
|
+
const [enabled] = req.args;
|
|
73
|
+
viewer.setFormEditMode(enabled);
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
case "toggleFormEditMode":
|
|
77
|
+
return viewer.toggleFormEditMode();
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`Unknown KViewer embed method: ${req.method}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function handleMessage(event) {
|
|
83
|
+
if (!originAllowed(event.origin)) return;
|
|
84
|
+
if (!isRequest(event.data)) return;
|
|
85
|
+
const req = event.data;
|
|
86
|
+
let reply;
|
|
87
|
+
try {
|
|
88
|
+
const result = await dispatch(req);
|
|
89
|
+
reply = {
|
|
90
|
+
kind: RESPONSE_KIND,
|
|
91
|
+
v: KVIEWER_EMBED_PROTOCOL_VERSION,
|
|
92
|
+
id: req.id,
|
|
93
|
+
ok: true,
|
|
94
|
+
result
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
reply = {
|
|
98
|
+
kind: RESPONSE_KIND,
|
|
99
|
+
v: KVIEWER_EMBED_PROTOCOL_VERSION,
|
|
100
|
+
id: req.id,
|
|
101
|
+
ok: false,
|
|
102
|
+
error: {
|
|
103
|
+
message: err instanceof Error ? err.message : String(err),
|
|
104
|
+
name: err instanceof Error ? err.name : void 0
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const target = event.source ?? parent;
|
|
109
|
+
if (!target) return;
|
|
110
|
+
target.postMessage(reply, event.origin === "" ? options.parentOrigin : event.origin);
|
|
111
|
+
}
|
|
112
|
+
win.addEventListener("message", handleMessage);
|
|
113
|
+
let stopFormEditWatch = null;
|
|
114
|
+
const stopViewerWatch = watch(
|
|
115
|
+
viewerRef,
|
|
116
|
+
(viewer) => {
|
|
117
|
+
stopFormEditWatch?.();
|
|
118
|
+
stopFormEditWatch = null;
|
|
119
|
+
if (!viewer) return;
|
|
120
|
+
postEvent("ready", { protocolVersion: KVIEWER_EMBED_PROTOCOL_VERSION });
|
|
121
|
+
stopFormEditWatch = watch(
|
|
122
|
+
viewer.formEditMode,
|
|
123
|
+
(enabled) => postEvent("formEditMode-changed", { enabled })
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
{ immediate: true }
|
|
127
|
+
);
|
|
128
|
+
function unmount() {
|
|
129
|
+
win.removeEventListener("message", handleMessage);
|
|
130
|
+
stopViewerWatch();
|
|
131
|
+
stopFormEditWatch?.();
|
|
132
|
+
stopFormEditWatch = null;
|
|
133
|
+
}
|
|
134
|
+
onScopeDispose(unmount);
|
|
135
|
+
return { unmount };
|
|
136
|
+
}
|