ngx-turnstile
Version:
Angular component for Cloudflare Turnstile
369 lines (360 loc) • 16.4 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, signal, computed, afterNextRender, PLATFORM_ID, Component, Inject, Input, Output, NgModule, forwardRef, Directive } from '@angular/core';
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
class NgxTurnstileService {
constructor() { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
const SCRIPT_ID = 'ngx-turnstile';
const CALLBACK_NAME = 'onloadTurnstileCallback';
// Every mounted component waiting for the Turnstile script to finish loading.
// A single global onload callback notifies all of them, so multiple widgets on
// the same page each get rendered (not just the last one that was created).
const scriptLoadListeners = new Set();
class NgxTurnstileComponent {
elementRef;
zone;
document;
platformId;
siteKey;
action;
cData;
theme = 'auto';
language = 'auto';
version = '0';
tabIndex;
appearance = 'always';
execution;
retry = 'auto';
retryInterval;
refreshExpired;
refreshTimeout;
size = 'normal';
responseField;
responseFieldName;
feedbackEnabled;
offlabelShowPrivacy;
offlabelShowHelp;
resolved = new EventEmitter();
errored = new EventEmitter();
beforeInteractive = new EventEmitter();
afterInteractive = new EventEmitter();
unsupported = new EventEmitter();
timeout = new EventEmitter();
widgetId = signal(undefined);
/** Whether the Cloudflare Turnstile script has finished loading. */
scriptLoaded = signal(false);
/** Whether a widget is currently rendered. Clients can watch this signal. */
widgetLoaded = computed(() => !!this.widgetId());
// Notifies this instance when the shared script finishes loading. Kept as a
// stable reference so it can be removed from the listener set on destroy.
onScriptLoad = () => this.zone.run(() => this.scriptLoaded.set(true));
constructor(elementRef, zone, document, platformId) {
this.elementRef = elementRef;
this.zone = zone;
this.document = document;
this.platformId = platformId;
// Touching `window` is only safe in the browser (skip during SSR).
if (isPlatformBrowser(this.platformId)) {
this.loadScript();
}
// Render once the host element exists (if the script is already loaded).
afterNextRender(() => {
if (this.scriptLoaded() && !this.widgetLoaded()) {
this.createWidget();
}
});
// Render once the script finishes loading (if not already rendered).
toObservable(this.scriptLoaded)
.pipe(takeUntilDestroyed())
.subscribe((scriptLoaded) => {
if (scriptLoaded && !this.widgetLoaded()) {
this.createWidget();
}
});
}
_getCloudflareTurnstileUrl() {
if (this.version === '0') {
return 'https://challenges.cloudflare.com/turnstile/v0/api.js';
}
throw 'Version not defined in ngx-turnstile component.';
}
loadScript() {
// Script already present (e.g. loaded by another widget or a prior route).
if (window.turnstile) {
this.scriptLoaded.set(true);
return;
}
// Register to be notified when the shared script finishes loading.
scriptLoadListeners.add(this.onScriptLoad);
// A single global callback fans out to every waiting instance.
window[CALLBACK_NAME] = () => {
// Copy then clear so a listener re-registering mid-notification is safe.
const listeners = Array.from(scriptLoadListeners);
scriptLoadListeners.clear();
listeners.forEach((listener) => listener());
};
// Only inject the script once, even with several widgets on the page.
const scriptPending = !!this.document.getElementById(SCRIPT_ID);
if (!scriptPending) {
const script = this.document.createElement('script');
script.src = `${this._getCloudflareTurnstileUrl()}?render=explicit&onload=${CALLBACK_NAME}`;
script.id = SCRIPT_ID;
script.async = true;
script.defer = true;
this.document.head.appendChild(script);
}
}
createWidget() {
// Only render once the script is loaded and the host element exists.
if (!this.scriptLoaded() || !this.elementRef?.nativeElement) {
return;
}
const turnstileOptions = {
sitekey: this.siteKey,
theme: this.theme,
language: this.language,
tabindex: this.tabIndex,
action: this.action,
cData: this.cData,
appearance: this.appearance,
retry: this.retry,
size: this.size,
callback: (token) => {
this.zone.run(() => this.resolved.emit(token));
},
'error-callback': (errorCode) => {
this.zone.run(() => this.errored.emit(errorCode));
// Returning false causes Turnstile to log error code as a console warning.
return false;
},
'expired-callback': () => {
this.zone.run(() => this.reset());
},
'before-interactive-callback': () => {
this.zone.run(() => this.beforeInteractive.emit());
},
'after-interactive-callback': () => {
this.zone.run(() => this.afterInteractive.emit());
},
'unsupported-callback': () => {
this.zone.run(() => this.unsupported.emit());
},
'timeout-callback': () => {
this.zone.run(() => this.timeout.emit());
},
};
// Only forward optional parameters when set, so we never override
// Cloudflare's defaults with an explicit `undefined`.
if (this.execution !== undefined) {
turnstileOptions.execution = this.execution;
}
if (this.retryInterval !== undefined) {
turnstileOptions['retry-interval'] = this.retryInterval;
}
if (this.refreshExpired !== undefined) {
turnstileOptions['refresh-expired'] = this.refreshExpired;
}
if (this.refreshTimeout !== undefined) {
turnstileOptions['refresh-timeout'] = this.refreshTimeout;
}
if (this.responseField !== undefined) {
turnstileOptions['response-field'] = this.responseField;
}
if (this.responseFieldName !== undefined) {
turnstileOptions['response-field-name'] = this.responseFieldName;
}
if (this.feedbackEnabled !== undefined) {
turnstileOptions['feedback-enabled'] = this.feedbackEnabled;
}
if (this.offlabelShowPrivacy !== undefined) {
turnstileOptions['offlabel-show-privacy'] = this.offlabelShowPrivacy;
}
if (this.offlabelShowHelp !== undefined) {
turnstileOptions['offlabel-show-help'] = this.offlabelShowHelp;
}
// Remove any existing widget so re-rendering doesn't create duplicates.
this.remove();
this.widgetId.set(window.turnstile.render(this.elementRef.nativeElement, turnstileOptions));
}
reset() {
if (this.widgetLoaded()) {
this.resolved.emit(null);
window.turnstile.reset(this.widgetId());
}
}
remove() {
if (this.widgetLoaded()) {
window.turnstile.remove(this.widgetId());
this.widgetId.set(undefined);
}
}
ngOnDestroy() {
scriptLoadListeners.delete(this.onScriptLoad);
this.remove();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: DOCUMENT }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: NgxTurnstileComponent, isStandalone: true, selector: "ngx-turnstile", inputs: { siteKey: "siteKey", action: "action", cData: "cData", theme: "theme", language: "language", version: "version", tabIndex: "tabIndex", appearance: "appearance", execution: "execution", retry: "retry", retryInterval: "retryInterval", refreshExpired: "refreshExpired", refreshTimeout: "refreshTimeout", size: "size", responseField: "responseField", responseFieldName: "responseFieldName", feedbackEnabled: "feedbackEnabled", offlabelShowPrivacy: "offlabelShowPrivacy", offlabelShowHelp: "offlabelShowHelp" }, outputs: { resolved: "resolved", errored: "errored", beforeInteractive: "beforeInteractive", afterInteractive: "afterInteractive", unsupported: "unsupported", timeout: "timeout" }, exportAs: ["ngx-turnstile"], ngImport: i0, template: ``, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileComponent, decorators: [{
type: Component,
args: [{
selector: 'ngx-turnstile',
template: ``,
exportAs: 'ngx-turnstile',
standalone: true,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: Document, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }], propDecorators: { siteKey: [{
type: Input
}], action: [{
type: Input
}], cData: [{
type: Input
}], theme: [{
type: Input
}], language: [{
type: Input
}], version: [{
type: Input
}], tabIndex: [{
type: Input
}], appearance: [{
type: Input
}], execution: [{
type: Input
}], retry: [{
type: Input
}], retryInterval: [{
type: Input
}], refreshExpired: [{
type: Input
}], refreshTimeout: [{
type: Input
}], size: [{
type: Input
}], responseField: [{
type: Input
}], responseFieldName: [{
type: Input
}], feedbackEnabled: [{
type: Input
}], offlabelShowPrivacy: [{
type: Input
}], offlabelShowHelp: [{
type: Input
}], resolved: [{
type: Output
}], errored: [{
type: Output
}], beforeInteractive: [{
type: Output
}], afterInteractive: [{
type: Output
}], unsupported: [{
type: Output
}], timeout: [{
type: Output
}] } });
class NgxTurnstileModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileModule, imports: [NgxTurnstileComponent], exports: [NgxTurnstileComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileModule, decorators: [{
type: NgModule,
args: [{
imports: [NgxTurnstileComponent],
exports: [NgxTurnstileComponent],
}]
}] });
class NgxTurnstileValueAccessorDirective {
turnstileComp;
onChange;
onTouched;
resolved = false;
constructor(turnstileComp) {
this.turnstileComp = turnstileComp;
}
ngOnInit() {
this.turnstileComp.resolved.subscribe((token) => {
this.resolved = !!token;
if (this.onChange) {
this.onChange(token);
}
if (this.onTouched) {
this.onTouched();
}
});
}
// Prevent form control from setting token value
writeValue(value) {
// reset turnstile component if form control sets the value after already receiving a valid token
if (this.resolved) {
this.resolved = false;
this.turnstileComp.reset();
}
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileValueAccessorDirective, deps: [{ token: NgxTurnstileComponent }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: NgxTurnstileValueAccessorDirective, selector: "ngx-turnstile[formControl], ngx-turnstile[formControlName], ngx-turnstile[ngModel]", providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxTurnstileValueAccessorDirective),
multi: true,
},
], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileValueAccessorDirective, decorators: [{
type: Directive,
args: [{
selector: 'ngx-turnstile[formControl], ngx-turnstile[formControlName], ngx-turnstile[ngModel]',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxTurnstileValueAccessorDirective),
multi: true,
},
],
}]
}], ctorParameters: () => [{ type: NgxTurnstileComponent }] });
class NgxTurnstileFormsModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileFormsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileFormsModule, declarations: [NgxTurnstileValueAccessorDirective], imports: [NgxTurnstileModule], exports: [NgxTurnstileValueAccessorDirective] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileFormsModule, imports: [NgxTurnstileModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgxTurnstileFormsModule, decorators: [{
type: NgModule,
args: [{
declarations: [NgxTurnstileValueAccessorDirective],
imports: [NgxTurnstileModule],
exports: [NgxTurnstileValueAccessorDirective],
}]
}] });
/*
* Public API Surface of ngx-turnstile
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxTurnstileComponent, NgxTurnstileFormsModule, NgxTurnstileModule, NgxTurnstileService, NgxTurnstileValueAccessorDirective };
//# sourceMappingURL=ngx-turnstile.mjs.map