225 lines
7.5 KiB
TypeScript
225 lines
7.5 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// 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<number, PendingCallback>();
|
|
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<string> {
|
|
return new Promise<string>((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';
|
|
}
|