1import { Controller } from "@hotwired/stimulus"; 2 3const VALID_SIZES = new Set(["normal", "compact", "flexible"]); 4 5export default class extends Controller { 6 static targets = ["widget", "status"]; 7 8 connect() { 9 this.loadStartedAt = Date.now(); 10 this.renderTurnstile(); 11 } 12 13 disconnect() { 14 if (this.widgetId !== undefined && window.turnstile) { 15 try { 16 window.turnstile.remove(this.widgetId); 17 } catch (e) { 18 // Widget might already be removed 19 } 20 } 21 22 this.widgetId = undefined; 23 clearTimeout(this.renderTimeout); 24 } 25 26 renderTurnstile() { 27 if (!this.element.isConnected || this.widgetId !== undefined) return; 28 29 if (typeof window.turnstile === "undefined") { 30 if (Date.now() - this.loadStartedAt > 10000) { 31 this.updateStatus( 32 "The security check is taking longer than expected. Check your connection or content blocker.", 33 "error", 34 ); 35 } 36 37 this.renderTimeout = setTimeout(() => this.renderTurnstile(), 250); 38 return; 39 } 40 41 const widget = this.widgetElement; 42 const sitekey = this.element.dataset.sitekey; 43 const theme = this.element.dataset.theme || "auto"; 44 const size = this.element.dataset.size; 45 const invisible = size === "invisible"; 46 const execution = this.element.dataset.execution || "render"; 47 const appearance = this.element.dataset.appearance || (invisible ? "interaction-only" : "always"); 48 49 if (invisible) { 50 widget.classList.add("turnstile-invisible"); 51 } 52 53 const options = { 54 sitekey, 55 theme, 56 execution, 57 appearance, 58 retry: this.element.dataset.retry || "auto", 59 "retry-interval": Number(this.element.dataset.retryInterval || 3000), 60 "refresh-expired": this.element.dataset.refreshExpired || "auto", 61 "refresh-timeout": this.element.dataset.refreshTimeout || "auto", 62 callback: (token) => { 63 this.token = token; 64 this.updateStatus("Security check complete.", "success"); 65 this.element.dispatchEvent( 66 new CustomEvent("turnstile:success", { 67 detail: { token }, 68 bubbles: true, 69 }), 70 ); 71 }, 72 "error-callback": (errorCode) => { 73 this.token = null; 74 this.updateStatus( 75 "The security check had a problem and is retrying automatically.", 76 "error", 77 ); 78 this.element.dispatchEvent( 79 new CustomEvent("turnstile:error", { 80 detail: { errorCode }, 81 bubbles: true, 82 }), 83 ); 84 return false; 85 }, 86 "expired-callback": () => { 87 this.token = null; 88 this.updateStatus("Security check expired. Refreshing it nowâ¦", "pending"); 89 this.element.dispatchEvent( 90 new CustomEvent("turnstile:expired", { 91 bubbles: true, 92 }), 93 ); 94 }, 95 "timeout-callback": () => { 96 this.token = null; 97 this.updateStatus("Security check timed out. Refreshing it nowâ¦", "pending"); 98 this.element.dispatchEvent( 99 new CustomEvent("turnstile:timeout", { 100 bubbles: true, 101 }), 102 ); 103 }, 104 }; 105 106 if (!invisible && size && VALID_SIZES.has(size)) { 107 options.size = size; 108 } 109 110 try { 111 this.widgetId = window.turnstile.render(widget, options); 112 } catch (e) { 113 console.error("Turnstile render error:", e); 114 this.updateStatus( 115 "The security check couldn't load. Refresh the page or disable your content blocker.", 116 "error", 117 ); 118 } 119 } 120 121 // Method to execute the challenge (for execution mode) 122 execute() { 123 if (this.widgetId !== undefined && window.turnstile) { 124 window.turnstile.execute(this.widgetElement); 125 } 126 } 127 128 // Method to get the current token 129 getToken() { 130 if (this.widgetId !== undefined && window.turnstile) { 131 return window.turnstile.getResponse(this.widgetId); 132 } 133 return null; 134 } 135 136 // Method to reset the widget if needed 137 reset() { 138 if (this.widgetId !== undefined && window.turnstile) { 139 window.turnstile.reset(this.widgetId); 140 this.token = null; 141 } 142 } 143 144 get widgetElement() { 145 return this.hasWidgetTarget ? this.widgetTarget : this.element; 146 } 147 148 updateStatus(message, state) { 149 if (!this.hasStatusTarget) return; 150 151 this.statusTarget.textContent = message; 152 this.statusTarget.dataset.state = state; 153 this.statusTarget.classList.toggle("text-red-600", state === "error"); 154 this.statusTarget.classList.toggle("dark:text-red-400", state === "error"); 155 this.statusTarget.classList.toggle("text-green-600", state === "success"); 156 this.statusTarget.classList.toggle("dark:text-green-400", state === "success"); 157 this.statusTarget.classList.toggle("text-neutral-500", state === "pending"); 158 this.statusTarget.classList.toggle("dark:text-neutral-400", state === "pending"); 159 } 160};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.