UNPKG

ngx-turnstile

Version:

Angular component for Cloudflare Turnstile

369 lines (360 loc) 16.4 kB
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