PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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 1.7.0.9 All 211 releases
← All changes | assets/js/charitable.js +281 -64 1.6.8 → 1.8.13 View file →
@@ -17,8 +17,15 @@
17 17 */
18 18 this.errors = [];
19 19
20 20 /**
21 + * Pending processes array.
22 + *
23 + * @access public
24 + */
25 + this.pending_processes = [];
26 +
27 + /**
21 28 * Form object.
22 29 *
23 30 * @access public
24 31 */
@@ -45,8 +52,15 @@
45 52 */
46 53 this.total = 0;
47 54
48 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 + /**
49 63 * Reference to this object.
50 64 *
51 65 * @access private
52 66 */
@@ -84,8 +98,23 @@
84 98 self.form.on( 'focus', 'input.custom-donation-input', on_focus_custom_amount );
85 99 };
86 100
87 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 + /**
88 117 * Focus event handler for changes to custom donation amount input field.
89 118 *
90 119 * @access private
91 120 */
@@ -94,8 +123,10 @@
94 123
95 124 if ( $.trim( unformatted ) > 0 ) {
96 125 $( this ).val( self.format_amount( unformatted ) );
97 126 }
127 +
128 + trigger_amount_change_events();
98 129 };
99 130
100 131 /**
101 132 * Select a donation amount.
@@ -111,13 +142,15 @@
111 142 }
112 143
113 144 $li.parents( '.charitable-donation-form' ).find( '.donation-amount.selected' ).removeClass( 'selected' );
114 145
115 - $li.addClass( 'selected' );
146 + $li.addClass( 'selected' ).find( '[name=donation_amount]' ).prop( 'checked', true );
116 147
117 148 if ( $li.hasClass( 'custom-donation-amount' ) ) {
118 149 $li.find( 'input.custom-donation-input' ).focus();
119 150 }
151 +
152 + trigger_amount_change_events();
120 153 };
121 154
122 155 /**
123 156 * Change event handler for payment gateway selector.
@@ -124,13 +157,29 @@
124 157 *
125 158 * @access private
126 159 */
127 160 var on_change_payment_gateway = function() {
161 + var selectedGateway = $(this).val();
162 +
128 163 self.hide_inactive_payment_methods();
129 - self.show_active_payment_methods( $(this).val() );
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 + }
130 172 };
131 173
132 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 + /**
133 182 * Submit event handler for donation form.
134 183 *
135 184 * @access private
136 185 */
@@ -147,17 +196,10 @@
147 196 helper.show_processing();
148 197
149 198 /* Validate the form submission before going further. */
150 199 if ( false === helper.validate() ) {
151 -
152 - helper.hide_processing();
153 -
154 - helper.print_errors();
155 -
156 - helper.scroll_to_top();
157 -
200 + helper.cancel_processing();
158 201 return false;
159 -
160 202 }
161 203
162 204 /* If processing has been paused, return false now. */
163 205 if ( false !== helper.pause_processing ) {
@@ -168,15 +210,37 @@
168 210 if ( 1 !== helper.form.data( 'use-ajax' ) ) {
169 211 return true;
170 212 }
171 213
172 - /* If we're still here, trigger the processing event. */
173 - $body.trigger( 'charitable:form:process', helper );
214 + /* Continue on to process the donation. */
215 + maybe_process( helper, function() {
216 + $body.trigger( 'charitable:form:process', helper );
217 + } );
174 218
175 219 return false;
176 220 };
177 221
178 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 + /**
179 243 * Process the donation.
180 244 *
181 245 * This is a callback for the 'charitable:form:process' event. It's called
182 246 * after validation has taken place.
@@ -184,9 +248,11 @@
184 248 * @param object Event
185 249 * @param object Donation_Form
186 250 */
187 251 var process_donation = function( event, helper ) {
252 +
188 253 var data = helper.get_data();
254 +
189 255 var form = helper.form;
190 256
191 257 /* Cancel the default Charitable action, but pass it along as the form_action variable */
192 258 data.action = 'make_donation';
@@ -202,37 +268,29 @@
202 268 xhrFields: {
203 269 withCredentials: true
204 270 },
205 271 success: function (response) {
272 + $body.trigger( 'charitable:form:processed', [ response, helper ] );
206 273
207 274 if ( response.success ) {
208 - window.location.href = response.redirect_to;
275 + maybe_process( helper, function() {
276 + window.location.href = response.redirect_to;
277 + } );
209 278 }
210 279 else {
211 - helper.hide_processing();
280 + helper.cancel_processing( response.errors );
212 281
213 - helper.print_errors( response.errors );
214 -
215 - helper.scroll_to_top();
216 -
217 282 if ( response.donation_id ) {
218 283 helper.set_donation_id( response.donation_id );
219 284 }
220 285 }
221 286 }
222 -
223 287 }).fail(function (response, textStatus, errorThrown) {
224 -
225 288 if ( window.console && window.console.log ) {
226 289 console.log( response );
227 290 }
228 291
229 - helper.hide_processing();
230 -
231 - helper.print_errors( [ CHARITABLE_VARS.error_unknown ] );
232 -
233 - helper.scroll_to_top();
234 -
292 + helper.cancel_processing( [ CHARITABLE_VARS.error_unknown ] );
235 293 });
236 294
237 295 return false;
238 296 }
@@ -259,8 +317,10 @@
259 317 self.hide_inactive_payment_methods();
260 318 self.form.on( 'change', '#charitable-gateway-selector input[name=gateway]', on_change_payment_gateway );
261 319 }
262 320
321 + self.form.on( 'click', '.change-donation', on_click_change_amount_link );
322 +
263 323 // Handle donation form submission
264 324 self.form.on( 'submit', {
265 325 helper : self,
266 326 }, on_submit );
@@ -301,8 +361,21 @@
301 361 return this.form.find( '[name=email]' ).val();
302 362 };
303 363
304 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 + /**
305 378 * Get the chosen suggested amount.
306 379 *
307 380 * Note that when no option has been selected, or a custom donation amount has been added,
308 381 * this will return 0.
@@ -327,12 +400,12 @@
327 400 *
328 401 * @return float
329 402 */
330 403 Donation_Form.prototype.get_custom_amount = function() {
331 - var input = this.form.find( '.charitable-donation-options.active .custom-donation-input' );
404 + var input = this.form.find( '.charitable-donation-options.active input.custom-donation-input,.charitable-donation-options.active input.custom-donation-amount' );
332 405
333 406 if ( 0 === input.length ) {
334 - input = this.form.find( '.custom-donation-input' );
407 + input = this.form.find( 'input.custom-donation-input' );
335 408 }
336 409
337 410 return accounting.unformat(
338 411 input.val(),
@@ -513,15 +586,41 @@
513 586 }).trim();
514 587 };
515 588
516 589 /**
517 - * Select a donation amount.
590 + * Unformat a currency string to a plain float.
518 591 *
519 - * @param int price
520 - * @return string
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
521 605 */
522 606 Donation_Form.prototype.unformat_amount = function( price ) {
523 - return Math.abs( parseFloat( accounting.unformat( price, CHARITABLE_VARS.currency_format_decimal_sep ) ) );
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 ) ) );
524 623 };
525 624
526 625 /**
527 626 * Add an error message.
@@ -553,15 +652,16 @@
553 652
554 653 /**
555 654 * Print the errors at the top of the form.
556 655 *
557 - * @param array
656 + * @param array
558 657 */
559 658 Donation_Form.prototype.print_errors = function( errors ) {
560 659 var e = errors || this.errors,
561 660 i = 0,
562 661 count = e.length,
563 - output = '';
662 + output = '',
663 + error_notice_donation_display = typeof CHARITABLE_VARS.error_notice_donation_display === 'undefined' ? 'top' : CHARITABLE_VARS.error_notice_donation_display;
564 664
565 665 if ( 0 === count ) {
566 666 return;
567 667 }
@@ -573,9 +673,14 @@
573 673 output += '<div class="charitable-form-errors charitable-notice"><ul class="errors"><li>';
574 674 output += e.join( '</li><li>' );
575 675 output += '</li></ul></div>';
576 676
577 - this.form.prepend( output );
677 + if ( error_notice_donation_display === 'bottom' ) {
678 + this.form.append( output );
679 + } else {
680 + this.form.prepend( output );
681 + }
682 +
578 683 }
579 684
580 685 /**
581 686 * Clear the errors and remove the printed errors.
@@ -588,8 +693,57 @@
588 693 }
589 694 }
590 695
591 696 /**
697 + * Return whether we are waiting for an asynchronous process to finish.
698 + *
699 + * @since 1.6.9
700 + *
701 + * @return boolean
702 + */
703 + Donation_Form.prototype.waiting = function() {
704 + return this.pending_processes.length > 0;
705 + }
706 +
707 + /**
708 + * Add a pending process.
709 + *
710 + * @since 1.6.9
711 + *
712 + * @param string Process identifier.
713 + * @return int Index of the process.
714 + */
715 + Donation_Form.prototype.add_pending_process = function( process ) {
716 + var index = this.pending_processes.indexOf( process );
717 + return -1 === index ? ( this.pending_processes.push( process ) - 1 ) : index;
718 + }
719 +
720 + /**
721 + * Remove a pending process by index.
722 + *
723 + * @since 1.6.9
724 + *
725 + * @param int Index of the process.
726 + * @return void
727 + */
728 + Donation_Form.prototype.remove_pending_process = function( index ) {
729 + this.pending_processes.splice( index, 1 );
730 + }
731 +
732 + /**
733 + * Remove a pending process by process name.
734 + *
735 + * @since 1.6.17
736 + *
737 + * @param string Name of the process.
738 + * @return void
739 + */
740 + Donation_Form.prototype.remove_pending_process_by_name = function( process ) {
741 + var index = this.pending_processes.indexOf( process );
742 + return -1 !== index && this.remove_pending_process( index );
743 + }
744 +
745 + /**
592 746 * Show that the donation form is processing.
593 747 */
594 748 Donation_Form.prototype.show_processing = function() {
595 749 this.form.find( '.charitable-form-processing' ).show();
@@ -610,8 +764,13 @@
610 764 /**
611 765 * Scroll to the top of the form.
612 766 */
613 767 Donation_Form.prototype.scroll_to_top = function() {
768 + if ( this.prevent_scroll_to_top ) {
769 + this.prevent_scroll_to_top = false;
770 + return;
771 + }
772 +
614 773 var $modal = this.form.parents( '.charitable-modal' );
615 774
616 775 if ( $modal.length ) {
617 776 $modal.scrollTop( 0 );
@@ -616,13 +775,29 @@
616 775 if ( $modal.length ) {
617 776 $modal.scrollTop( 0 );
618 777 }
619 778 else {
620 - window.scrollTo( this.form.position().left, this.form.position().top );
779 + window.scrollTo( this.form.offset().left, this.form.offset().top );
621 780 }
622 781 };
623 782
624 783 /**
784 + * Cancel donation processing.
785 + *
786 + * @param array errors Error messages to show.
787 + */
788 + Donation_Form.prototype.cancel_processing = function( errors ) {
789 + this.hide_processing();
790 + this.print_errors( errors );
791 + var error_notice_donation_display = typeof CHARITABLE_VARS.error_notice_donation_display === 'undefined' ? 'top' : CHARITABLE_VARS.error_notice_donation_display;
792 + if ( '' === error_notice_donation_display || 'top' === error_notice_donation_display ) {
793 + this.scroll_to_top();
794 + }
795 +
796 + this.form.trigger( 'charitable:form:cancelled', this );
797 + };
798 +
799 + /**
625 800 * Set the donation ID.
626 801 */
627 802 Donation_Form.prototype.set_donation_id = function( donation_id ) {
628 803 this.form.find( '[name=ID]' ).val( donation_id );
@@ -633,9 +808,8 @@
633 808 *
634 809 * @return object
635 810 */
636 811 Donation_Form.prototype.get_data = function() {
637 -
638 812 return this.form.serializeArray().reduce( function( obj, item ) {
639 813 if ( '[]' === item.name.slice( -2 ) ) {
640 814 var name = item.name.slice( 0, -2 );
641 815
@@ -675,16 +849,31 @@
675 849 return fields;
676 850 };
677 851
678 852 /**
853 + * Check whether the submitted amount is greater than the maximum permitted.
854 + *
855 + * @returns boolean
856 + */
857 + Donation_Form.prototype.subtotal_exceeds_maximum = function() {
858 + return '' !== CHARITABLE_VARS.maximum_donation && this.get_subtotal() > parseFloat( CHARITABLE_VARS.maximum_donation );
859 + }
860 +
861 + /**
679 862 * Make sure that the submitted amount is valid.
680 863 *
681 - * @return boolean
864 + * @return boolean
682 865 */
683 866 Donation_Form.prototype.is_valid_amount = function() {
684 867 var minimum = parseFloat( CHARITABLE_VARS.minimum_donation );
685 868
686 - return minimum > 0 ? this.get_subtotal() >= minimum : this.get_subtotal() >= minimum;
869 + if ( this.subtotal_exceeds_maximum() ) {
870 + return false;
871 + }
872 +
873 + return minimum > 0 || CHARITABLE_VARS.permit_0_donation
874 + ? this.get_subtotal() >= minimum
875 + : this.get_subtotal() > minimum;
687 876 };
688 877
689 878 /**
690 879 * Validate the amount. If not valid, set an error.
@@ -692,9 +881,13 @@
692 881 * @return boolean
693 882 */
694 883 Donation_Form.prototype.validate_amount = function() {
695 884 if ( false === this.is_valid_amount() ) {
696 - this.add_error( CHARITABLE_VARS.error_invalid_amount );
885 + if ( this.get_subtotal() > 0 && this.subtotal_exceeds_maximum() ) {
886 + this.add_error( CHARITABLE_VARS.error_max_exceeded );
887 + } else {
888 + this.add_error( CHARITABLE_VARS.error_invalid_amount );
889 + }
697 890 return false;
698 891 }
699 892
700 893 return true;
@@ -747,9 +940,8 @@
747 940 /**
748 941 * Set up Toggle object.
749 942 */
750 943 ( function( exports, $ ){
751 -
752 944 var Toggle = function() {
753 945
754 946 /**
755 947 * Hide toggle target.
@@ -818,42 +1010,64 @@
818 1010 exports.Toggle = Toggle();
819 1011
820 1012 })( CHARITABLE, jQuery );
821 1013
1014 +
822 1015 /**
823 - * Set up Charitable helper functions.
1016 + * Do a version check.
1017 + *
1018 + * @since 1.6.19
1019 + *
1020 + * @param string version The version we are comparing with.
1021 + * @param string compare If compare is left empty, use the Charitable version.
1022 + * @return integer|boolean
824 1023 */
825 -( function( exports, $ ) {
1024 +CHARITABLE.VersionCompare = function( version, compare ) {
1025 + compare = compare || CHARITABLE_VARS.version;
826 1026
827 - var Helpers = function() {
1027 + if ( typeof version + typeof compare != 'stringstring')
1028 + return false;
828 1029
829 - /**
830 - * Sanitize URLs.
831 - */
832 - this.sanitize_url = function( input ) {
1030 + var a = version.split( '.' ),
1031 + b = compare.split( '.' ),
1032 + i = 0,
1033 + len = Math.max( a.length, b.length );
833 1034
834 - var url = input.value.toLowerCase();
1035 + for ( ; i < len; i++ ) {
1036 + if ((a[i] && !b[i] && parseInt(a[i]) > 0) || (parseInt(a[i]) > parseInt(b[i]))) {
1037 + return 1;
1038 + } else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) {
1039 + return -1;
1040 + }
1041 + }
835 1042
836 - if ( !/^https?:\/\//i.test( url ) && url.length > 0 ) {
837 - url = 'http://' + url;
1043 + return 0;
1044 +};
838 1045
839 - input.value = url;
1046 +/**
1047 + * Set up ResetSuggestedDonations object.
1048 + */
1049 +( function( exports, $ ){
1050 +
1051 + CHARITABLE.ResetSuggestedDonations = function() {
1052 +
1053 + if ( $('.charitable-donation-options').length = 0 ) {
1054 + // the options probably aren't on this page.
1055 + return false;
1056 + }
1057 +
1058 + $( 'ul.donation-amounts input[type="radio"]' ).each( function() {
1059 + if ( $( this ).is(':checked') ) {
1060 + $( this ).closest( "li" ).addClass( "selected" );
1061 + } else {
1062 + $( this ).closest( "li" ).removeClass( "selected" );
840 1063 }
841 - }
1064 + });
842 1065
843 - };
1066 + }
844 1067
845 1068 })( CHARITABLE, jQuery );
846 1069
847 -/**
848 - * URL sanitization.
849 - *
850 - * This is provided for backwards compatibility.
851 - */
852 -CHARITABLE.SanitizeURL = function( input ) {
853 - CHARITABLE.Helpers.sanitize_url( input );
854 -};
855 -
856 1070 /***
857 1071 * Finally queue up all the scripts.
858 1072 */
859 1073 ( function( $ ) {
@@ -866,13 +1080,16 @@
866 1080 $( document ).ready( function() {
867 1081
868 1082 $( 'html' ).addClass( 'js' );
869 1083
1084 + var forms = [];
1085 +
870 1086 $( '.charitable-donation-form' ).each( function() {
871 - new CHARITABLE.Donation_Form( $( this ) );
1087 + forms.push( new CHARITABLE.Donation_Form( $( this ) ) );
872 1088 });
873 1089
874 1090 CHARITABLE.Toggle();
875 1091
1092 + CHARITABLE.ResetSuggestedDonations();
876 1093 });
877 1094
878 1095 })( jQuery );