Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 41 additions & 41 deletions resources/views/filament/forms/components/widget.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,38 @@
capture the token without blocking the auto-discovery flow.

--}}
@php
$isExplicitMode = $isExplicit();
$fieldId = $getId();
$statePath = $getStatePath();
$dataAttrs = $getDataAttributes();
$hasCustomCallback = isset($dataAttrs['callback']);
$customCallbackName = $dataAttrs['callback'] ?? null;

$renderOptions = [];
foreach ($dataAttrs as $key => $value) {
if ($key === 'field-name') {
$renderOptions['response-field-name'] = $value;
} elseif ($key !== 'callback') {
$renderOptions[$key] = $value;
}
}
@endphp
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<div
x-data="{
token: null,
token: $wire.entangle(@js($statePath)),
widgetId: null,
_removeHook: null,

/**
* Lifecycle
*/
init() {
@if($isExplicit())
this._mountExplicit();
@else
this._mountImplicit();
@endif
{{ $isExplicitMode ? 'this._mountExplicit();' : 'this._mountImplicit();' }}

// Allow external code (like after a failed submission) to reset
// the widget via a custom DOM event or a global window event.
Expand All @@ -66,7 +79,7 @@
// Scope the commit hook to our Livewire component only.
const wireEl = this.$el.closest('[wire\\:id]');
const wireId = wireEl ? wireEl.getAttribute('wire:id') : null;
const path = @js($getStatePath());
const path = @js($statePath);

if (wireId && window.Livewire) {
this._removeHook = Livewire.hook('commit', ({ component, commit }) => {
Expand Down Expand Up @@ -98,12 +111,9 @@
this._removeHook();
}

@unless ($isExplicit())
// Clean up implicit-mode global callbacks.
delete window['_turnstileToken_{{ $getId() }}'];
delete window['_turnstileExpired_{{ $getId() }}'];
delete window['_turnstileError_{{ $getId() }}'];
@endunless
{{ $isExplicitMode ? '' : "delete window['_turnstileToken_{$fieldId}'];
delete window['_turnstileExpired_{$fieldId}'];
delete window['_turnstileError_{$fieldId}'];" }}
},

/**
Expand All @@ -116,7 +126,7 @@
}

// 2. Identify the script tag
const script = document.querySelector('script[src*='turnstile/v0/api.js']');
const script = document.querySelector(`script[src*='turnstile/v0/api.js']`);

// If the script tag exists but turnstile isn't ready, listen for the load event.
if (script) {
Expand All @@ -125,7 +135,7 @@
// 3. Fallback: If the script isn't even in the DOM yet, observe the <head>
{
const observer = new MutationObserver((mutations, obs) => {
const addedScript = document.querySelector('script[src*='turnstile/v0/api.js']');
const addedScript = document.querySelector(`script[src*='turnstile/v0/api.js']`);

if (addedScript) {
addedScript.addEventListener('load', () => this._renderExplicit(), { once: true });
Expand All @@ -138,36 +148,26 @@
},

_renderExplicit() {
this.widgetId = turnstile.render(this.$refs.widget, {
{{-- --------------------------------------------------------
Forward all PHP-side data attributes as render options,
translating the internal 'field-name' key to Cloudflare's
'response-field-name' parameter so the hidden input that
Turnstile injects uses the correct name on non-Livewire
(traditional POST) form submissions as well.
--------------------------------------------------------- --}}
@foreach ($getDataAttributes() as $key => $value)
@if ($key === 'field-name')
'response-field-name': @js($value),
@elseif ($key === 'callback')
{{-- User-supplied extra callback; still call it after
we store the token in our local state. --}}
callback: (t) => {
this.token = t;
if (typeof window[@js($value)] === 'function') {
window[@js($value)](t);
}
},
@else
@js($key): @js($value),
@endif
@endforeach
@unless (isset($getDataAttributes()['callback']))
callback: (t) => { this.token = t; },
@endunless
'expired-callback': () => { this.token = null; this._reset(); },
'error-callback': () => { this.token = null; },
});
const opts = @js($renderOptions);
const customCb = {!! $hasCustomCallback ? \Illuminate\Support\Js::from($customCallbackName) : 'null' !!};

opts.callback = (t) => {
this.token = t;
if (customCb && typeof window[customCb] === 'function') {
window[customCb](t);
}
};
opts['expired-callback'] = () => { this.token = null; this._reset(); };
opts['error-callback'] = () => { this.token = null; };

this.widgetId = turnstile.render(this.$refs.widget, opts);
},

/**
Expand All @@ -178,9 +178,9 @@
// script can call once it has auto-discovered the .cf-turnstile
// div. Using the field's unique ID avoids collisions when
// multiple Turnstile widgets appear on the same page.
window['_turnstileToken_{{ $getId() }}'] = (t) => { this.token = t; };
window['_turnstileExpired_{{ $getId() }}'] = () => { this.token = null; this._reset(); };
window['_turnstileError_{{ $getId() }}'] = () => { this.token = null; };
window['_turnstileToken_{{ $fieldId }}'] = (t) => { this.token = t; };
window['_turnstileExpired_{{ $fieldId }}'] = () => { this.token = null; this._reset(); };
window['_turnstileError_{{ $fieldId }}'] = () => { this.token = null; };
},

// -----------------------------------------------------------------
Expand Down
19 changes: 10 additions & 9 deletions tests/Filament/Forms/TurnstileWidgetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,8 @@ public function test_explicit_mode_emits_sitekey_as_js_render_option(): void
public function test_explicit_mode_renders_default_alpine_callback_when_no_user_callback_is_set(): void
{
Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml('callback: (t) => { this.token = t; }');
->assertSeeHtml('opts.callback = (t) => {')
->assertSeeHtml('this.token = t');
}

// When a user callback IS configured, the default short form must NOT be
Expand Down Expand Up @@ -548,7 +549,7 @@ public function test_explicit_mode_maps_field_name_to_response_field_name_js_opt
];

Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml("'response-field-name'")
->assertSeeHtml('response-field-name')
->assertSeeHtml('my-captcha')
->assertDontSeeHtml('data-field-name');
}
Expand All @@ -571,7 +572,7 @@ public function test_explicit_mode_emits_flexible_size_as_js_render_option(): vo
];

Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml("'size': 'flexible'");
->assertSeeHtml('size\u0022:\u0022flexible');
}

public function test_explicit_mode_emits_dark_theme_as_js_render_option(): void
Expand All @@ -581,7 +582,7 @@ public function test_explicit_mode_emits_dark_theme_as_js_render_option(): void
];

Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml("'theme': 'dark'");
->assertSeeHtml('theme\u0022:\u0022dark');
}

public function test_explicit_mode_emits_language_as_js_render_option(): void
Expand All @@ -591,7 +592,7 @@ public function test_explicit_mode_emits_language_as_js_render_option(): void
];

Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml("'language': 'ja'");
->assertSeeHtml('language\u0022:\u0022ja');
}

public function test_explicit_mode_emits_action_as_js_render_option(): void
Expand All @@ -601,7 +602,7 @@ public function test_explicit_mode_emits_action_as_js_render_option(): void
];

Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml("'action': 'register'");
->assertSeeHtml('action\u0022:\u0022register');
}

public function test_explicit_mode_emits_tabindex_as_js_render_option(): void
Expand All @@ -611,7 +612,7 @@ public function test_explicit_mode_emits_tabindex_as_js_render_option(): void
];

Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml("'tabindex': 2");
->assertSeeHtml('tabindex\u0022:2');
}

public function test_explicit_mode_emits_expired_and_error_callbacks(): void
Expand Down Expand Up @@ -888,11 +889,11 @@ public function test_destroy_hook_removes_livewire_commit_listener(): void
->assertSeeHtml('_removeHook');
}

// Alpine's local token state must be initialized to null (not entangled).
// Alpine's local token state must be entangled with the Livewire state path.
public function test_alpine_token_state_is_initialised_to_null(): void
{
Livewire::test(TurnstileWidgetPage::class)
->assertSeeHtml('token: null');
->assertSeeHtml('token: $wire.entangle(');
}

/*
Expand Down
Loading