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.