PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 2.22.0
GiveWP – Donation Plugin and Fundraising Platform v2.22.0
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
977 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 setupInputIcons();
410 }
411
412 if ($(node).prop('tagName') && $(node).prop('tagName').toLowerCase() === 'select') {
413 const placeholder = $(node).attr('placeholder');
414 if (placeholder) {
415 $(node).prepend(`<option value="" disabled selected>${placeholder}</option>`);
416 }
417 }
418
419 if ($(node).find('.required[placeholder]').length) {
420 setupRequiredFieldPlaceholders(
421 Array.from($(node).get(0).querySelectorAll('.required[placeholder]'))
422 );
423 }
424 }
425 });
426 });
427
428 observer.observe(document.body, {
429 childList: true,
430 subtree: true,
431 attributes: false,
432 characterData: false,
433 });
434 },
435 },
436 ];
437
438 navigator.init();
439
440 // Check if only a single gateway is enabled
441 if ($paymentGatewayContainer.length && $paymentGatewayContainer.css('display') !== 'none') {
442 // Move payment information section when document load.
443 moveFieldsUnderPaymentGateway();
444
445 // Move payment information section when gateway updated.
446 $(document).on('give_gateway_loaded', function () {
447 setTimeout(setupTabOrder, 200);
448
449 moveFieldsUnderPaymentGateway();
450 setupLegacyConsumerCheckboxAndRadio();
451 setupRegistrationFormInputFields();
452 setupFFMInputs();
453 setupInputIcons();
454 setupSelectInputs();
455
456 $('#give_purchase_form_wrap').slideDown(200, function () {
457 gatewayAnimating = false;
458 });
459 });
460 $(document).on('Give:onPreGatewayLoad', function () {
461 gatewayAnimating = true;
462 $('#give_purchase_form_wrap').slideUp(200);
463 });
464
465 // Clear gateway related errors
466 $(document).on('Give:onPreGatewayLoad', function () {
467 const persistedNotices = ['give_error_test_mode'];
468
469 $('.give_errors, .give_notices, .give_error').each(function () {
470 if (!persistedNotices.includes($(this).attr('id'))) {
471 $(this).slideUp(200, function () {
472 $(this).remove();
473 });
474 }
475 });
476 });
477 } else {
478 $('#give_purchase_form_wrap').addClass('give-single-gateway-wrap');
479 }
480
481 // Refresh personal information section.
482 $(document).on('give_gateway_loaded', refreshPersonalInformationSection);
483
484 // Setup fields.
485 setupLegacyConsumerCheckboxAndRadio();
486 setupSelectInputs();
487 setupRegistrationFormInputFields();
488 setupFFMInputs();
489 setupInputIcons();
490 setupRequiredFieldPlaceholders(Array.from(document.querySelectorAll('.give-form .required[placeholder]')));
491
492 /**
493 * Denote required fields as required by adding a asterisks (*) prefix to placeholder.
494 *
495 * @since 2.21.2
496 *
497 * @param {array} inputs An iteratable list of input elements.
498 */
499 function setupRequiredFieldPlaceholders(inputs) {
500 inputs.map(function (input) {
501 if ('*' !== input.placeholder.trim().slice(-1)) {
502 input.placeholder = `${input.placeholder.trim()}*`;
503 }
504 });
505 }
506
507 /**
508 * Move error notices to error notice container at the top of the payment section
509 * @since 2.7.0
510 * @since 2.12.0 Prevents a race condition that caused an error message not to be visible.
511 * @param {node} node The error notice node to be moved
512 */
513 function moveErrorNotice(node) {
514 //If the error is inside stripe payment button, do nothing
515 if ($(node).parent().hasClass('give-stripe-payment-request-button')) {
516 return;
517 }
518
519 // First check if specific donation notice container has been set up
520 if ($('.donation-errors').length === 0) {
521 $('.payment').prepend('<div class="donation-errors"></div>');
522 }
523
524 /**
525 * @link https://github.com/impress-org/givewp/issues/5867
526 *
527 * This 1 millisecond timeout prevents a race condition between
528 * the `.donation-errors` element being appended to the form
529 * and relocation of the specific error message node.
530 */
531 setTimeout(function () {
532 // If a specific notice does not already exist, proceed with moving the error
533 if (
534 typeof $('.donation-errors').html() !== undefined &&
535 !$('.donation-errors').html().includes($(node).html())
536 ) {
537 $(node).appendTo('.donation-errors');
538 } else {
539 // If the specific notice already exists, do not add it
540 $(node).remove();
541 }
542 }, 1);
543 }
544
545 /**
546 * Setup registration form input fields.
547 * @since 2.9.6
548 */
549 function setupRegistrationFormInputFields() {
550 const handleInput = function (evt) {
551 if (!$(evt.target).is('input')) {
552 return;
553 }
554
555 if ($(evt.target).hasClass('give-disabled')) {
556 return;
557 }
558
559 // Registration account fields only contains checkboxes.
560 $(evt.target).closest('label').toggleClass('checked');
561 };
562
563 $('[id*="give-register-account-fields"]').off('click', handleInput).on('click', handleInput);
564 }
565
566 /**
567 * Add listeners and starting states to FFM inputs
568 * @since 2.7.0
569 */
570 function setupFFMInputs() {
571 $('#give-ffm-section').off('click', handleFFMInput).on('click', handleFFMInput);
572
573 $('#give-ffm-section input').each(function () {
574 switch ($(this).prop('type')) {
575 case 'checkbox': {
576 if ($(this).prop('checked')) {
577 $(this).parent().addClass('checked');
578 } else {
579 $(this).parent().removeClass('checked');
580 }
581 break;
582 }
583 case 'radio': {
584 if ($(this).prop('checked')) {
585 $(this).parent().addClass('selected');
586 } else {
587 $(this).parent().removeClass('selected');
588 }
589 break;
590 }
591 }
592 });
593 }
594
595 /**
596 * Handle updating label classes for FFM radios and checkboxes
597 *
598 * @since 2.7.0
599 * @param {object} evt Reference to FFM input element click event
600 */
601 function handleFFMInput(evt) {
602 if ($(evt.target).is('input')) {
603 switch ($(evt.target).prop('type')) {
604 case 'checkbox': {
605 $(evt.target).closest('label').toggleClass('checked');
606 break;
607 }
608 case 'radio': {
609 $(evt.target).closest('label').addClass('selected');
610 $(evt.target).parent().siblings().removeClass('selected');
611 break;
612 }
613 }
614 }
615 }
616
617 /**
618 * Move form field under payment gateway
619 * @since 2.7.0
620 */
621 function moveFieldsUnderPaymentGateway() {
622 // Check if donate fieldset area has been created, if not set it up below payment gateways
623 // This area is necessary for correctly placing various elements (fee recovery notice, newsletters, submit button, etc)
624 if ($('#donate-fieldset').length === 0) {
625 $('.give-section.payment').append('<fieldset id="donate-fieldset"></fieldset>');
626 }
627
628 // Elements to move into donate fieldset (located at bottom of form)
629 // The elements will appear in order of array
630 const donateFieldsetElements = [
631 '.give-constant-contact-fieldset',
632 '.give-activecampaign-fieldset',
633 '.give-mailchimp-fieldset',
634 '#give_terms_agreement',
635 '.give-donation-submit',
636 ];
637
638 // Handle moving elements into donate fieldset
639 donateFieldsetElements.forEach(function (selector) {
640 if ($(`#donate-fieldset ${selector}`).length === 0) {
641 $(`#donate-fieldset ${selector}`).remove();
642 $('#donate-fieldset').append($(`#give_purchase_form_wrap ${selector}`));
643 } else if (
644 $(`#donate-fieldset ${selector}`).html() !== $(`#give_purchase_form_wrap ${selector}`).html() &&
645 $(`#give_purchase_form_wrap ${selector}`).html() !== undefined
646 ) {
647 $(`#donate-fieldset ${selector}`).remove();
648 $('#donate-fieldset').append($(`#give_purchase_form_wrap ${selector}`));
649 } else {
650 $(`#give_purchase_form_wrap ${selector}`).remove();
651 }
652 });
653
654 // Move purchase fields (credit card, billing, etc)
655 $('li.give-gateway-option-selected').after($('#give_purchase_form_wrap'));
656
657 // Add gateway class to fields wrapper, indicating which gateway is active
658 const gatewayClass = 'gateway-' + $('.give-gateway-option-selected input').attr('value').replace('_', '-');
659 $('#give_purchase_form_wrap').attr('class', gatewayClass);
660 }
661
662 /**
663 * Refresh personal information section
664 *
665 * @since 2.7.0
666 * @since 2.10.2 Rename function
667 * @param {boolean} ev Event object
668 * @param {object} response Response object
669 * @param {number} formID Form ID
670 */
671 function refreshPersonalInformationSection(ev, response, formID) {
672 if (navigator.currentStep === 2) {
673 $('.give-form-templates').css('min-height', '');
674 }
675
676 const $form = $(`#${formID}`);
677
678 // This function will run only for embed donation form.
679 // Show payment information section fields.
680 if ($form.parent().hasClass('give-embed-form')) {
681 const data = {
682 action: 'give_cancel_login',
683 form_id: $form.find('[name="give-form-id"]').val(),
684 };
685
686 // AJAX get the payment fields.
687 $.post(Give.fn.getGlobalVar('ajaxurl'), data, function (postResponse) {
688 const container = $form.find('[id^=give-checkout-login-register]');
689
690 // Remove existing personal information field if response contains new fields.
691 if (container.length && postResponse.fields.includes('give_checkout_user_info')) {
692 $form.find('#give_checkout_user_info').remove();
693 }
694
695 container.replaceWith($.parseJSON(postResponse.fields));
696 container.css({display: 'block'});
697
698 $form.find('.give-submit-button-wrap').show();
699 }).done(function () {
700 // Trigger float-labels
701 window.give_fl_trigger();
702
703 setupInputIcons();
704 });
705 }
706 }
707
708 function setupInputIcon(selector, icon) {
709 $(selector).each(function () {
710 if ($(this).html() !== '' && $(this).html().includes(`<i class="fas fa-${icon}"></i>`) === false) {
711 const property = isRTL() ? 'padding-right' : 'padding-left';
712 $(this).prepend(`<i class="fas fa-${icon}"></i>`);
713 $(this)
714 .children('input, selector')
715 .each(function () {
716 $(this).attr('style', property + ': 33px!important;');
717 });
718 }
719 });
720 }
721
722 function setupInputIcons() {
723 setupInputIcon('#give-first-name-wrap', 'user');
724 setupInputIcon('#give-email-wrap', 'envelope');
725 setupInputIcon('#give-company-wrap', 'building');
726 setupInputIcon('#date_field-wrap', 'calendar-alt');
727 setupInputIcon('#url_field-wrap', 'globe');
728 setupInputIcon('#phone_field-wrap', 'phone');
729 setupInputIcon('#give-phone-wrap', 'phone');
730 setupInputIcon('#email_field-wrap', 'envelope');
731 }
732
733 /**
734 * Setup tab order for elements in form
735 *
736 * @since 2.7.0
737 */
738 function setupTabOrder() {
739 $('select, button, input, textarea, multiselect, a').attr('tabindex', -1);
740
741 const tabOrder = steps[navigator.currentStep].tabOrder;
742 tabOrder.forEach((selector, index) => {
743 $(selector).attr('tabindex', index + 1);
744 });
745 }
746
747 /**
748 * Loop through gateway li elements and setup fontawesome icons
749 *
750 * @since 2.7.0
751 */
752 function setupGatewayIcons() {
753 $('#give-gateway-radio-list li').each(function () {
754 const value = $('input', this).val();
755 let icon;
756 switch (value) {
757 case 'manual':
758 icon = 'fas fa-tools';
759 break;
760 case 'offline':
761 icon = 'fas fa-wallet';
762 break;
763 case 'paypal':
764 icon = 'fab fa-paypal';
765 break;
766 case 'stripe':
767 icon = 'far fa-credit-card';
768 break;
769 case 'stripe_checkout':
770 icon = 'far fa-credit-card';
771 break;
772 case 'stripe_sepa':
773 icon = 'fas fa-university';
774 break;
775 case 'stripe_ach':
776 icon = 'fas fa-university';
777 break;
778 case 'stripe_ideal':
779 icon = 'fas fa-university';
780 break;
781 case 'stripe_becs':
782 icon = 'fas fa-university';
783 break;
784 case 'paypalpro_payflow':
785 icon = 'far fa-credit-card';
786 break;
787 case 'paypal-commerce':
788 icon = 'far fa-credit-card';
789 break;
790 case 'stripe_google_pay':
791 icon = 'fab fa-google';
792 break;
793 case 'stripe_apple_pay':
794 icon = 'fab fa-apple';
795 break;
796 default:
797 icon = 'fas fa-hand-holding-heart';
798 break;
799 }
800 $(this).append(`<i class="${icon}"></i>`);
801 });
802 }
803
804 /**
805 * Setup prominent checkboxes (field api) (that use persistent borders on select)
806 *
807 * @since 2.14.0
808 */
809 function setupLegacyConsumerCheckboxAndRadio() {
810 const customCheckboxes = document.querySelectorAll('[data-field-type="checkbox"] input');
811 const customRadios = document.querySelectorAll('[data-field-type="radio"] input');
812 Array.from(customCheckboxes).forEach((el) => {
813 const uniqueInputSelector = `#${el.getAttribute('id')}`;
814 const uniqueLabelSelector = `label[for=${el.getAttribute('id')}]`;
815 setupCheckbox({
816 container: uniqueLabelSelector,
817 label: uniqueLabelSelector,
818 input: uniqueInputSelector,
819 });
820 });
821
822 Array.from(customRadios).forEach((el) => {
823 const uniqueInputSelector = `#${el.getAttribute('id')}`;
824 const uniqueLabelSelector = `label[for=${el.getAttribute('id')}]`;
825 setupRadio({
826 label: uniqueLabelSelector,
827 input: uniqueInputSelector,
828 });
829 });
830 }
831
832 /**
833 * Setup prominent checkboxes (that use persistent borders on select)
834 *
835 * @since 2.7.0
836 * @since 2.15.0 update click handler callback
837 * @param {object} args Argument object containing: container, label, input selectors
838 */
839 function setupCheckbox({container, label, input}) {
840 // If checkbox is opted in by default, add border on load
841 if ($(input).prop('checked') === true) {
842 $(container).addClass('active');
843 }
844
845 // Persist checkbox input border when selected
846 $(document).on('click', label, function (evt) {
847 if ('INPUT' === evt.target.nodeName) {
848 return;
849 }
850
851 $(container).toggleClass('active');
852 });
853 }
854
855 /**
856 * Handle updating label classes for FFM radios and checkboxes
857 *
858 * @since 2.7.0
859 * @since 2.15.0 update click handler callback
860 * @param {object} evt Reference to FFM input element click event
861 */
862 function setupRadio({label, input}) {
863 // If checkbox is opted in by default, add border on load
864 if ($(input).prop('checked') === true) {
865 $(label).addClass('active');
866 }
867
868 // Persist checkbox input border when selected
869 $(document).on('click', label, function (evt) {
870 if ('INPUT' === evt.target.nodeName) {
871 return;
872 }
873
874 $(evt.target.parentElement).find('label').not(evt.target).removeClass('active');
875 $(evt.target).toggleClass('active');
876 });
877 }
878
879 function setupHeightChangeCallback(callback) {
880 let lastHeight = 0;
881 function checkHeightChange() {
882 const selector = $(steps[navigator.currentStep].selector);
883 const changed = lastHeight !== $(selector).outerHeight();
884 if (changed) {
885 callback($(selector).outerHeight());
886 lastHeight = $(selector).outerHeight();
887 }
888 window.requestAnimationFrame(checkHeightChange);
889 }
890 window.requestAnimationFrame(checkHeightChange);
891 }
892
893 /**
894 * Get initial step to show donor.
895 *
896 * @since 2.7.0
897 * @returns {number} Step to start on
898 */
899 function getInitialStep() {
900 return Give.fn.getParameterByName('showDonationProcessingError') ||
901 Give.fn.getParameterByName('showFailedDonationError')
902 ? 2
903 : 0;
904 }
905
906 function clearLoginNotices() {
907 $('#give_error_must_log_in').remove();
908 }
909
910 /**
911 * Setup select inputs
912 *
913 * @since 2.7.0
914 */
915 function setupSelectInputs() {
916 if ($('select option[selected="selected"][value=""]').length > 0) {
917 $('select option[selected="selected"][value=""]').each(function () {
918 if ($(this).parent().siblings('label').length) {
919 $(this).text($(this).parent().siblings('label').text().replace('*', '').trim());
920 $(this).attr('disabled', true);
921 }
922 });
923 }
924 }
925
926 /**
927 * Check if document is RTL
928 *
929 * @since 2.8.0
930 * @return {boolean} Whether or not document is RTL
931 */
932 function isRTL() {
933 return $('html').attr('dir') === 'rtl';
934 }
935
936 /**
937 * Scroll to parent window to iframe top
938 *
939 * @since 2.9.0
940 */
941 function scrollToIframeTop() {
942 if ('parentIFrame' in window) {
943 window.parentIFrame.sendMessage({action: 'giveScrollIframeInToView'});
944 }
945 }
946 })(jQuery);
947
948 /**
949 * Support to FFM restore custom field value logic.
950 *
951 * FFM restore value of custom fields correctly but field state does not reflect correctly
952 * in donation form because we are using custom styling for Radio and Checkbox.
953 *
954 * Below code will add class to Checkbox and Radio label if checked.
955 *
956 * @since 2.15.0
957 */
958 document.addEventListener('readystatechange', function (evt) {
959 if (evt.target.readyState !== 'complete') {
960 return;
961 }
962
963 const customCheckboxes = document.querySelectorAll('[data-field-type="checkbox"] input');
964 const customRadios = document.querySelectorAll('[data-field-type="radio"] input');
965
966 const addActiveClass = (el) => {
967 if (el.checked) {
968 el.parentElement.classList.add('active');
969 } else {
970 el.parentElement.classList.remove('active');
971 }
972 };
973
974 customCheckboxes.forEach(addActiveClass);
975 customRadios.forEach(addActiveClass);
976 });
977