@@ -28,26 +28,28 @@ const VALID_CHANNELS = new Map([
2828] ) ;
2929
3030// Unified amount validation function
31- function verifyDonateInput ( amount , sponsor_channel , is_corporate , on_verified ) {
31+ function verifyDonateInput ( amount , sponsor_channel , is_corporate , trigger , on_verified ) {
3232 const channelSelect = document . getElementById ( "channel-options" ) ;
3333 const channel = channelSelect ? channelSelect . value : "" ;
3434
3535 const value = parseCurrency ( amount ) ;
3636
37+ const modal_options = { triggerEl : trigger } ;
38+
3739 // Empty check
3840 if ( ! amount || amount . trim ( ) === "" ) {
39- showModal ( "Missing Amount" , "Please input amount before confirming sponsorship." ) ;
41+ showModal ( "Missing Amount" , "Please input amount before confirming sponsorship." , modal_options ) ;
4042 return ;
4143 }
4244
4345 // Must be >= 1
4446 if ( isNaN ( value ) || value < 1 ) {
45- showModal ( "Invalid Amount" , "Please enter a valid amount greater than 0." ) ;
47+ showModal ( "Invalid Amount" , "Please enter a valid amount greater than 0." , modal_options ) ;
4648 return ;
4749 }
4850
4951 if ( ! VALID_CHANNELS . has ( sponsor_channel ) ) {
50- showModal ( "Invalid Channel" , `Unsupported sponsor channel: ${ sponsor_channel } ` ) ;
52+ showModal ( "Invalid Channel" , `Unsupported sponsor channel: ${ sponsor_channel } ` , modal_options ) ;
5153 return ;
5254 }
5355
@@ -58,13 +60,15 @@ function verifyDonateInput(amount, sponsor_channel, is_corporate, on_verified) {
5860
5961 const bodyEl = modalEl . querySelector ( ".modal-body" ) ;
6062 bodyEl . innerHTML = `
61- For corporate sponsorships <strong>≥ USD $${ INDIVIDUAL_MAX_AMOUNT } </strong>,
63+ For corporate sponsorships <strong>> USD$${ INDIVIDUAL_MAX_AMOUNT } </strong>,
6264 we recommend using <strong>Open Source Collective</strong> for transparency and compliance.<br>
6365 PayPal/GitHub are intended for individual backers and not suitable for large payments.
6466 ` ;
6567
68+ modalEl . dataset . triggerId = trigger . id ;
69+
6670 const modal = new bootstrap . Modal ( modalEl ) ;
67-
71+
6872 modalEl . querySelector ( ".btn-secondary" ) . onclick = ( ) => {
6973 modal . hide ( ) ;
7074 modalEl . addEventListener ( "hidden.bs.modal" , ( ) => {
@@ -87,7 +91,8 @@ function verifyDonateInput(amount, sponsor_channel, is_corporate, on_verified) {
8791 } else {
8892 showModal (
8993 "Limit Exceeded" ,
90- `Individual sponsorship cannot exceed USD $${ INDIVIDUAL_MAX_AMOUNT } . Please adjust your amount.`
94+ `Individual sponsorship cannot exceed USD$${ INDIVIDUAL_MAX_AMOUNT } . Please adjust your amount.` ,
95+ modal_options
9196 ) ;
9297 }
9398 return ;
@@ -175,25 +180,30 @@ const sandbox = window.location.host.includes('local.') || window.location.host.
175180
176181document . querySelector ( '#confirm-btn' ) . addEventListener ( 'click' , ( e ) => {
177182 e . preventDefault ( ) ;
183+
184+ // selected sponosr teir card
178185 const selected = document . querySelector ( 'input[name="sponsor"]:checked' ) ;
179- const isMonthly = document . getElementById ( 'monthly' ) . checked ;
180186
181187 if ( selected ) {
182188 const is_custom = selected . id === 'custom' ;
183189 const amount_str = is_custom
184190 ? document . querySelector ( '.amount-input' ) . value
185191 : selected . value ;
186192
193+ const is_monthly = is_custom
194+ ? document . querySelector ( 'input[name="custom-cycle"]:checked' ) . value === 'monthly'
195+ : true ;
196+
187197 const is_corporate = document . getElementById ( "btn-corporate-tiers" ) . classList . contains ( "active" ) ;
188198 const channel = document . getElementById ( "channel-options" ) . value ;
189- verifyDonateInput ( amount_str , channel , is_corporate , ( verified_channel ) => {
190- if ( ( window . cv_sel_amount !== amount_str ) || ( window . cv_is_mouthly !== isMonthly ) ) {
199+ verifyDonateInput ( amount_str , channel , is_corporate , e . currentTarget , ( verified_channel ) => {
200+ if ( ( window . cv_sel_amount !== amount_str ) || ( window . cv_is_mouthly !== is_monthly ) ) {
191201 window . cv_sel_amount = amount_str ;
192- window . cv_is_mouthly = isMonthly ;
202+ window . cv_is_mouthly = is_monthly ;
193203 window . cv_orderid = genOrderId ( ) ;
194204 }
195205
196- console . log ( `Amount: $${ amount_str } ${ isMonthly ? '/month' : '' } ` ) ;
206+ console . log ( `Amount: $${ amount_str } ${ is_monthly ? '/month' : '' } ` ) ;
197207
198208 const amount = parseCurrency ( amount_str ) ;
199209 const teir_info = is_corporate ? corporateTiers [ selected . id ] : individualTiers [ selected . id ] ;
@@ -205,39 +215,39 @@ document.querySelector('#confirm-btn').addEventListener('click', (e) => {
205215 form . attr ( 'action' , actionUrl ) ;
206216 form . children ( '#WIDprod' ) . attr ( 'value' , teir_info . prod_id ) ;
207217 form . children ( '#WIDout_trade_no' ) . attr ( 'value' , window . cv_orderid ) ;
208- form . children ( '#WIDmonthly' ) . attr ( 'value' , isMonthly ? '1' : '0' ) ;
218+ form . children ( '#WIDmonthly' ) . attr ( 'value' , is_monthly ? '1' : '0' ) ;
209219 form . children ( '#WIDamount' ) . attr ( 'value' , amount . toString ( ) ) ;
210220 form . submit ( ) ;
211221 }
212222 else if ( verified_channel == 'github' ) {
213- const gh_freq = isMonthly ? 'recurring' : 'one-time' ;
223+ const gh_freq = is_monthly ? 'recurring' : 'one-time' ;
214224 const actionUrl = `https://github.com/sponsors/axmolengine/sponsorships?preview=false&frequency=${ gh_freq } &amount=${ amount } ` ;
215225 window . open ( actionUrl , '_blank' ) ;
216226 }
217227 else if ( verified_channel == 'osc' ) {
218228 let actionUrl = '#' ;
219229 if ( is_corporate ) {
220- if ( isMonthly ) {
230+ if ( is_monthly ) {
221231 const osc_teir = teir_info . osc_teir ;
222232 isPresetTeir = true ;
223233 actionUrl = `https://opencollective.com/axmol/contribute/${ osc_teir } /checkout?interval=month&amount=${ amount } &contributeAs=me` ;
224234 }
225235 }
226236 else {
227- if ( isMonthly ) {
237+ if ( is_monthly ) {
228238 isPresetTeir = true ;
229239 actionUrl = `https://opencollective.com/axmol/contribute/backers-69887/checkout?interval=month&amount=${ amount } &contributeAs=me` ;
230240 }
231241 }
232242 if ( actionUrl == '#' ) { // means no preset teir, use osc custom card
233- const osc_interval = isMonthly ? 'month' : 'oneTime' ;
243+ const osc_interval = is_monthly ? 'month' : 'oneTime' ;
234244 actionUrl = `https://opencollective.com/axmol/donate?interval=${ osc_interval } &amount=${ amount } &contributeAs=me` ;
235245 }
236246 window . open ( actionUrl , '_blank' ) ;
237247 }
238248 } ) ;
239249 } else {
240- showModal ( "Missing Tier" , "Please select a tier before confirming sponsorship." ) ;
250+ showModal ( "Missing Tier" , "Please select a tier before confirming sponsorship." , { triggerEl : e . currentTarget } ) ;
241251 }
242252} ) ;
243253
@@ -490,7 +500,7 @@ document.addEventListener("DOMContentLoaded", function () {
490500 radio . dataset . prod_id = tier . prod_id ; // store sponsor id in data attribute
491501 radio . value = tier . amount ;
492502 titleEl . textContent = tier . title ;
493- priceEl . textContent = `USD$${ tier . amount } ` ;
503+ priceEl . textContent = `USD$${ tier . amount } /month ` ;
494504 } ) ;
495505
496506 // Step 3: restore previous selection if possible, otherwise select the first radio
@@ -537,6 +547,7 @@ document.addEventListener("DOMContentLoaded", function () {
537547 // Show Bootstrap modal warning
538548 const modalEl = document . getElementById ( "channelWarning" ) ;
539549 const modal = new bootstrap . Modal ( modalEl ) ;
550+ modalEl . dataset . triggerId = e . currentTarget . id ;
540551 const bodyEl = modalEl . querySelector ( ".modal-body" ) ;
541552 bodyEl . innerHTML = `
542553 For corporate sponsorship, we recommend using <strong>Open Source
@@ -548,21 +559,32 @@ document.addEventListener("DOMContentLoaded", function () {
548559 }
549560 } ) ;
550561
551- const modalEl = document . getElementById ( "channelWarning" ) ;
552- // update focus when hide
553- modalEl . addEventListener ( "hide.bs.modal" , ( ) => {
554- document . getElementById ( "confirm-btn" ) . focus ( ) ;
555- } ) ;
562+ // handle focus when modal dlgs hide
563+ const modalDlgs = [ 'channelWarning' , 'commonModal' ] ;
564+ modalDlgs . forEach ( name => {
565+ const modalEl = document . getElementById ( name ) ;
566+ if ( ! modalEl ) return ;
556567
557- // disable interaction when hidden
558- modalEl . addEventListener ( "hidden.bs.modal" , ( ) => {
559- modalEl . setAttribute ( "inert" , "" ) ;
560- } ) ;
568+ // Store the trigger element when modal is shown
569+ modalEl . addEventListener ( "show.bs.modal" , event => {
570+ // Bootstrap passes the trigger element in event.relatedTarget
571+ modalEl . removeAttribute ( "inert" ) ;
572+ } ) ;
561573
562- // enable interaction when show
563- modalEl . addEventListener ( "show.bs.modal" , ( ) => {
564- modalEl . removeAttribute ( "inert" ) ;
574+ // Restore focus to the trigger element when modal is about to hide
575+ modalEl . addEventListener ( "hide.bs.modal" , ( ) => {
576+ const triggerId = modalEl . dataset . triggerId ;
577+ if ( triggerId ) {
578+ document . getElementById ( triggerId ) ?. focus ( ) ;
579+ }
580+ } ) ;
581+
582+ // Disable interaction when modal is fully hidden
583+ modalEl . addEventListener ( "hidden.bs.modal" , ( ) => {
584+ modalEl . setAttribute ( "inert" , "" ) ;
585+ } ) ;
565586 } ) ;
587+
566588} ) ;
567589
568590// Helper function to switch back to OSC from modal
@@ -611,20 +633,15 @@ function showToast(message, type = "primary") {
611633
612634// Show a common modal with dynamic content
613635function showModal ( title , message , options = { } ) {
614- // Get modal elements
615636 const modalTitle = document . getElementById ( "commonModalTitle" ) ;
616637 const modalBody = document . getElementById ( "commonModalBody" ) ;
617638 const modalFooter = document . getElementById ( "commonModalFooter" ) ;
618639 const modalEl = document . getElementById ( "commonModal" ) ;
619640
620- // Update title and body
621641 modalTitle . textContent = title ;
622642 modalBody . innerHTML = message ;
623-
624- // Clear old footer buttons
625643 modalFooter . innerHTML = "" ;
626644
627- // Default button if no options provided
628645 if ( ! options . buttons || options . buttons . length === 0 ) {
629646 const defaultBtn = document . createElement ( "button" ) ;
630647 defaultBtn . type = "button" ;
@@ -633,7 +650,6 @@ function showModal(title, message, options = {}) {
633650 defaultBtn . textContent = "OK" ;
634651 modalFooter . appendChild ( defaultBtn ) ;
635652 } else {
636- // Create custom buttons
637653 options . buttons . forEach ( btn => {
638654 const buttonEl = document . createElement ( "button" ) ;
639655 buttonEl . type = "button" ;
@@ -649,7 +665,12 @@ function showModal(title, message, options = {}) {
649665 } ) ;
650666 }
651667
652- // Show modal
668+ // Store trigger element if provided
669+ if ( options . triggerEl ) {
670+ modalEl . dataset . triggerId = options . triggerEl . id || "" ;
671+ }
672+
653673 const modal = new bootstrap . Modal ( modalEl ) ;
654674 modal . show ( ) ;
655675}
676+
0 commit comments