Component import from other project
All checks were successful
CI / test-and-build (push) Successful in 9m32s

This commit is contained in:
2026-03-24 09:50:34 +01:00
parent d08a148b7e
commit 6407ea531e
28 changed files with 4744 additions and 7 deletions

View File

@@ -0,0 +1,98 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import LimelightButton from './LimelightButton.vue';
describe('LimelightButton', () => {
it('renders slot content', () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Click me' } });
expect(wrapper.text()).toContain('Click me');
});
it('renders a <button> element', () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Test' } });
expect(wrapper.element.tagName).toBe('BUTTON');
});
it('applies the variant class', () => {
const wrapper = mount(LimelightButton, {
props: { variant: 'danger' },
slots: { default: 'Delete' },
});
expect(wrapper.classes()).toContain('btn--danger');
});
it.each(['primary', 'outline', 'ghost', 'danger'] as const)(
'applies btn--%s class for variant %s',
(variant) => {
const wrapper = mount(LimelightButton, {
props: { variant },
slots: { default: 'Btn' },
});
expect(wrapper.classes()).toContain(`btn--${variant}`);
},
);
it('is disabled when disabled prop is true', () => {
const wrapper = mount(LimelightButton, {
props: { disabled: true },
slots: { default: 'Disabled' },
});
expect((wrapper.element as HTMLButtonElement).disabled).toBe(true);
expect(wrapper.classes()).toContain('btn--disabled');
});
it('emits click when clicked', async () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Click' } });
await wrapper.trigger('click');
expect(wrapper.emitted('click')).toBeTruthy();
});
it('does not emit click when disabled', async () => {
const wrapper = mount(LimelightButton, {
props: { disabled: true },
slots: { default: 'Click' },
});
await wrapper.trigger('click');
// Native disabled button blocks the click event
expect((wrapper.element as HTMLButtonElement).disabled).toBe(true);
});
it('always has the btn base class', () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Btn' } });
expect(wrapper.classes()).toContain('btn');
});
it('has type="button" to prevent accidental form submission', () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Btn' } });
expect((wrapper.element as HTMLButtonElement).type).toBe('button');
});
it('does not add a variant class when variant is omitted', () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Btn' } });
const variantClasses = wrapper.classes().filter((c) => c.startsWith('btn--'));
expect(variantClasses).toEqual([]);
});
it('keeps the variant class when also disabled', () => {
const wrapper = mount(LimelightButton, {
props: { variant: 'primary', disabled: true },
slots: { default: 'Btn' },
});
expect(wrapper.classes()).toContain('btn--primary');
expect(wrapper.classes()).toContain('btn--disabled');
});
it('is not disabled by default', () => {
const wrapper = mount(LimelightButton, { slots: { default: 'Btn' } });
expect((wrapper.element as HTMLButtonElement).disabled).toBe(false);
expect(wrapper.classes()).not.toContain('btn--disabled');
});
it('renders HTML slot content', () => {
const wrapper = mount(LimelightButton, {
slots: { default: '<strong>Bold</strong>' },
});
expect(wrapper.find('strong').exists()).toBe(true);
expect(wrapper.find('strong').text()).toBe('Bold');
});
});

View File

@@ -0,0 +1,84 @@
<template>
<button
type="button"
class="btn"
:class="[variant && `btn--${variant}`, { 'btn--disabled': disabled }]"
:disabled="disabled"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'outline' | 'ghost' | 'danger'
disabled?: boolean
}>()
</script>
<style scoped>
@import 'tailwindcss';
.btn {
@apply h-10 text-sm;
padding: 0 1.5rem;
font-family: 'Barlow', sans-serif;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
border: none;
clip-path: polygon(8px 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%);
transition:
background 0.15s,
color 0.15s,
border-color 0.15s,
transform 0.1s;
@apply select-none;
}
.btn--primary {
background: var(--color-primary);
color: var(--color-secondary-dark);
}
.btn--primary:hover {
background: var(--color-primary-light);
transform: scaleX(1.03);
}
.btn--outline {
background: transparent;
color: var(--color-primary);
border: 1.5px solid var(--color-primary);
}
.btn--outline:hover {
background: var(--color-primary-dark);
transform: scaleX(1.03);
}
.btn--ghost {
background: transparent;
color: var(--color-secondary-light);
border: 1px solid var(--color-secondary-light);
}
.btn--ghost:hover {
color: var(--color-primary);
border-color: var(--color-primary);
transform: scaleX(1.03);
}
.btn--danger {
background: var(--color-red);
color: #fff;
}
.btn--danger:hover {
filter: brightness(1.2);
transform: scaleX(1.03);
}
.btn:active {
transform: scale(0.97);
}
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

2
src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { default as LimelightButton } from './components/LimelightButton.vue'
export * from './utils/webviewer'

82
src/utils/webviewer.d.ts vendored Normal file
View File

@@ -0,0 +1,82 @@
/**
* TypeScript definitions for the FileMaker WebViewer JavaScript API
*
* These bindings cover the `FileMaker` global object injected into web viewers
* by Claris FileMaker Pro / WebDirect (FileMaker 19+).
*
* @see https://help.claris.com/en/pro-help/content/scripting-javascript-in-web-viewers.html
*/
// ---------------------------------------------------------------------------
// Core FileMaker global namespace
// ---------------------------------------------------------------------------
/**
* The `FileMaker` object is automatically injected into every web viewer's
* JavaScript context by the FileMaker runtime. It is **not** available in
* ordinary browsers.
*
* ### Important notes
* - All `PerformScript*` calls are **asynchronous** FileMaker does not block
* JavaScript execution while the script runs.
* - The object is only available after the web page has **finished loading**.
* - The web viewer must have *"Allow JavaScript to perform FileMaker scripts"*
* enabled in its object settings.
* - In WebDirect the page source must use the `data:text/html,` MIME prefix
* (not `data:text/html; charset=UTF-8,`) for these calls to work.
*/
export interface FileMakerAPI {
/**
* Calls a FileMaker script by name.
*
* Runs asynchronously JavaScript does not wait for the script to finish
* and no return value is provided back to JavaScript.
*
* @param script - Name of the FileMaker script to execute (not
* case-sensitive).
* @param parameter - Optional string parameter accessible inside the script
* via `Get(ScriptParameter)`.
*
* @example
* FileMaker.PerformScript("Save Record", JSON.stringify({ id: 42 }));
*/
PerformScript(script: string, parameter?: string): void
/**
* Calls a FileMaker script by name with an explicit concurrency option.
*
* Behaves identically to `PerformScript` when `option` is `"0"` (pause
* current script).
*
* @param script - Name of the FileMaker script to execute.
* @param parameter - Optional string parameter for the script.
* @param option - How to handle any currently running script.
* See {@link ScriptOption} for the full table.
*
* @example
* // Run concurrently without disturbing existing scripts
* FileMaker.PerformScriptWithOption("Sync Data", "", "3");
*/
PerformScriptWithOption(script: string, parameter?: string, option?: ScriptOption): void
}
// ---------------------------------------------------------------------------
// Global augmentation
// ---------------------------------------------------------------------------
declare global {
/**
* Global `FileMaker` object injected by the FileMaker runtime.
*
* May be `undefined` when the page is loaded outside of a FileMaker web
* viewer (e.g. in a regular browser during development).
*
* Always guard access with a runtime check:
* ```ts
* if (typeof FileMaker !== "undefined") {
* FileMaker.PerformScript("My Script");
* }
* ```
*/
const FileMaker: FileMakerAPI | undefined
}

206
src/utils/webviewer.ts Normal file
View File

@@ -0,0 +1,206 @@
// ---------------------------------------------------------------------------
// 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';
}