1/*jshint esversion: 6 */ 2(function($, ts) { 3 var payment = { 4 name: 'CRM.payment', 5 form: null, 6 submitButtons: null, 7 scripts: {}, 8 9 /** 10 * Get the total amount on the form 11 * @returns {number} 12 */ 13 getTotalAmount: function() { 14 var totalAmount = 0.0; 15 16 if (document.getElementById('totalAmount') && document.getElementById('totalAmount').getAttribute('data-totalAmount')) { 17 // See https://github.com/civicrm/civicrm-core/pull/32574 / https://lab.civicrm.org/extensions/stripe/-/issues/348 18 // Get the total amount from the lineItems list 19 totalAmount = parseFloat(document.getElementById('totalAmount').getAttribute('data-totalAmount')); 20 } 21 else if (this.getIsDrupalWebform()) { 22 // This is how webform civicrm calculates the amount in webform_civicrm_payment.js 23 $('.line-item:visible', '#wf-crm-billing-items').each(function() { 24 totalAmount += parseFloat($(this).data('amount')); 25 }); 26 } 27 else if (typeof calculateTotalFee == 'function') { 28 // This is ONLY triggered in the following circumstances on a CiviCRM contribution page: 29 // - With a priceset that allows a 0 amount to be selected. 30 // - When we are the ONLY payment processor configured on the page. 31 // It is ALSO replaced by percentagepricesetfield and extrafee extensions 32 totalAmount = parseFloat(calculateTotalFee()); 33 } 34 else if (document.getElementById('totalTaxAmount') !== null) { 35 totalAmount = this.calculateTaxAmount(); 36 this.debugging(this.name, 'Calculated amount using internal calculateTaxAmount()'); 37 } 38 else if ($("#priceset [price]").length > 0) { 39 // This is ONLY triggered in the following circumstances on a CiviCRM contribution page: 40 // - With a priceset that allows a 0 amount to be selected. 41 // - When we are the ONLY payment processor configured on the page. 42 $("#priceset [price]").each(function () { 43 totalAmount = totalAmount + $(this).data('line_raw_total'); 44 }); 45 } 46 else if (document.getElementById('total_amount')) { 47 // The input#total_amount field exists on backend contribution forms 48 totalAmount = parseFloat(document.getElementById('total_amount').value); 49 } 50 if (this.isEventAdditionalParticipants()) { 51 // The amount shown on the initial page is the total amount for 1 registration. 52 // It is "impossible" to calculate the total amount because we don't know what will be selected 53 // for each additional participant. 54 // Set totalAmount = null to force the use of a setupIntent 55 if (CRM.vars.cividiscount !== undefined && CRM.vars.cividiscount.discountApplied && CRM.vars.cividiscount.totalAmountZero) { 56 // Special case for CiviDiscount and 100% discount. We know that total will be zero for all participants 57 // Setting this to 0.0 means the stripe element will not be loaded or validated. 58 totalAmount = 0.0; 59 } 60 else { 61 // Force a setupIntent because we don't know the total amount 62 totalAmount = null; 63 } 64 } 65 this.debugging(this.name, 'getTotalAmount: ' + totalAmount); 66 return totalAmount; 67 }, 68 69 /** 70 * Round a monetary amount to a fixed number of decimal places the way 71 * PHP's round() does. 72 * 73 * Number.prototype.toFixed() rounds directly from the underlying 74 * binary floating point value, which does not always match decimal 75 * expectations for a value that lands exactly on a rounding boundary. 76 * Eg. $20 with a 7.625% tax rate gives a total of exactly $21.525, 77 * but a double can only store this as very slightly above or below 78 * that (here, 21.524999999999998), so (21.524999999999998).toFixed(2) 79 * gives "21.52" instead of "21.53". PHP's round() does not have this 80 * problem, so without correcting for it here, the amount a payment 81 * processor uses to create/confirm a payment client-side can end up 1 82 * cent different from the amount CiviCRM calculates server-side for 83 * the same transaction - and once a payment has been confirmed with
84 * the processor, some (eg. Stripe) refuse to let its amount be 85 * corrected afterwards, so the payment fails. 86 * @see https://lab.civicrm.org/extensions/stripe/-/work_items/510 87 * 88 * @param {number} amount 89 * @param {number} decimals 90 * @returns {string} 91 */ 92 roundMoney: function(amount, decimals) { 93 decimals = (typeof decimals === 'undefined') ? 2 : decimals; 94 95 // toFixed() gives a correctly-rounded decimal string of the 96 // underlying double (per spec) at high precision, recovering the 97 // "intended" value (eg. "21.52500000") that plain float rounding 98 // would otherwise lose to binary noise - eg. (21.525).toFixed(2) 99 // wrongly gives "21.52" because the nearest double to 21.525 is 100 // very slightly below it. 101 var str = amount.toFixed(decimals + 8); 102 var cut = str.indexOf('.') + decimals + 1; 103 var cents = Number(str.slice(0, cut).replace('.', '')); 104 if (str.charAt(cut) >= '5') { 105 cents += amount < 0 ? -1 : 1; 106 } 107 108 var digits = Math.abs(cents).toString(); 109 while (digits.length <= decimals) { 110 digits = '0' + digits; 111 } 112 var split = digits.length - decimals; 113 return (cents < 0 ? '-' : '') + digits.slice(0, split) + '.' + digits.slice(split); 114 }, 115 116 /** 117 * This is calculated in CRM/Contribute/Form/Contribution.tpl and is used to calculate the total 118 * amount with tax on backend submit contribution forms. 119 * The only way we can get the amount is by parsing the text field and extracting the final bit after the space. 120 * eg. "Amount including Tax: $ 4.50" gives us 4.50. 121 * The PHP side is responsible for converting money formats (we just parse to cents and remove any ,. chars). 122 * 123 * @returns {float} 124 */ 125 calculateTaxAmount: function() { 126 var totalTaxAmount = 0; 127 if (document.getElementById('totalTaxAmount') === null) { 128 return totalTaxAmount; 129 } 130 131 // If tax and invoicing is disabled totalTaxAmount div exists but is empty 132 if (document.getElementById('totalTaxAmount').textContent.length === 0) { 133 totalTaxAmount = document.getElementById('total_amount').value; 134 } 135 else { 136 // Otherwise totalTaxAmount div contains a textual amount including currency symbol 137 138 // Use the "separator" variable declared in Contribution.tpl or default to . for decimal point 139 var dPoint = (typeof separator !== 'undefined') && separator || '.'; 140 141 // Regular expression to comb for numeric parts of the totalTaxAmount div. 142 var matcher = new RegExp('\\d{1,3}(' + dPoint.replace(/\W/g, '\\$&') + '\\d{0,2})?', 'g'); 143 144 // Join all parts and ensure the decimal point is per javascript format. 145 totalTaxAmount = document.getElementById('totalTaxAmount').textContent.match(matcher).join('').replace(dPoint, '.'); 146 } 147 totalTaxAmount = parseFloat(totalTaxAmount); 148 if (isNaN(totalTaxAmount)) { 149 totalTaxAmount = 0.0; 150 } 151 return totalTaxAmount; 152 }, 153 154 /** 155 * Get currency on the form 156 * @param defaultCurrency 157 * @returns {string} 158 */ 159 getCurrency: function(defaultCurrency) { 160 var currency = defaultCurrency; 161 if (this.form.querySelector('#currency')) { 162 currency = this.form.querySelector('#currency').value; 163 } 164 this.debugging(this.name, 'Currency is: ' + currency); 165 return currency; 166 }, 167 168 /** 169 * Is the event registering additional participants (this means we do not know the full amount) 170 * 171 * @returns {boolean} 172 */ 173 isEventAdditionalParticipants: function() { 174 if ((document.getElementById('additional_participants') !== null) && 175 (document.getElementById('additional_participants').value.length !== 0)) { 176 this.debugging(this.name, 'Event has additional participants'); 177 return true; 178 } 179 return false; 180 }, 181 182 /** 183 * Are we currently loaded on a drupal webform? 184 * 185 * @returns {boolean} 186 */ 187 getIsDrupalWebform: function() { 188 // form class for drupal webform: webform-client-form (drupal 7); webform-submission-form (drupal 8) 189 if (this.form !== null) { 190 return this.form.classList.contains('webform-client-form') || this.form.classList.contains('webform-submission-form'); 191 } 192 return false;
193 }, 194 195 /** 196 * Get the Billing Form as a DOM/HTMLElement. 197 * Also set the "form" property on CRM.payment. 198 * 199 * @returns {HTMLElement} 200 */ 201 getBillingForm: function() { 202 // If we have a billing form on the page with our processor 203 var billingFormID = $('div#crm-payment-js-billing-form-container').closest('form').attr('id'); 204 if ((typeof billingFormID === 'undefined') || (!billingFormID.length)) { 205 // If we have multiple payment processors to select and we are not currently loaded 206 billingFormID = $('input[name=hidden_processor]').closest('form').prop('id'); 207 } 208 if (typeof billingFormID === 'undefined' || (!billingFormID.length)) { 209 billingFormID = $('div#billing-payment-block').closest('form').prop('id'); 210 } 211 if (typeof billingFormID === 'undefined' || (!billingFormID.length)) { 212 this.debugging(this.name, 'no billing form'); 213 this.form = null; 214 return this.form; 215 } 216 // We have to use document.getElementById here so we have the right elementtype for appendChild() 217 this.form = document.getElementById(billingFormID); 218 return this.form; 219 }, 220 221 /** 222 * Get all the billing submit buttons on the form as DOM elements 223 * Also set the "submitButtons" property on CRM.payment. 224 * 225 * @returns {NodeList} 226 */ 227 getBillingSubmit: function() { 228 if (CRM.payment.getIsDrupalWebform()) { 229 this.submitButtons = this.form.querySelectorAll('[type="submit"].webform-submit'); 230 if (this.submitButtons.length === 0) { 231 // drupal 8 webform 232 this.submitButtons = this.form.querySelectorAll('[type="submit"].webform-button--submit'); 233 } 234 } 235 else { 236 this.submitButtons = this.form.querySelectorAll('[type="submit"].validate'); 237 } 238 if (this.submitButtons.length === 0) { 239 this.debugging(this.name, 'No submit button found!'); 240 } 241 return this.submitButtons; 242 }, 243 244 /** 245 * Are we creating a recurring contribution? 246 * @returns {boolean} 247 */ 248 getIsRecur: function() { 249 if (!this.supportsRecur()) { 250 return false; 251 } 252 var isRecur = false;
253 // Auto-renew contributions for CiviCRM Webforms. 254 if (this.getIsDrupalWebform()) { 255 if (($('input[data-civicrm-field-key$="contribution_installments"]').length !== 0 && $('input[data-civicrm-field-key$="contribution_installments"]').val() != 1) || 256 ($('input[data-civicrm-field-key$="contribution_frequency_interval"]').length !== 0 && $('input[data-civicrm-field-key$="contribution_frequency_interval"]').val() > 0) 257 ) { 258 isRecur = true; 259 } 260 } 261 262 var membershipAutoRenewCheckBox = document.getElementById('auto_renew'); 263 var isRecurCheckBox = document.getElementById('is_recur'); 264 // Auto-renew contributions 265 if (isRecurCheckBox !== null) { 266 if (isRecurCheckBox.type == 'hidden') { 267 isRecur = (isRecurCheckBox.value == 1); 268 } 269 else { 270 isRecur = Boolean(isRecurCheckBox.checked); 271 } 272 } 273 // Auto-renew memberships 274 else if (membershipAutoRenewCheckBox !== null) { 275 isRecur = Boolean(membershipAutoRenewCheckBox.checked); 276 } 277 if (!isRecur) { 278 // multi-installment pledges are also recurring.... 279 var is_pledge = $('input[name="is_pledge"]:checked'); 280 isRecur = is_pledge.length === 1 && parseInt(is_pledge.val()) !== 0 && parseInt($('#pledge_installments').val()) > 1; 281 } 282 this.debugging(this.name, 'isRecur is ' + isRecur); 283 return isRecur; 284 }, 285 286 /** 287 * Does the form support recurring contributions? 288 * @returns {boolean} 289 */ 290 supportsRecur: function() { 291 var supportsRecur = false;
292 // Auto-renew contributions for CiviCRM Webforms. 293 if (this.getIsDrupalWebform()) { 294 if (($('input[data-civicrm-field-key$="contribution_installments"]').length !== 0) || 295 ($('input[data-civicrm-field-key$="contribution_frequency_interval"]').length !== 0) 296 ) { 297 supportsRecur = true; 298 } 299 } 300 // Auto-renew contributions 301 if (document.getElementById('is_recur') !== null) { 302 supportsRecur = true; 303 } 304 // Auto-renew memberships 305 // input[name="auto_renew"] : set to 1 when there is a force-renew membership with no priceset. 306 else if ($('input[name="auto_renew"]').length !== 0) { 307 supportsRecur = true; 308 } 309 else if ($('input[name=\'is_pledge\']').length !== 0) { 310 // Pledge payments 311 supportsRecur = true; 312 } 313 this.debugging(this.name, 'supportsRecur is ' + supportsRecur); 314 return supportsRecur; 315 }, 316 317 /** 318 * Try and get the billing email(s) from the form 319 * @returns {string} separated by ; 320 */ 321 getBillingEmail: function() { 322 var billingEmail = ''; 323 $(this.form).find('input[id^=email]').filter(':visible').each(function() { billingEmail += $(this).val() + ';'; }); 324 return billingEmail; 325 }, 326 327 /** 328 * Try and get the billing contact name from the form 329 * @returns {string} separated by ; 330 */ 331 getBillingName: function() { 332 var billingName = ''; 333 $(this.form).find('input#first_name,input#last_name').each(function() { billingName += $(this).val() + ';'; }); 334 return billingName; 335 }, 336 337 /** 338 * Get the selected payment processor on the form 339 * @returns {null|number} 340 */ 341 getPaymentProcessorSelectorValue: function() { 342 // Frontend radio selector 343 var paymentProcessorSelected = this.form.querySelector('input[name="payment_processor_id"]:checked'); 344 if (paymentProcessorSelected !== null) { 345 return parseInt(paymentProcessorSelected.value); 346 } 347 else { 348 // Backend select dropdown 349 paymentProcessorSelected = this.form.querySelector('select[name="payment_processor_id"]'); 350 if (paymentProcessorSelected !== null) { 351 return parseInt(paymentProcessorSelected.value); 352 } 353 } 354 return null; 355 }, 356 357 /** 358 * Is the AJAX request a payment form? 359 * @param {string} url 360 * @returns {bool} 361 */ 362 isAJAXPaymentForm: function(url) { 363 // /civicrm/payment/form? occurs when a payproc is selected on page 364 // /civicrm/contact/view/participant occurs when payproc is first loaded on event credit card payment 365 // On WordPress these are urlencoded 366 var patterns = [ 367 "(\/|%2F)payment(\/|%2F)form", 368 "(\/|\%2F)contact(\/|\%2F)view(\/|\%2F)participant", 369 "(\/|\%2F)contact(\/|\%2F)view(\/|\%2F)membership", 370 "(\/|\%2F)contact(\/|\%2F)view(\/|\%2F)contribution" 371 ]; 372 373 if (CRM.config.isFrontend && CRM.vars.payment && CRM.vars.payment.basePage !== 'civicrm') { 374 for (const pattern of patterns) { 375 if (url.match(CRM.vars.payment.basePage + pattern) !== null) { 376 return true; 377 } 378 } 379 } 380 for (const pattern of patterns) { 381 if (url.match('civicrm' + pattern) !== null) { 382 return true; 383 } 384 } 385 386 }, 387 388 /** 389 * Call this function before submitting the form to CiviCRM (if you ran setBillingFieldsRequiredForJQueryValidate()). 390 * The "name" parameter on a group of checkboxes where at least one must be checked must be the same or val
390idation will require all of them! 391 * Reset the name of the checkboxes before submitting otherwise CiviCRM will not get the checkbox values. 392 */ 393 resetBillingFieldsRequiredForJQueryValidate: function() { 394 $('div#priceset input[type="checkbox"], fieldset.crm-profile input[type="checkbox"], #on-behalf-block input[type="checkbox"]').each(function() { 395 if ($(this).attr('data-name') !== undefined) { 396 $(this).attr('name', $(this).attr('data-name')); 397 } 398 }); 399 }, 400 401 /** 402 * Call this function before running jQuery validation 403 * 404 * CustomField checkboxes in profiles do not get the "required" class. 405 * This should be fixed in CRM_Core_BAO_CustomField::addQuickFormElement but requires that the "name" is fixed as well. 406 */ 407 setBillingFieldsRequiredForJQueryValidate: function() { 408 $('div.label span.crm-marker').each(function() { 409 $(this).closest('div').next('div').find('input[type="checkbox"]').addClass('required'); 410 }); 411 412 // The "name" parameter on a set of checkboxes where at least one must be checked must be the same or val
412idation will require all of them! 413 // Checkboxes for custom fields are added as quickform "advcheckbox" which seems to require a unique name for each checkbox. But that breaks 414 // jQuery validation because each checkbox in a required group must have the same name. 415 // We store the original name and then change it. resetBillingFieldsRequiredForJQueryValidate() must be called before submit. 416 // Most checkboxes get names like: "custom_63[1]" but "onbehalf" checkboxes get "onbehalf[custom_63][1]". We change them to "custom_63" and "onbehalf[custom_63]". 417 $('div#priceset input[type="checkbox"], fieldset.crm-profile input[type="checkbox"], #on-behalf-block input[type="checkbox"]').each(function() { 418 var name = $(this).attr('name'); 419 if (name !== undefined) { 420 $(this).attr('data-name', name); 421 $(this).attr('name', name.replace('[' + name.split('[').pop(), '')); 422 $(this).removeAttr('required'); 423 $(this).removeClass('required'); 424 $(this).removeAttr('aria-required'); 425 } 426 }); 427 428 // Default email validator accepts test@example but on [email protected] is valid (https://jqueryvalidation.org/jQuery.validator.methods/) 429 $.validator.methods.email = function(value, element) { 430 // Regex from https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address 431 return this.optional(element) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value); 432 }; 433 }, 434 435 /** 436 * Drupal webform requires a custom element to determine how it should process the submission 437 * or if it should do another action. Because we trigger submit via javascript we have to add this manually. 438 * @param submitAction 439 */ 440 addDrupalWebformActionElement: function(submitAction) { 441 var hiddenInput = null; 442 if (document.getElementById('action') !== null) { 443 hiddenInput = document.getElementById('action'); 444 } 445 else { 446 hiddenInput = document.createElement('input'); 447 } 448 hiddenInput.setAttribute('type', 'hidden'); 449 hiddenInput.setAttribute('name', 'op'); 450 hiddenInput.setAttribute('id', 'action'); 451 hiddenInput.setAttribute('value', submitAction); 452 this.form.appendChild(hiddenInput); 453 }, 454 455 /** 456 * Delegate to standard form submission (we do not submit via our javascript processing). 457 * @returns {*} 458 */ 459 doStandardFormSubmit: function() { 460 // Disable the submit button to prevent repeated clicks 461 for (i = 0; i < this.submitButtons.length; ++i) { 462 this.submitButtons[i].setAttribute('disabled', true); 463 } 464 this.resetBillingFieldsRequiredForJQueryValidate(); 465 return this.form.submit(); 466 }, 467 468 /** 469 * Validate a reCaptcha if it exists on the form. 470 * Ideally we would use grecaptcha.getResponse() but the reCaptcha is already 471 * render()ed by CiviCRM so we don't have clientID and can't be sure we are 472 * checking the reCaptcha that is on our form. 473 * 474 * @returns {boolean} 475 */ 476 validateReCaptcha: function() { 477 if (typeof grecaptcha === 'undefined') { 478 // No reCaptcha library loaded 479 this.debugging(this.name, 'reCaptcha library not loaded'); 480 return true; 481 } 482 if ($(this.form).find('[name=g-recaptcha-response]').length === 0) { 483 // no reCaptcha on form - we check this first because there could be reCaptcha on another form on the same page that we don't want to validate 484 this.debugging(this.name, 'no reCaptcha on form'); 485 return true; 486 } 487 if ($(this.form).find('[name=g-recaptcha-response]').val().length > 0) { 488 // We can't use grecaptcha.getResponse because there might be multiple reCaptchas on the page and we might not be the first one. 489 this.debugging(this.name, 'recaptcha is valid'); 490 return true; 491 } 492 this.debugging(this.name, 'recaptcha active and not valid'); 493 $('div#card-errors').hide(); 494 this.swalFire({ 495 icon: 'warning', 496 text: '', 497 title: ts('Please complete the reCaptcha') 498 }, '.recaptcha-section', true); 499 this.triggerEvent('crmBillingFormNotValid');
500 this.form.dataset.submitted = 'false'; 501 return false; 502 }, 503 504 /** 505 * Validate if the discount code has been applied or the text-field is empty 506 * 507 * @returns {boolean} 508 */ 509 validateCiviDiscount: function() { 510 // cividiscount: when a code is applied it stays in the text-field 511 // If it is valid discount-applied attribute is 1, otherwise it's undefined. 512 // Logic: If we have a discountcode field, text in the discountcode field and the discount-applied attribute is not set do not submit form 513 if ($('input#discountcode').length && ($('input#discountcode').val().length > 0) && ($('input#discountcode').attr('discount-applied') != 1)) { 514 this.debugging(this.name,'Discount Code Entered but not applied'); 515 this.swalFire({ 516 icon: 'error', 517 text: ts('Please apply the Discount Code or clear the Discount Code text-field'), 518 title: '' 519 }, '#crm-container', true); 520 this.triggerEvent('crmBillingFormNotValid'); 521 this.form.dataset.submitted = 'false'; 522 return false; 523 } 524 return true; 525 }, 526 527 /** 528 * Check if form is valid (required fields filled in etc). 529 * @returns {boolean} 530 */ 531 validateForm: function() { 532 if (($(this.form).valid() === false) || $(this.form).data('crmBillingFormValid') === false) { 533 this.debugging(this.name, 'Form not valid'); 534 $('div#card-errors').hide(); 535 CRM.payment.swalFire({ 536 icon: 'error', 537 text: ts('Please check and fill in all required fields!'), 538 title: '' 539 }, '#crm-container', true); 540 CRM.payment.triggerEvent('crmBillingFormNotValid'); 541 CRM.payment.form.dataset.submitted = 'false'; 542 return false; 543 } 544 return true; 545 }, 546 547 /** 548 * Find submit buttons which should not submit payment 549 */ 550 addHandlerNonPaymentSubmitButtons: function() { 551 // Find submit buttons which should not submit payment 552 var nonPaymentSubmitButtons = this.form.querySelectorAll('[type="submit"][formnovalidate="1"], ' + 553 '[type="submit"][formnovalidate="formnovalidate"], ' + 554 '[type="submit"].cancel, ' + 555 '[type="submit"].webform-previous'), i; 556 for (i = 0; i < nonPaymentSubmitButtons.length; ++i) { 557 nonPaymentSubmitButtons[i].addEventListener('click', submitDontProcess(nonPaymentSubmitButtons[i])); 558 } 559 560 function submitDontProcess(element) { 561 CRM.payment.debugging(CRM.payment.scriptName, 'adding submitdontprocess: ' + element.id); 562 CRM.payment.form.dataset.submitdontprocess = 'true'; 563 } 564 }, 565 566 /** 567 * This adds handling for the CiviDiscount extension "apply" button. 568 * A better way should really be found. 569 */ 570 addSupportForCiviDiscount: function() { 571 // Add a keypress handler to set flag if enter is pressed 572 var cividiscountElements = this.form.querySelectorAll('input#discountcode'); 573 var cividiscountHandleKeydown = function(event) { 574 if (event.code === 'Enter') { 575 event.preventDefault(); 576 CRM.payment.debugging(this.name, 'adding submitdontprocess'); 577 CRM.payment.form.dataset.submitdontprocess = 'true'; 578 } 579 }; 580 581 for (i = 0; i < cividiscountElements.length; ++i) { 582 cividiscountElements[i].addEventListener('keydown', cividiscountHandleKeydown); 583 } 584 }, 585 586 /** 587 * Display an error for the payment element 588 * 589 * @param {string} errorMessage - the error string 590 * @param {boolean} notify - whether to popup a notification as well as 591 * display on the form. 592 */ 593 displayError: function(errorMessage, notify) { 594 // Display error.message in your UI. 595 this.debugging(this.name, 'error: ' + errorMessage); 596 // Inform the user if there was an error 597 var errorElement = document.getElementById('card-errors'); 598 errorElement.style.display = 'block'; 599 errorElement.textContent = errorMessage; 600 this.form.dataset.submitted = 'false'; 601 if (this.submitButtons !== null) { 602 for (i = 0; i < this.submitButtons.length; ++i) { 603 this.submitButtons[i].removeAttribute('disabled'); 604 } 605 } 606 this.triggerEvent('crmBillingFormNotValid'); 607 if (notify) { 608 this.swalClose(); 609 CRM.payment.swalFire({ 610 icon: 'error', 611 text: errorMessage, 612 title: '' 613 }, '#card-element', true); 614 } 615 }, 616 617 /** 618 * Wrapper around Swal.fire() 619 * @param {array} parameters 620 * @param {string} scrollToElement 621 * @param {boolean} fallBackToAlert 622 */ 623 swalFire: function(parameters, scrollToElement, fallBackToAlert) { 624 if (typeof Swal === 'function') { 625 if (scrollToElement.length > 0) { 626 parameters.didClose = function() { window.scrollTo($(scrollToElement).position()); }; 627 } 628 Swal.fire(parameters); 629 } 630 else if (fallBackToAlert) { 631 window.alert(parameters.title + ' ' + parameters.text); 632 } 633 }, 634 635 /** 636 * Wrapper around Swal.close() 637 */ 638 swalClose: function() { 639 if (typeof Swal === 'function') { 640 Swal.close(); 641 } 642 }, 643 644 /** 645 * Trigger a jQuery event 646 * @param {string} event 647 */ 648 triggerEvent: function(event, scriptName) { 649 var triggerNow = true; 650 if (typeof scriptName !== 'undefined') { 651 if (event === 'crmBillingFormReloadComplete') { 652 this.scripts[scriptName].reloadComplete = true; 653 $.each(CRM.payment.scripts, function(scriptName,scriptObject) { 654 if (scriptObject.reloadComplete !== true) { 655 triggerNow = false;
656 return; 657 } 658 }); 659 } 660 } 661 662 if (triggerNow) { 663 this.debugging((typeof scriptName !== 'undefined') ? scriptName : this.name, 'Firing Event: ' + event); 664 $(this.form).trigger(event); 665 } 666 else { 667 this.debugging((typeof scriptName !== 'undefined') ? scriptName : this.name, 'Waiting for other scripts (' + event + ')'); 668 } 669 }, 670 671 /** 672 * This should be called as soon as a script is executed so CRM.payment can handle multiple scripts on the DOM 673 * 674 * @param scriptName 675 */ 676 registerScript: function(scriptName) { 677 this.scripts[scriptName] = { reloadComplete: false }; 678 }, 679 680 /** 681 * Output debug information 682 * @param {string} scriptName 683 * @param {string} errorCode 684 */ 685 debugging: function(scriptName, errorCode) { 686 if ((typeof(CRM.vars.payment) !== 'undefined') && (Boolean(CRM.vars.payment.jsDebug) === true)) { 687 console.log(new Date().toISOString() + ' ' + scriptName + ': ' + errorCode); 688 } 689 } 690 691 }; 692 693 if (typeof CRM.payment === 'undefined') { 694 CRM.payment = payment; 695 } 696 else { 697 if (CRM.payment.hasOwnProperty('scriptName') && (CRM.payment.scriptName === 'CRM.payment')) { 698 return; 699 } 700 if (CRM.payment.hasOwnProperty('getTotalAmount')) { 701 delete(payment.getTotalAmount); 702 payment.debugging(payment.name, 'Deferring to client getTotalAmount function'); 703 } 704 $.extend(CRM.payment, payment); 705 } 706 707 document.addEventListener('DOMContentLoaded', function() { 708 CRM.payment.debugging('CRM.payment', 'loaded via DOMContentLoaded'); 709 CRM.payment.getBillingForm(); 710 }); 711 712 // Re-prep form when we've loaded a new payproc via ajax or via webform 713 $(document).ajaxComplete(function(event, xhr, settings) { 714 // /civicrm/payment/form? occurs when a payproc is selected on page 715 // /civicrm/contact/view/participant occurs when payproc is first loaded on event credit card payment 716 // On wordpress these are urlencoded 717 if (CRM.payment.isAJAXPaymentForm(settings.url)) { 718 CRM.payment.debugging('CRM.payment', 'triggered via ajax'); 719 CRM.payment.getBillingForm(); 720 // This resets the reload status of all scripts when CRM.payment is reloaded 721 $.each(CRM.payment.scripts, function(scriptName,scriptObject) { 722 CRM.payment.scripts[scriptName].reloadComplete = false; 723 }); 724 } 725 }); 726 727}(CRM.$, CRM.ts('mjwshared')));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.