// --------------------------------------------------------------------------- // FileMaker global API // --------------------------------------------------------------------------- /** The FileMaker object injected by the runtime into web viewers. */ export interface FileMakerAPI { PerformScript(script: string, parameter?: string): void PerformScriptWithOption(script: string, parameter?: string, option?: ScriptOption): void } declare global { /** Available only inside a FileMaker web viewer. Always guard with `typeof FileMaker !== 'undefined'`. */ const FileMaker: FileMakerAPI | undefined interface Window { resolveFileMakerCallback: typeof resolveFileMakerCallback } } // --------------------------------------------------------------------------- // Script execution options // --------------------------------------------------------------------------- /** * Controls how a currently running FileMaker script is handled when a new * script is started via `FileMaker.PerformScriptWithOption`. * * | Value | Behaviour | * |-------|------------------------------------------------------------------| * | "0" | Pause the current script, run the new one, then resume | * | "1" | Abort the current script and run the new one | * | "2" | Exit the current script and run the new one | * | "3" | Run the new script concurrently (default async behaviour) | * | "4" | Trigger script (same as 3 but fires as a script trigger would) | * | "5" | Suspend the current script; resume it after the new one exits | */ export type ScriptOption = '0' | '1' | '2' | '3' | '4' | '5'; // --------------------------------------------------------------------------- // Helper utilities // --------------------------------------------------------------------------- /** * Type-safe wrapper that calls `FileMaker.PerformScript` only when the * runtime is available (i.e. the page is running inside a web viewer). * * @returns `true` if the script was dispatched, `false` otherwise. * * @example * performScript("Delete Record", String(recordId)); */ export function performScript(script: string, parameter?: string): boolean { if (typeof FileMaker === 'undefined') return false; FileMaker.PerformScript(script, parameter); return true; } /** * Type-safe wrapper around `FileMaker.PerformScriptWithOption`. * * @returns `true` if the script was dispatched, `false` otherwise. */ export function performScriptWithOption( script: string, parameter?: string, option?: ScriptOption, ): boolean { if (typeof FileMaker === 'undefined') return false; FileMaker.PerformScriptWithOption(script, parameter, option); return true; } // --------------------------------------------------------------------------- // Callback-based async bridge // --------------------------------------------------------------------------- /** * A pending callback registered by {@link callFileMakerScript}. * @internal */ interface PendingCallback { resolve: (data: string) => void; reject: (error: FileMakerScriptError) => void; } /** Error thrown when a FileMaker script signals failure. */ export interface FileMakerScriptError { /** The callback ID that was active when the error occurred. */ callbackId: number; /** Optional error message forwarded from the FileMaker script. */ message?: string; } /** * Pending callbacks keyed by an auto-incrementing integer ID. * @internal */ const _pendingCallbacks = new Map(); let _nextCallbackId = 1; /** * The shape of the payload that `callFileMakerScript` wraps around the * caller-supplied parameter before sending it to FileMaker. * @internal */ interface WrappedPayload { callbackId: number; parameter: string; } /** * Invokes a FileMaker script and returns a `Promise` that resolves when * FileMaker calls back into JavaScript via {@link resolveFileMakerCallback}. * * **FileMaker side:** the script must eventually call * *Perform JavaScript in Web Viewer* targeting `resolveFileMakerCallback` * and pass back the same `callbackId` together with the result data. * * ``` * // FileMaker script (pseudocode) * Set Variable [$payload ; Value: Get(ScriptParameter)] * Set Variable [$id ; Value: JSONGetElement($payload; "callbackId")] * Set Variable [$result ; Value: \* … your work … *\] * Perform JavaScript in Web Viewer [ * Object Name: "MyWebViewer" * Function: "resolveFileMakerCallback" * Parameters: $id, $result * ] * ``` * * @param script - FileMaker script name. * @param parameter - Arbitrary string payload for the script. * @param timeout - Optional ms before the promise is rejected automatically * (defaults to no timeout). * * @example * const json = await callFileMakerScript("Get Customer", String(customerId)); * const customer = JSON.parse(json); */ export function callFileMakerScript( script: string, parameter = '', timeout?: number, ): Promise { return new Promise((resolve, reject) => { if (typeof FileMaker === 'undefined') { reject({ callbackId: -1, message: 'FileMaker runtime not available' }); return; } const callbackId = _nextCallbackId++; _pendingCallbacks.set(callbackId, { resolve, reject }); const payload: WrappedPayload = { callbackId, parameter }; FileMaker.PerformScript(script, JSON.stringify(payload)); if (timeout !== undefined) { setTimeout(() => { if (_pendingCallbacks.has(callbackId)) { _pendingCallbacks.delete(callbackId); reject({ callbackId, message: `Timed out after ${timeout}ms` }); } }, timeout); } }); } /** * Must be exposed as a global function so that FileMaker's * *Perform JavaScript in Web Viewer* script step can invoke it. * * Call this from your FileMaker script to resolve a promise created by * {@link callFileMakerScript}. * * @param callbackId - The numeric ID forwarded from the script parameter. * @param result - The string result to hand back to JavaScript. * @param isError - When truthy, the promise is rejected instead. * * @example * // Expose globally so FileMaker can reach it * (window as any).resolveFileMakerCallback = resolveFileMakerCallback; */ export function resolveFileMakerCallback(callbackId: string, result = '', isError?: boolean): void { const callbackNumId = parseInt(callbackId); const pending = _pendingCallbacks.get(callbackNumId); if (!pending) return; _pendingCallbacks.delete(callbackNumId); if (isError) { pending.reject({ callbackId: callbackNumId, message: result }); } else { pending.resolve(result); } } export function waitForFileMaker( callback: () => void, onError?: () => void, maxAttempts = 10, // 1 Sekunde bei 100ms Intervall attempt = 0, ) { if (isFileMakerEnvironment()) { callback(); } else if (attempt >= maxAttempts) { onError?.(); // Fehler-Callback aufrufen } else { setTimeout(() => waitForFileMaker(callback, onError, maxAttempts, attempt + 1), 100); } } // --------------------------------------------------------------------------- // Environment detection // --------------------------------------------------------------------------- /** * Returns `true` when the page is running inside a FileMaker web viewer. * * @example * if (isFileMakerEnvironment()) { * FileMaker!.PerformScript("On Load"); * } */ export function isFileMakerEnvironment(): boolean { return typeof FileMaker !== 'undefined'; }