PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / assets / js / charitable.js

charitable.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13.1, at assets/js/charitable.js

1,171 lines 34.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 CHARITABLE = 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 * The donation amount.
50 *
51 * @access public
52 */
53 this.total = 0;
54
55 /**
56 * Whether to scroll to the top of the form after a submission with errors.
57 *
58 * @access private
59 */
60 this.prevent_scroll_to_top = false;
61
62 /**
63 * Reference to this object.
64 *
65 * @access private
66 */
67 var self = this;
68
69 /**
70 * Body element reference.
71 *
72 * @access private
73 */
74 var $body = $( 'body' );
75
76 /**
77 * Handle click of terms link.
78 *
79 * @access private
80 */
81 var on_click_terms = function() {
82 self.form.find( '.charitable-terms-text' ).addClass( 'active' );
83 return false;
84 };
85
86 /**
87 * Focus event handler on custom donation amount input field.
88 *
89 * @access private
90 */
91 var on_focus_custom_amount = function() {
92 $( this ).closest( 'li' ).trigger( 'click' ).find( 'input[name=donation_amount]' ).prop( 'checked', true ).trigger( 'change' );
93
94 self.form.off( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
95
96 $( this ).focus();
97
98 self.form.on( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
99 };
100
101 /**
102 * Trigger jQuery events to broadcast the change in donation amount.
103 *
104 * @since 1.6.19
105 *
106 * @access private
107 */
108 var trigger_amount_change_events = function() {
109 /* The chosen donation amount has changed. */
110 $body.trigger( 'charitable:form:amount:changed', self );
111
112 /* The overall donation amount has changed. */
113 $body.trigger( 'charitable:form:total:changed', self );
114 };
115
116 /**
117 * Focus event handler for changes to custom donation amount input field.
118 *
119 * @access private
120 */
121 var on_change_custom_donation_amount = function() {
122 var unformatted = self.unformat_amount( $( this ).val() );
123
124 if ( $.trim( unformatted ) > 0 ) {
125 $( this ).val( self.format_amount( unformatted ) );
126 }
127
128 trigger_amount_change_events();
129 };
130
131 /**
132 * Select a donation amount.
133 *
134 * @return void
135 */
136 var on_select_donation_amount = function() {
137 var $li = $( this ).closest( 'li' );
138
139 // Already selected, quit early to prevent focus/change loop
140 if ( $li.hasClass( 'selected' ) ) {
141 return;
142 }
143
144 $li.parents( '.charitable-donation-form' ).find( '.donation-amount.selected' ).removeClass( 'selected' );
145
146 $li.addClass( 'selected' ).find( '[name=donation_amount]' ).prop( 'checked', true );
147
148 if ( $li.hasClass( 'custom-donation-amount' ) ) {
149 $li.find( 'input.custom-donation-input' ).focus();
150 }
151
152 trigger_amount_change_events();
153 };
154
155 /**
156 * Change event handler for payment gateway selector.
157 *
158 * @access private
159 */
160 var on_change_payment_gateway = function() {
161 var selectedGateway = $(this).val();
162
163 self.hide_inactive_payment_methods();
164 self.show_active_payment_methods( selectedGateway );
165
166 // Trigger resize event for Square gateway to fix iframe height issue
167 if ( selectedGateway === 'square' || selectedGateway === 'square_core' ) {
168 setTimeout(function() {
169 window.dispatchEvent(new Event('resize'));
170 }, 100);
171 }
172 };
173
174 /**
175 * Event handler when the "Change" link is clicked for pre-filled donations.
176 */
177 var on_click_change_amount_link = function() {
178 $(this).parent().addClass( 'charitable-hidden' );
179 };
180
181 /**
182 * Submit event handler for donation form.
183 *
184 * @access private
185 */
186 var on_submit = function( event ) {
187 var helper = event.data.helper;
188
189 if ( helper.submit_processing ) {
190 return false;
191 }
192
193 helper.submit_processing = true;
194
195 /* Display the processing spinner and hide the button */
196 helper.show_processing();
197
198 /* Validate the form submission before going further. */
199 if ( false === helper.validate() ) {
200 helper.cancel_processing();
201 return false;
202 }
203
204 /* If processing has been paused, return false now. */
205 if ( false !== helper.pause_processing ) {
206 return false;
207 }
208
209 /* If we're not using AJAX to process the donation further, return now. */
210 if ( 1 !== helper.form.data( 'use-ajax' ) ) {
211 return true;
212 }
213
214 /* Continue on to process the donation. */
215 maybe_process( helper, function() {
216 $body.trigger( 'charitable:form:process', helper );
217 } );
218
219 return false;
220 };
221
222 /**
223 * Check whether there are any asynchronous threads we need to wait for.
224 *
225 * If not, trigger processing. If there are, check again in 500ms.
226 */
227 var maybe_process = function( helper, callback ) {
228 if ( ! helper.waiting() ) {
229 /* Double-check that there are still no errors. */
230 if ( helper.get_errors().length > 0 ) {
231 helper.cancel_processing();
232 } else {
233 callback();
234 }
235
236 return;
237 }
238
239 setTimeout( maybe_process, 500, helper, callback );
240 }
241
242 /**
243 * Process the donation.
244 *
245 * This is a callback for the 'charitable:form:process' event. It's called
246 * after validation has taken place.
247 *
248 * @param object Event
249 * @param object Donation_Form
250 */
251 var process_donation = function( event, helper ) {
252
253 var data = helper.get_data();
254
255 var form = helper.form;
256
257 /* Cancel the default Charitable action, but pass it along as the form_action variable */
258 data.action = 'make_donation';
259 data.form_action = data.charitable_action;
260 delete data.charitable_action;
261
262 $.ajax({
263 type: "POST",
264 data: data,
265 dataType: "json",
266 url: CHARITABLE_VARS.ajaxurl,
267 timeout: 0,
268 xhrFields: {
269 withCredentials: true
270 },
271 success: function (response) {
272 $body.trigger( 'charitable:form:processed', [ response, helper ] );
273
274 if ( response.success ) {
275 maybe_process( helper, function() {
276 window.location.href = response.redirect_to;
277 } );
278 }
279 else {
280 helper.cancel_processing( response.errors );
281
282 if ( response.donation_id ) {
283 helper.set_donation_id( response.donation_id );
284 }
285 }
286 }
287 }).fail(function (response, textStatus, errorThrown) {
288 if ( window.console && window.console.log ) {
289 console.log( response );
290 }
291
292 helper.cancel_processing( [ CHARITABLE_VARS.error_unknown ] );
293 });
294
295 return false;
296 }
297
298 /**
299 * Set up event handlers for donation forms.
300 *
301 * @return void
302 */
303 var init = function() {
304 // Init donation amount selection
305 self.form.on( 'click', '.donation-amount', on_select_donation_amount );
306 self.form.on( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
307 self.form.on( 'click', '.charitable-terms-link', on_click_terms );
308
309 // Init currency formatting
310 self.form.on( 'blur', '.custom-donation-input', on_change_custom_donation_amount );
311
312 self.form.find( '.donation-amount input:checked' ).each( function() {
313 $( this ).closest( 'li' ).addClass( 'selected' );
314 });
315
316 if ( self.get_all_payment_methods().length ) {
317 self.hide_inactive_payment_methods();
318 self.form.on( 'change', '#charitable-gateway-selector input[name=gateway]', on_change_payment_gateway );
319 }
320
321 self.form.on( 'click', '.change-donation', on_click_change_amount_link );
322
323 // Handle donation form submission
324 self.form.on( 'submit', {
325 helper : self,
326 }, on_submit );
327
328 // This only fires after the first form instance is initialized.
329 if ( false === CHARITABLE.forms_initialized ) {
330
331 // Process the donation on the 'charitable:form:process' event.
332 $body.on( 'charitable:form:process', process_donation );
333
334 $body.trigger( 'charitable:form:initialize', self );
335
336 CHARITABLE.forms_initialized = true;
337 }
338
339 $body.trigger( 'charitable:form:loaded', self );
340 }
341
342 init();
343 };
344
345 /**
346 * Return a particular input field.
347 *
348 * @param string The field name to retrieve.
349 * @return object
350 */
351 Donation_Form.prototype.get_input = function( field ) {
352 return this.form.find( '[name=' + field + ']' );
353 }
354
355 /**
356 * Return the submitted email address.
357 *
358 * @return string
359 */
360 Donation_Form.prototype.get_email = function() {
361 return this.form.find( '[name=email]' ).val();
362 };
363
364 /**
365 * Returns whether this is a recurring donation.
366 *
367 * @since 1.4.21
368 *
369 * @return boolean
370 */
371 Donation_Form.prototype.is_recurring_donation = function() {
372 var recurring = this.form.find( '[name=recurring_donation]:checked, [name=recurring_donation][type=hidden]' );
373
374 return recurring.length && 'once' !== recurring.val();
375 };
376
377 /**
378 * Get the chosen suggested amount.
379 *
380 * Note that when no option has been selected, or a custom donation amount has been added,
381 * this will return 0.
382 *
383 * @since 1.4.19
384 *
385 * @return float
386 */
387 Donation_Form.prototype.get_suggested_amount = function() {
388 return accounting.unformat(
389 this.form.find( '[name=donation_amount]:checked, input[type=hidden][name=donation_amount]' ).val(),
390 CHARITABLE_VARS.currency_format_decimal_sep
391 );
392 }
393
394 /**
395 * Get the custom amount.
396 *
397 * Note that this will return 0 when no custom amount has been entered or a 0 has been entered.
398 *
399 * @since 1.4.19
400 *
401 * @return float
402 */
403 Donation_Form.prototype.get_custom_amount = function() {
404 var input = this.form.find( '.charitable-donation-options.active input.custom-donation-input,.charitable-donation-options.active input.custom-donation-amount' );
405
406 if ( 0 === input.length ) {
407 input = this.form.find( 'input.custom-donation-input' );
408 }
409
410 return accounting.unformat(
411 input.val(),
412 CHARITABLE_VARS.currency_format_decimal_sep
413 );
414 }
415
416 /**
417 * Get the donation subtotal, which is the amount passed by the donor.
418 *
419 * Notably, this does not include any additional amounts that may be added onto the donation
420 * total, such as processing fees that the donor agrees to pay.
421 *
422 * @since 1.6.7
423 *
424 * @return float
425 */
426 Donation_Form.prototype.get_subtotal = function() {
427 return this.get_suggested_amount() || this.get_custom_amount();
428 };
429
430 /**
431 * Get the submitted amount, taking into account both the custom & suggested donation fields.
432 *
433 * @return float
434 */
435 Donation_Form.prototype.get_amount = function() {
436 this.total = this.get_subtotal();
437
438 this.form.trigger( 'charitable:form:get_amount', this );
439
440 return this.total;
441 };
442
443 /**
444 * Get the submitted amount, taking into account both the custom & suggested donation fields.
445 *
446 * @since 1.6.8
447 *
448 * @param float add
449 * @return void
450 */
451 Donation_Form.prototype.add_amount = function( add ) {
452 this.total += add;
453 };
454
455 /**
456 * Get the submitted amount, taking into account both the custom & suggested donation fields.
457 *
458 * @since 1.6.8
459 *
460 * @param float remove Amount to be removed
461 * @return void
462 */
463 Donation_Form.prototype.remove_amount = function( remove ) {
464 this.total -= remove;
465 };
466
467 /**
468 * Get a description of the donation.
469 *
470 * @return string
471 */
472 Donation_Form.prototype.get_description = function() {
473 return this.form.find( '[name=description]' ).val() || '';
474 };
475
476 /**
477 * Get credit card number.
478 *
479 * @return string
480 */
481 Donation_Form.prototype.get_cc_number = function() {
482 return this.form.find( '#charitable_field_cc_number input' ).val() || '';
483 };
484
485 /**
486 * Get credit card CVC number.
487 *
488 * @return string
489 */
490 Donation_Form.prototype.get_cc_cvc = function() {
491 return this.form.find( '#charitable_field_cc_cvc input' ).val() || '';
492 };
493
494 /**
495 * Get credit card expiry month.
496 *
497 * @return string
498 */
499 Donation_Form.prototype.get_cc_expiry_month = function() {
500 return this.form.find( '#charitable_field_cc_expiration select.month' ).val() || '';
501 };
502
503 /**
504 * Get credit card expiry year.
505 *
506 * @return string
507 */
508 Donation_Form.prototype.get_cc_expiry_year = function() {
509 return this.form.find( '#charitable_field_cc_expiration select.year' ).val() || '';
510 };
511
512 /**
513 * Clear credit card fields.
514
515 *
516 * This is used by gateways that create tokens through Javascript (such as Stripe), to
517 * avoid credit card details hitting the server.
518 *
519 * @return void
520 */
521 Donation_Form.prototype.clear_cc_fields = function() {
522 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' );
523 };
524
525 /**
526 * Return the selected payment method.
527 *
528 * @return string
529 */
530 Donation_Form.prototype.get_payment_method = function() {
531 return this.form.find( '[type=hidden][name=gateway], [name=gateway]:checked' ).val() || '';
532 };
533
534 /**
535 * Return all payment methods.
536 *
537 * @return object
538 */
539 Donation_Form.prototype.get_all_payment_methods = function() {
540 return this.form.find( '#charitable-gateway-selector input[name=gateway]' );
541 }
542
543 /**
544 * Hide inactive payment methods.
545 *
546 * @return void
547 */
548 Donation_Form.prototype.hide_inactive_payment_methods = function() {
549 var active = this.get_payment_method();
550 var fields = this.form.find( '.charitable-gateway-fields[data-gateway!=' + active + ']' );
551
552 fields.hide();
553 fields.find( 'input[required],select[required],textarea[required]' ).attr( 'data-required', 1 ).attr( 'required', false );
554 };
555
556 /**
557 * Show active payment methods.
558 *
559 * @return void
560 */
561 Donation_Form.prototype.show_active_payment_methods = function( active ) {
562 var active = active || this.get_payment_method();
563 var fields = this.form.find( '.charitable-gateway-fields[data-gateway=' + active + ']' );
564
565 fields.show();
566 fields.find( '[data-required]' ).attr( 'required', true );
567 };
568
569 /**
570 * Select a donation amount.
571 *
572 * @param int price
573 * @param string symbol
574 * @return string
575 */
576 Donation_Form.prototype.format_amount = function( price, symbol ){
577 if ( typeof symbol === 'undefined' )
578 symbol = '';
579
580 return accounting.formatMoney( price, {
581 symbol : symbol,
582 decimal : CHARITABLE_VARS.currency_format_decimal_sep,
583 thousand: CHARITABLE_VARS.currency_format_thousand_sep,
584 precision : CHARITABLE_VARS.currency_format_num_decimals,
585 format: CHARITABLE_VARS.currency_format
586 }).trim();
587 };
588
589 /**
590 * Unformat a currency string to a plain float.
591 *
592 * Handles the case where the site uses a non-period decimal separator (e.g. comma)
593 * but the user types a period as the decimal point — a common mistake on non-US
594 * keyboards. accounting.unformat() would otherwise strip the period as if it were
595 * a thousands separator, turning "5.33" into 533.
596 *
597 * Heuristic: if the configured decimal is not a period, the value contains a period,
598 * and the value does NOT already contain the configured decimal separator:
599 * - One period followed by 1–2 digits at end → treat as decimal (e.g. "5.33" → "5,33")
600 * - One period followed by exactly 3 digits at end → treat as thousands (e.g. "1.000" → 1000)
601 * - Multiple periods → all thousands separators; let accounting.unformat strip them
602 *
603 * @param string|number price
604 * @return float
605 */
606 Donation_Form.prototype.unformat_amount = function( price ) {
607 var decimal = CHARITABLE_VARS.currency_format_decimal_sep;
608
609 price = String( price );
610
611 if ( decimal !== '.' && price.indexOf( '.' ) !== -1 && price.indexOf( decimal ) === -1 ) {
612 var periodCount = ( price.match( /\./g ) || [] ).length;
613
614 if ( periodCount === 1 && ! /\.\d{3}$/.test( price ) ) {
615 // Single period not followed by exactly 3 digits — treat as decimal separator.
616 price = price.replace( '.', decimal );
617 }
618 // Single period followed by 3 digits (thousands) or multiple periods:
619 // fall through and let accounting.unformat strip them.
620 }
621
622 return Math.abs( parseFloat( accounting.unformat( price, decimal ) ) );
623 };
624
625 /**
626 * Add an error message.
627 *
628 * @param string message
629 * @return void
630 */
631 Donation_Form.prototype.add_error = function( message ) {
632 this.errors.push( message );
633 };
634
635 /**
636 * Return the errors.
637 *
638 * @return []
639 */
640 Donation_Form.prototype.get_errors = function() {
641 return this.errors;
642 };
643
644 /**
645 * Prints out a nice string describing the errors.
646 *
647 * @return string
648 */
649 Donation_Form.prototype.get_error_message = function() {
650 return this.errors.join( ' ' );
651 };
652
653 /**
654 * Print the errors at the top of the form.
655 *
656 * @param array
657 */
658 Donation_Form.prototype.print_errors = function( errors ) {
659 var e = errors || this.errors,
660 i = 0,
661 count = e.length,
662 output = '',
663 error_notice_donation_display = typeof CHARITABLE_VARS.error_notice_donation_display === 'undefined' ? 'top' : CHARITABLE_VARS.error_notice_donation_display;
664
665 if ( 0 === count ) {
666 return;
667 }
668
669 if ( this.form.find( '.charitable-form-errors' ).length ) {
670 this.form.find( '.charitable-form-errors' ).remove();
671 }
672
673 /*
674 * role="alert" + tabindex="-1" + the summary marker mirror
675 * templates/form-fields/errors.php. The role is what announces these errors,
676 * because this node is inserted into a form that is already on screen. See
677 * CHARITABLE.focus_error_summary for why focus is moved as well.
678 * WCAG 4.1.3 / 3.3.1, 1.8.13.1.
679 */
680 output += '<div class="charitable-form-errors charitable-notice" role="alert" tabindex="-1" data-charitable-error-summary="1"><ul class="errors"><li>';
681 output += e.join( '</li><li>' );
682 output += '</li></ul></div>';
683
684 if ( error_notice_donation_display === 'bottom' ) {
685 this.form.append( output );
686 } else {
687 this.form.prepend( output );
688 }
689
690 CHARITABLE.focus_error_summary( this.form.find( '.charitable-form-errors' ) );
691
692 }
693
694 /**
695 * Clear the errors and remove the printed errors.
696 */
697 Donation_Form.prototype.clear_errors = function() {
698 this.errors = [];
699
700 if ( this.form.find( '.charitable-form-errors' ).length ) {
701 this.form.find( '.charitable-form-errors' ).remove();
702 }
703 }
704
705 /**
706 * Return whether we are waiting for an asynchronous process to finish.
707 *
708 * @since 1.6.9
709 *
710 * @return boolean
711 */
712 Donation_Form.prototype.waiting = function() {
713 return this.pending_processes.length > 0;
714 }
715
716 /**
717 * Add a pending process.
718 *
719 * @since 1.6.9
720 *
721 * @param string Process identifier.
722 * @return int Index of the process.
723 */
724 Donation_Form.prototype.add_pending_process = function( process ) {
725 var index = this.pending_processes.indexOf( process );
726 return -1 === index ? ( this.pending_processes.push( process ) - 1 ) : index;
727 }
728
729 /**
730 * Remove a pending process by index.
731 *
732 * @since 1.6.9
733 *
734 * @param int Index of the process.
735 * @return void
736 */
737 Donation_Form.prototype.remove_pending_process = function( index ) {
738 this.pending_processes.splice( index, 1 );
739 }
740
741 /**
742 * Remove a pending process by process name.
743 *
744 * @since 1.6.17
745 *
746 * @param string Name of the process.
747 * @return void
748 */
749 Donation_Form.prototype.remove_pending_process_by_name = function( process ) {
750 var index = this.pending_processes.indexOf( process );
751 return -1 !== index && this.remove_pending_process( index );
752 }
753
754 /**
755 * Show that the donation form is processing.
756 */
757 Donation_Form.prototype.show_processing = function() {
758 this.form.find( '.charitable-form-processing' ).show();
759 this.form.find( 'button[name="donate"]' ).hide();
760 }
761
762 /**
763 * Hide the processing spinner and show the donate button.
764 */
765 Donation_Form.prototype.hide_processing = function() {
766 this.form.find( '.charitable-form-processing' ).hide();
767 this.form.find( 'button[name="donate"]' ).show();
768
769 this.submit_processing = false;
770 this.pause_processing = false;
771 }
772
773 /**
774 * Scroll to the top of the form.
775 */
776 Donation_Form.prototype.scroll_to_top = function() {
777 if ( this.prevent_scroll_to_top ) {
778 this.prevent_scroll_to_top = false;
779 return;
780 }
781
782 var $modal = this.form.parents( '.charitable-modal' );
783
784 if ( $modal.length ) {
785 $modal.scrollTop( 0 );
786 }
787 else {
788 window.scrollTo( this.form.offset().left, this.form.offset().top );
789 }
790 };
791
792 /**
793 * Cancel donation processing.
794 *
795 * @param array errors Error messages to show.
796 */
797 Donation_Form.prototype.cancel_processing = function( errors ) {
798 this.hide_processing();
799 this.print_errors( errors );
800 var error_notice_donation_display = typeof CHARITABLE_VARS.error_notice_donation_display === 'undefined' ? 'top' : CHARITABLE_VARS.error_notice_donation_display;
801 if ( '' === error_notice_donation_display || 'top' === error_notice_donation_display ) {
802 this.scroll_to_top();
803 }
804
805 this.form.trigger( 'charitable:form:cancelled', this );
806 };
807
808 /**
809 * Set the donation ID.
810 */
811 Donation_Form.prototype.set_donation_id = function( donation_id ) {
812 this.form.find( '[name=ID]' ).val( donation_id );
813 }
814
815 /**
816 * Returns all of the submitted data as key=>value object.
817 *
818 * @return object
819 */
820 Donation_Form.prototype.get_data = function() {
821 return this.form.serializeArray().reduce( function( obj, item ) {
822 if ( '[]' === item.name.slice( -2 ) ) {
823 var name = item.name.slice( 0, -2 );
824
825 if ( ! obj.hasOwnProperty( name ) ) {
826 obj[name] = [];
827 }
828
829 obj[name].push( item.value );
830 } else {
831 obj[ item.name ] = item.value;
832 }
833
834 return obj;
835 }, {} );
836
837 };
838
839 /**
840 * Returns all of the required fields.
841 *
842 * @return object
843 */
844 Donation_Form.prototype.get_required_fields = function() {
845 var fields = this.form.find( '.charitable-fieldset .required-field' ).not( '#charitable-gateway-fields .required-field' ),
846 method = this.get_payment_method();
847
848 if ( '' !== method ) {
849
850 var gateway_fields = this.form.find( '[data-gateway=' + method + '] .required-field' );
851
852 if ( gateway_fields.length ) {
853 fields = $.merge( fields, gateway_fields );
854 }
855
856 }
857
858 return fields;
859 };
860
861 /**
862 * Check whether the submitted amount is greater than the maximum permitted.
863 *
864 * @returns boolean
865 */
866 Donation_Form.prototype.subtotal_exceeds_maximum = function() {
867 return '' !== CHARITABLE_VARS.maximum_donation && this.get_subtotal() > parseFloat( CHARITABLE_VARS.maximum_donation );
868 }
869
870 /**
871 * Make sure that the submitted amount is valid.
872 *
873 * @return boolean
874 */
875 Donation_Form.prototype.is_valid_amount = function() {
876 var minimum = parseFloat( CHARITABLE_VARS.minimum_donation );
877
878 if ( this.subtotal_exceeds_maximum() ) {
879 return false;
880 }
881
882 return minimum > 0 || CHARITABLE_VARS.permit_0_donation
883 ? this.get_subtotal() >= minimum
884 : this.get_subtotal() > minimum;
885 };
886
887 /**
888 * Validate the amount. If not valid, set an error.
889 *
890 * @return boolean
891 */
892 Donation_Form.prototype.validate_amount = function() {
893 if ( false === this.is_valid_amount() ) {
894 if ( this.get_subtotal() > 0 && this.subtotal_exceeds_maximum() ) {
895 this.add_error( CHARITABLE_VARS.error_max_exceeded );
896 } else {
897 this.add_error( CHARITABLE_VARS.error_invalid_amount );
898 }
899 return false;
900 }
901
902 return true;
903 };
904
905 /**
906 * Verify that all required fields are filled out.
907 *
908 * @return boolean
909 */
910 Donation_Form.prototype.validate_required_fields = function() {
911 var has_all_required_fields = true;
912
913 this.get_required_fields().each( function() {
914 if ( '' === $( this ).find( 'input, select, textarea' ).val() ) {
915 has_all_required_fields = false;
916 }
917 });
918
919 if ( ! has_all_required_fields ) {
920 this.add_error( CHARITABLE_VARS.error_required_fields );
921 }
922
923 return has_all_required_fields;
924 };
925
926 /**
927 * Verifies the submission and returns true if it all looks ok.
928 *
929 * @param this.form The submitted form.
930 * @return boolean
931 */
932 Donation_Form.prototype.validate = function() {
933 /* First clear out the errors. */
934 this.clear_errors();
935
936 this.validate_amount();
937
938 this.validate_required_fields();
939
940 this.form.trigger( 'charitable:form:validate', this );
941
942 return this.errors.length === 0;
943 };
944
945 exports.Donation_Form = Donation_Form;
946
947 })( CHARITABLE, jQuery );
948
949 /**
950 * Set up Toggle object.
951 */
952 ( function( exports, $ ){
953 var Toggle = function() {
954
955 /**
956 * Hide toggle target.
957 */
958 var hide_target = function() {
959 get_target( $(this) ).addClass( 'charitable-hidden' );
960 };
961
962 /**
963 * Get the target element.
964 */
965 var get_target = function( $el ) {
966 var target = $el.data('charitable-toggle');
967
968 if ( target[0] !== '.' && target[0] !== '#' ) {
969 target = '#' + target;
970 }
971
972 return $( target );
973 }
974
975 /**
976 * Toggle event handler for any fields with the [data-charitable-toggle] attribute.
977 *
978 * @access private
979 */
980 var on_toggle = function() {
981 var $this = $( this ),
982 $target = get_target( $this );
983
984 $target.toggleClass( 'charitable-hidden', ( function(){
985 if ( $this.is( ':checkbox' ) ) {
986 return ! $this.is( ':checked' );
987 }
988 return false === $target.hasClass( 'charitable-hidden' );
989 })()
990 );
991
992 return false;
993 };
994
995 /**
996 * Initializing function.
997 */
998 var init = function() {
999 $( '[data-charitable-toggle]' ).each( hide_target );
1000 };
1001
1002 // Initialization only required once.
1003 $( 'body' ).on( 'click', '[data-charitable-toggle]', on_toggle );
1004
1005 // Check whether content is being loaded from the session.
1006 if ( exports.content_loading ) {
1007 var timer = setInterval(function(){
1008 if ( false === exports.content_loading ) {
1009 init();
1010 clearInterval(timer);
1011 }
1012 }, 500);
1013 }
1014
1015 // Initialization that will be performed everytime
1016 return init;
1017 }
1018
1019 exports.Toggle = Toggle();
1020
1021 })( CHARITABLE, jQuery );
1022
1023
1024 /**
1025 * Do a version check.
1026 *
1027 * @since 1.6.19
1028 *
1029 * @param string version The version we are comparing with.
1030 * @param string compare If compare is left empty, use the Charitable version.
1031 * @return integer|boolean
1032 */
1033 CHARITABLE.VersionCompare = function( version, compare ) {
1034 compare = compare || CHARITABLE_VARS.version;
1035
1036 if ( typeof version + typeof compare != 'stringstring')
1037 return false;
1038
1039 var a = version.split( '.' ),
1040 b = compare.split( '.' ),
1041 i = 0,
1042 len = Math.max( a.length, b.length );
1043
1044 for ( ; i < len; i++ ) {
1045 if ((a[i] && !b[i] && parseInt(a[i]) > 0) || (parseInt(a[i]) > parseInt(b[i]))) {
1046 return 1;
1047 } else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) {
1048 return -1;
1049 }
1050 }
1051
1052 return 0;
1053 };
1054
1055 /**
1056 * Set up ResetSuggestedDonations object.
1057 */
1058 ( function( exports, $ ){
1059
1060 CHARITABLE.ResetSuggestedDonations = function() {
1061
1062 if ( $('.charitable-donation-options').length = 0 ) {
1063 // the options probably aren't on this page.
1064 return false;
1065 }
1066
1067 $( 'ul.donation-amounts input[type="radio"]' ).each( function() {
1068 if ( $( this ).is(':checked') ) {
1069 $( this ).closest( "li" ).addClass( "selected" );
1070 } else {
1071 $( this ).closest( "li" ).removeClass( "selected" );
1072 }
1073 });
1074
1075 }
1076
1077 })( CHARITABLE, jQuery );
1078
1079 /***
1080 * Finally queue up all the scripts.
1081 */
1082 ( function( $ ) {
1083
1084 /**
1085 * Prevent re-initializing form handlers.
1086 */
1087 CHARITABLE.forms_initialized = false;
1088
1089 /**
1090 * Move focus to a form's error summary so it is announced and reachable.
1091 *
1092 * Needed because role="alert" only covers half the problem. A live region
1093 * announces content that changes AFTER the region exists, so it works for the
1094 * summary charitable.js injects on a client-side validation failure, but a
1095 * summary rendered by PHP on a failed submit is already present when the page
1096 * loads and is therefore silent. Moving focus announces that case, and in both
1097 * cases it lands a keyboard user on the error rather than at the top of the page.
1098 *
1099 * Focus is deferred a tick because the node has usually just been inserted; taking
1100 * focus before layout settles is unreliable in Safari and Firefox.
1101 *
1102 * Declared inside this jQuery-bound closure rather than at file scope because
1103 * WordPress loads jQuery in noConflict mode, so `$` is not a global.
1104 *
1105 * WCAG 4.1.3 / 3.3.1.
1106 *
1107 * @since 1.8.13.1
1108 *
1109 * @param object $errors jQuery collection of error-summary nodes.
1110 */
1111 CHARITABLE.focus_error_summary = function( $errors ) {
1112 if ( ! $errors || ! $errors.length ) {
1113 return;
1114 }
1115
1116 var $summary = $errors.first();
1117
1118 // Templates carry tabindex="-1", but a theme override may predate it.
1119 if ( 'undefined' === typeof $summary.attr( 'tabindex' ) ) {
1120 $summary.attr( 'tabindex', '-1' );
1121 }
1122
1123 window.setTimeout( function() {
1124 var node = $summary.get( 0 );
1125
1126 if ( ! node ) {
1127 return;
1128 }
1129
1130 try {
1131 node.focus();
1132 } catch ( e ) {
1133 $summary.trigger( 'focus' );
1134 }
1135 }, 0 );
1136 };
1137
1138 $( document ).ready( function() {
1139
1140 $( 'html' ).addClass( 'js' );
1141
1142 var forms = [];
1143
1144 $( '.charitable-donation-form' ).each( function() {
1145 forms.push( new CHARITABLE.Donation_Form( $( this ) ) );
1146 });
1147
1148 CHARITABLE.Toggle();
1149
1150 CHARITABLE.ResetSuggestedDonations();
1151
1152 /*
1153 * A failed server-side submit re-renders the page with the error summary
1154 * already in the DOM, where role="alert" cannot announce it. Move focus so
1155 * assistive tech reads it and a keyboard user starts at the error.
1156 *
1157 * Gated on data-charitable-error-summary, NOT on the class. The class
1158 * .charitable-form-errors is shared by page-level notices that merely borrow
1159 * the error styling -- notably the admin-only "Test mode is active" notice,
1160 * which sits on an ordinary GET of every donation form. Matching the class
1161 * alone would steal focus on page load for any admin running test mode. The
1162 * attribute is emitted only for a genuine error summary, and in the generic
1163 * notices template only on a POST. See templates/form-fields/notices.php.
1164 *
1165 * WCAG 4.1.3 / 3.3.1, 1.8.13.1.
1166 */
1167 CHARITABLE.focus_error_summary( $( '.charitable-form-errors[data-charitable-error-summary]:visible' ) );
1168 });
1169
1170 })( jQuery );
1171