Files
gh-jezweb-claude-skills-ski…/templates/turnstile-widget-explicit.ts
2025-11-30 08:24:31 +08:00

263 lines
6.5 KiB
TypeScript

/**
* Turnstile Widget - Explicit Rendering
*
* Use explicit rendering when you need programmatic control over:
* - When the widget renders
* - Widget lifecycle (reset, remove)
* - Multiple widgets on the same page
* - Dynamic UI / Single Page Applications
*/
declare const turnstile: {
render: (container: string | HTMLElement, options: TurnstileOptions) => string
reset: (widgetId: string) => void
remove: (widgetId: string) => void
execute: (widgetId: string) => void
getResponse: (widgetId: string) => string | undefined
isExpired: (widgetId: string) => boolean
}
interface TurnstileOptions {
sitekey: string
callback?: (token: string) => void
'error-callback'?: (error: string) => void
'expired-callback'?: () => void
'timeout-callback'?: () => void
theme?: 'light' | 'dark' | 'auto'
size?: 'normal' | 'flexible' | 'compact'
execution?: 'render' | 'execute'
appearance?: 'always' | 'execute' | 'interaction-only'
retry?: 'auto' | 'never'
'retry-interval'?: number
action?: string
cdata?: string
}
/**
* TurnstileManager - Lifecycle management wrapper
*/
export class TurnstileManager {
private widgetId: string | null = null
private sitekey: string
constructor(sitekey: string) {
this.sitekey = sitekey
}
/**
* Render the Turnstile widget
*/
render(
containerId: string,
callbacks: {
onSuccess: (token: string) => void
onError: (error: string) => void
onExpired?: () => void
},
options?: Partial<TurnstileOptions>
): string {
// Reset if already rendered
if (this.widgetId !== null) {
this.reset()
}
this.widgetId = turnstile.render(containerId, {
sitekey: this.sitekey,
callback: callbacks.onSuccess,
'error-callback': callbacks.onError,
'expired-callback': callbacks.onExpired || (() => this.reset()),
theme: options?.theme || 'auto',
size: options?.size || 'normal',
execution: options?.execution || 'render',
appearance: options?.appearance || 'always',
retry: options?.retry || 'auto',
action: options?.action,
cdata: options?.cdata,
})
return this.widgetId
}
/**
* Reset the widget (clears current state)
*/
reset(): void {
if (this.widgetId !== null) {
turnstile.reset(this.widgetId)
}
}
/**
* Remove the widget completely
*/
remove(): void {
if (this.widgetId !== null) {
turnstile.remove(this.widgetId)
this.widgetId = null
}
}
/**
* Manually trigger challenge (execution: 'execute' mode only)
*/
execute(): void {
if (this.widgetId !== null) {
turnstile.execute(this.widgetId)
}
}
/**
* Get current token
*/
getToken(): string | undefined {
if (this.widgetId === null) return undefined
return turnstile.getResponse(this.widgetId)
}
/**
* Check if token is expired
*/
isExpired(): boolean {
if (this.widgetId === null) return true
return turnstile.isExpired(this.widgetId)
}
}
/**
* Usage Example
*/
export function initializeTurnstile() {
const SITE_KEY = 'YOUR_SITE_KEY' // Replace with actual sitekey
const manager = new TurnstileManager(SITE_KEY)
// Render widget when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
manager.render(
'#turnstile-container',
{
onSuccess: (token) => {
console.log('Turnstile success:', token)
// Enable submit button
const submitBtn = document.querySelector('#submit-btn') as HTMLButtonElement
if (submitBtn) submitBtn.disabled = false
},
onError: (error) => {
console.error('Turnstile error:', error)
// Show error message
const errorDiv = document.querySelector('#error-message')
if (errorDiv) {
errorDiv.textContent = 'Verification failed. Please try again.'
}
},
onExpired: () => {
console.warn('Turnstile token expired')
// Disable submit button
const submitBtn = document.querySelector('#submit-btn') as HTMLButtonElement
if (submitBtn) submitBtn.disabled = true
},
},
{
theme: 'auto',
size: 'normal',
action: 'login', // Optional: track action in analytics
}
)
})
// Example: Reset on form submission
const form = document.querySelector('#myForm') as HTMLFormElement
form?.addEventListener('submit', async (e) => {
e.preventDefault()
const token = manager.getToken()
if (!token || manager.isExpired()) {
alert('Please complete the verification')
return
}
// Submit form with token
const formData = new FormData(form)
formData.append('cf-turnstile-response', token)
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: formData,
})
if (response.ok) {
alert('Success!')
form.reset()
manager.reset() // Reset Turnstile for next submission
} else {
alert('Submission failed')
}
} catch (error) {
console.error('Submission error:', error)
alert('Network error')
}
})
}
/**
* Advanced Example: Manual Execution Mode
*/
export function manualExecutionExample() {
const manager = new TurnstileManager('YOUR_SITE_KEY')
// Render in manual execution mode
manager.render(
'#turnstile-container',
{
onSuccess: (token) => {
console.log('Challenge complete:', token)
},
onError: (error) => {
console.error('Challenge failed:', error)
},
},
{
execution: 'execute', // Manual trigger
appearance: 'interaction-only', // Show only when needed
}
)
// Trigger challenge when user clicks submit
document.querySelector('#submit-btn')?.addEventListener('click', () => {
manager.execute()
})
}
/**
* Advanced Example: Multiple Widgets
*/
export function multipleWidgetsExample() {
const loginManager = new TurnstileManager('YOUR_SITE_KEY')
const signupManager = new TurnstileManager('YOUR_SITE_KEY')
// Login widget
loginManager.render(
'#login-turnstile',
{
onSuccess: (token) => console.log('Login token:', token),
onError: (error) => console.error('Login error:', error),
},
{
action: 'login',
}
)
// Signup widget
signupManager.render(
'#signup-turnstile',
{
onSuccess: (token) => console.log('Signup token:', token),
onError: (error) => console.error('Signup error:', error),
},
{
action: 'signup',
}
)
}