PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 2.19.8
GiveWP – Donation Plugin and Fundraising Platform v2.19.8
4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / Views / Form / Templates / Sequoia / assets / js / form.js
give / src / Views / Form / Templates / Sequoia / assets / js Last commit date
form.js 4 years ago
form.js
976 lines
1 /* globals jQuery, Give */
2 (function ($) {
3 const templateL10n = window.sequoiaTemplateL10n;
4 const templateOptions = window.sequoiaTemplateOptions;
5 const $container = $('.give-embed-form');
6 const $advanceButton = $('.advance-btn', $container);
7 const $backButton = $('.back-btn');
8 const $navigatorTitle = $('.give-form-navigator .title');
9 const $paymentGatewayContainer = $('#give-payment-mode-select');
10 let gatewayAnimating = false;
11 const {wp: parentWp} = window.parent;
12
13 // Make donation form block selectable in editor
14 if (parentWp && parentWp.data) {
15 $container.on('click', function () {
16 const blockId = window.frameElement.closest('.wp-block').getAttribute('data-block');
17 parentWp.data.dispatch('core/block-editor').selectBlock(blockId);
18 });
19 }
20
21 // Attach the navigator to the window as an API for changing parts.
22 const navigator = (window.formNavigator = {
23 currentStep: templateOptions.introduction.enabled === 'enabled' ? 0 : 1,
24 animating: false,
25 firstFocus: false,
26 goToStep: (step) => {
27 // Adjust body height before animating step, to prevent choppy iframe resizing
28 // Compare next step to current step, and increase body height if next step is taller.
29 const nextStepHeight = steps[step].title
30 ? $(steps[step].selector).height() + 50
31 : $(steps[step].selector).height();
32 const currentStepHeight = steps[navigator.currentStep].title
33 ? $(steps[navigator.currentStep].selector).height() + 50
34 : $(steps[navigator.currentStep].selector).height();
35 if (nextStepHeight > currentStepHeight) {
36 $('.give-form-templates').css('min-height', `${nextStepHeight + 123}px`);
37 } else {
38 // Delay setting body height if next step is shorter than current step
39 setTimeout(function () {
40 $('.give-form-templates').css('min-height', `${nextStepHeight + 123}px`);
41 }, 200);
42 }
43
44 $('.step-tracker').removeClass('current');
45 $('.step-tracker[data-step="' + step + '"]').addClass('current');
46
47 // Handle introduction step (whether disabled or not).
48 if (templateOptions.introduction.enabled === 'disabled') {
49 if ($('.step-tracker').length === 3) {
50 $('.step-tracker').remove();
51 }
52
53 step = step > 0 ? step : 1;
54 if (step === 1) {
55 $('.back-btn', $container).hide();
56 } else {
57 $('.back-btn', $container).show();
58 }
59
60 $('.give-form-navigator', $container).addClass('nav-visible');
61 $(steps[step].selector).css('padding-top', '50px');
62 } else if (step === 0) {
63 // First step is enabled.
64 $('.give-form-navigator', $container).removeClass('nav-visible');
65 $(steps[step].selector).css('padding-top', '');
66 } else {
67 // Other steps besides intro and payment amounts:
68 $('.give-form-navigator', $container).addClass('nav-visible');
69 $(steps[step].selector).css('padding-top', '50px');
70 // Scroll to top after animation finished
71 // @see https://github.com/impress-org/givewp/issues/5969
72 scrollToIframeTop();
73 }
74
75 if (steps[step].title) {
76 $navigatorTitle.text(steps[step].title);
77 }
78
79 const hide = steps.map((obj, index) => {
80 if (index === step || index === navigator.currentStep) {
81 return null;
82 }
83 return obj.selector;
84 });
85 const hideSelector = hide.filter(Boolean).join(', ');
86
87 $(hideSelector).hide();
88
89 if (navigator.currentStep !== step) {
90 const directionClasses = 'slide-in-right slide-in-left slide-out-right slide-out-left';
91 const outDirection = navigator.currentStep < step ? 'left' : 'right';
92 const inDirection = navigator.currentStep < step ? 'right' : 'left';
93 $(steps[navigator.currentStep].selector)
94 .removeClass(directionClasses)
95 .addClass(`slide-out-${outDirection}`);
96 $(steps[step].selector).show().removeClass(directionClasses).addClass(`slide-in-${inDirection}`);
97 }
98 navigator.currentStep = step;
99
100 setTimeout(function () {
101 setupTabOrder();
102 // Do not auto-focus form on the page load if the first step is disabled
103 if (!navigator.firstFocus && templateOptions.introduction.enabled === 'disabled') {
104 return (navigator.firstFocus = true);
105 }
106 if (steps[navigator.currentStep].firstFocus) {
107 $(steps[navigator.currentStep].firstFocus).trigger('focus');
108 }
109 }, 200);
110 },
111 init: () => {
112 steps.forEach((step) => {
113 if (step.setup !== undefined) {
114 step.setup();
115 }
116 $(step.selector).css('position', 'absolute');
117 });
118 $advanceButton.on('click', function (e) {
119 e.preventDefault();
120 navigator.forward();
121 });
122 $backButton.on('click', function (e) {
123 e.preventDefault();
124 navigator.back();
125 });
126 $('.step-tracker').on('click', function (e) {
127 e.preventDefault();
128 navigator.goToStep(parseInt($(e.target).attr('data-step')));
129 });
130 setupHeightChangeCallback(function (height) {
131 if (gatewayAnimating === false) {
132 $('.form-footer').css('transition', 'margin-top 0.2s ease');
133 } else {
134 $('.form-footer').css('transition', '');
135 }
136 $('.form-footer').css('margin-top', `${height}px`);
137 });
138
139 // If Fee Recovery input is not inside any step, move it to above payment options
140 if ($('.give-fee-recovery-donors-choice').parent().hasClass('give-form')) {
141 $('#give_checkout_user_info').after($('.give-fee-recovery-donors-choice'));
142 }
143 navigator.goToStep(getInitialStep());
144 },
145 back: () => {
146 const prevStep = navigator.currentStep !== 0 ? navigator.currentStep - 1 : 0;
147 navigator.goToStep(prevStep);
148 navigator.currentStep = prevStep;
149 },
150 forward: () => {
151 const nextStep = navigator.currentStep !== null ? navigator.currentStep + 1 : 1;
152 navigator.goToStep(nextStep);
153 navigator.currentStep = nextStep;
154 },
155 });
156
157 const steps = [
158 {
159 id: 'introduction',
160 title: null,
161 selector: '.give-section.introduction',
162 label: templateOptions.introduction.donate_label,
163 showErrors: false,
164 tabOrder: ['.introduction .advance-btn', '.step-tracker'],
165 },
166 {
167 id: 'choose-amount',
168 title: templateOptions.payment_amount.header_label,
169 selector: '.give-section.choose-amount',
170 label: templateOptions.payment_amount.next_label,
171 showErrors: false,
172 tabOrder: [
173 'select.give-cs-select-currency',
174 'input.give-amount-top',
175 '.give-donation-levels-wrap button',
176 '.give-recurring-period',
177 '.give-recurring-donors-choice-period',
178 '.give_fee_mode_checkbox',
179 '.choose-amount .advance-btn',
180 '.step-tracker',
181 '.back-btn',
182 ],
183 firstFocus: '.give-default-level',
184 setup: () => {
185 // Dynamically set grid columns based on number of buttons
186 const buttonCount = $('.give-donation-level-btn').length;
187 if (buttonCount === 1) {
188 $('.give-donation-levels-wrap').attr('style', 'display: none!important;');
189 } else if (buttonCount % 2 === 0 && buttonCount < 6) {
190 $('.give-donation-levels-wrap').css('grid-template-columns', 'repeat(2, minmax(0, 1fr))');
191 }
192
193 $('#give-amount').on('blur', function () {
194 if (!Give.form.fn.isValidDonationAmount($('form'))) {
195 $('.advance-btn').attr('disabled', true);
196 } else {
197 $('.advance-btn').attr('disabled', false);
198 }
199 });
200 $('.give-donation-level-btn').each(function () {
201 const hasTooltip = $(this).attr('has-tooltip');
202 if (hasTooltip) {
203 return;
204 }
205
206 const value = $(this).attr('value');
207 const text = $(this).text();
208 const $form = $('form');
209 const symbol = Give.form.fn.getInfo( 'currency_symbol', $form );
210 const position = Give.form.fn.getInfo( 'currency_position', $form );
211
212 if (value !== 'custom') {
213 const html =
214 position === 'before'
215 ? `<div class="currency currency--before">${symbol}</div>${value}`
216 : `${value}<div class="currency currency--after">${symbol}</div>`;
217 $(this).html(html);
218 }
219
220 // Setup string to check tooltip label ga
221 const compare = position === 'before' ? symbol + value : value + symbol;
222 // Setup tooltip unless for custom donation level, or if level label matches value
223 if (value !== 'custom' && text !== compare) {
224 const wrap = document.createElement('span');
225 wrap.classList.add('give-tooltip', 'hint--top', 'hint--bounce');
226 if (text.length < 50) {
227 wrap.classList.add('narrow');
228 }
229 wrap.style.width = '100%';
230 wrap.setAttribute('aria-label', text.length < 50 ? text : text.substr(0, 50) + '...');
231 $(this).wrap(wrap);
232 $(this).attr('has-tooltip', true);
233 }
234 });
235 },
236 },
237 {
238 id: 'payment',
239 title: templateOptions.payment_information.header_label,
240 label: templateOptions.payment_information.checkout_label,
241 selector: '.give-section.payment',
242 showErrors: true,
243 tabOrder: [
244 '.payment input, .payment a, .payment button, .payment select, .payment multiselect, .payment textarea, .payment .button',
245 '.give-submit',
246 '.step-tracker',
247 '.back-btn',
248 ],
249 firstFocus: '#give-first',
250 setup: () => {
251 // Setup payment information screen
252
253 $('.give-section.payment').on(
254 'click',
255 '.give-cancel-login, .give-checkout-register-cancel',
256 clearLoginNotices
257 );
258
259 // Show Sequoia loader on click/touchend
260 $('.give-section.payment').on('click touchend', 'input[name="give_login_submit"]', function () {
261 //Override submit loader with Sequoia loader
262 $('input[name="give_login_submit"] + .give-loading-animation')
263 .removeClass('give-loading-animation')
264 .addClass('sequoia-loader spinning');
265 });
266
267 // Remove purchase_loading text
268 window.give_global_vars.purchase_loading = '';
269
270 // Move errors
271 $('.give_error').each(function () {
272 moveErrorNotice($(this));
273 });
274
275 // Setup anonymous donations opt-in event listeners
276 setupCheckbox({
277 container: '#give-anonymous-donation-wrap label',
278 label: '#give-anonymous-donation-wrap label',
279 input: '#give-anonymous-donation',
280 });
281
282 // Setup recurring donations opt-in event listeners
283 setupCheckbox({
284 container: '.give-recurring-donors-choice',
285 label: '.give-recurring-donors-choice label',
286 input: 'input[name="give-recurring-period"]',
287 });
288
289 // Setup fee recovery opt-in event listeners
290 setupCheckbox({
291 container: '.give-fee-recovery-donors-choice',
292 label: '.give-fee-message-label-text',
293 input: 'input[name="give_fee_mode_checkbox"]',
294 });
295
296 // Setup activecampaign opt-in event listeners
297 setupCheckbox({
298 container: '.give-activecampaign-fieldset',
299 label: '.give-activecampaign-optin-label',
300 input: '.give-activecampaign-optin-input',
301 });
302
303 // Setup mailchimp opt-in event listeners
304 setupCheckbox({
305 container: '.give-mailchimp-fieldset',
306 label: '.give-mc-message-text',
307 input: 'input[name="give_mailchimp_signup"]',
308 });
309
310 // Setup constant contact opt-in event listeners
311 setupCheckbox({
312 container: '.give-constant-contact-fieldset',
313 label: '.give-constant-contact-fieldset span',
314 input: 'input[name="give_constant_contact_signup"]',
315 });
316
317 // Setup terms and conditions opt-in event listeners
318 setupCheckbox({
319 container: '#give_terms_agreement',
320 label: '#give_terms_agreement label',
321 input: 'input[name="give_agree_to_terms"]',
322 });
323
324 // Show Sequoia loader on click/touchend
325 $('body.give-form-templates').on(
326 'click touchend',
327 'form.give-form input[name="give-purchase"].give-submit',
328 function () {
329 //Override submit loader with Sequoia loader
330 $('#give-purchase-button + .give-loading-animation')
331 .removeClass('give-loading-animation')
332 .addClass('sequoia-loader');
333
334 // Only show spinner if form is valid
335 if ($('form').get(0).checkValidity()) {
336 $('.sequoia-loader').addClass('spinning');
337 }
338 }
339 );
340
341 // Go to choose amount step when donation maximum error is clicked
342 $('body.give-form-templates').on('click touchend', '#give_error_invalid_donation_maximum', function () {
343 // Go to choose amount step
344 navigator.goToStep(1);
345 });
346
347 // Go to choose amount step when invalid donation error is clicked
348 $('body.give-form-templates').on('click touchend', '#give_error_invalid_donation_amount', function () {
349 // Go to choose amount step
350 navigator.goToStep(1);
351 });
352
353 // Setup gateway icons
354 setupGatewayIcons();
355
356 // Setup Form Field Manager input listeners
357 setupFFMInputs();
358
359 // Setup Icons for inputs
360 setupInputIcons();
361
362 const observer = new window.MutationObserver(function (mutations) {
363 mutations.forEach(function (mutation) {
364 if (!mutation.addedNodes) {
365 return;
366 }
367
368 for (let i = 0; i < mutation.addedNodes.length; i++) {
369 // do things to your newly added nodes here
370 const node = mutation.addedNodes[i];
371
372 if ($(node).find('.give_error').length > 0) {
373 moveErrorNotice($(node).find('.give_error'));
374 scrollToIframeTop();
375 }
376
377 if ($(node).children().hasClass('give_errors')) {
378 if (!$(node).parent().hasClass('donation-errors')) {
379 $(node)
380 .children('.give_errors')
381 .each(function () {
382 const notice = $(this);
383 moveErrorNotice(notice);
384 });
385 }
386 scrollToIframeTop();
387 }
388
389 if ($(node).hasClass('give_errors') && !$(node).parent().hasClass('donation-errors')) {
390 moveErrorNotice($(node));
391 $('.sequoia-loader').removeClass('spinning');
392 scrollToIframeTop();
393 }
394
395 if ($(node).attr('id') === 'give_tributes_address_state') {
396 const placeholder = $(node).attr('placeholder');
397 $(node).prepend(`<option selected disabled>${placeholder}</option>`);
398 }
399
400 if (
401 $(node).attr('name') === 'give_tributes_address_state' &&
402 $(node).attr('class').includes('give-input')
403 ) {
404 $(node).attr('placeholder', $(node).siblings('label').text().trim());
405 }
406
407 if ($(node).attr('id') && $(node).attr('id').includes('give-checkout-login-register')) {
408 setupRegistrationFormInputFields();
409 }
410
411 if ($(node).prop('tagName') && $(node).prop('tagName').toLowerCase() === 'select') {
412 const placeholder = $(node).attr('placeholder');
413 if (placeholder) {
414 $(node).prepend(`<option value="" disabled selected>${placeholder}</option>`);
415 }
416 }
417 }
418 });
419 });
420
421 observer.observe(document.body, {
422 childList: true,
423 subtree: true,
424 attributes: false,
425 characterData: false,
426 });
427 },
428 },
429 ];
430
431 navigator.init();
432
433 // Check if only a single gateway is enabled
434 if ($paymentGatewayContainer.length && $paymentGatewayContainer.css('display') !== 'none') {
435 // Move payment information section when document load.
436 moveFieldsUnderPaymentGateway();
437
438 // Move payment information section when gateway updated.
439 $(document).on('give_gateway_loaded', function () {
440 setTimeout(setupTabOrder, 200);
441
442 moveFieldsUnderPaymentGateway();
443 setupLegacyConsumerCheckboxAndRadio();
444 setupRegistrationFormInputFields();
445 setupFFMInputs();
446 setupInputIcons();
447 setupSelectInputs();
448
449 $('#give_purchase_form_wrap').slideDown(200, function () {
450 gatewayAnimating = false;
451 });
452 });
453 $(document).on('Give:onPreGatewayLoad', function () {
454 gatewayAnimating = true;
455 $('#give_purchase_form_wrap').slideUp(200);
456 });
457
458 // Clear gateway related errors
459 $(document).on('Give:onPreGatewayLoad', function () {
460 const persistedNotices = ['give_error_test_mode'];
461
462 $('.give_errors, .give_notices, .give_error').each(function () {
463 if (!persistedNotices.includes($(this).attr('id'))) {
464 $(this).slideUp(200, function () {
465 $(this).remove();
466 });
467 }
468 });
469 });
470 } else {
471 $('#give_purchase_form_wrap').addClass('give-single-gateway-wrap');
472 }
473
474 // Refresh personal information section.
475 $(document).on('give_gateway_loaded', refreshPersonalInformationSection);
476
477 // Setup fields.
478 setupLegacyConsumerCheckboxAndRadio();
479 setupSelectInputs();
480 setupRegistrationFormInputFields();
481 setupFFMInputs();
482 setupInputIcons();
483
484 /**
485 * Limited scope of optional input labels, specifically to User Info, see issue #5160.
486 */
487 setupOptionalInputLabels(Array.from(document.querySelectorAll('#give_checkout_user_info input[type="text"]')));
488
489 /**
490 * Denote non-required fields as optional.
491 *
492 * @since 2.8.0
493 *
494 * @param {array} inputs An iteratable list of input elements.
495 */
496 function setupOptionalInputLabels(inputs) {
497 inputs
498 .filter(function (input) {
499 return !input.required;
500 })
501 .map(function (input) {
502 input.placeholder += templateL10n.optionalLabel;
503 });
504 }
505
506 /**
507 * Move error notices to error notice container at the top of the payment section
508 * @since 2.7.0
509 * @since 2.12.0 Prevents a race condition that caused an error message not to be visible.
510 * @param {node} node The error notice node to be moved
511 */
512 function moveErrorNotice(node) {
513 //If the error is inside stripe payment button, do nothing
514 if ($(node).parent().hasClass('give-stripe-payment-request-button')) {
515 return;
516 }
517
518 // First check if specific donation notice container has been set up
519 if ($('.donation-errors').length === 0) {
520 $('.payment').prepend('<div class="donation-errors"></div>');
521 }
522
523 /**
524 * @link https://github.com/impress-org/givewp/issues/5867
525 *
526 * This 1 millisecond timeout prevents a race condition between
527 * the `.donation-errors` element being appended to the form
528 * and relocation of the specific error message node.
529 */
530 setTimeout(function () {
531 // If a specific notice does not already exist, proceed with moving the error
532 if (
533 typeof $('.donation-errors').html() !== undefined &&
534 !$('.donation-errors').html().includes($(node).html())
535 ) {
536 $(node).appendTo('.donation-errors');
537 } else {
538 // If the specific notice already exists, do not add it
539 $(node).remove();
540 }
541 }, 1);
542 }
543
544 /**
545 * Setup registration form input fields.
546 * @since 2.9.6
547 */
548 function setupRegistrationFormInputFields() {
549 const handleInput = function (evt) {
550 if (!$(evt.target).is('input')) {
551 return;
552 }
553
554 if ($(evt.target).hasClass('give-disabled')) {
555 return;
556 }
557
558 // Registration account fields only contains checkboxes.
559 $(evt.target).closest('label').toggleClass('checked');
560 };
561
562 $('[id*="give-register-account-fields"]').off('click', handleInput).on('click', handleInput);
563 }
564
565 /**
566 * Add listeners and starting states to FFM inputs
567 * @since 2.7.0
568 */
569 function setupFFMInputs() {
570 $('#give-ffm-section').off('click', handleFFMInput).on('click', handleFFMInput);
571
572 $('#give-ffm-section input').each(function () {
573 switch ($(this).prop('type')) {
574 case 'checkbox': {
575 if ($(this).prop('checked')) {
576 $(this).parent().addClass('checked');
577 } else {
578 $(this).parent().removeClass('checked');
579 }
580 break;
581 }
582 case 'radio': {
583 if ($(this).prop('checked')) {
584 $(this).parent().addClass('selected');
585 } else {
586 $(this).parent().removeClass('selected');
587 }
588 break;
589 }
590 }
591 });
592 }
593
594 /**
595 * Handle updating label classes for FFM radios and checkboxes
596 *
597 * @since 2.7.0
598 * @param {object} evt Reference to FFM input element click event
599 */
600 function handleFFMInput(evt) {
601 if ($(evt.target).is('input')) {
602 switch ($(evt.target).prop('type')) {
603 case 'checkbox': {
604 $(evt.target).closest('label').toggleClass('checked');
605 break;
606 }
607 case 'radio': {
608 $(evt.target).closest('label').addClass('selected');
609 $(evt.target).parent().siblings().removeClass('selected');
610 break;
611 }
612 }
613 }
614 }
615
616 /**
617 * Move form field under payment gateway
618 * @since 2.7.0
619 */
620 function moveFieldsUnderPaymentGateway() {
621 // Check if donate fieldset area has been created, if not set it up below payment gateways
622 // This area is necessary for correctly placing various elements (fee recovery notice, newsletters, submit button, etc)
623 if ($('#donate-fieldset').length === 0) {
624 $('.give-section.payment').append('<fieldset id="donate-fieldset"></fieldset>');
625 }
626
627 // Elements to move into donate fieldset (located at bottom of form)
628 // The elements will appear in order of array
629 const donateFieldsetElements = [
630 '.give-constant-contact-fieldset',
631 '.give-activecampaign-fieldset',
632 '.give-mailchimp-fieldset',
633 '#give_terms_agreement',
634 '.give-donation-submit',
635 ];
636
637 // Handle moving elements into donate fieldset
638 donateFieldsetElements.forEach(function (selector) {
639 if ($(`#donate-fieldset ${selector}`).length === 0) {
640 $(`#donate-fieldset ${selector}`).remove();
641 $('#donate-fieldset').append($(`#give_purchase_form_wrap ${selector}`));
642 } else if (
643 $(`#donate-fieldset ${selector}`).html() !== $(`#give_purchase_form_wrap ${selector}`).html() &&
644 $(`#give_purchase_form_wrap ${selector}`).html() !== undefined
645 ) {
646 $(`#donate-fieldset ${selector}`).remove();
647 $('#donate-fieldset').append($(`#give_purchase_form_wrap ${selector}`));
648 } else {
649 $(`#give_purchase_form_wrap ${selector}`).remove();
650 }
651 });
652
653 // Move purchase fields (credit card, billing, etc)
654 $('li.give-gateway-option-selected').after($('#give_purchase_form_wrap'));
655
656 // Add gateway class to fields wrapper, indicating which gateway is active
657 const gatewayClass = 'gateway-' + $('.give-gateway-option-selected input').attr('value').replace('_', '-');
658 $('#give_purchase_form_wrap').attr('class', gatewayClass);
659 }
660
661 /**
662 * Refresh personal information section
663 *
664 * @since 2.7.0
665 * @since 2.10.2 Rename function
666 * @param {boolean} ev Event object
667 * @param {object} response Response object
668 * @param {number} formID Form ID
669 */
670 function refreshPersonalInformationSection(ev, response, formID) {
671 if (navigator.currentStep === 2) {
672 $('.give-form-templates').css('min-height', '');
673 }
674
675 const $form = $(`#${formID}`);
676
677 // This function will run only for embed donation form.
678 // Show payment information section fields.
679 if ($form.parent().hasClass('give-embed-form')) {
680 const data = {
681 action: 'give_cancel_login',
682 form_id: $form.find('[name="give-form-id"]').val(),
683 };
684
685 // AJAX get the payment fields.
686 $.post(Give.fn.getGlobalVar('ajaxurl'), data, function (postResponse) {
687 const container = $form.find('[id^=give-checkout-login-register]');
688
689 // Remove existing personal information field if response contains new fields.
690 if (container.length && postResponse.fields.includes('give_checkout_user_info')) {
691 $form.find('#give_checkout_user_info').remove();
692 }
693
694 container.replaceWith($.parseJSON(postResponse.fields));
695 container.css({display: 'block'});
696
697 $form.find('.give-submit-button-wrap').show();
698 }).done(function () {
699 // Trigger float-labels
700 window.give_fl_trigger();
701
702 setupInputIcons();
703 });
704 }
705 }
706
707 function setupInputIcon(selector, icon) {
708 $(selector).each(function () {
709 if ($(this).html() !== '' && $(this).html().includes(`<i class="fas fa-${icon}"></i>`) === false) {
710 const property = isRTL() ? 'padding-right' : 'padding-left';
711 $(this).prepend(`<i class="fas fa-${icon}"></i>`);
712 $(this)
713 .children('input, selector')
714 .each(function () {
715 $(this).attr('style', property + ': 33px!important;');
716 });
717 }
718 });
719 }
720
721 function setupInputIcons() {
722 setupInputIcon('#give-first-name-wrap', 'user');
723 setupInputIcon('#give-email-wrap', 'envelope');
724 setupInputIcon('#give-company-wrap', 'building');
725 setupInputIcon('#date_field-wrap', 'calendar-alt');
726 setupInputIcon('#url_field-wrap', 'globe');
727 setupInputIcon('#phone_field-wrap', 'phone');
728 setupInputIcon('#give-phone-wrap', 'phone');
729 setupInputIcon('#email_field-wrap', 'envelope');
730 }
731
732 /**
733 * Setup tab order for elements in form
734 *
735 * @since 2.7.0
736 */
737 function setupTabOrder() {
738 $('select, button, input, textarea, multiselect, a').attr('tabindex', -1);
739
740 const tabOrder = steps[navigator.currentStep].tabOrder;
741 tabOrder.forEach((selector, index) => {
742 $(selector).attr('tabindex', index + 1);
743 });
744 }
745
746 /**
747 * Loop through gateway li elements and setup fontawesome icons
748 *
749 * @since 2.7.0
750 */
751 function setupGatewayIcons() {
752 $('#give-gateway-radio-list li').each(function () {
753 const value = $('input', this).val();
754 let icon;
755 switch (value) {
756 case 'manual':
757 icon = 'fas fa-tools';
758 break;
759 case 'offline':
760 icon = 'fas fa-wallet';
761 break;
762 case 'paypal':
763 icon = 'fab fa-paypal';
764 break;
765 case 'stripe':
766 icon = 'far fa-credit-card';
767 break;
768 case 'stripe_checkout':
769 icon = 'far fa-credit-card';
770 break;
771 case 'stripe_sepa':
772 icon = 'fas fa-university';
773 break;
774 case 'stripe_ach':
775 icon = 'fas fa-university';
776 break;
777 case 'stripe_ideal':
778 icon = 'fas fa-university';
779 break;
780 case 'stripe_becs':
781 icon = 'fas fa-university';
782 break;
783 case 'paypalpro_payflow':
784 icon = 'far fa-credit-card';
785 break;
786 case 'paypal-commerce':
787 icon = 'far fa-credit-card';
788 break;
789 case 'stripe_google_pay':
790 icon = 'fab fa-google';
791 break;
792 case 'stripe_apple_pay':
793 icon = 'fab fa-apple';
794 break;
795 default:
796 icon = 'fas fa-hand-holding-heart';
797 break;
798 }
799 $(this).append(`<i class="${icon}"></i>`);
800 });
801 }
802
803 /**
804 * Setup prominent checkboxes (field api) (that use persistent borders on select)
805 *
806 * @since 2.14.0
807 */
808 function setupLegacyConsumerCheckboxAndRadio() {
809 const customCheckboxes = document.querySelectorAll('[data-field-type="checkbox"] input');
810 const customRadios = document.querySelectorAll('[data-field-type="radio"] input');
811 Array.from(customCheckboxes).forEach((el) => {
812 const uniqueInputSelector = `#${el.getAttribute('id')}`;
813 const uniqueLabelSelector = `label[for=${el.getAttribute('id')}]`;
814 setupCheckbox({
815 container: uniqueLabelSelector,
816 label: uniqueLabelSelector,
817 input: uniqueInputSelector,
818 });
819 });
820
821 Array.from(customRadios).forEach((el) => {
822 const uniqueInputSelector = `#${el.getAttribute('id')}`;
823 const uniqueLabelSelector = `label[for=${el.getAttribute('id')}]`;
824 setupRadio({
825 label: uniqueLabelSelector,
826 input: uniqueInputSelector,
827 });
828 });
829 }
830
831 /**
832 * Setup prominent checkboxes (that use persistent borders on select)
833 *
834 * @since 2.7.0
835 * @since 2.15.0 update click handler callback
836 * @param {object} args Argument object containing: container, label, input selectors
837 */
838 function setupCheckbox({container, label, input}) {
839 // If checkbox is opted in by default, add border on load
840 if ($(input).prop('checked') === true) {
841 $(container).addClass('active');
842 }
843
844 // Persist checkbox input border when selected
845 $(document).on('click', label, function (evt) {
846 if ('INPUT' === evt.target.nodeName) {
847 return;
848 }
849
850 $(container).toggleClass('active');
851 });
852 }
853
854 /**
855 * Handle updating label classes for FFM radios and checkboxes
856 *
857 * @since 2.7.0
858 * @since 2.15.0 update click handler callback
859 * @param {object} evt Reference to FFM input element click event
860 */
861 function setupRadio({label, input}) {
862 // If checkbox is opted in by default, add border on load
863 if ($(input).prop('checked') === true) {
864 $(label).addClass('active');
865 }
866
867 // Persist checkbox input border when selected
868 $(document).on('click', label, function (evt) {
869 if ('INPUT' === evt.target.nodeName) {
870 return;
871 }
872
873 $(evt.target.parentElement).find('label').not(evt.target).removeClass('active');
874 $(evt.target).toggleClass('active');
875 });
876 }
877
878 function setupHeightChangeCallback(callback) {
879 let lastHeight = 0;
880 function checkHeightChange() {
881 const selector = $(steps[navigator.currentStep].selector);
882 const changed = lastHeight !== $(selector).outerHeight();
883 if (changed) {
884 callback($(selector).outerHeight());
885 lastHeight = $(selector).outerHeight();
886 }
887 window.requestAnimationFrame(checkHeightChange);
888 }
889 window.requestAnimationFrame(checkHeightChange);
890 }
891
892 /**
893 * Get initial step to show donor.
894 *
895 * @since 2.7.0
896 * @returns {number} Step to start on
897 */
898 function getInitialStep() {
899 return Give.fn.getParameterByName('showDonationProcessingError') ||
900 Give.fn.getParameterByName('showFailedDonationError')
901 ? 2
902 : 0;
903 }
904
905 function clearLoginNotices() {
906 $('#give_error_must_log_in').remove();
907 }
908
909 /**
910 * Setup select inputs
911 *
912 * @since 2.7.0
913 */
914 function setupSelectInputs() {
915 if ($('select option[selected="selected"][value=""]').length > 0) {
916 $('select option[selected="selected"][value=""]').each(function () {
917 if ($(this).parent().siblings('label').length) {
918 $(this).text($(this).parent().siblings('label').text().replace('*', '').trim());
919 $(this).attr('disabled', true);
920 }
921 });
922 }
923 }
924
925 /**
926 * Check if document is RTL
927 *
928 * @since 2.8.0
929 * @return {boolean} Whether or not document is RTL
930 */
931 function isRTL() {
932 return $('html').attr('dir') === 'rtl';
933 }
934
935 /**
936 * Scroll to parent window to iframe top
937 *
938 * @since 2.9.0
939 */
940 function scrollToIframeTop() {
941 if ('parentIFrame' in window) {
942 window.parentIFrame.sendMessage({action: 'giveScrollIframeInToView'});
943 }
944 }
945 })(jQuery);
946
947 /**
948 * Support to FFM restore custom field value logic.
949 *
950 * FFM restore value of custom fields correctly but field state does not reflect correctly
951 * in donation form because we are using custom styling for Radio and Checkbox.
952 *
953 * Below code will add class to Checkbox and Radio label if checked.
954 *
955 * @since 2.15.0
956 */
957 document.addEventListener('readystatechange', function (evt) {
958 if (evt.target.readyState !== 'complete') {
959 return;
960 }
961
962 const customCheckboxes = document.querySelectorAll('[data-field-type="checkbox"] input');
963 const customRadios = document.querySelectorAll('[data-field-type="radio"] input');
964
965 const addActiveClass = (el) => {
966 if (el.checked) {
967 el.parentElement.classList.add('active');
968 } else {
969 el.parentElement.classList.remove('active');
970 }
971 };
972
973 customCheckboxes.forEach(addActiveClass);
974 customRadios.forEach(addActiveClass);
975 });
976