1// Milestone Extensions for EPiServer.Forms module 2// 3// Because EPiServerForms.js is largely hardcoded, the main approach to adding custom functionality is to 4// add event listeners to elements through the EPiServerForms jQuery object ($$epiforms) 5// 6// To extend functionality, add functions in the following manner: 7// (function ($) { 8// $(document).ready(function () { 9// // ... logic here 10// }); 11// })($$epiforms || $) 12 13// EXTENSIONS 14// Extends the original EPiServerForms.js with additional functionality 15 16(function ($) { 17 var _utilsSvc = epi.EPiServer.Forms.Utils, 18 _extensionSvc = epi.EPiServer.Forms.Extension, 19 _validationSvc = epi.EPiServer.Forms.Validation; 20 21 $(document).ready(function () { 22 23 var originalGetCustomElementValue = epi.EPiServer.Forms.Extension.getCustomElementValue; 24 25 var loqateUrl = 'https://api.addressy.com/EmailValidation/Interactive/Validate/v2/json3.ws'; 26 var loqateServiceKey = 'DG57-RP74-EN19-FX73'; 27 var loqateServiceTimeout = 15000; 28 29 // Extend the EpiForm JavaScript API 30 $.extend(true, epi.EPiServer.Forms, { 31 // Add ReCaptcha validator 32 Validators: { 33 "Milestone.Web.Main.Site.Areas.Blocks.FormSectionBlock.Validators.RecaptchaFormElementValidator": function () { 34 // Validation happens server-side, so client-side validation is always true 35 return { isValid: true }; 36 }, 37 "Milestone.Web.Main.Site.Areas.Blocks.FormSectionBlock.Validators.CountryCodeFormElementValidator": function (fieldName, fieldValue, validatorMetadata) { 38 // Validation happens server-side, so client-side validation is always true 39 if (fieldValue === "") { 40 return { isValid: true }; 41 } 42 43 if (fieldValue === "+") { 44 return { isValid: false, message: validatorMetadata.model.message }; 45 } 46 return { isValid: true }; 47 }, 48 "Milestone.Web.Main.Site.Areas.Blocks.FormSectionBlock.Validators.PhoneNumberFormElementValidator": function (fieldName, fieldValue, validatorMetadata) { 49 if (fieldValue === "") { 50 return { isValid: true }; 51 } 52 const countryCodeAndPhoneNumber = fieldValue.split('+'); 53 const phoneNumber = countryCodeAndPhoneNumber[1]; 54 55 var regex = new RegExp("^(\\d+[\\s.-]?\\d+)+$"); 56 if (regex.test(phoneNumber)) { 57 return { isValid: true }; 58 } 59 return { isValid: false, message: validatorMetadata.model.message }; 60 }, 61 }, 62 Extension: { 63 // OVERRIDE - Before submission, get a ReCaptcha token and validate email 64 onBeforeSubmit: function (currentForm) { 65 // If honeypot value has been set, reject the submission 66 if (this._isHoneypotSet(currentForm)) { 67 var $deferred = $.Deferred(); 68 $deferred.reject(); 69 return $deferred.promise(); 70 } 71 $(currentForm).find(".FormSubmitButton").prop("disabled", true); 72 $(currentForm).find(".msforms__submission-loader__container").removeClass("hide"); 73 74 // Set timestamp value of the respective hidden input elements. This is so that pardot forms get the required timestamp data. 75 // The event "formsSubmitted" is triggered too late for the new timestamp to be persisted, this is why timestamp data is handled here on the submit button click event. 76 var timestamps = document.getElementsByClassName("Form__Element--hidden-timestamp"); 77 var element = document.getElementsByClassName("FormChoice__Input FormChoice__Input--Checkbox FormChoice__Input--Consent-Checkbox msds-checkbox__input"); 78 79 if (timestamps.length > 0) { 80 var currentDateTime = new Date(); 81 var currentDateTimeString = currentDateTime.toISOString(); 82 timestamps.forEach((t) => { 83 t.value = currentDateTimeString; 84 }); 85 } 86
87 return this._validateEmail(currentForm).catch(function () { 88 $(currentForm).find(".FormSubmitButton").prop("disabled", false); 89 $(currentForm).find(".msforms__submission-loader__container").addClass("hide"); 90 var rejection = $.Deferred().reject(); 91 return rejection.promise(); 92 }); 93 }, 94 95 _isHoneypotSet: function (form) { 96 var $honeypotElement = $(form).find(".honeypot-form-field"); 97 98 if ($honeypotElement.length == 0) { 99 return false; 100 } 101 102 return $honeypotElement.val() !== ""; 103 }, 104 105 _validateEmail: function (form) { 106 var $deferred = $.Deferred(), 107 $emailElement = $(form).find(".Form__Element.msforms__email"); 108 109 // If we don't have an email element, do nothing 110 if ($emailElement.length == 0) { 111 $deferred.resolve(); 112 return $deferred.promise(); 113 } 114 115 // If the email value has not been validated client-side, reject 116 // Because onBeforeSubmit is called before validation of all fields, 117 // email validation may occur before normal validation (required, regex), 118 // which may lead to unexpected issues. 119 if (!$emailElement.hasClass("ValidationSuccess")) { 120 var isValid = validate($emailElement); 121 if (!isValid) { 122 scrollToElement($emailElement); 123 $deferred.reject(); 124 return $deferred.promise(); 125 } 126 } 127 128 this._checkForEmailValidity($emailElement).then(function () { 129 $deferred.resolve(); 130 }).catch(function () { 131 // Scroll to the email element, show validation error 132 $(".msforms__email__loqate-message").removeClass("hide"); 133 $(".msforms__email").addClass("msds-input--error"); 134 $(".msforms__email").removeClass("ValidationSuccess"); 135 $(".msforms__email").addClass("ValidationFail"); 136 scrollToElement($emailElement, 500); 137 $deferred.reject(); 138 }) 139 140 return $deferred.promise(); 141 }, 142 143 // Internal function used to validate an email using Loqate 144 // Copied from Static/Scripts/Form.js, modified to execute asynchronously 145 _checkForEmailValidity: function (element) { 146 147 var $deferred = $.Deferred(); 148 var email = element.find("input").val(); 149 var params = '?'; 150 params += "&Key=" + encodeURIComponent(loqateServiceKey); 151 params += "&Email=" + encodeURIComponent(email); 152 params += "&Timeout=" + encodeURIComponent(loqateServiceTimeout); 153 154 $.ajax({
155 url: loqateUrl + params, 156 type: "POST", 157 contentType: "'application/x-www-form-urlencoded'", 158 async: true, 159 data: {}, 160 success: function (data) { 161 // Test for an error 162 if (data.Items.length == 1 && typeof (data.Items[0].Error) != "undefined") { 163 $deferred.reject(); 164 } else { 165 // Check if there were any items found 166 if (data.Items.length == 0) { 167 $deferred.reject(); 168 } 169 else { 170 const item = data.Items[0]; 171 if (item.IsDisposableOrTemporary || item.IsComplainerOrFraudRisk) { 172 $deferred.reject(); 173 } else { 174 $deferred.resolve(); 175 } 176 } 177 } 178 }, 179 error: function (xhr, textStatus, error) { 180 console.log(xhr); 181 console.log(textStatus); 182 console.log(error); 183 $deferred.reject(); 184 } 185 186 }); 187 return $deferred.promise(); 188 }, 189 190 // OVERRIDE - Custom method for retrieving value from elements 191 getCustomElementValue: function ($element) { 192 if ($element.hasClass("msforms__recaptcha")) { 193 return grecaptcha.getResponse(); 194 } 195 196 // If element is a PhoneNumberElement, combine input fields to get a formatted value 197 if ($element.hasClass("msforms__phone-number-selector")) { 198 var countryCode = $('.msforms__phone-number-selector__country-code', $element).first().val() || "", 199 phoneNumber = $('.msforms__phone-number-selector__phone-number', $element).first().val() || ""; 200 201 //return an empty string if there is no country code selected and number phone entered. 202 if (countryCode.length == 0 && phoneNumber.length == 0) return ""; 203 204 // Prepend a "+", removing leading zeroes from country code, then combine 205 var value = "+" + countryCode.replace(/^0+/, '') + phoneNumber; 206 207 return value; 208 } 209 210 // Otherwise, use the original getCustomElementValue 211 return originalGetCustomElementValue.apply(this, [$element]); 212 } 213 } 214 }); 215 }); 216 217 // DISPLAY ON SUBMIT MESSAGES AFTER FORM SUBMISSION 218 // Episerver forms exposes the event "formsSubmitted". By listening for this, we can display the on-submit block 219 // when the user successfully submits a form, as well as scroll to the top of the form so the user sees the message. 220 $(".EPiServerForms").on("formsSubmitted", function (event) { 221 if (event.isFinalizedSubmission && event.isSuccess) { 222 // if msforms__confirmation-content exists, show it and hide the default episerver status message 223 var confirmationContent = $(".msforms__confirmation-content"); 224 if (confirmationContent.length != 0) { 225 $(".msforms__confirmation-content").removeClass("hide"); 226 $(".form-section-block__container").addClass("hide"); 227 scrollToTopOfForm(500); 228 return; 229 } 230 } 231 232 $(".Form__Status").removeClass("hide"); 233 scrollToTopOfForm(500); 234 });
235 236 $(".FormChoice__Input--Consent-Checkbox").change(function () { 237 var element = $(this)[0]; 238 var checked = element.checked; 239 element.value = checked ? "True" : "False"; 240 }); 241 242 // REMOVE CUSTOM MSDS CLASSES ON FORM RESET 243 // When the form is reset, we need to remove all msds error classes ourselves 244 $(".EPiServerForms").on("formsReset", function (e) { 245 $workingForm = e.target; 246 _extensionSvc.getAllDataElements($workingForm).each(function loopEachFormElementToClear(i, formElement) { 247 var $el = $(formElement); 248 $el.removeClass("msds-input--error"); 249 if ($el.hasClass("msds-text-area")) { 250 $el.find("textarea").removeClass("msds-text-area__text-input--error"); 251 } 252 253 }); 254 }); 255 256 // HIDE SPINNER ON SUBMISSION / SUBMISSION ERROR 257 $(".EPiServerForms").on("formsSubmitted formSubmittedError", function (e) { 258 $(".EPiServerForms").find(".FormSubmitButton").prop("disabled", false); 259 $(".EPiServerForms").find(".msforms__submission-loader__container").addClass("hide"); 260 }); 261 262 // ON FOCUS LOSS VALIDATION 263 // Unfortunately, epi.EPiServer.Forms does not expose client-side validation, and only validates on moving to the next step / submitting. 264 // To validate when elements lose focus, we add an onfocusout listener to all elements with .Form__Element which triggers our own validation function 265 // Because the validateElement and _getValidateMessage functions used by epi.EPiServer.Forms are not exposed, we have to redefine them here instead. 266 $(".Form__Element").on("focusout", function (e) { 267 // We don't want to trigger validation on the country code field. To avoid this, we check that it did not trigger the event 268 if ($(e.target).hasClass("msforms__phone-number-selector__country-code")) { 269 return; 270 } 271 validate($(this)); 272 }); 273 274 // ON NEXT STEP VALIDATION 275 // Validate all elements on on attempting to move to next step to apply MSDS classes 276 $(".EPiServerForms").on("formsStepValidating", function (e) { 277 $workingForm = e.target; 278 _extensionSvc.getAllDataElements($workingForm).each(function loopEachFormElementToClear(i, formElement) { 279 var $el = $(formElement); 280 if ($el.hasClass("Form__Element")) { 281 validate($el); 282 } 283 }); 284 }); 285 286 // Additional rules can be added here, e.g. to validate textbox and textarea on value changed 287 // SHOW MESSAGE ERROR ON FORM SUBMITTED WITH ERRORS 288 $(".EPiServerForms").on("formsSubmitted", function (event) { 289 if (!event.isSuccess) { 290 $(".Form__Status").removeClass("hide"); 291 } 292 }); 293 294 //Remove msds-select-input__select--no-selection when the user selects an option 295 $(".msds-select-input__select").bind("blur", function () { 296 if (this.value && this.value.length > 0) { 297 $(this).removeClass('msds-select-input__select--no-selection'); 298 } 299 }); 300 301 // UTILITY FUNCTIONS 302 function validate($element) { 303 var $workingForm = _utilsSvc.getWorkingFormFromInnerElement($element); 304 var workingFormGuid = _utilsSvc.getFormIdentifier($workingForm); 305 var workingFormInfo = epi.EPiServer.Forms[workingFormGuid]; 306 workingFormInfo.$workingForm = $workingForm; 307 return validateElement($element, workingFormInfo); 308 } 309 310 function validateElement(element, workingFormInfo) { 311 var $element = $(element), 312 $messageContainer = _getValidationMessage($element), 313 elementName = _utilsSvc.getElementName($element), 314 elementIdentifier = $element.attr("id") || elementName, 315 validators = _validationSvc.getElementValidators(workingFormInfo.ValidationInfo, elementIdentifier), 316 isValid = true; 317 318 // reset element's validation status 319 $element.removeClass("ValidationFail ValidationSuccess msds-input--error"); 320 321 // If element is a text area, remove validation error from child textarea 322 if ($element.hasClass("msds-text-area")) { 323 $element.find("textarea").removeClass("msds-text-area__text-input--error") 324 } 325 326 // If element is an email input, hide loqate error message and exlude free provider email message 327 if ($element.hasClass("msforms__email")) { 328 if ($element.hasClass("form-email-field-exclude-free-email-providers")) { 329 showHideFreeProviderEmailMessage(false); 330 } 331 $element.find(".msforms__email__loqate-message").addClass("hide"); 332 } 333 334 $messageContainer.hide(); 335 var elementValue = _utilsSvc.getElementValue($element); 336 337 if (workingFormInfo.DependencyInactiveElements.indexOf(elementName) < 0 && (validators instanceof Array) && validators.length > 0) {
338 var results = _validationSvc.validateFormValue(elementName, elementValue, validators), 339 invalidResultItems = $.grep(results, function (item) { 340 return item.isValid === false; 341 }), 342 targetElement = $.grep(workingFormInfo.ValidationInfo, function (item) { 343 return item.targetElementId === elementIdentifier || item.targetElementName === elementIdentifier; 344 })[0], 345 validatedElement = $("#" + targetElement.targetElementId); 346 347 if (invalidResultItems && invalidResultItems.length > 0) { 348 var messages = $.map(invalidResultItems, function (item) { 349 return item.message; 350 }); 351 352 validatedElement.attr("aria-invalid", "true"); 353 $element.addClass("ValidationFail"); 354 355 // Depending on element classes (e.g. msds-input, msds-textarea), set different error classes 356 if ($element.hasClass("msds-text-area")) { 357 $element.find("textarea").addClass("msds-text-area__text-input--error"); 358 } 359 else { 360 $element.addClass("msds-input--error"); 361 } 362 363 $messageContainer.text(messages.join(" ")).show(); 364 365 isValid = false; 366 } else { 367 368 //Check if we should exclude free provider email addresses when necessary 369 if ($element.hasClass("msforms__email") && $element.hasClass("form-email-field-exclude-free-email-providers")) { 370 if (window.validate.isNotEmail(elementValue)) { 371 showHideFreeProviderEmailMessage(true); 372 scrollToElement($element, 500); 373 isValid = false; 374 return; 375 } 376 } 377 378 $element.addClass("ValidationSuccess"); 379 validatedElement.attr("aria-invalid", "false"); 380 isValid = true; 381 } 382 } 383 384 _utilsSvc.raiseFormsEvent(workingFormInfo, { 385 type: "elementValidated", 386 isValid: isValid, 387 388 elementName: elementName, 389 elementValue: elementValue, 390 elementIdentifier: elementIdentifier, 391 element: element, 392 $messageContainer: $messageContainer 393 }); 394 return isValid; 395 } 396 397 function showHideFreeProviderEmailMessage(showMessage) { 398 if (showMessage) { 399 $(".msforms__email__free-email-provider-message").removeClass("hide"); 400 $(".msforms__email").addClass("msds-input--error"); 401 $(".msforms__email").removeClass("ValidationSuccess"); 402 $(".msforms__email").addClass("ValidationFail"); 403 } 404 else { 405 if (!$(".msforms__email__free-email-provider-message").hasClass("hide")) { 406 $(".msforms__email__free-email-provider-message").addClass("hide"); 407 } 408 } 409 } 410 411 function _getValidationMessage( /*Object*/ $element) { 412 // summary: 413 // Gets message container object by the given element (find by [data-f-validationerror] has same name with $element) 414 // $element: [Object] 415 // Current element as jQuery object 416 // returns: [Object] 417 // Message container as jQuery object 418 // tags: 419 // private 420 421 // try to find data-f-validationerror based on name attribute first 422 var elementName = _utilsSvc.getElementName($element); 423 var selector1 = _utilsSvc.stringFormat("{0}[data-epiforms-linked-name='{1}'], {0}[data-epiforms-linked-name='{2}']", 424 [".Form__Element__ValidationError", elementName, $element.attr("id")]); 425 var selector2 = _utilsSvc.stringFormat("{0}[data-f-linked-name='{1}'], {0}[data-f-linked-name='{2}']", 426 ["[data-f-validationerror]", elementName, $element.attr("id")]); 427 return $(selector1).add(selector2); 428 } 429 430 function scrollToTopOfForm(animationTime) { 431 // If floating header is shown, offset scroll position by its height 432 var header = $("#HeaderSection"); 433 var offset = header.hasClass("smalldevice") ? header.height() : 0; 434 435 $([document.documentElement, document.body]).animate({ 436 scrollTop: $(".form-section-block").offset().top - offset 437 }, animationTime); 438 } 439 440 function scrollToElement($element, animationTime) { 441 $([document.documentElement, document.body]).animate({ 442 scrollTop: $element.offset().top 443 }, animationTime); 444 } 445})($$epiforms || $); 446 447// GTM / TRACKING 448(function ($) { 449 450 $(document).ready(function () { 451 452 // Add GTM event for when form is submitted 453 $(".EPiServerForms").on("formsSubmitted", function (event) { 454 if (event.isFinalizedSubmission && event.isSuccess) { 455 // we have our custom dataLayer object, we could also have this code somewhere: window.dataLayer = window.dataLayer || []; and then use that 456 if (typeof dataLayer !== 'undefined') { 457 var currentForm = $(this).get(0); 458
459 // using dom native methods, to support wider range of browsers that don't support the 'dataset' 460 var categoryName = currentForm.getAttribute("data-gtm-formcategory"); 461 var formName = currentForm.getAttribute("data-gtm-formname"); 462 463 // TODO: Replace with push-to-GTM 464 dataLayer.push({ 465 'event': 'formSubmission', 466 'formCategory': categoryName, 467 'formName': formName 468 }); 469 } 470 } 471 }); 472 473 //Add the dependent List scripts if any 474 $('.EPiServerForms .dependent-select').each(function () { 475 var $select = $(this).find('select'); 476 var $primarySelect = $('#' + $select.data("dependent-on")); 477 478 //If any dependent dropdown list then update it on page load if for ex if the dependent select options has to be updated based on the primary select selection. 479 if ($primarySelect.val()) { 480 updateDependentSelect($primarySelect); 481 } 482 483 // connect the primary select item to the dependent select 484 $primarySelect.change(function () { 485 updateDependentSelect($(this)) 486 }); 487 }); 488 489 function updateDependentSelect($primarySelect) { 490 491 // Get the depedent select element 492 var allSelectDivs = Array.from(document.getElementsByClassName("msforms__select")); 493 var dependentSelectDiv = allSelectDivs.find(e => e.hasAttribute("data-dependent-option-items")); 494 var dependentSelectElement = dependentSelectDiv.getElementsByTagName("select")[0]; 495 496 // Get data for the dependent select's options from data attribute 497 var allDependentDataJsonString = dependentSelectDiv.dataset.dependentOptionItems; 498 var allDependentDataObject = JSON.parse(allDependentDataJsonString); 499 500 // Get default option's text from data attribute 501 var dependentSelectElementDefaultText = dependentSelectDiv.dataset.defaultOptionItemText; 502 503 // Filter option data, based on the currently primary select's value 504 var primaryValue = $primarySelect.val(); 505 var filteredDependentData = allDependentDataObject.filter((state) => state.ValueDependentOn === primaryValue); 506 507 // If dependent select already has children options remove them 508 while (dependentSelectElement.firstChild) { 509 dependentSelectElement.removeChild(dependentSelectElement.firstChild); 510 } 511 512 // One future vulnerabiliy might be that in case a new country that has states is added, it should also be added inside the condition below 513 if (["Australia", "Canada", "India", "United States", "Mexico"].indexOf(primaryValue) != -1) { 514 515 var updatedOptions = new DocumentFragment(); 516 517 var placeholderDeaultOption = document.createElement("option"); 518 placeholderDeaultOption.text = dependentSelectElementDefaultText; 519 placeholderDeaultOption.setAttribute("disabled", "disabled"); 520 dependentSelectElement.appendChild(placeholderDeaultOption); 521 dependentSelectElement.value = placeholderDeaultOption.value; 522 523 updatedOptions.appendChild(placeholderDeaultOption); 524 525 filteredDependentData.forEach((datapoint) => { 526 var optionElement = document.createElement("option"); 527 optionElement.setAttribute("data-f-default-value", true); 528 optionElement.setAttribute("data-f-datainput", true); 529 optionElement.setAttribute("data-relevant-to", primaryValue); 530 optionElement.value = datapoint.Value; 531 optionElement.text = datapoint.Caption; 532 updatedOptions.appendChild(optionElement); 533 }); 534 535 dependentSelectElement.appendChild(updatedOptions); 536 } 537 } 538 539 // Add more GTM events here, e.g. on element focus / focusout 540 });
541})($$epiforms || $);
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.