@usefillo/dom 0.16.1 → 0.17.0
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/index.d.ts +13 -1
- package/dist/index.js +161 -4
- package/dist/standalone.global.js +1 -1
- package/dist/styles.css +9 -0
- package/dist/styles.unlayered.css +9 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Field, FieldValue, FormSchema, FilloClient, ResponseData, FormStatus as FormStatus$1, FormPage, Block, FilloRespondent, CodeForm, FormTheme, ChallengeConfig, FieldKind, FilloError, FormControllerOptions, FormController } from '@usefillo/core';
|
|
1
|
+
import { Field, FieldValue, FormSchema, FilloClient, ResponseData, FormStatus as FormStatus$1, FormPage, Block, FilloRespondent, CodeForm, FormTheme, ChallengeConfig, ChallengeTheme, FieldKind, FilloError, FormControllerOptions, FormController } from '@usefillo/core';
|
|
2
2
|
export { CodeForm, Field, FieldKind, FieldValue, FileValue, FilloClient, FilloClientOptions, FilloError, FormController, FormControllerOptions, FormControllerState, FormSchema, FormTheme, ProvisionWorkspaceResult, ResponseData, createClient, defineForm, provisionWorkspace } from '@usefillo/core';
|
|
3
3
|
|
|
4
4
|
type FormStatus = FormStatus$1 | "loading" | "closed";
|
|
@@ -59,6 +59,18 @@ interface RenderFormBaseOptions {
|
|
|
59
59
|
* hosted page does this). Wins over anything the server delivers.
|
|
60
60
|
*/
|
|
61
61
|
challenge?: ChallengeConfig;
|
|
62
|
+
/**
|
|
63
|
+
* Theme for the human-verification widget. Defaults to "auto" (the
|
|
64
|
+
* visitor's OS preference); apps with their own theme switch pass
|
|
65
|
+
* "light"/"dark" so the widget matches the surrounding form.
|
|
66
|
+
*/
|
|
67
|
+
challengeTheme?: ChallengeTheme;
|
|
68
|
+
/**
|
|
69
|
+
* Bridge-mode visibility for the human check. "interaction-only" (default)
|
|
70
|
+
* keeps it invisible unless Cloudflare needs the visitor to act; "always"
|
|
71
|
+
* shows the classic widget box the whole time.
|
|
72
|
+
*/
|
|
73
|
+
challengeAppearance?: "always" | "interaction-only";
|
|
62
74
|
className?: string;
|
|
63
75
|
components?: Partial<Record<FieldKind, FieldRenderer>>;
|
|
64
76
|
customComponents?: Record<string, FieldRenderer>;
|
package/dist/index.js
CHANGED
|
@@ -1758,6 +1758,8 @@ function defaultFieldRenderer(context) {
|
|
|
1758
1758
|
}
|
|
1759
1759
|
var TURNSTILE_SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
1760
1760
|
var TURNSTILE_SCRIPT_ID = "fillo-turnstile-script";
|
|
1761
|
+
var BRIDGE_READY_TIMEOUT_MS = 2e4;
|
|
1762
|
+
var CHALLENGE_CDATA_RE = /^[A-Za-z0-9_-]{1,255}$/;
|
|
1761
1763
|
var turnstileScriptPromise = null;
|
|
1762
1764
|
function turnstileGlobal() {
|
|
1763
1765
|
return globalThis.turnstile;
|
|
@@ -1883,6 +1885,15 @@ var DomFormController = class {
|
|
|
1883
1885
|
// Invalidated on teardown so a stale load/render callback can't land after
|
|
1884
1886
|
// a later attempt replaced the container (or double-render into it).
|
|
1885
1887
|
turnstileAttempt = 0;
|
|
1888
|
+
// Bridge mode (challenge.bridgeUrl): the Fillo-hosted iframe that runs the
|
|
1889
|
+
// widget on Fillo's OWN hostname (so the check works on any embedding
|
|
1890
|
+
// domain) plus its postMessage wiring. Mutually exclusive with the direct
|
|
1891
|
+
// Cloudflare-script fields above; teardownTurnstileWidget() clears both.
|
|
1892
|
+
challengeFrame = null;
|
|
1893
|
+
challengeFrameSrc = null;
|
|
1894
|
+
challengeFrameOrigin = null;
|
|
1895
|
+
challengeMessageHandler = null;
|
|
1896
|
+
challengeWatchdog = null;
|
|
1886
1897
|
// Clear-then-set (one frame apart) so an identical repeat still re-fires —
|
|
1887
1898
|
// a same-tick ""→text write can collapse into a no-op net change for AT.
|
|
1888
1899
|
announce = (text) => {
|
|
@@ -2503,6 +2514,142 @@ var DomFormController = class {
|
|
|
2503
2514
|
}
|
|
2504
2515
|
this.overlays.clear();
|
|
2505
2516
|
}
|
|
2517
|
+
/** Route the challenge to bridge or direct mode per the server's config.
|
|
2518
|
+
* Bridge (bridgeUrl present — every current server) works on any embedding
|
|
2519
|
+
* domain; direct is the pre-bridge fallback for older self-hosted servers.
|
|
2520
|
+
* Message contract mirrors @usefillo/react's turnstile module; keep both
|
|
2521
|
+
* in sync with apps/web src/app/embed/challenge/route.ts. */
|
|
2522
|
+
ensureChallengeWidget(challenge) {
|
|
2523
|
+
if (challenge.bridgeUrl) this.ensureChallengeBridge(challenge.bridgeUrl);
|
|
2524
|
+
else this.ensureTurnstileWidget(challenge.siteKey);
|
|
2525
|
+
}
|
|
2526
|
+
/** Idempotent per src: mounts the Fillo-hosted bridge iframe into the
|
|
2527
|
+
* persistent container and wires its postMessage token flow. */
|
|
2528
|
+
ensureChallengeBridge(bridgeUrl) {
|
|
2529
|
+
if (typeof window === "undefined") return;
|
|
2530
|
+
let url;
|
|
2531
|
+
try {
|
|
2532
|
+
url = new URL(bridgeUrl);
|
|
2533
|
+
} catch {
|
|
2534
|
+
this.handleChallengeError();
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
2538
|
+
this.handleChallengeError();
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
url.searchParams.set("origin", window.location.origin);
|
|
2542
|
+
url.searchParams.set("theme", this.options.challengeTheme ?? "auto");
|
|
2543
|
+
if (this.formId && CHALLENGE_CDATA_RE.test(this.formId)) {
|
|
2544
|
+
url.searchParams.set("cdata", this.formId);
|
|
2545
|
+
}
|
|
2546
|
+
url.searchParams.set(
|
|
2547
|
+
"appearance",
|
|
2548
|
+
(this.options.challengeAppearance ?? "interaction-only") === "interaction-only" ? "interaction-only" : "always"
|
|
2549
|
+
);
|
|
2550
|
+
const src = url.toString();
|
|
2551
|
+
if (this.challengeFrame && this.challengeFrameSrc === src) return;
|
|
2552
|
+
this.teardownTurnstileWidget();
|
|
2553
|
+
const attempt = ++this.turnstileAttempt;
|
|
2554
|
+
const frame = el("iframe", {
|
|
2555
|
+
className: "fillo-turnstile-frame",
|
|
2556
|
+
attrs: {
|
|
2557
|
+
src,
|
|
2558
|
+
title: "Human verification",
|
|
2559
|
+
// Cloudflare's normal widget footprint; attributes so host CSS wins.
|
|
2560
|
+
width: "300",
|
|
2561
|
+
height: "65",
|
|
2562
|
+
referrerpolicy: "no-referrer"
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
this.challengeFrame = frame;
|
|
2566
|
+
this.challengeFrameSrc = src;
|
|
2567
|
+
this.challengeFrameOrigin = url.origin;
|
|
2568
|
+
this.turnstileContainer.appendChild(frame);
|
|
2569
|
+
this.setChallengeFrameVisible(
|
|
2570
|
+
(this.options.challengeAppearance ?? "interaction-only") !== "interaction-only"
|
|
2571
|
+
);
|
|
2572
|
+
const onMessage = (event) => {
|
|
2573
|
+
if (attempt !== this.turnstileAttempt || this.destroyed) return;
|
|
2574
|
+
if (event.origin !== this.challengeFrameOrigin) return;
|
|
2575
|
+
if (!this.challengeFrame || event.source !== this.challengeFrame.contentWindow) return;
|
|
2576
|
+
const data = event.data;
|
|
2577
|
+
switch (data?.type) {
|
|
2578
|
+
case "fillo:challenge:ready":
|
|
2579
|
+
this.settleChallengeWatchdog();
|
|
2580
|
+
break;
|
|
2581
|
+
case "fillo:challenge:token":
|
|
2582
|
+
this.settleChallengeWatchdog();
|
|
2583
|
+
if (typeof data.token === "string" && data.token.length > 0) {
|
|
2584
|
+
this.handleChallengeToken(data.token);
|
|
2585
|
+
}
|
|
2586
|
+
if ((this.options.challengeAppearance ?? "interaction-only") === "interaction-only") {
|
|
2587
|
+
this.setChallengeFrameVisible(false);
|
|
2588
|
+
}
|
|
2589
|
+
break;
|
|
2590
|
+
case "fillo:challenge:interactive":
|
|
2591
|
+
this.setChallengeFrameVisible(true);
|
|
2592
|
+
break;
|
|
2593
|
+
case "fillo:challenge:interactive-done":
|
|
2594
|
+
if ((this.options.challengeAppearance ?? "interaction-only") === "interaction-only") {
|
|
2595
|
+
this.setChallengeFrameVisible(false);
|
|
2596
|
+
}
|
|
2597
|
+
break;
|
|
2598
|
+
case "fillo:challenge:expired":
|
|
2599
|
+
this.handleChallengeToken(void 0);
|
|
2600
|
+
break;
|
|
2601
|
+
case "fillo:challenge:error":
|
|
2602
|
+
this.settleChallengeWatchdog();
|
|
2603
|
+
this.handleChallengeToken(void 0);
|
|
2604
|
+
this.handleChallengeError();
|
|
2605
|
+
break;
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
this.challengeMessageHandler = onMessage;
|
|
2609
|
+
window.addEventListener("message", onMessage);
|
|
2610
|
+
this.challengeWatchdog = window.setTimeout(() => {
|
|
2611
|
+
if (attempt !== this.turnstileAttempt || this.destroyed) return;
|
|
2612
|
+
this.handleChallengeToken(void 0);
|
|
2613
|
+
this.handleChallengeError();
|
|
2614
|
+
}, BRIDGE_READY_TIMEOUT_MS);
|
|
2615
|
+
}
|
|
2616
|
+
/** Collapse/expand the bridge frame (interaction-only mode). The frame stays
|
|
2617
|
+
* mounted either way — height 0 keeps the invisible check running. */
|
|
2618
|
+
setChallengeFrameVisible(visible) {
|
|
2619
|
+
const frame = this.challengeFrame;
|
|
2620
|
+
if (!frame) return;
|
|
2621
|
+
frame.setAttribute("height", visible ? "65" : "0");
|
|
2622
|
+
if (visible) {
|
|
2623
|
+
frame.removeAttribute("aria-hidden");
|
|
2624
|
+
frame.removeAttribute("tabindex");
|
|
2625
|
+
} else {
|
|
2626
|
+
frame.setAttribute("aria-hidden", "true");
|
|
2627
|
+
frame.setAttribute("tabindex", "-1");
|
|
2628
|
+
}
|
|
2629
|
+
this.turnstileContainer.setAttribute(
|
|
2630
|
+
"data-fillo-challenge-visible",
|
|
2631
|
+
visible ? "true" : "false"
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
settleChallengeWatchdog() {
|
|
2635
|
+
if (this.challengeWatchdog !== null && typeof window !== "undefined") {
|
|
2636
|
+
window.clearTimeout(this.challengeWatchdog);
|
|
2637
|
+
}
|
|
2638
|
+
this.challengeWatchdog = null;
|
|
2639
|
+
}
|
|
2640
|
+
/** Unwire and remove the bridge frame (listener, watchdog, iframe). Safe to
|
|
2641
|
+
* call when nothing is mounted. */
|
|
2642
|
+
teardownChallengeBridge() {
|
|
2643
|
+
if (this.challengeMessageHandler && typeof window !== "undefined") {
|
|
2644
|
+
window.removeEventListener("message", this.challengeMessageHandler);
|
|
2645
|
+
}
|
|
2646
|
+
this.challengeMessageHandler = null;
|
|
2647
|
+
this.settleChallengeWatchdog();
|
|
2648
|
+
this.challengeFrame?.remove();
|
|
2649
|
+
this.challengeFrame = null;
|
|
2650
|
+
this.challengeFrameSrc = null;
|
|
2651
|
+
this.challengeFrameOrigin = null;
|
|
2652
|
+
}
|
|
2506
2653
|
/** Idempotent: at most one load+render per armed container (guarded by
|
|
2507
2654
|
* widget id / in-flight / already-failed); re-arms on a changed site key
|
|
2508
2655
|
* (mirrors react's effect, keyed on siteKey). */
|
|
@@ -2558,7 +2705,14 @@ var DomFormController = class {
|
|
|
2558
2705
|
* expiry, or timeout) — not a teardown, the widget stays mounted. */
|
|
2559
2706
|
resetChallengeWidget() {
|
|
2560
2707
|
this.challengeToken = void 0;
|
|
2561
|
-
if (this.
|
|
2708
|
+
if (this.challengeFrame && this.challengeFrameOrigin) {
|
|
2709
|
+
this.challengeFrame.contentWindow?.postMessage(
|
|
2710
|
+
{ type: "fillo:challenge:reset" },
|
|
2711
|
+
this.challengeFrameOrigin
|
|
2712
|
+
);
|
|
2713
|
+
} else if (this.turnstileWidgetId) {
|
|
2714
|
+
turnstileGlobal()?.reset(this.turnstileWidgetId);
|
|
2715
|
+
}
|
|
2562
2716
|
this.queueRender();
|
|
2563
2717
|
}
|
|
2564
2718
|
/** Fully remove any active/pending widget (mirrors react's unmount effect):
|
|
@@ -2566,8 +2720,11 @@ var DomFormController = class {
|
|
|
2566
2720
|
* fresh container replaces the old one for a pristine next mount. Safe to
|
|
2567
2721
|
* call when nothing is active. */
|
|
2568
2722
|
teardownTurnstileWidget() {
|
|
2569
|
-
if (this.turnstileWidgetId === null && !this.turnstileLoadInFlight)
|
|
2723
|
+
if (this.turnstileWidgetId === null && !this.turnstileLoadInFlight && this.challengeFrame === null) {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2570
2726
|
this.turnstileAttempt++;
|
|
2727
|
+
this.teardownChallengeBridge();
|
|
2571
2728
|
const api = turnstileGlobal();
|
|
2572
2729
|
if (this.turnstileWidgetId && api) {
|
|
2573
2730
|
try {
|
|
@@ -3039,8 +3196,8 @@ var DomFormController = class {
|
|
|
3039
3196
|
);
|
|
3040
3197
|
}
|
|
3041
3198
|
content.appendChild(slot);
|
|
3042
|
-
this.
|
|
3043
|
-
} else if (this.turnstileWidgetId !== null || this.turnstileLoadInFlight) {
|
|
3199
|
+
this.ensureChallengeWidget(this.challenge);
|
|
3200
|
+
} else if (this.turnstileWidgetId !== null || this.turnstileLoadInFlight || this.challengeFrame !== null) {
|
|
3044
3201
|
this.teardownTurnstileWidget();
|
|
3045
3202
|
}
|
|
3046
3203
|
const submitError = api.status === "error" ? "This form can't submit right now. Please try again in a moment." : this.engine?.getState().submitError;
|