PageSourceSearch

https://www.milestonesys.com/Areas/Blocks/FormSectionBlock/Frontend/FormSectionBlock.js

js milestonesys.com collected 2026-10-01 09:13:58 UTC 26,913 bytes, 541 lines download raw bytes

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.