PageSourceSearch

https://ihr.org/wp-content/plugins/charitable-pro/assets/js/charitable.js?ver=1.8.16.4

js ihr.org collected 2026-09-24 09:06:29 UTC 119,980 bytes, 3,057 lines download raw bytes

1CHARITABLE = window.CHARITABLE || {};
2
3/**
4 * Set up Donation_Form object.
5 */
6( function( exports, $ ) {
7
8    /**
9     * Donation_Form expects a jQuery this.form object.
10     */
11    var Donation_Form = function( form ) {
12
13        /**
14         * Array of errors.
15         *
16         * @access public
17         */
18        this.errors = [];
19
20        /**
21         * Pending processes array.
22         *
23         * @access public
24         */
25        this.pending_processes = [];
26
27        /**
28         * Form object.
29         *
30         * @access public
31         */
32        this.form = form;
33
34        /**
35         * Flag to allow processing to be paused (i.e. while something like Stripe is processing).
36         *
37         * @access public
38         */
39        this.pause_processing = false;
40
41        /**
42         * Flag to prevent the on_submit handler from sending multiple concurrent AJAX requests
43         *
44         * @access public
45         */
46        this.submit_processing = false;
47
48        /**
49         * Reference to the current AJAX request (for aborting on back navigation).
50         *
51         * @since 1.8.10.1
52         * @access public
53         */
54        this.current_xhr = null;
55
56        /**
57         * The donation amount.
58         *
59         * @access public
60         */
61        this.total = 0;
62
63        /**
64         * Whether to scroll to the top of the form after a submission with errors.
65         *
66         * @access private
67         */
68        this.prevent_scroll_to_top = false;
69
70        /**
71         * Reference to this object.
72         *
73         * @access private
74         */
75        var self = this;
76
77        /**
78         * Body element reference.
79         *
80         * @access private
81         */
82        var $body = $( 'body' );
83
84        /**
85         * Handle click of terms link.
86         *
87         * @access private
88         */
89        var on_click_terms = function() {
90            self.form.find( '.charitable-terms-text' ).addClass( 'active' );
91            return false;
92        };
93
94        /**
95         * Focus event handler on custom donation amount input field.
96         *
97         * @access private
98         */
99        var on_focus_custom_amount = function() {
100            $( this ).closest( 'li' ).trigger( 'click' ).find( 'input[name=donation_amount]' ).prop( 'checked', true ).trigger( 'change' );
101
102            self.form.off( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
103
104            $( this ).focus();
105
106            self.form.on( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
107        };
108
109        /**
110         * Trigger jQuery events to broadcast the change in donation amount.
111         *
112         * @since  1.6.19
113         *
114         * @access private
115         */
116        var trigger_amount_change_events = function() {
117            /* The chosen donation amount has changed. */
118            $body.trigger( 'charitable:form:amount:changed', self );
119
120            /* The overall donation amount has changed. */
121            $body.trigger( 'charitable:form:total:changed', self );
122        };
123
124        /**
125         * Focus event handler for changes to custom donation amount input field.
126         *
127         * @access private
128         */
129        var on_change_custom_donation_amount = function() {
130            var unformatted = self.unformat_amount( $( this ).val() );
131
132            if ( $.trim( unformatted ) > 0 ) {
133                $( this ).val( self.format_amount( unformatted ) );
134            }
135
136            trigger_amount_change_events();
137        };
138
139        /**
140         * Select a donation amount.
141         *
142         * @return  void
143         */
144        var on_select_donation_amount = function() {
145            var $li = $( this ).closest( 'li' );
146
147            // Already selected, quit early to prevent focus/change loop
148            if ( $li.hasClass( 'selected' ) ) {
149                return;
150            }
151
152            $li.parents( '.charitable-donation-form' ).find( '.donation-amount.selected' ).removeClass( 'selected' );
153
154            $li.addClass( 'selected' ).find( '[name=donation_amount]' ).prop( 'checked', true );
155
156            if ( $li.hasClass( 'custom-donation-amount' ) ) {
157                $li.find( 'input.custom-donation-input' ).focus();
158            }
159
160            trigger_amount_change_events();
161        };
162
163        /**
164         * Change event handler for payment gateway selector.
165         *
166         * @access private
167         */
168        var on_change_payment_gateway = function() {
169            var selectedGateway = $(this).val();
170
171            self.hide_inactive_payment_methods();
172            self.show_active_payment_methods( selectedGateway );
173
174            // Trigger resize event for Square gateway to fix iframe height issue
175            if ( selectedGateway === 'square' || selectedGateway === 'square_core' ) {
176                setTimeout(function() {
177                    window.dispatchEvent(new Event('resize'));
178                }, 100);
179            }
180        };
181
182        /**
183         * Event handler when the "Change" link is clicked for pre-filled donations.
184         */
185        var on_click_change_amount_link = function() {
186            $(this).parent().addClass( 'charitable-hidden' );
187            $('#charitable_stripe_express_checkout_field').hide();
188            $('.charitable-payment-divider').hide();
189        };
190
191        /**
192         * Submit event handler for donation form.
193         *
194         * @access private
195         */
196        var on_submit = function( event ) {
197            var helper = event.data.helper;
198
199            // Always prevent default form submission if using AJAX (prevents duplicate submissions)
200            if ( helper.form.data( 'use-ajax' ) === '1' || helper.form.data( 'use-ajax' ) === 1 ) {
201                event.preventDefault();
202                event.stopPropagation();
203                event.stopImmediatePropagation();
204            }
205
206            // If already processing, prevent duplicate submission
207            if ( helper.submit_processing ) {
208                event.preventDefault();
209                event.stopPropagation();
210                event.stopImmediatePropagation();
211                return false;
212            }
213
214            helper.submit_processing = true;
215
216            /* Display the processing spinner and hide the button */
217            helper.show_processing();
218
219            /* Validate the form submission before going further. */
220            if ( false === helper.validate() ) {
221                helper.cancel_processing();
222                return false;
223            }
224
225            /* If processing has been paused, return false now. */
226            if ( false !== helper.pause_processing ) {
227                return false;
228            }
229
230            /* If we're not using AJAX to process the donation further, return now. */
231            if ( 1 !== helper.form.data( 'use-ajax' ) ) {
232                return true;
233            }
234
235            /* Continue on to process the donation. */
236            maybe_process( helper, function() {
237                $body.trigger( 'charitable:form:process', helper );
238            } );
239
240            return false;
241        };
242
243        /**
244         * Check whether there are any asynchronous threads we need to wait for.
245         *
246         * If not, trigger processing. If there are, check again in 500ms.
247         */
248        var maybe_process = function( helper, callback ) {
249            if ( ! helper.waiting() ) {
250                /* Double-check that there are still no errors. */
251                if ( helper.get_errors().length > 0 ) {
252                    helper.cancel_processing();
253                } else {
254                    callback();
255                }
256
257                return;
258            }
259
260            setTimeout( maybe_process, 500, helper, callback );
261        }
262
263        /**
264         * Process the donation.
265         *
266         * This is a callback for the 'charitable:form:process' event. It's called
267         * after validation has taken place.
268         *
269         * @param   object Event
270         * @param   object Donation_Form
271         */
272        var process_donation = function( event, helper ) {
273            var form = helper.form;
274            var data;
275            var processSettings = {
276                type: "POST",
277                dataType: "json",
278                url: CHARITABLE_VARS.ajaxurl,
279                timeout: 0,
280                xhrFields: {
281                    withCredentials: true
282                }
283            };
284
285            // Check if form has file inputs
286            var hasFiles = form.find('input[type="file"]').length > 0;
287
288            if ( hasFiles ) {
289                // Use FormData for file uploads
290                var formData = new FormData( form[0] );
291
292                // Add action and form_action
293                formData.append( 'action', 'make_donation' );
294                var charitableAction = formData.get( 'charitable_action' );
295                if ( charitableAction ) {
296                    formData.append( 'form_action', charitableAction );
297                    formData.delete( 'charitable_action' );
298                }
299
300                processSettings.data = formData;
301                processSettings.processData = false;
302                processSettings.contentType = false;
303            } else {
304                // Use regular serialization for non-file forms
305                data = helper.get_data();
306
307                /* Cancel the default Charitable action, but pass it along as the form_action variable */
308                data.action = 'make_donation';
309                data.form_action = data.charitable_action;
310                delete data.charitable_action;
311
312                processSettings.data = data;
313            }
314
315            // Store the xhr object so we can abort it if needed
316            // @since 1.8.10.1 - Fix for stuck forms on back navigation
317            var xhr = $.ajax( processSettings );
318            helper.current_xhr = xhr;
319
320            xhr.done(function (response) {
321                $body.trigger( 'charitable:form:processed', [ response, helper ] );
322
323                if ( response.success ) {
324                    // Trigger our custom event for GA4 tracking
325                    $body.trigger( 'charitable:donation:success', [ response, helper ] );
326
327                    maybe_process( helper, function() {
328                        window.location.href = response.redirect_to;
329                    } );
330                }
331                else {
332                    helper.cancel_processing( response.errors );
333
334                    if ( response.donation_id ) {
335                        helper.set_donation_id( response.donation_id );
336                    }
337                }
338            }).fail(function (response, textStatus, errorThrown) {
339                if ( window.console && window.console.log ) {
340                    console.log( response );
341                }
342
343                helper.cancel_processing( [ CHARITABLE_VARS.error_unknown ] );
344            }).always(function() {
345                // Clear xhr reference when request completes
346                // @since 1.8.10.1 - Fix for stuck forms on back navigation
347                helper.current_xhr = null;
348            });
349
350            return false;
351        }
352
353        /**
354         * Set up event handlers for donation forms.
355         *
356         * @return  void
357         */
358        var init = function() {
359            // Store form instance on the form element first so campaign selector and other code can find it
360            // even if something later in init throws (e.g. when form is replaced via AJAX).
361            // @since 1.8.12.0 - Store instance for campaign selector and other AJAX reloads
362            self.form.data( 'charitable-donation-form', self );
363
364            // Init donation amount selection
365            self.form.on( 'click', '.donation-amount', on_select_donation_amount );
366            self.form.on( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
367            self.form.on( 'click', '.charitable-terms-link', on_click_terms );
368
369            // Init currency formatting
370            self.form.on( 'blur', '.custom-donation-input', on_change_custom_donation_amount );
371
372            // Handle pre-selected amounts from session
373            self.ensure_session_amount_selected();
374
375            self.form.find( '.donation-amount input:checked' ).each( function() {
376                $( this ).closest( 'li' ).addClass( 'selected' );
377            });
378
379            if ( self.get_all_payment_methods().length ) {
380                self.hide_inactive_payment_methods();
381
382                // Show the default gateway field immediately to prevent flash
383                var defaultGateway = self.get_payment_method();
384                if ( defaultGateway ) {
385                    self.show_active_payment_methods( defaultGateway );
386                }
387
388                self.form.on( 'change', '#charitable-gateway-selector input[name=gateway]', on_change_payment_gateway );
389
390                // Also listen for the custom event triggered by GatewayTabHandler (for dropdown items)
391                self.form.on( 'charitable:gateway:changed', function( event, gatewayId ) {
392                    // Find the radio button for this gateway and trigger the change
393                    var $radio = self.form.find( 'input[name=gateway][value="' + gatewayId + '"]' );
394                    if ( $radio.length ) {
395                        $radio.prop( 'checked', true );
396                        on_change_payment_gateway.call( $radio[0] );
397                    }
398                } );
399            }
400
401            self.form.on( 'click', '.change-donation', on_click_change_amount_link );
402
403            // Handle donation form submission
404            self.form.on( 'submit', {
405                helper : self,
406            }, on_submit );
407
408            // This only fires after the first form instance is initialized.
409            if ( false === CHARITABLE.forms_initialized ) {
410
411                // Process the donation on the 'charitable:form:process' event.
412                $body.on( 'charitable:form:process', process_donation );
413
414                $body.trigger( 'charitable:form:initialize', self );
415
416                CHARITABLE.forms_initialized = true;
417            }
418
419            $body.trigger( 'charitable:form:loaded', self );
420        }
421
422        init();
423    };
424
425    /**
426     * Ensure that session amounts are properly selected when the form loads.
427     *
428     * @since  1.8.7.5
429     *
430     * @return  void
431     */
432    Donation_Form.prototype.ensure_session_amount_selected = function() {
433        // Check if there's a "Your Donation Amount" section indicating a session amount
434        var $sessionAmount = this.form.find( '.charitable-your-donation-amount' );
435
436        if ( $sessionAmount.length ) {
437            // Find the amount from the session display
438            var amountText = $sessionAmount.find( 'strong' ).text();
439            var amount = accounting.unformat( amountText, CHARITABLE_VARS.currency_format_decimal_sep );
440
441            if ( amount > 0 ) {
442                // Look for a matching radio button
443                var $matchingRadio = this.form.find( 'input[name="donation_amount"][value="' + amount + '"]' );
444
445                if ( $matchingRadio.length ) {
446                    $matchingRadio.prop( 'checked', true );
447                    $matchingRadio.closest( 'li' ).addClass( 'selected' );
448                } else {
449                    // Check if it's a custom amount
450                    var $customRadio = this.form.find( 'input[name="donation_amount"][value="custom"]' );
451                    var $customInput = this.form.find( 'input.custom-donation-input' );
452
453                    if ( $customRadio.length && $customInput.length ) {
454                        $customRadio.prop( 'checked', true );
455                        $customRadio.closest( 'li' ).addClass( 'selected' );
456                        $customInput.val( amountText );
457                    }
458                }
459            }
460        }
461    };
462
463    /**
464     * Return a particular input field.
465     *
466     * @param   string The field name to retrieve.
467     * @return  object
468     */
469    Donation_Form.prototype.get_input = function( field ) {
470        return this.form.find( '[name=' + field + ']' );
471    }
472
473    /**
474     * Return the submitted email address.
475     *
476     * @return  string
477     */
478    Donation_Form.prototype.get_email = function() {
479        return this.form.find( '[name=email]' ).val();
480    };
481
482    /**
483     * Returns whether this is a recurring donation.
484     *
485     * @since  1.4.21
486     *
487     * @return boolean
488     */
489    Donation_Form.prototype.is_recurring_donation = function() {
490        var recurring = this.form.find( '[name=recurring_donation]:checked, [name=recurring_donation][type=hidden]' );
491
492        return recurring.length && 'once' !== recurring.val();
493    };
494
495    /**
496     * Get the chosen suggested amount.
497     *
498     * Note that when no option has been selected, or a custom donation amount has been added,
499     * this will return 0.
500     *
501     * @since   1.4.19
502     *
503     * @return  float
504     */
505    /**
506     * Get the suggested donation amount.
507     *
508     * @since   1.4.19
509     * @version 1.8.10.5 - Fixed session amount validation when donation options are hidden
510     *
511     * @return  float
512     */
513    Donation_Form.prototype.get_suggested_amount = function() {
514        // Check if this is a one-time donation to filter out recurring radio buttons.
515        // @version 1.8.11.3 - Also check hidden input for recurring_donation (recurring-only forms use hidden input)
516        var $recurring_checked = this.form.find( '[name=recurring_donation]:checked' );
517        var $recurring_hidden = this.form.find( 'input[type=hidden][name=recurring_donation]' );
518        var recurring_donation = $recurring_checked.val() || $recurring_hidden.val() || 'once';
519
520        var value = null;
521        var $checked = null;
522
523        // Debug logging for troubleshooting donation amount issues.
524        // Only logs when both CHARITABLE_VARS.debug and CHARITABLE_VARS.form_debug are true.
525        var debug_enabled = typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.debug && CHARITABLE_VARS.form_debug;
526
527        if ( debug_enabled ) {
528            console.log( '[Charitable Debug] get_suggested_amount() called' );
529            console.log( '[Charitable Debug] recurring_donation value:', recurring_donation );
530            console.log( '[Charitable Debug] $recurring_checked found:', $recurring_checked.length );
531            console.log( '[Charitable Debug] $recurring_hidden found:', $recurring_hidden.length, $recurring_hidden.val() );
532        }
533
534        // Check if we're in session state (user has pre-selected amount from session)
535        // @version 1.8.10.5 - Prioritize hidden input when in session state to fix validation bug
536        var $session_amount_display = this.form.find( '.charitable-your-donation-amount' );
537        var is_session_state = $session_amount_display.length > 0;
538
539        // If one-time donation, prioritize checking the active/visible donation options section.
540        if ( 'once' === recurring_donation ) {
541            // When in session state, prioritize hidden input first (it contains the session amount)
542            // @version 1.8.10.5 - Fix: Read hidden input first when session amount is present
543            if ( is_session_state ) {
544                var $hidden_input = this.form.find( 'input[type=hidden][name=donation_amount]' );
545
546                if ( $hidden_input.length > 0 ) {
547                    var hidden_value = $hidden_input.val();
548
549                    // Only use hidden input if it's a valid numeric value (not 'custom')
550                    if ( hidden_value && hidden_value !== 'custom' && hidden_value !== 'recurring-custom' ) {
551                        value = hidden_value;
552                    }
553                }
554            }
555
556            // If no value from hidden input (or not in session state), try to find checked radio buttons
557            if ( ! value || value === 'custom' || value === 'recurring-custom' ) {
558                // First, try to find checked radio buttons in the active/visible donation options section.
559                var $active_section = this.form.find( '.charitable-donation-options.active' );
560
561                if ( $active_section.length ) {
562                    var $all_in_active = $active_section.find( '[name=donation_amount]:checked' );
563
564                    $checked = $all_in_active.filter( function() {
565                        var $radio = $( this );
566                        // Exclude any recurring radio buttons (value="recurring-custom" or has data-recurring).
567                        return 'recurring-custom' !== $radio.val() && ! $radio.attr( 'data-recurring' );
568                    } );
569
570                    if ( $checked.length > 0 ) {
571                        value = $checked.first().val();
572                    }
573                }
574
575                // If nothing found in active section, fall back to searching entire form but filter properly.
576                // Prioritize checked radio buttons over hidden inputs.
577                if ( ! value || value === 'custom' || value === 'recurring-custom' ) {
578                    // First, search ONLY for checked radio buttons (prioritize these over hidden inputs)
579                    var $all_checked_radios = this.form.find( '[name=donation_amount]:checked' );
580
581                    $checked = $all_checked_radios.filter( function() {
582                        var $radio = $( this );
583                        // Exclude recurring donation radio buttons (those with data-recurring attribute or value="recurring-custom").
584                        // Also exclude if it's in the recurring donation options section (not active).
585                        var $closest_section = $radio.closest( '.charitable-donation-options' );
586                        var is_recurring_section = $closest_section.hasClass( 'charitable-recurring-donation-options' ) ||
587                                                   $closest_section.hasClass( 'recurring-donation-options' );
588                        var is_active_section = $closest_section.hasClass( 'active' );
589
590                        // Include if: not recurring value, not data-recurring attribute, and either in active section or not in recurring section.
591                        return 'recurring-custom' !== $radio.val() &&
592                               ! $radio.attr( 'data-recurring' ) &&
593                               ( is_active_section || ! is_recurring_section );
594                    } );
595
596                    if ( $checked.length > 0 ) {
597                        value = $checked.first().val();
598                    } else if ( ! is_session_state ) {
599                        // Only use hidden inputs as a last resort when:
600                        // 1. No checked radio buttons found, AND
601                        // 2. The donation options section is hidden (charitable-hidden class)
602                        // 3. NOT in session state (we already checked that above)
603                        var $donation_options = this.form.find( '.charitable-donation-options' );
604                        var is_options_hidden = $donation_options.length > 0 && $donation_options.hasClass( 'charitable-hidden' );
605
606                        if ( is_options_hidden ) {
607                            var $hidden_input_fallback = this.form.find( '
607input[type=hidden][name=donation_amount]' );
608
609                            if ( $hidden_input_fallback.length > 0 ) {
610                                var hidden_value_fallback = $hidden_input_fallback.val();
611                                // Only use hidden input if it's a valid numeric value (not 'custom')
612                                if ( hidden_value_fallback && hidden_value_fallback !== 'custom' && hidden_value_fallback !== 'recurring-custom' ) {
613                                    value = hidden_value_fallback;
614                                }
615                            }
616                        }
617                    }
618                }
619            }
620        } else {
621            // For recurring donations, prioritize checked radio buttons over hidden inputs.
622            if ( debug_enabled ) {
623                console.log( '[Charitable Debug] In RECURRING branch' );
624            }
625            var $all_checked_radios = this.form.find( '[name=donation_amount]:checked' );
626            if ( debug_enabled ) {
627                console.log( '[Charitable Debug] Checked radios found:', $all_checked_radios.length );
628            }
629
630            if ( $all_checked_radios.length > 0 ) {
631                $checked = $all_checked_radios;
632                value = $checked.first().val();
633                if ( debug_enabled ) {
634                    console.log( '[Charitable Debug] Using checked radio value:', value );
635                }
636            } else {
637                // No radio button is checked. Check for fallback hidden input first.
638                // @version 1.8.11.3 - Added support for donation_amount_fallback hidden input
639                var $fallback_input = this.form.find( 'input[type=hidden][name=donation_amount_fallback]' );
640                if ( debug_enabled ) {
641                    console.log( '[Charitable Debug] Fallback input found:', $fallback_input.length );
642                }
643                if ( $fallback_input.length > 0 ) {
644                    var fallback_value = $fallback_input.val();
645                    if ( debug_enabled ) {
646                        console.log( '[Charitable Debug] Fallback value:', fallback_value );
647                    }
648                    if ( fallback_value && fallback_value !== 'custom' && fallback_value !== 'recurring-custom' && parseFloat( fallback_value ) > 0 ) {
649                        value = fallback_value;
650                        $checked = $fallback_input;
651                        if ( debug_enabled ) {
652                            console.log( '[Charitable Debug] Using fallback value:', value );
653                        }
654                    }
655                }
656
657                // If no fallback, try hidden donation_amount input as last resort when options are hidden.
658                if ( ! value ) {
659                    if ( debug_enabled ) {
660                        console.log( '[Charitable Debug] No fallback value, checking hidden donation_amount' );
661                    }
662                    var $donation_options = this.form.find( '.charitable-donation-options' );
663                    var is_options_hidden = $donation_options.length > 0 && $donation_options.hasClass( 'charitable-hidden' );
664                    if ( debug_enabled ) {
665                        console.log( '[Charitable Debug] Options hidden:', is_options_hidden );
666                    }
667
668                    if ( is_options_hidden ) {
669                        var $hidden_input = this.form.find( 'input[type=hidden][name=donation_amount]' );
670                        if ( $hidden_input.length > 0 ) {
671                            var hidden_value = $hidden_input.val();
672                            if ( hidden_value && hidden_value !== 'custom' && hidden_value !== 'recurring-custom' ) {
673                                value = hidden_value;
674                                $checked = $hidden_input;
675                            }
676                        }
677                    }
678                }
679            }
680        }
681
682        // If no valid value found, return 0.
683        if ( ! value || value === 'custom' || value === 'recurring-custom' ) {
684            if ( debug_enabled ) {
685                console.log( '[Charitable Debug] No valid value found, returning 0. value was:', value );
686            }
687            return 0;
688        }
689
690        var amount = accounting.unformat( value, CHARITABLE_VARS.currency_format_decimal_sep );
691        if ( debug_enabled ) {
692            console.log( '[Charitable Debug] Unformatted amount:', amount, 'from value:', value );
693        }
694
695        // Handle NaN or invalid values - treat as 0.
696        if ( isNaN( amount ) || ! isFinite( amount ) ) {
697            if ( debug_enabled ) {
698                console.log( '[Charitable Debug] Amount is NaN or not finite, returning 0' );
699            }
700            amount = 0;
701        }
702
703        if ( debug_enabled ) {
704            console.log( '[Charitable Debug] FINAL get_suggested_amount returning:', amount );
705        }
706        return amount || 0;
707    }
708
709    /**
710     * Get the custom amount.
711     *
712     * Note that this will return 0 when no custom amount has been entered or a 0 has been entered.
713     *
714     * @since   1.4.19
715     *
716     * @return  float
717     */
718    Donation_Form.prototype.get_custom_amount = function() {
719        var input = this.form.find( '.charitable-donation-options.active input.custom-donation-input,.charitable-donation-options.active input.custom-donation-amount' );
720
721        if ( 0 === input.length ) {
722            input = this.form.find( 'input.custom-donation-input' );
723        }
724
725        // Check if this is a one-time donation to exclude recurring custom inputs.
726        // @version 1.8.13.1 - Also check hidden input for recurring_donation (recurring-only forms use hidden input, not a radio)
727        var $recurring_checked = this.form.find( '[name=recurring_donation]:checked' );
728        var $recurring_hidden  = this.form.find( 'input[type=hidden][name=recurring_donation]' );
729        var recurring_donation = $recurring_checked.val() || $recurring_hidden.val() || 'once';
730
731        // If one-time donation, exclude recurring custom donation inputs.
732        if ( 'once' === recurring_donation ) {
733            input = input.filter( function() {
734                var $input = $( this );
735                // Exclude recurring custom donation inputs (those with name="custom_recurring_donation_amount").
736                return 'custom_recurring_donation_amount' !== $input.attr( 'name' );
737            } );
738        }
739
740        var value = input.val();
741        var amount = accounting.unformat( value, CHARITABLE_VARS.currency_format_decimal_sep );
742
743
744        return amount;
745    }
746
747    /**
748     * Get the donation subtotal, which is the amount passed by the donor.
749     *
750     * Notably, this does not include any additional amounts that may be added onto the donation
751     * total, such as processing fees that the donor agrees to pay.
752     *
753     * @since  1.6.7
754     *
755     * @return float
756     */
757    Donation_Form.prototype.get_subtotal = function() {
758        var suggested = this.get_suggested_amount();
759        var custom    = this.get_custom_amount();
760
761        // If the custom amount radio is explicitly selected and a custom amount has been
762        // entered, use it directly. This prevents a lingering session amount (stored in a
763        // hidden input) from overriding a custom amount the donor has typed.
764        var $custom_radio = this.form.find( '[name=donation_amount][value="custom"]:checked' );
765        if ( $custom_radio.length && custom > 0 ) {
766            return custom;
767        }
768
769        var total = suggested || custom;
770
771        return total || 0;
772    };
773
774    /**
775     * Get the submitted amount, taking into account both the custom & suggested donation fields.
776     *
777     * @return  float
778     */
779    Donation_Form.prototype.get_amount = function() {
780        this.total = this.get_subtotal();
781
782        this.form.trigger( 'charitable:form:get_amount', this );
783
784        return this.total;
785    };
786
787    /**
788     * Get the submitted amount, taking into account both the custom & suggested donation fields.
789     *
790     * @since  1.6.8
791     *
792     * @param  float add
793     * @return void
794     */
795    Donation_Form.prototype.add_amount = function( add ) {
796        this.total += add;
797    };
798
799    /**
800     * Get the submitted amount, taking into account both the custom & suggested donation fields.
801     *
802     * @since  1.6.8
803     *
804     * @param  float remove Amount to be removed
805     * @return void
806     */
807    Donation_Form.prototype.remove_amount = function( remove ) {
808        this.total -= remove;
809    };
810
811    /**
812     * Get a description of the donation.
813     *
814     * @return  string
815     */
816    Donation_Form.prototype.get_description = function() {
817        return this.form.find( '[name=description]' ).val() || '';
818    };
819
820    /**
821     * Get credit card number.
822     *
823     * @return  string
824     */
825    Donation_Form.prototype.get_cc_number = function() {
826        return this.form.find( '#charitable_field_cc_number input' ).val() || '';
827    };
828
829    /**
830     * Get credit card CVC number.
831     *
832     * @return  string
833     */
834    Donation_Form.prototype.get_cc_cvc = function() {
835        return this.form.find( '#charitable_field_cc_cvc input' ).val() || '';
836    };
837
838    /**
839     * Get credit card expiry month.
840     *
841     * @return  string
842     */
843    Donation_Form.prototype.get_cc_expiry_month = function() {
844        return this.form.find( '#charitable_field_cc_expiration select.month' ).val() || '';
845    };
846
847    /**
848     * Get credit card expiry year.
849     *
850     * @return  string
851     */
852    Donation_Form.prototype.get_cc_expiry_year = function() {
853        return this.form.find( '#charitable_field_cc_expiration select.year' ).val() || '';
854    };
855
856    /**
857     * Clear credit card fields.
858
859     *
860     * This is used by gateways that create tokens through Javascript (such as Stripe), to
861     * avoid credit card details hitting the server.
862     *
863     * @return  void
864     */
865    Donation_Form.prototype.clear_cc_fields = function() {
866        this.form.find( '#charitable_field_cc_number input, #charitable_field_cc_name input, #charitable_field_cc_cvc input, #charitable_field_cc_expiration select' ).removeAttr( 'name' );
867    };
868
869    /**
870     * Return the selected payment method.
871     *
872     * @return  string
873     */
874    Donation_Form.prototype.get_payment_method = function() {
875        return this.form.find( '[type=hidden][name=gateway], [name=gateway]:checked' ).val() || '';
876    };
877
878    /**
879     * Return all payment methods.
880     *
881     * @return  object
882     */
883    Donation_Form.prototype.get_all_payment_methods = function() {
884        return this.form.find( '#charitable-gateway-selector input[name=gateway]' );
885    }
886
887    /**
888     * Hide inactive payment methods.
889     *
890     * @return  void
891     */
892    Donation_Form.prototype.hide_inactive_payment_methods = function() {
893        var active = this.get_payment_method();
894        var fields = this.form.find( '.charitable-gateway-fields[data-gateway!=' + active + ']' );
895
896        fields.hide();
897        fields.find( 'input[required],select[required],textarea[required]' ).attr( 'data-required', 1 ).attr( 'required', false );
898    };
899
900    /**
901     * Show active payment methods.
902     *
903     * @return  void
904     */
905    Donation_Form.prototype.show_active_payment_methods = function( active ) {
906        var active = active || this.get_payment_method();
907        var fields = this.form.find( '.charitable-gateway-fields[data-gateway=' + active + ']' );
908
909        fields.show();
910        fields.find( '[data-required]' ).attr( 'required', true );
911    };
912
913    /**
914     * Select a donation amount.
915     *
916     * @param   int price
917     * @param   string symbol
918     * @return  string
919     */
920    Donation_Form.prototype.format_amount = function( price, symbol ){
921        if ( typeof symbol === 'undefined' )
922            symbol = '';
923
924        return accounting.formatMoney( price, {
925                symbol : symbol,
926                decimal : CHARITABLE_VARS.currency_format_decimal_sep,
927                thousand: CHARITABLE_VARS.currency_format_thousand_sep,
928                precision : CHARITABLE_VARS.currency_format_num_decimals,
929                format: CHARITABLE_VARS.currency_format
930        }).trim();
931    };
932
933    /**
934     * Select a donation amount.
935     *
936     * @param   int price
937     * @return  string
938     */
939    Donation_Form.prototype.unformat_amount = function( price ) {
940        return Math.abs( parseFloat( accounting.unformat( price, CHARITABLE_VARS.currency_format_decimal_sep ) ) );
941    };
942
943    /**
944     * Add an error message.
945     *
946     * @param   string message
947     * @return  void
948     */
949    Donation_Form.prototype.add_error = function( message ) {
950        if (message && message.trim().length > 0) {
951            this.errors.push( message );
952        }
953    };
954
955    /**
956     * Return the errors.
957     *
958     * @return  []
959     */
960    Donation_Form.prototype.get_errors = function() {
961        return this.errors;
962    };
963
964    /**
965     * Prints out a nice string describing the errors.
966     *
967     * @return  string
968     */
969    Donation_Form.prototype.get_error_message = function() {
970        return this.errors.join( ' ' );
971    };
972
973    /**
974     * Print the errors at the top of the form.
975     *
976     * @param array
977     */
978    Donation_Form.prototype.print_errors = function( errors ) {
979        var e = errors || this.errors,
980            i = 0,
981            count = e.length,
982            output = '',
983            error_notice_donation_display = typeof CHARITABLE_VARS.error_notice_donation_display === 'undefined' ? 'top' : CHARITABLE_VARS.error_notice_donation_display;
984
985        // Filter out any empty error messages
986        e = e.filter(function(error) {
987            return error && error.trim().length > 0;
988        });
989
990        count = e.length;
991
992        if ( 0 === count ) {
993            return;
994        }
995
996        if ( this.form.find( '.charitable-form-errors' ).length ) {
997            this.form.find( '.charitable-form-errors' ).remove();
998        }
999
1000        output += '<div class="charitable-form-errors charitable-notice"><ul class="errors"><li>';
1001        output += e.join( '</li><li>' );
1002        output += '</li></ul></div>';
1003
1004        if ( error_notice_donation_display === 'bottom' ) {
1005            this.form.append( output );
1006        } else {
1007            this.form.prepend( output );
1008        }
1009
1010    }
1011
1012    /**
1013     * Clear the errors and remove the printed errors.
1014     */
1015    Donation_Form.prototype.clear_errors = function() {
1016        this.errors = [];
1017
1018        if ( this.form.find( '.charitable-form-errors' ).length ) {
1019            this.form.find( '.charitable-form-errors' ).remove();
1020        }
1021    }
1022
1023    /**
1024     * Return whether we are waiting for an asynchronous process to finish.
1025     *
1026     * @since  1.6.9
1027     *
1028     * @return boolean
1029     */
1030    Donation_Form.prototype.waiting = function() {
1031        return this.pending_processes.length > 0;
1032    }
1033
1034    /**
1035     * Add a pending process.
1036     *
1037     * @since  1.6.9
1038     *
1039     * @param  string Process identifier.
1040     * @return int Index of the process.
1041     */
1042    Donation_Form.prototype.add_pending_process = function( process ) {
1043        var index = this.pending_processes.indexOf( process );
1044        return -1 === index ? ( this.pending_processes.push( process ) - 1 ) : index;
1045    }
1046
1047    /**
1048     * Remove a pending process by index.
1049     *
1050     * @since  1.6.9
1051     *
1052     * @param  int Index of the process.
1053     * @return void
1054     */
1055    Donation_Form.prototype.remove_pending_process = function( index ) {
1056        this.pending_processes.splice( index, 1 );
1057    }
1058
1059    /**
1060     * Remove a pending process by process name.
1061     *
1062     * @since  1.6.17
1063     *
1064     * @param  string Name of the process.
1065     * @return void
1066     */
1067    Donation_Form.prototype.remove_pending_process_by_name = function( process ) {
1068        var index = this.pending_processes.indexOf( process );
1069        return -1 !== index && this.remove_pending_process( index );
1070    }
1071
1072    /**
1073     * Show that the donation form is processing.
1074     */
1075    Donation_Form.prototype.show_processing = function() {
1076        this.form.find( '.charitable-form-processing' ).show();
1077        this.form.find( 'button[name="donate"]' ).hide();
1078    }
1079
1080    /**
1081     * Hide the processing spinner and show the donate button.
1082     */
1083    Donation_Form.prototype.hide_processing = function() {
1084        this.form.find( '.charitable-form-processing' ).hide();
1085        this.form.find( 'button[name="donate"]' ).show();
1086
1087        this.submit_processing = false;
1088        this.pause_processing = false;
1089
1090        // Abort any pending AJAX requests
1091        // @since 1.8.10.1 - Fix for stuck forms on back navigation
1092        if ( this.current_xhr ) {
1093            this.current_xhr.abort();
1094            this.current_xhr = null;
1095        }
1096    }
1097
1098    /**
1099     * Reset form if it's stuck in processing state.
1100     * This is useful when the page is restored from bfcache or back navigation.
1101     *
1102     * @since 1.8.10.1
1103     * @return boolean True if form was reset, false otherwise.
1104     */
1105    Donation_Form.prototype.reset_if_stuck = function() {
1106        var $spinner = this.form.find( '.charitable-form-processing' );
1107        var $button = this.form.find( 'button[name="donate"]' );
1108
1109        // Check if form is stuck: spinner visible and button hidden
1110        var isStuck = ( $spinner.is( ':visible' ) || $spinner.css( 'display' ) !== 'none' ) &&
1111                      ( $button.is( ':hidden' ) || $button.css( 'display' ) === 'none' );
1112
1113        if ( isStuck ) {
1114            this.hide_processing();
1115            return true;
1116        }
1117
1118        return false;
1119    }
1120
1121    /**
1122     * Scroll to the top of the form.
1123     */
1124    Donation_Form.prototype.scroll_to_top = function() {
1125        if ( this.prevent_scroll_to_top ) {
1126            this.prevent_scroll_to_top = false;
1127            return;
1128        }
1129
1130        var $modal = this.form.parents( '.charitable-modal' );
1131
1132        if ( $modal.length ) {
1133            $modal.scrollTop( 0 );
1134        }
1135        else {
1136            window.scrollTo( this.form.offset().left, this.form.offset().top );
1137        }
1138    };
1139
1140    /**
1141     * Cancel donation processing.
1142     *
1143     * @param array errors Error messages to show.
1144     */
1145    Donation_Form.prototype.cancel_processing = function( errors ) {
1146        this.hide_processing();
1147        this.print_errors( errors );
1148        var error_notice_donation_display = typeof CHARITABLE_VARS.error_notice_donation_display === 'undefined' ? 'top' : CHARITABLE_VARS.error_notice_donation_display;
1149        if ( '' === error_notice_donation_display || 'top' === error_notice_donation_display ) {
1150            this.scroll_to_top();
1151        }
1152
1153        this.form.trigger( 'charitable:form:cancelled', this );
1154    };
1155
1156    /**
1157     * Set the donation ID.
1158     */
1159    Donation_Form.prototype.set_donation_id = function( donation_id ) {
1160        this.form.find( '[name=ID]' ).val( donation_id );
1161    }
1162
1163    /**
1164     * Returns all of the submitted data as key=>value object.
1165     *
1166     * @return  object
1167     */
1168    Donation_Form.prototype.get_data = function() {
1169        return this.form.serializeArray().reduce( function( obj, item ) {
1170            if ( '[]' === item.name.slice( -2 ) ) {
1171                var name = item.name.slice( 0, -2 );
1172
1173                if ( ! obj.hasOwnProperty( name ) ) {
1174                    obj[name] = [];
1175                }
1176
1177                obj[name].push( item.value );
1178            } else {
1179                obj[ item.name ] = item.value;
1180            }
1181
1182            return obj;
1183        }, {} );
1184
1185    };
1186
1187    /**
1188     * Returns all of the required fields.
1189     *
1190     * @return  object
1191     */
1192    Donation_Form.prototype.get_required_fields = function() {
1193        var fields = this.form.find( '.charitable-fieldset .required-field' ).not( '#charitable-gateway-fields .required-field' ),
1194            method = this.get_payment_method();
1195
1196        if ( '' !== method ) {
1197
1198            var gateway_fields = this.form.find( '[data-gateway=' + method + '] .required-field' );
1199
1200            if ( gateway_fields.length ) {
1201                fields = $.merge( fields, gateway_fields );
1202            }
1203
1204        }
1205
1206        return fields;
1207    };
1208
1209    /**
1210     * Check whether the submitted amount is greater than the maximum permitted.
1211     *
1212     * @returns boolean
1213     */
1214    Donation_Form.prototype.subtotal_exceeds_maximum = function() {
1215        var subtotal = this.get_subtotal();
1216        var maximum = CHARITABLE_VARS.maximum_donation;
1217
1218        // If maximum_donation is empty, null, undefined, or not a valid number, there's no maximum
1219        if ( '' === maximum || null === maximum || undefined === maximum ) {
1220            return false;
1221        }
1222
1223        var maximumFloat = parseFloat( maximum );
1224
1225        // If parsed value is NaN or 0 or negative, there's no valid maximum
1226        if ( isNaN( maximumFloat ) || maximumFloat <= 0 ) {
1227            return false;
1228        }
1229
1230        return subtotal > maximumFloat;
1231    }
1232
1233    /**
1234     * Make sure that the submitted amount is valid.
1235     *
1236     * @return boolean
1237     */
1238    Donation_Form.prototype.is_valid_amount = function() {
1239        var minimum = parseFloat( CHARITABLE_VARS.minimum_donation );
1240
1241        if ( this.subtotal_exceeds_maximum() ) {
1242            return false;
1243        }
1244
1245        return minimum > 0 || CHARITABLE_VARS.permit_0_donation
1246            ? this.get_subtotal() >= minimum
1247            : this.get_subtotal() > minimum;
1248    };
1249
1250    /**
1251     * Validate the amount. If not valid, set an error.
1252     *
1253     * @return  boolean
1254     */
1255    /**
1256     * Validate the amount. If not valid, set an error.
1257     *
1258     * @since   1.4.19
1259     * @version 1.8.10.5 - Fixed validation when session amount is present
1260     *
1261     * @return  boolean
1262     */
1263    Donation_Form.prototype.validate_amount = function() {
1264        var subtotal = this.get_subtotal();
1265        var minimum = parseFloat( CHARITABLE_VARS.minimum_donation );
1266        var isValid = this.is_valid_amount();
1267
1268        if ( false === isValid ) {
1269            if ( subtotal > 0 && this.subtotal_exceeds_maximum() && CHARITABLE_VARS.error_max_exceeded ) {
1270                this.add_error( CHARITABLE_VARS.error_max_exceeded );
1271            } else if ( CHARITABLE_VARS.error_invalid_amount ) {
1272                this.add_error( CHARITABLE_VARS.error_invalid_amount );
1273            }
1274            return false;
1275        }
1276
1277        return true;
1278    };
1279
1280    /**
1281     * Verify that all required fields are filled out.
1282     *
1283     * @return  boolean
1284     */
1285    Donation_Form.prototype.validate_required_fields = function() {
1286        var missing_fields = [];
1287
1288        // Check if this is a visual form (which supports conditional logic)
1289        var is_visual_form = this.form.hasClass('charitable-visual-donation-form') ||
1290                            this.form.closest('.charitable-visual-donation-form').length > 0;
1291
1292        this.get_required_fields().each( function( index ) {
1293            var $field = $( this );
1294
1295            // For visual forms only: Skip validation of hidden fields (conditional logic)
1296            if ( is_visual_form && !$field.is(':visible') ) {
1297                return true; // Continue to next field
1298            }
1299
1300            var $input = $field.find( 'input, select, textarea' );
1301            var input_type = $input.attr('type') || 'no-type';
1302            var input_name = $input.attr('name') || 'no-name';
1303            var input_value = $input.val() || '';
1304            var field_id = $field.attr('id') || 'no-id';
1305            var field_label = '';
1306
1307            // Get user-friendly field label
1308            var $label = $field.find('label');
1309            if ( $label.length > 0 ) {
1310                // Remove required asterisk and clean up the text
1311                field_label = $label.clone().find('.required').remove().end().text().trim();
1312            }
1313
1314            // Special handling for International Telephone Input (ITI) fields
1315            if ( $field.find('.iti__tel-input').length > 0 ) {
1316                // Find the actual ITI input element
1317                var $iti_input = $field.find('.iti__tel-input');
1318                $input = $iti_input; // Update the input reference
1319                input_type = $iti_input.attr('type') || 'tel';
1320                input_name = $iti_input.attr('name') || 'iti-field';
1321                input_value = $iti_input.val() || '';
1322            }
1323
1324            // Check if field is empty and collect missing field data
1325            var is_empty = false;
1326
1327            // Special handling for radio buttons (like donation amounts)
1328            if ( input_type === 'radio' && input_name === 'donation_amount' ) {
1329                var checked_radio = $('input[name="donation_amount"]:checked');
1330                var checked_value = checked_radio.val() || '';
1331
1332                // For radio buttons, check if ANY radio in the group is selected
1333                if ( checked_radio.length === 0 || checked_value === '' ) {
1334                    is_empty = true;
1335                    missing_fields.push({
1336                        field_id: field_id,
1337                        input_name: input_name,
1338                        input_type: input_type,
1339                        field_label: field_label || 'Donation Amount',
1340                        category: 'donation_amount'
1341                    });
1342                }
1343            } else {
1344                // Regular field validation
1345                if ( '' === input_value ) {
1346                    is_empty = true;
1347
1348                    // Determine field category for better messaging
1349                    var category = 'regular';
1350                    if ( input_type === 'email' ) {
1351                        category = 'email';
1352                    } else if ( input_type === 'tel' || input_name.indexOf('phone') !== -1 ) {
1353                        category = 'phone';
1354                    } else if ( input_name.indexOf('address') !== -1 || input_name.indexOf('city') !== -1 || input_name.indexOf('state') !== -1 || input_name.indexOf('postcode') !== -1 ) {
1355                        category = 'address';
1356                    } else if ( is_visual_form && $field.closest('.charitable-conditional-field').length > 0 ) {
1357                        category = 'conditional';
1358                    }
1359
1360                    missing_fields.push({
1361                        field_id: field_id,
1362                        input_name: input_name,
1363                        input_type: input_type,
1364                        field_label: field_label || input_name.replace(/_/g, ' ').replace(/\b\w/g, function(l){ return l.toUpperCase(); }),
1365                        category: category
1366                    });
1367                }
1368            }
1369        });
1370
1371        // Generate context-aware error message
1372        if ( missing_fields.length > 0 ) {
1373            var error_message = this.generate_missing_fields_message( missing_fields );
1374            this.add_error( error_message );
1375            return false;
1376        }
1377
1378        return true;
1379    };
1380
1381    /**
1382     * Generate context-aware error message for missing required fields.
1383     *
1384     * @param   array missing_fields Array of missing field objects
1385     * @return  string Error message
1386     */
1387    Donation_Form.prototype.generate_missing_fields_message = function( missing_fields ) {
1388        if ( missing_fields.length === 0 ) {
1389            return CHARITABLE_VARS.error_required_fields; // Fallback to original message
1390        }
1391
1392        // Separate donation amount from other fields
1393        var donation_amount_missing = [];
1394        var regular_fields_missing = [];
1395        var conditional_fields_missing = [];
1396
1397        missing_fields.forEach( function( field ) {
1398            if ( field.category === 'donation_amount' ) {
1399                donation_amount_missing.push( field );
1400            } else if ( field.category === 'conditional' ) {
1401                conditional_fields_missing.push( field );
1402            } else {
1403                regular_fields_missing.push( field );
1404            }
1405        });
1406
1407        var message_parts = [];
1408
1409        // Handle donation amount separately with special message
1410        if ( donation_amount_missing.length > 0 ) {
1411            message_parts.push( 'Please select a donation amount' );
1412        }
1413
1414        // Handle conditional fields with special message
1415        if ( conditional_fields_missing.length > 0 ) {
1416            var conditional_labels = conditional_fields_missing.map( function( field ) {
1417                return field.field_label;
1418            });
1419
1420            if ( conditional_labels.length === 1 ) {
1421                message_parts.push( 'Please complete the required field: ' + conditional_labels[0] );
1422            } else {
1423                message_parts.push( 'Please complete the required fields: ' + conditional_labels.join( ', ' ) );
1424            }
1425        }
1426
1427        // Handle regular fields
1428        if ( regular_fields_missing.length > 0 ) {
1429            var field_labels = regular_fields_missing.map( function( field ) {
1430                return field.field_label;
1431            });
1432
1433            if ( field_labels.length === 1 ) {
1434                message_parts.push( 'Please fill out: ' + field_labels[0] );
1435            } else {
1436                message_parts.push( 'Please fill out: ' + field_labels.join( ', ' ) );
1437            }
1438        }
1439
1440        // Combine message parts
1441        if ( message_parts.length === 1 ) {
1442            return message_parts[0] + '.';
1443        } else if ( message_parts.length === 2 ) {
1444            return message_parts.join( ' and ' ) + '.';
1445        } else {
1446            // More than 2 parts, use commas and 'and'
1447            var last_part = message_parts.pop();
1448            return message_parts.join( ', ' ) + ', and ' + last_part + '.';
1449        }
1450    };
1451
1452    /**
1453     * Verifies the submission and returns true if it all looks ok.
1454     *
1455     * @param   this.form The submitted form.
1456     * @return  boolean
1457     */
1458    Donation_Form.prototype.validate = function() {
1459        /* First clear out the errors. */
1460        this.clear_errors();
1461
1462        this.validate_amount();
1463
1464        this.validate_required_fields();
1465
1466        this.validate_phone_fields();
1467
1468        this.form.trigger( 'charitable:form:validate', this );
1469
1470        return this.errors.length === 0;
1471    };
1472
1473    /**
1474     * Validate phone fields based on their format.
1475     *
1476     * @return  boolean
1477     */
1478    Donation_Form.prototype.validate_phone_fields = function() {
1479        var self = this;
1480        var isValid = true;
1481
1482        // Validate Smart format fields
1483        this.form.find('input[data-rule-smart-phone-field="true"]').each(function() {
1484            if (!CHARITABLE.PhoneField.validateSmart($(this))) {
1485                isValid = false;
1486            }
1487        });
1488
1489        // Validate US format fields
1490        this.form.find('input[data-rule-us-phone-field="true"]').each(function() {
1491            if (!CHARITABLE.PhoneField.validateUS($(this))) {
1492                isValid = false;
1493            }
1494        });
1495
1496        if (!isValid) {
1497            this.add_error( 'Please correct the phone number errors.' );
1498        }
1499
1500        return isValid;
1501    };
1502
1503    exports.Donation_Form = Donation_Form;
1504
1505})( CHARITABLE, jQuery );
1506
1507/**
1508 * Set up Toggle object.
1509 */
1510( function( exports, $ ){
1511    var Toggle = function() {
1512
1513        /**
1514         * Hide toggle target.
1515         */
1516        var hide_target = function() {
1517            get_target( $(this) ).addClass( 'charitable-hidden' );
1518        };
1519
1520        /**
1521         * Get the target element.
1522         */
1523        var get_target = function( $el ) {
1524            var target = $el.data('charitable-toggle');
1525
1526            if ( target[0] !== '.' && target[0] !== '#' ) {
1527                target = '#' + target;
1528            }
1529
1530            return $( target );
1531        }
1532
1533        /**
1534         * Toggle event handler for any fields with the [data-charitable-toggle] attribute.
1535         *
1536         * @access private
1537         */
1538        var on_toggle = function() {
1539            var $this   = $( this ),
1540                $target = get_target( $this );
1541
1542            $target.toggleClass( 'charitable-hidden', ( function(){
1543                    if ( $this.is( ':checkbox' ) ) {
1544                        return ! $this.is( ':checked' );
1545                    }
1546                    return false === $target.hasClass( 'charitable-hidden' );
1547                })()
1548            );
1549
1550            return false;
1551        };
1552
1553        /**
1554         * Initializing function.
1555         */
1556        var init = function() {
1557            $( '[data-charitable-toggle]' ).each( hide_target );
1558        };
1559
1560        // Initialization only required once.
1561        $( 'body' ).on( 'click', '[data-charitable-toggle]', on_toggle );
1562
1563        // Check whether content is being loaded from the session.
1564        if ( exports.content_loading ) {
1565            var timer = setInterval(function(){
1566                if ( false === exports.content_loading ) {
1567                    init();
1568                    clearInterval(timer);
1569                }
1570            }, 500);
1571        }
1572
1573        // Initialization that will be performed everytime
1574        return init;
1575    }
1576
1577    exports.Toggle = Toggle();
1578
1579})( CHARITABLE, jQuery );
1580
1581
1582/**
1583 * Do a version check.
1584 *
1585 * @since 1.6.19
1586 *
1587 * @param string version The version we are comparing with.
1588 * @param string compare If compare is left empty, use the Charitable version.
1589 * @return integer|boolean
1590 */
1591CHARITABLE.VersionCompare = function( version, compare ) {
1592    compare = compare || CHARITABLE_VARS.version;
1593
1594    if ( typeof version + typeof compare != 'stringstring')
1595        return false;
1596
1597    var a = version.split( '.' ),
1598        b = compare.split( '.' ),
1599        i = 0,
1600        len = Math.max( a.length, b.length );
1601
1602    for ( ; i < len; i++ ) {
1603        if ((a[i] && !b[i] && parseInt(a[i]) > 0) || (parseInt(a[i]) > parseInt(b[i]))) {
1604            return 1;
1605        } else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) {
1606            return -1;
1607        }
1608    }
1609
1610    return 0;
1611};
1612
1613/**
1614 * Set up ResetSuggestedDonations object.
1615 */
1616( function( exports, $ ){
1617
1618    CHARITABLE.ResetSuggestedDonations = function() {
1619
1620        if ( $('.charitable-donation-options').length === 0 ) {
1621            // the options probably aren't on this page.
1622            return false;
1623        }
1624
1625        $( 'ul.donation-amounts input[type="radio"]' ).each( function() {
1626            if ( $( this ).is(':checked') ) {
1627                $( this ).closest( "li" ).addClass( "selected" );
1628            } else {
1629                $( this ).closest( "li" ).removeClass( "selected" );
1630            }
1631        });
1632
1633        // Also handle custom donation amounts that might be pre-filled from session
1634        $( 'input.custom-donation-input' ).each( function() {
1635            var $input = $( this );
1636            var $radio = $input.closest( '.custom-donation-amount' ).find( 'input[type="radio"]' );
1637            var value = $input.val();
1638
1639            if ( value && value.trim() !== '' ) {
1640                $radio.prop( 'checked', true );
1641                $radio.closest( "li" ).addClass( "selected" );
1642            }
1643        });
1644
1645    }
1646
1647})( CHARITABLE, jQuery );
1648
1649/**
1650 * Campaign Pagination AJAX Handler
1651 */
1652( function( exports, $ ) {
1653    var CampaignPagination = function() {
1654        var self = this;
1655
1656        /**
1657         * Initialize campaign pagination
1658         */
1659        var init = function() {
1660            $(document).on('click', '.charitable-campaigns-pagination .page-numbers', function(e) {
1661                // Don't process if already on this page
1662                if ($(this).hasClass('current')) {
1663                    return;
1664                }
1665
1666                // Prevent default link behavior
1667                e.preventDefault();
1668                e.stopPropagation();
1669
1670                var $link = $(this);
1671                var $container = $link.closest('.charitable-campaigns-shortcode');
1672
1673                // Don't process if already loading
1674                if ($container.hasClass('charitable-loading')) {
1675                    return;
1676                }
1677
1678                // Show loading state
1679                $container.addClass('charitable-loading');
1680
1681                // Get the shortcode attributes from the data attribute
1682                var shortcodeAtts = $container.data('shortcode-atts');
1683
1684                // Determine the page number
1685                var page = 1;
1686                if ($link.hasClass('next')) {
1687                    // Get current page and add 1
1688                    var currentPage = parseInt($container.find('.page-numbers.current').text()) || 1;
1689                    page = currentPage + 1;
1690                } else if ($link.hasClass('prev')) {
1691                    // Get current page and subtract 1
1692                    var currentPage = parseInt($container.find('.page-numbers.current').text()) || 1;
1693                    page = currentPage - 1;
1694                } else {
1695                    // Get page number from link text
1696                    page = parseInt($link.text()) || 1;
1697                }
1698
1699                // Make the AJAX request
1700                $.ajax({
1701                    url: CHARITABLE_VARS.ajaxurl,
1702                    type: 'POST',
1703                    data: {
1704                        action: 'charitable_load_campaigns_page',
1705                        nonce: CHARITABLE_VARS.nonce,
1706                        page: page,
1707                        shortcode_atts: shortcodeAtts
1708                    },
1709                    success: function(response) {
1710                        if (response.success) {
1711                            // Update campaigns
1712                            $container.find('.campaign-loop').html(response.data.campaigns);
1713
1714                            // Update pagination and ensure links are properly handled
1715                            var $pagination = $(response.data.pagination);
1716
1717                            // Update all pagination links to use AJAX
1718                            $pagination.find('a.page-numbers').each(function() {
1719                                var $this = $(this);
1720                                var href = $this.attr('href');
1721                                var pageNum = href.match(/paged=(\d+)/);
1722
1723                                if (pageNum) {
1724                                    $this.attr('href', '#');
1725                                    $this.data('page', pageNum[1]);
1726                                }
1727                            });
1728
1729                            // Replace the pagination HTML
1730                            $container.find('.charitable-campaigns-pagination').html($pagination);
1731
1732                            // Update the current page in the container's data
1733                            $container.data('current-page', response.data.current_page);
1734
1735                            // Update the current page indicator
1736                            $container.find('.page-numbers').removeClass('current');
1737                            $container.find('.page-numbers').each(function() {
1738                                var $this = $(this);
1739                                if ($this.text() == response.data.current_page) {
1740                                    $this.addClass('current');
1741                                }
1742                            });
1743                        } else {
1744                            console.error('Error loading campaigns:', response.data.message);
1745                        }
1746                    },
1747                    error: function(xhr, status, error) {
1748                        console.error('AJAX error:', error);
1749                    },
1750                    complete: function() {
1751                        // Remove loading state
1752                        $container.removeClass('charitable-loading');
1753                    }
1754                });
1755            });
1756        };
1757
1758        init();
1759    };
1760
1761    new CampaignPagination();
1762
1763})( window, jQuery );
1764
1765/**
1766 * Set up PhoneField object for Smart/US phone format handling.
1767 */
1768( function( exports, $ ) {
1769    var PhoneField = {
1770        /**
1771         * Initialize phone fields on the page.
1772         */
1773        init: function() {
1774            this.initSmartFields();
1775            this.initUSFields();
1776        },
1777
1778        /**
1779         * Initialize Smart format fields using intlTelInput.
1780         */
1781        initSmartFields: function() {
1782            var $smartFields = $('input[data-rule-smart-phone-field="true"]');
1783
1784            if ($smartFields.length === 0) {
1785                return;
1786            }
1787
1788            // Check if intlTelInput is loaded
1789            if (typeof intlTelInput === 'undefined') {
1790                // Debug helper function - check if debug mode is enabled
1791                var isDebug = function() {
1792                    return (typeof wpchar !== 'undefined' && wpchar.isDebug && wpchar.isDebug()) ||
1793                           (window.location.hash && '#charitabledebug' === window.location.hash);
1794                };
1795                if (isDebug()) {
1796                    if (typeof wpchar !== 'undefined' && wpchar.debug) {
1797                        wpchar.debug('intlTelInput library not loaded', 'PhoneField');
1798                    } else {
1799                        console.warn('intlTelInput library not loaded');
1800                    }
1801                }
1802                return;
1803            }
1804
1805            var utilsScriptUrl = (typeof charitable_phone_vars !== 'undefined' && charitable_phone_vars.utils_script_url)
1806                ? charitable_phone_vars.utils_script_url
1807                : false;
1808
1809            $smartFields.each(function() {
1810                var $input = $(this);
1811                
1812                // Determine initial country:
1813                // 1. Check if form has a country field with a value
1814                // 2. Fall back to CHARITABLE_VARS.country (from general settings)
1815                // 3. Fall back to 'us' as default
1816                var initialCountry = 'us'; // default fallback
1817                
1818                // Check form's country field first (if it exists and has a value)
1819                var $countryField = $('#charitable_field_country_element');
1820                if ($countryField.length && $countryField.val()) {
1821                    initialCountry = $countryField.val().toLowerCase();
1822                } 
1823                // Fall back to Charitable's general settings
1824                else if (typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.country) {
1825                    initialCountry = CHARITABLE_VARS.country.toLowerCase();
1826                }
1827                
1828                var itiOptions = {
1829                    initialCountry: initialCountry,
1830                    preferredCountries: ['us', 'ca', 'gb'],
1831                    separateDialCode: false
1832                };
1833
1834                if (utilsScriptUrl) {
1835                    itiOptions.utilsScript = utilsScriptUrl;
1836                }
1837
1838                var iti = intlTelInput($input[0], itiOptions);
1839
1840                // Store instance for validation
1841                $input.data('iti', iti);
1842            });
1843        },
1844
1845        /**
1846         * Initialize US format fields using inputmask.
1847         */
1848        initUSFields: function() {
1849            var $usFields = $('input[data-rule-us-phone-field="true"]');
1850
1851            // Debug helper function - check if debug mode is enabled
1852            var isDebug = function() {
1853                return (typeof wpchar !== 'undefined' && wpchar.isDebug && wpchar.isDebug()) ||
1854                       (window.location.hash && '#charitabledebug' === window.location.hash);
1855            };
1856
1857            if (isDebug()) {
1858                if (typeof wpchar !== 'undefined' && wpchar.debug) {
1859                    wpchar.debug('[PhoneField] initUSFields called, found fields: ' + $usFields.length, 'PhoneField');
1860                } else {
1861                    console.log('[PhoneField] initUSFields called, found fields:', $usFields.length);
1862                }
1863            }
1864
1865            if ($usFields.length === 0) {
1866                return;
1867            }
1868
1869            // Check if inputmask is loaded
1870            if (typeof $.fn.inputmask === 'undefined') {
1871                if (isDebug()) {
1872                    if (typeof wpchar !== 'undefined' && wpchar.debug) {
1873                        wpchar.debug('[PhoneField] jQuery inputmask library not loaded', 'PhoneField');
1874                    } else {
1875                        console.warn('[PhoneField] jQuery inputmask library not loaded');
1876                    }
1877                }
1878                return;
1879            }
1880
1881            if (isDebug()) {
1882                if (typeof wpchar !== 'undefined' && wpchar.debug) {
1883                    wpchar.debug('[PhoneField] Inputmask library loaded, initializing fields', 'PhoneField');
1884                } else {
1885                    console.log('[PhoneField] Inputmask library loaded, initializing fields');
1886                }
1887            }
1888
1889            $usFields.each(function() {
1890                var $input = $(this);
1891                // Get mask from data attribute (data-inputmask="(999) 999-9999")
1892                var mask = $input.attr('data-inputmask') || $input.data('inputmask') || '(999) 999-9999';
1893
1894                if (isDebug()) {
1895                    if (typeof wpchar !== 'undefined' && wpchar.debug) {
1896                        wpchar.debug('[PhoneField] Initializing inputmask on field: ' + $input.attr('id') + ' with mask: ' + mask, 'PhoneField');
1897                    } else {
1898                        console.log('[PhoneField] Initializing inputmask on field:', $input.attr('id'), 'with mask:', mask);
1899                    }
1900                }
1901
1902                // Check if already initialized by checking if inputmask instance exists on the DOM element
1903                // inputmask stores instance in $input[0].inputmask
1904                // NOTE: $input.data('inputmask') returns the data attribute value, not the instance!
1905                if ($input[0] && $input[0].inputmask) {
1906                    if (isDebug()) {
1907                        if (typeof wpchar !== 'undefined' && wpchar.debug) {
1908                            wpchar.debug('[PhoneField] Field already has inputmask instance, skipping', 'PhoneField');
1909                        } else {
1910                            console.log('[PhoneField] Field already has inputmask instance, skipping');
1911                        }
1912                    }
1913                    return;
1914                }
1915
1916                // Wait a bit to ensure library is fully loaded
1917                setTimeout(function() {
1918                    try {
1919                        if (typeof $.fn.inputmask !== 'undefined') {
1920                            // Check again if initialized (in case it was initialized in the delay)
1921                            if ($input[0] && $input[0].inputmask) {
1922                                console.log('[PhoneField] Field initialized during delay, skipping');
1923                                return;
1924                            }
1925
1926                            // Apply the mask - inputmask 5.x format
1927                            // Remove the data-inputmask attribute first to prevent conflicts
1928                            // Then apply the mask directly as a string
1929                            $input.removeAttr('data-inputmask');
1930
1931                            // Apply mask as a simple string pattern
1932                            // Format: (999) 999-9999 for US phone numbers
1933                            console.log('[PhoneField] Applying mask pattern:', mask);
1934
1935                            $input.inputmask(mask);
1936
1937                            console.log('[PhoneField] Inputmask initialized successfully on:', $input.attr('id'));
1938                        } else {
1939                            console.error('[PhoneField] Inputmask library n
1939ot available when trying to initialize');
1940                        }
1941                    } catch (e) {
1942                        console.error('[PhoneField] Error initializing inputmask:', e);
1943                    }
1944                }, 100);
1945            });
1946        },
1947
1948        /**
1949         * Validate Smart format phone field.
1950         *
1951         * @param {jQuery} $input The input element.
1952         * @return {boolean} True if valid, false otherwise.
1953         */
1954        validateSmart: function($input) {
1955            var iti = $input.data('iti');
1956            if (!iti) {
1957                return true; // If not initialized, skip validation
1958            }
1959
1960            if (typeof intlTelInputUtils === 'undefined') {
1961                return true; // Utils not loaded, skip validation
1962            }
1963
1964            var countryCode = iti.getSelectedCountryData().iso2;
1965            var isValid = intlTelInputUtils.isValidNumber($input.val(), countryCode);
1966
1967            if (!isValid) {
1968                $input.addClass('charitable-field-error');
1969                var errorMsg = 'Please enter a valid phone number.';
1970                if ($input.next('.charitable-field-error-message').length === 0) {
1971                    $input.after('<span class="charitable-field-error-message">' + errorMsg + '</span>');
1972                }
1973                return false;
1974            } else {
1975                $input.removeClass('charitable-field-error');
1976                $input.next('.charitable-field-error-message').remove();
1977                return true;
1978            }
1979        },
1980
1981        /**
1982         * Validate US format phone field.
1983         *
1984         * @param {jQuery} $input The input element.
1985         * @return {boolean} True if valid, false otherwise.
1986         */
1987        validateUS: function($input) {
1988            var value = $input.val().replace(/\D/g, ''); // Remove non-digits
1989            var isValid = value.length === 10;
1990
1991            if (!isValid) {
1992                $input.addClass('charitable-field-error');
1993                var errorMsg = 'Please enter a valid US phone number (10 digits).';
1994                if ($input.next('.charitable-field-error-message').length === 0) {
1995                    $input.after('<span class="charitable-field-error-message">' + errorMsg + '</span>');
1996                }
1997                return false;
1998            } else {
1999                $input.removeClass('charitable-field-error');
2000                $input.next('.charitable-field-error-message').remove();
2001                return true;
2002            }
2003        }
2004    };
2005
2006    exports.PhoneField = PhoneField;
2007
2008})( CHARITABLE, jQuery );
2009
2010/***
2011 * Finally queue up all the scripts.
2012 */
2013( function( $ ) {
2014
2015    /**
2016     * Prevent re-initializing form handlers.
2017     */
2018    CHARITABLE.forms_initialized = false;
2019
2020    /**
2021     * Store forms globally so we can access them for reset on back navigation.
2022     *
2023     * @since 1.8.10.1
2024     */
2025    CHARITABLE.forms = [];
2026
2027    /**
2028     * Reset all forms that are stuck in processing state.
2029     * This handles cases where the page is restored from bfcache or back navigation.
2030     *
2031     * @since 1.8.10.1
2032     */
2033    CHARITABLE.reset_stuck_forms = function() {
2034        var resetCount = 0;
2035
2036        // Check all registered forms
2037        CHARITABLE.forms.forEach( function( formInstance ) {
2038            if ( formInstance && typeof formInstance.reset_if_stuck === 'function' ) {
2039                if ( formInstance.reset_if_stuck() ) {
2040                    resetCount++;
2041                }
2042            }
2043        });
2044
2045        // Also check for any forms that might not be registered yet (e.g., in hidden tabs)
2046        $( '.charitable-donation-form' ).each( function() {
2047            var $form = $( this );
2048            var $spinner = $form.find( '.charitable-form-processing' );
2049            var $button = $form.find( 'button[name="donate"]' );
2050
2051            // Check if form is stuck: spinner visible and button hidden
2052            var isStuck = ( $spinner.is( ':visible' ) || $spinner.css( 'display' ) !== 'none' ) &&
2053                          ( $button.is( ':hidden' ) || $button.css( 'display' ) === 'none' );
2054
2055            if ( isStuck ) {
2056                // Reset the UI state directly
2057                $spinner.hide();
2058                $button.show();
2059                resetCount++;
2060            }
2061        });
2062
2063        return resetCount;
2064    };
2065
2066    $( document ).ready( function() {
2067
2068        $( 'html' ).addClass( 'js' );
2069
2070        var forms = [];
2071
2072        $( '.charitable-donation-form' ).each( function() {
2073            var formInstance = new CHARITABLE.Donation_Form( $( this ) );
2074            forms.push( formInstance );
2075            // @since 1.8.10.1 - Register forms globally for reset functionality
2076            CHARITABLE.forms.push( formInstance );
2077        });
2078
2079        // Check for stuck forms on initial page load (fallback)
2080        // @since 1.8.10.1 - Fix for stuck forms on back navigation
2081        CHARITABLE.reset_stuck_forms();
2082
2083        CHARITABLE.Toggle();
2084
2085        CHARITABLE.ResetSuggestedDonations();
2086
2087        // Initialize phone fields (Smart/US formats)
2088        CHARITABLE.PhoneField.init();
2089    });
2090
2091    /**
2092     * Handle browser back/forward navigation and bfcache restoration.
2093     * This fixes the issue where forms get stuck in processing state after back navigation.
2094     *
2095     * @since 1.8.10.1
2096     */
2097    ( function() {
2098        // Handle pageshow event (fires even when page is restored from bfcache)
2099        // @since 1.8.10.1
2100        window.addEventListener( 'pageshow', function( event ) {
2101            // event.persisted is true when page is restored from bfcache
2102            if ( event.persisted ) {
2103                // Small delay to ensure DOM is ready
2104                setTimeout( function() {
2105                    CHARITABLE.reset_stuck_forms();
2106                }, 100 );
2107            }
2108        } );
2109
2110        // Handle popstate event (back/forward button navigation)
2111        // @since 1.8.10.1
2112        window.addEventListener( 'popstate', function( event ) {
2113            // Small delay to ensure DOM is ready
2114            setTimeout( function() {
2115                CHARITABLE.reset_stuck_forms();
2116            }, 100 );
2117        } );
2118
2119        // Handle Elementor tab visibility changes
2120        // This ensures forms in hidden tabs are reset when they become visible
2121        // @since 1.8.10.1
2122        $( document ).on( 'elementor/popup/show', function() {
2123            setTimeout( function() {
2124                CHARITABLE.reset_stuck_forms();
2125            }, 100 );
2126        } );
2127
2128        // Listen for tab changes (works with various tab implementations including Elementor)
2129        // Check for stuck forms when tabs become visible
2130        // @since 1.8.10.1
2131        $( document ).on( 'click', '.ue-tab-btn, .r-tabs-anchor, [data-content-id]', function() {
2132            setTimeout( function() {
2133                CHARITABLE.reset_stuck_forms();
2134            }, 200 );
2135        } );
2136    } )();
2137
2138})( jQuery );
2139
2140/**
2141 * Gateway Tab Handler for donation form payment method tabs
2142 */
2143( function( $ ) {
2144
2145    var GatewayTabHandler = function() {
2146        this.initialized = false;
2147
2148        /**
2149         * Check if we're in a modal context
2150         */
2151        this.isModalContext = function() {
2152            var $container = $( '#charitable-gateway-tabs' );
2153            if ( ! $container.length ) {
2154                return false;
2155            }
2156
2157            // First check the server-side setting
2158            var donationFormDisplay = ( typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.donation_form_display ) ? CHARITABLE_VARS.donation_form_display : 'separate_page';
2159
2160            // Only check for actual modal containers, not any element with "modal" in the ID
2161            var $modalContainer = $container.closest('.lean-modal, .charitable-modal');
2162            if ( $modalContainer.length > 0 ) {
2163                return true;
2164            }
2165
2166            // Check if we're in a modal dialog
2167            var $modalDialog = $container.closest('[role="dialog"]');
2168            if ( $modalDialog.length > 0 ) {
2169                return true;
2170            }
2171
2172            // Additional check: if we're on a direct donation page URL, we're not in a modal
2173            var currentUrl = window.location.href;
2174            if ( currentUrl.indexOf('/donate/') !== -1 || currentUrl.indexOf('/donate') !== -1 ) {
2175                return false;
2176            }
2177
2178            return false;
2179        };
2180
2181        /**
2182         * Initialize the gateway tabs functionality
2183         */
2184        this.init = function() {
2185            if ( this.initialized ) {
2186                return;
2187            }
2188
2189            this.setupTabs();
2190            this.handleOverflow();
2191            this.bindEvents();
2192            this.setupPaymentMethodErrorDetection();
2193            this.initialized = true;
2194        };
2195
2196        /**
2197         * Setup the tab structure and move tabs to dropdown if needed
2198         */
2199        this.setupTabs = function() {
2200            var $container = $( '#charitable-gateway-tabs' );
2201            if ( ! $container.length ) {
2202                return;
2203            }
2204
2205            var $tabList = $container.find( '#charitable-gateway-selector' );
2206            var $tabs = $tabList.find( '.charitable-gateway-tab' );
2207
2208            // Create dropdown wrapper if it doesn't exist
2209            if ( ! $container.find( '.charitable-gateway-dropdown-wrapper' ).length ) {
2210                var $dropdownWrapper = $( '<div class="charitable-gateway-dropdown-wrapper" style="display: none;">' +
2211                    '<button type="button" class="charitable-gateway-dropdown-toggle" aria-expanded="false" aria-label="More payment methods">' +
2212                    '<span>⋯</span>' +
2213                    '</button>' +
2214                    '<ul class="charitable-gateway-dropdown-list" style="display: none;"></ul>' +
2215                    '</div>' );
2216                $tabList.after( $dropdownWrapper );
2217            }
2218        };
2219
2220        /**
2221         * Handle overflow by moving tabs to dropdown when needed
2222         */
2223        this.handleOverflow = function() {
2224            var $container = $( '#charitable-gateway-tabs' );
2225            if ( ! $container.length ) {
2226                return;
2227            }
2228
2229            var $tabList = $container.find( '#charitable-gateway-selector' );
2230            var $tabs = $tabList.find( '.charitable-gateway-tab' );
2231            var $dropdownWrapper = $container.find( '.charitable-gateway-dropdown-wrapper' );
2232            var $dropdownList = $dropdownWrapper.find( '.charitable-gateway-dropdown-list' );
2233
2234            // Check if dropdown is already properly configured
2235            var dropdownHasTabs = $dropdownList.find('.charitable-gateway-tab').length > 0;
2236            if ( dropdownHasTabs && $dropdownWrapper.is(':visible') ) {
2237                return;
2238            }
2239
2240            // Reset - move all tabs back to main list
2241            $tabs.each( function() {
2242                var $tab = $( this );
2243                if ( $tab.parent().hasClass( 'charitable-gateway-dropdown-list' ) ) {
2244                    $tabList.append( $tab );
2245                }
2246            } );
2247
2248            $dropdownWrapper.hide();
2249            $dropdownList.hide();
2250
2251            // Get the maximum number of visible tabs from WordPress filter
2252            var maxVisibleTabs = ( typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.gateway_max_visible_tabs ) ? CHARITABLE_VARS.gateway_max_visible_tabs : 4;
2253
2254            // Check if we're in a modal context and adjust maxVisibleTabs accordingly
2255            var isModal = this.isModalContext();
2256            var serverModalSetting = ( typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.donation_form_display === 'modal' );
2257
2258            if ( isModal && serverModalSetting ) {
2259                // For actual modal forms, allow more tabs to be visible since space is typically more constrained
2260                maxVisibleTabs = Math.max(maxVisibleTabs, 6);
2261            } else if ( serverModalSetting && !isModal ) {
2262                // Edge case: server says modal but we're not in a modal context (direct URL access)
2263                // Force non-modal behavior (maxVisibleTabs = 4) to ensure dropdown appears
2264                maxVisibleTabs = 4;
2265            } else {
2266                // For non-modal forms, use the default logic
2267            }
2268
2269            // Check if we need to handle overflow
2270            if ( $tabs.length <= maxVisibleTabs ) {
2271                return; // No overflow handling needed for maxVisibleTabs or fewer tabs
2272            }
2273
2274            var containerWidth = $tabList.width();
2275
2276            // If container width is 0 or very small, it might be hidden or not yet rendered
2277            // In this case, don't move any tabs to dropdown yet
2278            if ( containerWidth <= 10 ) {
2279                // Schedule another check after a short delay
2280                setTimeout( function() {
2281                    this.handleOverflow();
2282                }.bind(this), 100 );
2283                return;
2284            }
2285
2286            // Use the original logic to handle overflow based on maxVisibleTabs and container width
2287            var visibleTabs = [];
2288            var overflowTabs = [];
2289
2290            // Calculate total width and determine which tabs fit
2291            $tabs.each( function( index ) {
2292                var $tab = $( this );
2293                var tabWidth = $tab.outerWidth( true );
2294
2295                // Reserve space for dropdown button (48px + margin)
2296                var availableWidth = containerWidth - 56;
2297                var currentWidth = visibleTabs.reduce( function( sum, tab ) {
2298                    return sum + tab.width;
2299                }, 0 );
2300
2301                // Check if we've reached the maximum visible tabs limit
2302                if ( visibleTabs.length >= maxVisibleTabs ) {
2303                    overflowTabs.push( $tab );
2304                } else if ( currentWidth + tabWidth <= availableWidth ) {
2305                    visibleTabs.push( { element: $tab, width: tabWidth } );
2306                } else {
2307                    overflowTabs.push( $tab );
2308                }
2309            } );
2310
2311            // Move overflow tabs to dropdown
2312            if ( overflowTabs.length > 0 ) {
2313                // Safety check: don't move ALL tabs to dropdown
2314                if ( overflowTabs.length === $tabs.length ) {
2315                    // Keep at least 2 tabs visible, move the rest to dropdown
2316                    var tabsToKeep = Math.min(2, maxVisibleTabs);
2317                    var tabsToMove = overflowTabs.slice(tabsToKeep);
2318
2319                    tabsToMove.forEach( function( $tab ) {
2320                        $dropdownList.append( $tab );
2321                    } );
2322
2323                    if ( tabsToMove.length > 0 ) {
2324                        $dropdownWrapper.show();
2325                    }
2326                } else {
2327                    overflowTabs.forEach( function( $tab ) {
2328                        $dropdownList.append( $tab );
2329                    } );
2330                    $dropdownWrapper.show();
2331                }
2332
2333            } else {
2334            }
2335        };
2336
2337        /**
2338         * Bind event handlers
2339         */
2340        this.bindEvents = function() {
2341            var self = this;
2342
2343            // Tab selection
2344            $( document ).on( 'change', '.charitable-gateway-tab input[type="radio"]', function() {
2345                var $radio = $( this );
2346                var $tab = $radio.closest( '.charitable-gateway-tab' );
2347                var gatewayId = $radio.val();
2348                var $dropdownList = $tab.closest('.charitable-gateway-dropdown-list');
2349                var $tabList = $('#charitable-gateway-selector');
2350
2351                // If selected from dropdown, swap with last visible tab
2352                if ($dropdownList.length) {
2353                    var $mainTabs = $tabList.find('.charitable-gateway-tab');
2354                    var $lastMainTab = $mainTabs.last();
2355
2356                    // Swap DOM nodes
2357                    $tab.insertAfter($lastMainTab);
2358                    $dropdownList.append($lastMainTab);
2359
2360                    // Close the dropdown
2361                    $dropdownList.removeClass('show').css('display', 'none');
2362                    $dropdownList.parent().find('.charitable-gateway-dropdown-toggle').attr('aria-expanded', 'false');
2363                }
2364
2365                // Update active state
2366                $('.charitable-gateway-tab').removeClass('active');
2367                $tab.addClass('active');
2368
2369                // Trigger the original gateway change event
2370                $radio.trigger( 'charitable:gateway:changed', [ gatewayId ] );
2371            } );
2372
2373            // Dropdown toggle
2374            $( document ).on( 'click', '.charitable-gateway-dropdown-toggle', function( e ) {
2375                e.preventDefault();
2376                var $toggle = $( this );
2377                var $dropdownList = $toggle.parent().find( '.charitable-gateway-dropdown-list' );
2378                var isExpanded = $toggle.attr( 'aria-expanded' ) === 'true';
2379                if ( isExpanded ) {
2380                    $dropdownList.removeClass('show').css('display', 'none');
2381                    $toggle.attr( 'aria-expanded', 'false' );
2382                } else {
2383                    $dropdownList.addClass('show').css('display', 'block');
2384                    $toggle.attr( 'aria-expanded', 'true' );
2385                }
2386            } );
2387
2388            // Close dropdown when clicking outside
2389            $( document ).on( 'click', function( e ) {
2390                if ( ! $( e.target ).closest( '.charitable-gateway-dropdown-wrapper' ).length ) {
2391                    $( '.charitable-gateway-dropdown-list' ).hide();
2392                    $( '.charitable-gateway-dropdown-toggle' ).attr( 'aria-expanded', 'false' );
2393                }
2394            } );
2395
2396            // Handle window resize
2397            var resizeTimeout;
2398            $( window ).on( 'resize', function() {
2399                clearTimeout(resizeTimeout);
2400                resizeTimeout = setTimeout(function() {
2401                    self.handleOverflow();
2402                }, 250);
2403            } );
2404
2405            // Handle form initialization
2406            $( document ).on( 'charitable:form:initialize', function() {
2407                // Small delay to ensure DOM is ready
2408                setTimeout( function() {
2409                    self.handleOverflow();
2410                }, 100 );
2411            } );
2412
2413            // Handle modal-specific events
2414            $( document ).on( 'leanModal:onOpen', function() {
2415                // Modal opened, use modal-specific initialization
2416                self.handleModalInit();
2417            } );
2418
2419            // Also listen for any modal-related events
2420            $( document ).on( 'show.bs.modal shown.bs.modal', function() {
2421                self.handleModalInit();
2422            } );
2423
2424            // Handle any click events that might open modals
2425            $( document ).on( 'click', '[data-modal], [href*="#modal"], .charitable-modal-trigger', function() {
2426                setTimeout( function() {
2427                    self.handleModalInit();
2428                }, 100 );
2429            } );
2430
2431            // Specific handler for charitable modal forms
2432            $( document ).on( 'click', '.charitable-donate-button', function() {
2433                setTimeout( function() {
2434                    self.handleModalInit();
2435                }, 100 );
2436            } );
2437
2438            // Listen for when charitable modal content is loaded
2439            $( document ).on( 'charitable:modal:loaded', function() {
2440                self.handleModalInit();
2441            } );
2442        };
2443
2444        /**
2445         * Public method to refresh overflow handling
2446         */
2447        this.refresh = function() {
2448            this.handleOverflow();
2449        };
2450
2451        /**
2452         * Handle modal-specific initialization
2453         */
2454        this.handleModalInit = function() {
2455            // Reset any existing dropdown state
2456            var $container = $( '#charitable-gateway-tabs' );
2457            if ( $container.length ) {
2458                var $dropdownWrapper = $container.find( '.charitable-gateway-dropdown-wrapper' );
2459                var $dropdownList = $dropdownWrapper.find( '.charitable-gateway-dropdown-list' );
2460                var $tabList = $container.find( '#charitable-gateway-selector' );
2461
2462                // Move all tabs back to main list
2463                $dropdownList.find('.charitable-gateway-tab').each(function() {
2464                    $tabList.append($(this));
2465                });
2466
2467                $dropdownWrapper.hide();
2468                $dropdownList.hide();
2469            }
2470
2471            // Wait a bit longer for modal to be fully rendered
2472            setTimeout( function() {
2473                this.handleOverflow();
2474            }.bind(this), 400 );
2475        };
2476
2477        /**
2478         * Setup payment method error detection for Square and ACH Direct Debit
2479         */
2480        this.setupPaymentMethodErrorDetection = function() {
2481            var self = this;
2482
2483            // Track which payment methods have already shown errors to prevent duplicates
2484            var errorShown = {
2485                square_core: false,
2486                stripe_ach_direct_debit: false
2487            };
2488
2489            // Environment detection
2490            function isLocalEnvironment() {
2491                var hostname = window.location.hostname;
2492                var protocol = window.location.protocol;
2493                return hostname === 'localhost' ||
2494                       hostname === '127.0.0.1' ||
2495                       hostname === '0.0.0.0' ||
2496                       protocol === 'file:' ||
2497                       hostname.includes('.local') ||
2498                       hostname.includes('.test') ||
2499                       hostname.includes('.dev');
2500            }
2501
2502            // Check if HTTPS is required but not available
2503            function isHttpsRequired() {
2504                return window.location.protocol !== 'https:' &&
2505                       (window.location.hostname !== 'localhost' &&
2506                        window.location.hostname !== '127.0.0.1');
2507            }
2508
2509            // Payment method error detection
2510            function checkPaymentMethodAvailability() {
2511                var isLocal = isLocalEnvironment();
2512                var needsHttps = isHttpsRequired();
2513
2514                // Check Square payment method
2515                setTimeout(function() {
2516                    var $squareContainer = $('#charitable-square-wallet-buttons');
2517                    var $squareCardContainer = $('#square-card-container');
2518
2519                    if ($squareContainer.length || $squareCardContainer.length) {
2520                        // Check if Square elements are properly loaded
2521                        var squareLoaded = $squareContainer.find('button').length > 0 ||
2522                                          $squareCardContainer.find('iframe').length > 0 ||
2523                                          $squareCardContainer.find('div[data-elements-stable-field-name]').length > 0;
2524
2525                        if (!squareLoaded && !errorShown.square_core) {
2526                            errorShown.square_core = true;
2527                            self.showPaymentMethodError('square_core', isLocal, needsHttps);
2528                        }
2529                    }
2530                }, 2000);
2531
2532                // Check ACH Direct Debit payment method
2533                setTimeout(function() {
2534                    var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2535                    var $achGatewayFields = $('#charitable-gateway-fields-stripe_ach_direct_debit');
2536
2537                    if ($achContainer.length) {
2538                        // Only check ACH if its gateway fields are visible (meaning it's selected)
2539                        var isGatewayVisible = $achGatewayFields.is(':visible') && $achGatewayFields.css('display') !== 'none';
2540
2541                        // Check if ACH element is properly loaded
2542                        var achLoaded = $achContainer.find('iframe').length > 0 ||
2543                                       $achContainer.find('div[data-elements-stable-field-name]').length > 0;
2544
2545                        // Also check if the container has proper height (not zero)
2546                        var containerHeight = $achContainer.height();
2547                        var hasHeight = containerHeight > 0;
2548                        var isVisible = $achContainer.is(':visible');
2549                        var computedDisplay = $achContainer.css('display');
2550                        var computedVisibility = $achContainer.css('visibility');
2551
2552                        // Only show error if:
2553                        // 1. ACH gateway is visible (selected) AND
2554                        // 2. ACH element failed to load OR container has issues
2555                        if (isGatewayVisible) {
2556                            var shouldShowError = false;
2557                            var errorReason = '';
2558
2559                            if (!achLoaded) {
2560                                shouldShowError = true;
2561                                errorReason = 'ACH element not loaded (no iframe found)';
2562                            } else if (!hasHeight && !isVisible && computedDisplay === 'none') {
2563                                shouldShowError = true;
2564                                errorReason = 'ACH container hidden and has zero height';
2565                            } else if (!hasHeight && isVisible) {
2566                                // Container is visible but has no height - this might be normal during loading
2567                                // Don't show error yet, give it more time
2568                                return;
2569                            }
2570
2571                            if (shouldShowError && !errorShown.stripe_ach_direct_debit) {
2572
2573                                errorShown.stripe_ach_direct_debit = true;
2574                                self.showPaymentMethodError('stripe_ach_direct_debit', isLocal, needsHttps);
2575                            }
2576                        } else {
2577                        }
2578                    }
2579                }, 3000); // Increased delay to 3 seconds to give more time for loading
2580            }
2581
2582            // Show payment method error
2583            this.showPaymentMethodError = function(gatewayId, isLocal, needsHttps) {
2584                var $tab = $('.charitable-gateway-tab[data-gateway-id="' + gatewayId + '"]');
2585                var $fields = $('#charitable-gateway-fields-' + gatewayId);
2586
2587                if ($tab.length && $fields.length) {
2588                    // Create error message
2589                    var errorMessage = '';
2590                    var errorClass = 'charitable-payment-method-error';
2591
2592                    if (gatewayId === 'square_core') {
2593                        errorMessage = 'Square payment is currently unavailable. Please try another payment method.';
2594                    } else if (gatewayId === 'stripe_ach_direct_debit') {
2595                        errorMessage = 'ACH Direct Debit is currently unavailable. Please try another payment method.';
2596                        // Hide the ACH Direct Debit payment field
2597                        $('#charitable_stripe_ach_direct_debit_field').hide();
2598                    }
2599
2600                    // Add local environment message if applicable
2601                    if (isLocal) {
2602                        errorMessage += '<br><small>Note: This payment method requires HTTPS and may not work in local development environments.</small>';
2603                        errorClass += ' local-environment';
2604                    } else if (needsHttps) {
2605                        errorMessage += '<br><small>Note: This payment method requires a secure HTTPS connection.</small>';
2606                        errorClass += ' https-required';
2607                    }
2608
2609                    // Add error message to the payment fields
2610                    var $errorDiv = $('<div class="' + errorClass + '">' + errorMessage + '</div>');
2611                    $fields.prepend($errorDiv);
2612
2613                    // Add visual indicator to the tab (both in main list and dropdown)
2614                    $tab.addClass('payment-method-error');
2615
2616                    // Also add error indicator to dropdown version if it exists
2617                    var $dropdownTab = $('.charitable-gateway-dropdown-list .charitable-gateway-tab[data-gateway-id="' + gatewayId + '"]');
2618                    if ($dropdownTab.length) {
2619                        $dropdownTab.addClass('payment-method-error');
2620                    }
2621
2622                }
2623            };
2624
2625            // Bind click events for payment method tabs to show errors
2626            $(document).on('click', '.charitable-gateway-tab', function() {
2627                var $tab = $(this);
2628                var gatewayId = $tab.data('gateway-id');
2629                var $fields = $('#charitable-gateway-fields-' + gatewayId);
2630
2631                // Check if this payment method has an error
2632                if ($fields.find('.charitable-payment-method-error').length > 0) {
2633                    // Show error message more prominently
2634                    $fields.find('.charitable-payment-method-error').show();
2635                }
2636            });
2637
2638            // Also handle dropdown item clicks
2639            $(document).on('click', '.charitable-gateway-dropdown-list .charitable-gateway-tab', function() {
2640                var $tab = $(this);
2641                var gatewayId = $tab.data('gateway-id');
2642                var $fields = $('#charitable-gateway-fields-' + gatewayId);
2643
2644                // Check if this payment method has an error
2645                if ($fields.find('.charitable-payment-method-error').length > 0) {
2646                    // Show error message more prominently
2647                    $fields.find('.charitable-payment-method-error').show();
2648                }
2649            });
2650
2651            // Initial check after a delay to allow payment methods to load
2652            setTimeout(checkPaymentMethodAvailability, 3000);
2653
2654            // Additional check when form is loaded
2655            $(document).on('charitable:form:loaded', function() {
2656                setTimeout(checkPaymentMethodAvailability, 2000);
2657            });
2658
2659            // Remove iframe title attributes that cause unwanted hover text
2660            function removeIframeTitles() {
2661                // Remove title attributes from iframes in payment method tabs
2662                $('.charitable-gateway-tab iframe').removeAttr('title');
2663                $('.charitable-gateway-tab .StripeElement iframe').removeAttr('title');
2664                $('.charitable-gateway-tab #charitable_stripe_ach_direct_debit_field iframe').removeAttr('title');
2665            }
2666
2667            // Run title removal periodically
2668            setInterval(removeIframeTitles, 1000);
2669
2670            // Periodic check for payment method failures (including dropdown items)
2671            setInterval(function() {
2672                var isLocal = isLocalEnvironment();
2673                var needsHttps = isHttpsRequired();
2674
2675                // Check ACH Direct Debit specifically for zero height issues
2676                var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2677                var $achGatewayFields = $('#charitable-gateway-fields-stripe_ach_direct_debit');
2678
2679                if ($achContainer.length) {
2680                    // Only check ACH if its gateway fields are visible (meaning it's selected)
2681                    var isGatewayVisible = $achGatewayFields.is(':visible') && $achGatewayFields.css('display') !== 'none';
2682
2683                    if (isGatewayVisible) {
2684                        var containerHeight = $achContainer.height();
2685                        var hasIframe = $achContainer.find('iframe').length > 0;
2686                        var isVisible = $achContainer.is(':visible');
2687                        var computedDisplay = $achContainer.css('display');
2688
2689
2690
2691                        // If container has zero height or no iframe, show error
2692                        if ((containerHeight === 0 || !hasIframe) && !errorShown.stripe_ach_direct_debit) {
2693
2694                            // Try to fix visibility issues first
2695                            if (isVisible === false || computedDisplay === 'none') {
2696
2697                                $achContainer.show();
2698
2699                                // Force reflow
2700                                $achContainer[0].offsetHeight;
2701
2702                                // Check again after fix attempt
2703                                setTimeout(function() {
2704                                    var newHeight = $achContainer.height();
2705                                    var newVisible = $achContainer.is(':visible');
2706
2707
2708                                    // Only show error if still broken after fix attempt
2709                                    if (newHeight === 0 || !$achContainer.find('iframe').length) {
2710                                        errorShown.stripe_ach_direct_debit = true;
2711                                        self.showPaymentMethodError('stripe_ach_direct_debit', isLocal, needsHttps);
2712                                    }
2713                                }, 1000);
2714                            } else {
2715                                // Container is visible but still has issues
2716                                errorShown.stripe_ach_direct_debit = true;
2717                                self.showPaymentMethodError('stripe_ach_direct_debit', isLocal, needsHttps);
2718                            }
2719                        }
2720                    } else {
2721                    }
2722                }
2723            }, 5000); // Check every 5 seconds
2724
2725            // Additional ACH container monitoring and auto-fix
2726            setInterval(function() {
2727                var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2728                var $achGatewayFields = $('#charitable-gateway-fields-stripe_ach_direct_debit');
2729
2730                if ($achContainer.length) {
2731                    // Only monitor ACH if its gateway fields are visible (meaning it's selected)
2732                    var isGatewayVisible = $achGatewayFields.is(':visible') && $achGatewayFields.css('display') !== 'none';
2733
2734                    if (isGatewayVisible) {
2735                        var isVisible = $achContainer.is(':visible');
2736                        var computedDisplay = $achContainer.css('display');
2737                        var hasIframe = $achContainer.find('iframe').length > 0;
2738                        var containerHeight = $achContainer.height();
2739
2740                        // If container is hidden but has iframes (meaning it loaded but got hidden)
2741                        if ((!isVisible || computedDisplay === 'none') && hasIframe && containerHeight > 0) {
2742                            $achContainer.show();
2743
2744                            // Force reflow
2745                            $achContainer[0].offsetHeight;
2746
2747                        }
2748                    } else {
2749                    }
2750                }
2751            }, 2000); // Check every 2 seconds for auto-fix
2752
2753            // CSS conflict detection function
2754            function checkACHCSSConflicts() {
2755                var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2756                if ($achContainer.length) {
2757                    var computedStyles = window.getComputedStyle($achContainer[0]);
2758
2759
2760                    // Check if any parent elements are hidden
2761                    var $parents = $achContainer.parents();
2762                    $parents.each(function(index) {
2763                        var $parent = $(this);
2764                        var parentDisplay = $parent.css('display');
2765                        var parentVisibility = $parent.css('visibility');
2766                        var parentOpacity = $parent.css('opacity');
2767
2768                        if (parentDisplay === 'none' || parentVisibility === 'hidden' || parentOpacity === '0') {
2769
2770                        }
2771                    });
2772
2773                    // Check for conflicting CSS rules
2774                    var conflictingRules = [];
2775                    var allStyles = document.styleSheets;
2776                    for (var i = 0; i < allStyles.length; i++) {
2777                        try {
2778                            var rules = allStyles[i].cssRules || allStyles[i].rules;
2779                            if (rules) {
2780                                for (var j = 0; j < rules.length; j++) {
2781                                    var rule = rules[j];
2782                                    if (rule.selectorText &&
2783                                        (rule.selectorText.includes('#charitable_stripe_ach_direct_debit_field') ||
2784                                         rule.selectorText.includes('.StripeElement') ||
2785                                         rule.selectorText.includes('[id*="ach"]'))) {
2786                                        conflictingRules.push({
2787                                            selector: rule.selectorText,
2788                                            display: rule.style.display,
2789                                            visibility: rule.style.visibility,
2790                                            opacity: rule.style.opacity,
2791                                            source: allStyles[i].href || 'inline'
2792                                        });
2793                                    }
2794                                }
2795                            }
2796                        } catch (e) {
2797                            // Cross-origin stylesheets will throw errors
2798                            continue;
2799                        }
2800                    }
2801
2802                    if (conflictingRules.length > 0) {
2803
2804                    }
2805                }
2806            }
2807
2808            // Run CSS conflict check after a delay
2809            setTimeout(checkACHCSSConflicts, 4000);
2810
2811            // Function to clear ACH error if container becomes functional
2812            function clearACHErrorIfFixed() {
2813                var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2814                var $errorMessage = $('.charitable-payment-method-error');
2815                var $errorTab = $('.charitable-gateway-tab.payment-method-error[data-gateway-id="stripe_ach_direct_debit"]');
2816
2817                if ($achContainer.length && $errorMessage.length && $errorTab.length) {
2818                    var isVisible = $achContainer.is(':visible');
2819                    var hasIframe = $achContainer.find('iframe').length > 0;
2820                    var containerHeight = $achContainer.height();
2821
2822                    // If ACH is now working, clear the error
2823                    if (isVisible && hasIframe && containerHeight > 0) {
2824
2825                        // Remove error message
2826                        $errorMessage.remove();
2827
2828                        // Remove error class from tab
2829                        $errorTab.removeClass('payment-method-error');
2830
2831                        // Reset error shown flag
2832                        errorShown.stripe_ach_direct_debit = false;
2833
2834                    }
2835                }
2836            }
2837
2838            // Check periodically if ACH error should be cleared
2839            setInterval(clearACHErrorIfFixed, 3000);
2840
2841            // Clear ACH error when user selects ACH payment method
2842            function clearACHErrorOnSelection() {
2843                var $achGatewayFields = $('#charitable-gateway-fields-stripe_ach_direct_debit');
2844                var $errorMessage = $('.charitable-payment-method-error');
2845                var $errorTab = $('.charitable-gateway-tab.payment-method-error[data-gateway-id="stripe_ach_direct_debit"]');
2846
2847                if ($achGatewayFields.is(':visible') && $achGatewayFields.css('display') !== 'none') {
2848                    if ($errorMessage.length && $errorTab.length) {
2849
2850
2851                        // Remove error message
2852                        $errorMessage.remove();
2853
2854                        // Remove error class from tab
2855                        $errorTab.removeClass('payment-method-error');
2856
2857                        // Reset error shown flag
2858                        errorShown.stripe_ach_direct_debit = false;
2859
2860                    }
2861                }
2862            }
2863
2864            // Watch for ACH gateway selection and clear errors
2865            setInterval(clearACHErrorOnSelection, 1000);
2866
2867            // Also clear errors when user clicks on ACH tab
2868            $(document).on('click', '.charitable-gateway-tab[data-gateway-id="stripe_ach_direct_debit"]', function() {
2869                setTimeout(function() {
2870                    clearACHErrorOnSelection();
2871                }, 100);
2872            });
2873
2874            // Watch for DOM changes that might hide the ACH container
2875            function setupACHContainerWatcher() {
2876                var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2877                if ($achContainer.length) {
2878                    // Create a MutationObserver to watch for style changes
2879                    var observer = new MutationObserver(function(mutations) {
2880                        mutations.forEach(function(mutation) {
2881                            if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
2882                                var $target = $(mutation.target);
2883                                var isVisible = $target.is(':visible');
2884                                var computedDisplay = $target.css('display');
2885
2886                                // If the container was hidden by a style change
2887                                if (!isVisible || computedDisplay === 'none') {
2888
2889                                    // Check if it has content (iframes) before trying to show it
2890                                    var hasIframe = $target.find('iframe').length > 0;
2891                                    if (hasIframe) {
2892                                        $target.show();
2893
2894                                        // Force reflow
2895                                        $target[0].offsetHeight;
2896
2897                                    }
2898                                }
2899                            }
2900                        });
2901                    });
2902
2903                    // Start observing
2904                    observer.observe($achContainer[0], {
2905                        attributes: true,
2906                        attributeFilter: ['style', 'class']
2907                    });
2908
2909                }
2910            }
2911
2912            // Set up the watcher after a delay to ensure container exists
2913            setTimeout(setupACHContainerWatcher, 5000);
2914
2915            // Handle CSS transitions and animations that might affect ACH container
2916            function handleACHCSSEffects() {
2917                var $achContainer = $('#charitable_stripe_ach_direct_debit_field');
2918                var $achGatewayFields = $('#charitable-gateway-fields-stripe_ach_direct_debit');
2919
2920                if ($achContainer.length) {
2921                    // Only handle CSS effects if ACH gateway is visible (selected)
2922                    var isGatewayVisible = $achGatewayFields.is(':visible') && $achGatewayFields.css('display') !== 'none';
2923
2924                    if (isGatewayVisible) {
2925                        // Check for CSS transitions that might be hiding the container
2926                        var computedStyles = window.getComputedStyle($achContainer[0]);
2927                        var transition = computedStyles.transition;
2928                        var animation = computedStyles.animation;
2929                        var opacity = computedStyles.opacity;
2930                        var transform = computedStyles.transform;
2931
2932                        // If there are transitions or animations, ensure container stays visible
2933                        if (transition !== 'none' || animation !== 'none') {
2934
2935                            // Force the container to be visible during transitions
2936                            $achContainer.css({
2937                                'opacity': '1',
2938                                'visibility': 'visible'
2939                            });
2940
2941                            // Add a class to prevent hiding during transitions
2942                            $achContainer.addClass('ach-transition-protected');
2943                        }
2944
2945                        // Check if opacity or transform is causing issues
2946                        if (opacity === '0' || transform.includes('scale(0)') || transform.includes('translateY(-100%)')) {
2947                            $achContainer.css({
2948                                'opacity': '1',
2949                                'transform': 'none'
2950                            });
2951                        }
2952                    } else {
2953                    }
2954                }
2955            }
2956
2957            // Run CSS effects check periodically
2958            setInterval(handleACHCSSEffects, 4000);
2959        };
2960    };
2961
2962    /**
2963     * Pre-fill donation form from URL GET params.
2964     * Supports ?amount=X&period=Y (set by mini widget redirect mode).
2965     * Charitable core's add_donation_params_to_session() handles server-side session pre-fill.
2966     * This JS fallback updates the UI after page load in case the session hasn't applied yet.
2967     *
2968     * @since 1.8.13.1
2969     */
2970    function initMiniWidgetRedirectParams() {
2971        var urlParams = new URLSearchParams(window.location.search);
2972        var amount = urlParams.get('amount');
2973        var period = urlParams.get('period');
2974
2975        if (!amount && !period) {
2976            return;
2977        }
2978
2979        var $form = $('#charitable-donation-form, .charitable-donation-form').first();
2980        if (!$form.length) {
2981            return;
2982        }
2983
2984        // Pre-fill amount.
2985        // PHP session (add_donation_params_to_session) handles server-side pre-check.
2986        // This JS handles the visual selected state on the <li> wrapper via change events.
2987        if (amount) {
2988            var numAmount = parseFloat(amount);
2989            if (numAmount > 0) {
2990                // Find preset radio whose float value matches (values may be "15.00" not "15").
2991                var $preset = $form.find('input[type="radio"][name="donation_amount"]').filter(function() {
2992                    return parseFloat(this.value) === numAmount;
2993                });
2994                if ($preset.length) {
2995                    // Only trigger if not already checked (PHP may have pre-checked it).
2996                    if (!$preset.is(':checked')) {
2997                        $preset.prop('checked', true).trigger('change');
2998                    } else {
2999                        // Still trigger change to update selected CSS class on the <li>.
3000                        $preset.trigger('change');
3001                    }
3002                } else {
3003                    // Fall back to custom amount field.
3004                    var $custom = $form.find('#donation-amount-custom, input[name="custom_donation_amount"]').first();
3005                    if ($custom.length) {
3006                        var $otherRadio = $form.find('input[type="radio"][value="custom"]').first();
3007                        if ($otherRadio.length && !$otherRadio.is(':checked')) {
3008                            $otherRadio.prop('checked', true).trigger('change');
3009                        }
3010                        $custom.val(numAmount).trigger('input');
3011                    }
3012                }
3013            }
3014        }
3015
3016        // Pre-fill period (recurring vs one-time).
3017        // Charitable Recurring uses a radio with name="recurring_donation".
3018        if (period && period === 'monthly') {
3019            var $recurringRadio = $form.find('input[name="recurring_donation"][value="1"], input[name="recurring_donation"][value="monthly"]').first();
3020            if ($recurringRadio.length) {
3021                $recurringRadio.prop('checked', true).trigger('change');
3022            }
3023        }
3024    }
3025
3026    // Initialize gateway tabs when document is ready
3027    $( document ).ready( function() {
3028        CHARITABLE.GatewayTabs = new GatewayTabHandler();
3029        CHARITABLE.GatewayTabs.init();
3030
3031        initMiniWidgetRedirectParams();
3032
3033        // Periodic check for modal forms (in case events are missed)
3034        setInterval( function() {
3035            if ( CHARITABLE.GatewayTabs && CHARITABLE.GatewayTabs.isModalContext() ) {
3036                var $container = $( '#charitable-gateway-tabs' );
3037                if ( $container.length && $container.find( '.charitable-gateway-dropdown-wrapper' ).is(':visible') ) {
3038                    var $dropdownList = $container.find( '.charitable-gateway-dropdown-list' );
3039                    var $tabList = $container.find( '#charitable-gateway-selector' );
3040
3041                    // If all tabs are in dropdown, something went wrong - reset
3042                    if ( $dropdownList.find('.charitable-gateway-tab').length === $tabList.find('.charitable-gateway-tab').length + $dropdownList.find('.charitable-gateway-tab').length ) {
3043                        CHARITABLE.GatewayTabs.handleModalInit();
3044                    }
3045                }
3046            }
3047        }, 2000 );
3048    } );
3049
3050    // Re-initialize when forms are loaded dynamically
3051    $( document ).on( 'charitable:form:loaded', function() {
3052        if ( CHARITABLE.GatewayTabs ) {
3053            CHARITABLE.GatewayTabs.init();
3054        }
3055    } );
3056
3057})( jQuery );

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.