@@ -7,32 +7,20 @@ export default class extends Controller {
77 static sourceUrl = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
88 static callbackFunctionName = '__turnstileLoadedCallback'
99
10- loadingState = undefined
11- loadingPromise = {
12- resolve : ( ) => { } ,
13- reject : ( ) => { }
14- }
15- widgetId = undefined
16-
17- initialize ( ) {
18- this . loadingState = typeof window . turnstile !== 'undefined' ? 'ready' : 'unloaded'
10+ // Shared across all instances so that multiple widgets on the same page
11+ // only ever inject one script tag and all resolve together.
12+ static loadingState = typeof window . turnstile !== 'undefined' ? 'ready' : 'unloaded'
13+ static pendingResolvers = [ ]
14+ static pendingRejectors = [ ]
1915
20- // This defines a global function that can be called by Turnstile once it
21- // has been loaded and ready. The callbackFunctionName name is used in the
22- // URL of the script in the loadTurnstile() function.
23- window [ this . constructor . callbackFunctionName ] = ( ) => {
24- this . loadingPromise . resolve ( )
25- this . loadingState = 'ready'
26- delete window [ this . constructor . callbackFunctionName ]
27- }
28- }
16+ widgetId = undefined
2917
3018 connect ( ) {
3119 if ( ! this . hasOptionsValue ) return
3220
3321 // This call waits for the promise returned by loadTurnstile to resolve
3422 // before trying to call render().
35- this . loadTurnstile ( )
23+ this . constructor . loadTurnstile ( )
3624 . then ( ( ) => { this . render ( ) } )
3725 . catch ( ( e ) => console . log ( e ) )
3826 }
@@ -52,37 +40,46 @@ export default class extends Controller {
5240 this . widgetId = window . turnstile . render ( this . containerTarget , this . optionsValue )
5341 }
5442
55- // This returns a promise that resolves when the loading has completed, or
56- // rejects if an error is raised during loading.
57- loadTurnstile ( ) {
43+ // Static so that all instances share a single script load. Returns a promise
44+ // that resolves when Turnstile is ready, or rejects if the script fails to load.
45+ static loadTurnstile ( ) {
46+ if ( this . loadingState === 'ready' ) {
47+ return Promise . resolve ( )
48+ }
49+
5850 if ( this . loadingState === 'unloaded' ) {
5951 this . loadingState = 'loading'
6052
53+ // This defines a global function that Turnstile calls once it is ready.
54+ // The name is embedded in the script URL via the onload parameter.
6155 // See https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget
62- const url = `${ this . constructor . sourceUrl } ?onload=${ this . constructor . callbackFunctionName } &render=explicit`
56+ window [ this . callbackFunctionName ] = ( ) => {
57+ this . loadingState = 'ready'
58+ delete window [ this . callbackFunctionName ]
59+ this . pendingResolvers . splice ( 0 ) . forEach ( resolve => resolve ( ) )
60+ this . pendingRejectors . splice ( 0 )
61+ }
62+
63+ const url = `${ this . sourceUrl } ?onload=${ this . callbackFunctionName } &render=explicit`
6364 const script = document . createElement ( 'script' )
6465 script . src = url
6566 script . async = true
6667 script . defer = true
6768
6869 script . addEventListener ( 'error' , ( ) => {
69- this . loadingPromise . reject ( 'Failed to load Turnstile.' )
70- delete window [ this . constructor . callbackFunctionName ]
70+ this . loadingState = 'unloaded'
71+ delete window [ this . callbackFunctionName ]
72+ this . pendingRejectors . splice ( 0 ) . forEach ( reject => reject ( 'Failed to load Turnstile.' ) )
73+ this . pendingResolvers . splice ( 0 )
7174 } )
7275
7376 document . head . appendChild ( script )
7477 }
7578
76- // Return a promise that we can resolve when the callback function is
77- // called by the Turnstile JS when it is ready.
79+ // Loading is already in progress — queue this caller alongside any others.
7880 return new Promise ( ( resolve , reject ) => {
79- this . loadingPromise = { resolve, reject }
80-
81- // If turnstile is already defined, resolve immediately.
82- if ( this . loadingState === 'ready' ) {
83- resolve ( )
84- delete window [ this . constructor . callbackFunctionName ]
85- }
81+ this . pendingResolvers . push ( resolve )
82+ this . pendingRejectors . push ( reject )
8683 } )
8784 }
8885}
0 commit comments