1(function () { 2 // polyfill for matches and closest 3 if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector; 4 if (!Element.prototype.closest) Element.prototype.closest = function (selector) { 5 var el = this; 6 while (el) { 7 if (el.matches(selector)) { 8 return el; 9 } 10 el = el.parentElement; 11 } 12 }; 13 14 // Global collection to store initialized forms to prevent duplicate initialization 15 var initializedForms = new WeakSet(); 16 17 //execute init() on document ready 18 document.addEventListener("DOMContentLoaded", () => { 19 listen(document); 20 }); 21 22 //execute init() on dynamic injection 23 document.addEventListener("umbracoFormsReinitialize", (event) => { 24 var targetElement = event.detail && event.detail.element ? event.detail.element : document; 25 listen(targetElement); 26 }); 27 28 window.UmbracoForms = window.UmbracoForms || {}; 29 window.UmbracoForms.reinitialize = listen; 30 31 function listen(targetElement) { 32 targetElement = targetElement || document; 33 34 // Strip initialization markers from forms not yet tracked in the WeakSet, 35 // so dynamically injected or cloned forms can be properly initialized. 36 var formElements = targetElement.querySelectorAll 37 ? targetElement.querySelectorAll('.umbraco-forms-form') 38 : []; 39 for (var j = 0; j < formElements.length; j++) { 40 if (!initializedForms.has(formElements[j])) { 41 // Remove markers from the form element itself (e.g. accessibility init is set on the form). 42 delete formElements[j].dataset.umbracoFormsInitialized; 43 delete formElements[j].dataset.umbracoFormsAccessibilityInitialized; 44 45 // Remove markers from descendant elements (e.g. buttons, inputs). 46 var markedElements = formElements[j].querySelectorAll( 47 '[data-umbraco-forms-initialized], [data-umbraco-forms-accessibility-initialized]'); 48 for (var k = 0; k < markedElements.length; k++) { 49 delete markedElements[k].dataset.umbracoFormsInitialized; 50 delete markedElements[k].dataset.umbracoFormsAccessibilityInitialized; 51 } 52 } 53 } 54 55 // Inline script setting umbracoFormsCollection was removed in 8.11/9.3, and replaced by config elements. 56 // So if we have them, use it, otherwise fall-back to legacy method. 57 var configElements = targetElement.getElementsByClassName ? 58 targetElement.getElementsByClassName("umbraco-forms-form-config") : 59 document.querySelectorAll(".umbraco-forms-form-config"); 60 61 if (configElements.length > 0) { 62 var umbracoFormsCollectionFromConfig = []; 63 for (var i = 0; i < configElements.length; i++) { 64 var configElement = configElements[i]; 65 var parent = configElement.parentElement; 66 67 // Check if this form has already been initialized 68 var formElement = parent.querySelector('.umbraco-forms-form'); 69 70 // If the config element is not a direct child of the form element 71 if (!formElement) { 72 formElement = document.getElementById('umbraco_form_' + configElement.dataset.id); 73 } 74 75 // If there's no form element at all 76 if (!formElement) { 77 continue; 78 } 79 80 if (formElement && initializedForms.has(formElement)) { 81 continue; // Skip already initialized forms 82 } 83 84 var form = { 85 formId: configElement.dataset.id, 86 pageButtonConditions: JSON.parse(configElement.dataset.serializedPageButtonConditions), 87 fieldSetConditions: JSON.parse(configElement.dataset.serializedFieldsetConditions), 88 fieldConditions: JSON.parse(configElement.dataset.serializedFieldConditions), 89 triggerConditionsCheckOn: configElement.dataset.triggerConditionsCheckOn, 90 disableValidationDependencyCheck: configElement.dataset.disableValidationDependencyCheck === "true", 91 recordValues: JSON.parse(configElement.dataset.serializedFieldsNotDisplayed), 92 formElementHtmlIdPrefix: configElement.dataset.formElementHtmlIdPrefix, 93 validationRules: configElement.dataset.serializedValidationRules ? JSON.parse(configElement.dataset.serializedValidationRules) : [], 94 }; 95 umbracoFormsCollectionFromConfig.push(form); 96 } 97 98 if (umbracoFormsCollectionFromConfig.length > 0) { 99 initCollection(umbracoFormsCollectionFromConfig, true); // Set the 'jsonParsed' flag, indicating that we already have a JSON structure, which we don't have with the legacy, inline script method. 100 } 101 } else { 102 // Only register event listener on initial page load, not on reinitialize calls 103 if (targetElement === document) { 104 if (typeof umbracoFormsCollection === "undefined") { 105 //this will occur if this js file is loaded before the inline scripts, in which case 106 //we'll listen for the inline scripts to execute a custom event. 107 document.addEventListener("umbracoFormLoaded", init); 108 } 109 else { 110 initCollection(umbracoFormsCollection); 111 } 112 } 113 } 114 115 // Only handle this on initial page load, not on reinitialize calls 116 if (targetElement === document) { 117 // Scrolling to the submitted form was also moved from an inline script in 8.11/9.3. 118 var submittedFormElement = document.getElementById("umbraco-forms-form-submitted"); 119 if (submittedFormElement) { 120 var formClientId = submittedFormElement.dataset.formClientId; 121 scrollToSubmittedForm(formClientId); 122 } 123 } 124 } 125 126 function initCollection(formsCollection, jsonParsed) { 127 configureUmbracoFormsValidation(formsCollection); 128 129 for (var i = 0; i < formsCollection.length; i++) { 130 init({ form: formsCollection[i] }, jsonParsed); 131 } 132 } 133 134 function init(e, jsonParsed) { 135 136 var formItem = jsonParsed ? e.form : JSON.parse(decodeURI(e.form)); 137 138 var forms = document.querySelectorAll('.umbraco-forms-form'); 139 140 for (var i = 0; i < forms.length; i++) { 141 var form = forms[i]; 142 143 // Skip forms that have already been initialized 144 if (initializedForms.has(form)) { 145 continue; 146 } 147 148 // Check if this form matches the current formItem by comparing form IDs 149 var formIdElement = form.querySelector('[name="FormId"]'); 150 if (formIdElement) { 151 var currentFormId = formIdElement.value.replace(/-/g, ''); 152 if (currentFormId !== formItem.formId) { 153 continue; 154 } 155 } 156 157 // Mark this form as initialized immediately to prevent race conditions 158 initializedForms.add(form); 159 160 if (!formItem.disableValidationDependencyCheck) { 161 dependencyCheck(form); 162 } 163 164 var page = form.querySelector('.umbraco-forms-page'); 165 var conditions = new UmbracoFormsConditions(page, 166 formItem.pageButtonConditions, 167 formItem.fieldSetConditions, 168 formItem.fieldConditions, 169 formItem.triggerConditionsCheckOn && formItem.triggerConditionsCheckOn.length > 0 ? formItem.triggerConditionsCheckOn : "change", 170 formItem.recordValues, 171 formItem.formElementHtmlIdPrefix); 172 conditions.watch(); 173 174 applyFormAccessibility(form); 175 } 176 } 177 178 function scrollToSubmittedForm(formClientId) { 179 document.onreadystatechange = function () { 180 181 if (document.readyState == "complete") { 182 183 // We have various cases where we want to scroll to the form rendered on the page. 184 // - When we have a server-side validation error, we should scroll to the first error element (which class 'field-validation-error'). 185 // - When the form has been submitted, we should scroll to the displayed message (which will have class 'umbraco-forms-submitmessage' or 'umbraco-forms-submitmessage-html'). 186 // - When navigating back and forth on a multi-page form, we should scroll to the start of the form page (which will have an id of #umbraco_form_[form client id]).
187 var validationsMessageElements = document.querySelectorAll(".field-validation-error"); 188 var messageOnSubmitElement = document.querySelectorAll(".umbraco-forms-submitmessage"); 189 var messageOnSubmitHtmlElement = document.querySelectorAll(".umbraco-forms-submitmessage-html"); 190 var submittedFormElement = document.getElementById("umbraco_form_" + formClientId); 191 if (validationsMessageElements.length > 0) { 192 scrollToFirstFieldWithValidationError(validationsMessageElements); 193 } else if (messageOnSubmitElement.length > 0) { 194 scrollElementIntoView(messageOnSubmitElement[0].parentElement); 195 } else if (messageOnSubmitHtmlElement.length > 0) { 196 scrollElementIntoView(messageOnSubmitHtmlElement[0]); 197 } else if (submittedFormElement) { 198 scrollElementIntoView(submittedFormElement); 199 } 200 } 201 } 202 203 function scrollToFirstFieldWithValidationError(validationsMessageElements) { 204 var scrollY = 0; 205 for (var i = 0; i < validationsMessageElements.length; i++) { 206 var node = getFormFieldElement(validationsMessageElements[i]); 207 var offset = node.getBoundingClientRect().top; 208 if (0 < offset && (offset < scrollY || scrollY === 0)) { 209 scrollY = offset; 210 } 211 } 212 213 if (scrollY > 0) { 214 window.scrollTo(0, scrollY); 215 } 216 } 217 218 function scrollElementIntoView(element) { 219 element.scrollIntoView({ 220 behavior: "auto", 221 block: "start", 222 }); 223 } 224 225 function getFormFieldElement(node) { 226 var runner = node; 227 while (runner.tagName !== "BODY") { 228 if (runner.classList.contains("umbraco-forms-field")) { 229 return runner; 230 } 231 232 runner = runner.parentNode; 233 } 234 235 return node; 236 } 237 } 238 239 /** Configures the jquery validation for Umbraco forms */ 240 function configureUmbracoFormsValidation(formsCollection) { 241 242 var submitInputs, input; 243 244 if (window.aspnetValidation !== undefined) { 245 if (!window.umbracoFormsValidationService) { 246 // Asp-net validation setup: 247 var validationService = new aspnetValidation.ValidationService(); 248 var required = function (value, element) { 249 // Handle single and multiple checkboxes: 250 if (element.type.toLowerCase() === "checkbox" || element.type.toLowerCase() === "radio") { 251 var allCheckboxesOfThisName = element.form.querySelectorAll("input[name='" + element.name + "']"); 252 for (var i = 0; i < allCheckboxesOfThisName.length; i++) { 253 if (allCheckboxesOfThisName[i].checked === true) { 254 return true; 255 } 256 } 257 return false; 258 } 259 return Boolean(value); 260 } 261 validationService.addProvider("requiredcb", required); 262 validationService.addProvider("required", required);// this will go instead of the build-in required. 263 264 var umbracoforms_regex = function (value, element, params) { 265 if (!value || !params.pattern) { 266 return true; 267 } 268 269 var r = new RegExp(params.pattern); 270 return r.test(value); 271 } 272 validationService.addProvider("umbracoforms_regex", umbracoforms_regex); 273 274 var wrapProviderWithIgnorerBehaviour = function (provider) { 275 return function (value, element, params) { 276 277 // If field is hidden we ignore the validation. 278 if (element.offsetParent === null) { 279 return true; 280 } 281 282 return provider(value, element, params); 283 } 284 } 285 286 // we can only incept with default validator if we do it after bootstrapping but before window load event triggers validationservice. 287 window.addEventListener('load', function () { 288 289 // Wrap all providers with ignorer hidden fields logic: 290 for (var key in validationService.providers) { 291 validationService.providers[key] = wrapProviderWithIgnorerBehaviour(validationService.providers[key]); 292 } 293 }); 294 295 // bootstrap validation service. 296 validationService.bootstrap({ watch: true }); 297 298 // make validation service availalble globally for custom client-side code. 299 window.umbracoFormsValidationService = validationService; 300 } 301 302 else { 303 for (var i = 0; i < formsCollection.length; i++) { 304 var collectionItem = formsCollection[i]; 305 var formId = collectionItem.formId; 306 var configElement = document.querySelector('.umbraco-forms-form-config[data-id="' + formId + '"]'); 307 var formElement = configElement ? configElement.parentElement.querySelector('.umbraco-forms-form') : null; 308 if (formElement) { 309 window.umbracoFormsValidationService.scan(formElement); 310 } 311 } 312 } 313
314 // Without jquery validation, the previous page submit button click isn't sent with it's name, 315 // so can't be used server-side to determine whether to go forward or back. 316 // Hence we'll use an alternate method, setting a hidden field that's also used in the check. 317 var handlePreviousClicked = function () { 318 this.form.elements["PreviousClicked"].value = "clicked"; 319 }; 320 321 // - previous buttons are indicated by a data attribute of data-umb="prev-forms-form", introduced in 8.12/9.4. 322 var previousButtonElements = document.querySelectorAll("[data-umb='prev-forms-form']"); 323 324 // - prior to that classes were used to identify them, so for compatibility with custom themes, we'll check for those too 325 // if we haven't found any via the data attribute. 326 if (previousButtonElements.length === 0) { 327 previousButtonElements = document.getElementsByClassName("prev cancel"); 328 } 329 330 for (var i = 0; i < previousButtonElements.length; i++) { 331 var prevButton = previousButtonElements[i]; 332 if (!prevButton.dataset.umbracoFormsInitialized) { 333 // Mark as initialized immediately to prevent race conditions 334 prevButton.dataset.umbracoFormsInitialized = "true"; 335 336 prevButton.form.elements["PreviousClicked"].value = ""; 337 prevButton.addEventListener('click', handlePreviousClicked, false); 338 } 339 } 340 341 // Apply protection for multiple form submissions using the aspnet validation validator. 342 submitInputs = getSubmitInputs(); 343 for (let i = 0; i < submitInputs.length; i++) { 344 input = submitInputs[i]; 345 346 // Skip if already initialized 347 if (input.dataset.umbracoFormsInitialized) { 348 continue; 349 } 350 351 // Mark as initialized immediately to prevent race conditions 352 input.dataset.umbracoFormsInitialized = "true"; 353 354 input.addEventListener("click", function (e) { 355 e.preventDefault(); 356 var frm = e.currentTarget.form; 357 var validationRules = getValidationRulesForForm(frm, formsCollection); 358 window.umbracoFormsValidationService.validateForm(frm, (valid) => { 359 var formRulesValid = validateFormRules(frm, validationRules, "aspnet-client-validation", window.umbracoFormsValidationService); 360 valid = valid && formRulesValid; 361 if (valid) { 362 submitValidatedForm(frm); 363 var submitInputsForForm = frm.querySelectorAll("[type=submit]:not(.cancel)"); 364 for (let j = 0; j < submitInputsForForm.length; j++) { 365 submitInputsForForm[j].setAttribute("disabled", "disabled"); 366 } 367 } 368 }); 369 }); 370 } 371 } else if (typeof jQuery === "function" && $.validator && $.validator.unobtrusive) { 372 //Jquery validation setup 373 374 $.validator.setDefaults({ 375 ignore: ":hidden" 376 }); 377 378 $.validator.unobtrusive.adapters.addBool("requiredcb", "required"); 379 380 $.validator.addMethod("umbracoforms_regex", function (value, element) { 381 382 var regex = $(element).attr("data-regex"); 383 var val = $(element).val(); 384 if (val.length === 0) { 385 return true; 386 } 387 return val.match(regex); 388 }); 389 390 $.validator.unobtrusive.adapters.addBool("regex", "umbracoforms_regex"); 391 392 // Apply protection for multiple form submissions using the jquery validator. 393 submitInputs = getSubmitInputs(); 394 for (let i = 0; i < submitInputs.length; i++) { 395 input = submitInputs[i]; 396 397 // Skip if already initialized 398 if (input.dataset.umbracoFormsInitialized) { 399 continue; 400 } 401 402 // Mark as initialized immediately to prevent race conditions 403 input.dataset.umbracoFormsInitialized = "true"; 404 405 // Attempt to add validation again (in case the form has been dynamically added to the page) 406 var form = input.closest('form'); 407 $.validator.unobtrusive.parse(form); 408 409 input.addEventListener("click", function (evt) { 410 evt.preventDefault(); 411 var frm = $(this).closest("form"); 412 resetValidationMessages(frm[0]);
413 var validator = frm.validate(); 414 var validationRules = getValidationRulesForForm(frm[0], formsCollection); 415 var formValid = frm.valid(); 416 var formRulesValid = validateFormRules(frm[0], validationRules, "jquery", validator); 417 if (formValid && formRulesValid) { 418 submitValidatedForm(frm); 419 this.setAttribute("disabled", "disabled"); 420 } 421 }.bind(input)); 422 } 423 } 424 } 425 426 function getSubmitInputs() { 427 return document.querySelectorAll(".umbraco-forms-form [type=submit]:not(.cancel)"); 428 } 429 430 function getValidationRulesForForm(formElement, formsCollection) { 431 var filteredForms = formsCollection.filter(f => f.formId === formElement["FormId"].value.replaceAll("-", "")); 432 if (filteredForms.length > 0 && filteredForms[0].validationRules) { 433 434 // Work on a clone of the rules, so we don't modify in the originals and can still access the definitions that refer to 435 // field names. 436 var clonedRules = structuredClone(filteredForms[0].validationRules); 437 var formData = new FormData(formElement); 438 return clonedRules.map(r => getRuleWithFieldValues(r, formData)); 439 } 440 441 return []; 442 } 443 444 function getRuleWithFieldValues(validationRule, formData) { 445 var replacedRule = validationRule.rule; 446 for (var item of formData) { 447 replacedRule = replacedRule.replaceAll("{" + item[0] + "}", item[1]); 448 } 449 validationRule.rule = replacedRule; 450 return validationRule; 451 } 452 453 function validateFormRules(formElement, validationRules, validationLibrary, validator) { 454 if (typeof jsonLogic === 'undefined') { 455 // If we don't have the dependency, form rules aren't validated. 456 return true; 457 } 458 459 var rulesResult = true; 460 for (var i = 0; i < validationRules.length; i++) { 461 var validationRule = validationRules[i]; 462 463 // Skip if the rule contains unresolved field references. 464 var re = /{({*[\w]*}*)}/; 465 if (validationRule.rule.match(re)) { 466 continue; 467 } 468 469 var ruleAsJson;
470 try { 471 ruleAsJson = JSON.parse(validationRule.rule); 472 } catch (e) { 473 continue; // Skip also on invalid JSON. 474 } 475 476 var ruleResult = jsonLogic.apply(ruleAsJson); 477 indicateFormRuleValidation(formElement, validationLibrary, validator, ruleResult, validationRule); 478 rulesResult = rulesResult && ruleResult; 479 } 480 481 return rulesResult; 482 } 483 484 function indicateFormRuleValidation(formElement, validationLibrary, validator, result, validationRule) { 485 if (validationRule.fieldId) { 486 // Show or hide field error dependending on result. 487 var inputElement = formElement[validationRule.fieldId]; 488 489 if (!inputElement) { 490 return; 491 } 492 493 // If we have more than one element, e.g. for the date picker, use the one also matching the id. 494 if (inputElement.length && inputElement.length > 0) { 495 inputElement = Array.from(inputElement).filter(e => e.id === validationRule.fieldId)[0]; 496 } 497 498 if (validationLibrary === "aspnet-client-validation") { 499 if (result) { 500 // Only remove if it's the same message that we've added (could be other validation rules on the field we would want to keep). 501 var validationFieldMessageElement = getValidationFieldMessageElement(inputElement, "error"); 502 if (validationFieldMessageElement && validationFieldMessageElement.innerText === validationRule.errorMessage) { 503 validator.removeError(inputElement); 504 } 505 } else { 506 validator.addError(inputElement, validationRule.errorMessage); 507 } 508 }
508 else if (validationLibrary === "jquery") { 509 var validationSummaryElement = formElement.querySelector(".validation-summary-errors ul"); 510 if (result) { 511 var validationFieldMessageElement = getValidationFieldMessageElement(inputElement, "error"); 512 513 // Only remove if it's the same message that we've added (could be other validation rules on the field we would want to keep). 514 if (validationFieldMessageElement && validationFieldMessageElement.innerText === validationRule.errorMessage) { 515 validationFieldMessageElement.classList.remove("field-validation-error"); 516 validationFieldMessageElement.classList.add("field-validation-valid"); 517 validationFieldMessageElement.innerText = ""; 518 } 519 520 removeValidationSummaryMessage(validationSummaryElement, validationRule.errorMessage); 521 } else { 522 var validationFieldMessageElement = getValidationFieldMessageElement(inputElement, "valid"); 523 if (validationFieldMessageElement) { 524 validationFieldMessageElement.classList.remove("field-validation-valid"); 525 validationFieldMessageElement.classList.add("field-validation-error"); 526 validationFieldMessageElement.innerText = validationRule.errorMessage; 527 } 528 529 addValidationSummaryMessage(validationSummaryElement, validationRule.errorMessage); 530 } 531 } 532 } 533 } 534 535 function getValidationFieldMessageElement(inputElement, state) { 536 return inputElement.closest(".umbraco-forms-field-wrapper").querySelector(".field-validation-" + state); 537 } 538 539 function removeValidationSummaryMessage(validationSummaryElement, errorMessage) { 540 if (validationSummaryElement) { 541 validationSummaryElement.querySelectorAll("li").forEach(li => { 542 if (li.innerText === errorMessage) { 543 li.remove(); 544 } 545 }); 546 } 547 } 548 549 function addValidationSummaryMessage(validationSummaryElement, errorMessage) { 550 removeValidationSummaryMessage(validationSummaryElement, errorMessage); 551 if (validationSummaryElement) { 552 var validationSummaryItemElement = document.createElement("li"); 553 validationSummaryItemElement.appendChild(document.createTextNode(validationRule.errorMessage)); 554 validationSummaryElement.appendChild(validationSummaryItemElement); 555 } 556 } 557 558 function submitValidatedForm(form) { 559 // Normalize to native DOM element if jQuery object 560 if (form && form.jquery) { 561 form = form[0]; 562 } 563 564 // Use requestSubmit if available (see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit#usage_notes) 565 if (form.requestSubmit) { 566 form.requestSubmit(); 567 } else { 568 form.submit(); 569 } 570 } 571 572 /** 573 * method to determine if Umbraco Forms can run and has the required dependencies loaded 574 * @param {Form Element} formEl the element of the form 575 */ 576 function dependencyCheck(formEl) { 577 var errorElement = document.createElement("div"); 578 errorElement.className = "umbraco-forms missing-library"; 579 errorElement.style.color = "#fff"; 580 errorElement.style.backgroundColor = "#9d261d"; 581 errorElement.style.padding = "15px"; 582 errorElement.style.margin = "10px 0"; 583 var errorMessage = ""; 584 585 //Ensure umbracoForm is not null 586 if (formEl) { 587 588 //Check to see if the message for the form has been inserted already 589 var checkForExistinhgErr = formEl.getElementsByClassName('umbraco-forms missing-library'); 590 if (checkForExistinhgErr.length > 0) { 591 return; 592 } 593 594 var hasValidationFramework = false; 595 596 if (window.jQuery && $ && $.validator !== undefined) { 597 hasValidationFramework = true; 598 } else if (window.aspnetValidation !== undefined) { 599 hasValidationFramework = true; 600 } 601 602 if (hasValidationFramework === false) { 603 errorMessage = errorMessage + "Umbraco Forms requires a validation framework to run, please read documentation for posible options."; 604 } 605 606 if (errorMessage !== "") { 607 errorElement.innerHTML = errorMessage + '<br/> <a href="https://docs.umbraco.com/umbraco-forms/developer/prepping-frontend" target="_blank" style="text-decoration:underline;
607 color:#fff;">See Umbraco Forms Documentation</a>'; 608 609 formEl.insertBefore(errorElement, formEl.childNodes[0]); 610 } 611 } 612 } 613 614 /** 615 * Applies form accessibility improvements. 616 * @param {Element} formEl the element of the form. 617 */ 618 function applyFormAccessibility(formEl) { 619 setFocusToFirstElementOnValidationError(formEl); 620 } 621 622 /** 623 * Monitors for validation errors and when found sets the focus to the first field with an error. 624 * @param {Element} formEl the element of the form. 625 */ 626 function setFocusToFirstElementOnValidationError(formEl) { 627 if ("MutationObserver" in window === false) { 628 return; 629 } 630 631 // Skip if already initialized 632 if (formEl.dataset.umbracoFormsAccessibilityInitialized) { 633 return; 634 } 635 636 // Mark as initialized immediately to prevent race conditions 637 formEl.dataset.umbracoFormsAccessibilityInitialized = "true"; 638 639 // To implement this, we are relying on on monitoring for validation message elements, which only fires when there are changes. 640 // So if you have two errors, and fix the first one, it wouldn't then highlight the second one on re-submitting the form. 641 // Unless we reset the validation messages on submit, so they get changed back on errors. 642 if (window.aspnetValidation !== undefined) { 643 var form = formEl.getElementsByTagName('form')[0]; 644 var handleResetValidationMessages = function () { 645 resetValidationMessages(form); 646 }; 647 form.addEventListener('submit', handleResetValidationMessages, false); 648 } else { 649 // For jquery.validate, we need to hook this in as part of the submit handler coded in configureUmbracoFormsValidation(); 650 } 651 652 // Watch for changes to the validation error messages in the DOM tree using a MutationObserver. 653 // See: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 654 var observer = new MutationObserver(function (mutationRecords) { 655 for (var i = 0; i < mutationRecords.length; i++) { 656 var mutationRecord = mutationRecords[i]; 657 if (mutationRecord.target.className === 'field-validation-error') { 658 setFocusOnFormField(mutationRecord.target); 659 updateAriaInvalidState(mutationRecord.target, true); 660 break; 661 } else if (mutationRecord.target.className === 'field-validation-valid') { 662 updateAriaInvalidState(mutationRecord.target, false); 663 } 664 } 665 }); 666 667 observer.observe(formEl, { 668 attributes: true, 669 attributeFilter: ['class'], 670 childList: false, 671 characterData: false, 672 subtree: true 673 }); 674 } 675 676 /** 677 * Resets the validation messages for a form. 678 * @param {Element} formEl the element of the form. 679 */ 680 function resetValidationMessages(formEl) { 681 var validationErrorMessageElements = formEl.getElementsByClassName('field-validation-error'); 682 for (var i = 0; i < validationErrorMessageElements.length; i++) { 683 updateAriaInvalidState(validationErrorMessageElements[i], false); 684 validationErrorMessageElements[i].className = 'field-validation-valid'; 685 } 686 } 687 688 /** 689 * Updates the aria-invalid attribute on form field elements based on validation state. 690 * @param {Element} validationEl the validation message element. 691 * @param {boolean} isInvalid whether the field is invalid. 692 */ 693 function updateAriaInvalidState(validationEl, isInvalid) { 694 var formFieldElement = validationEl.previousElementSibling; 695 while (formFieldElement) { 696 // Skip hidden inputs - find the actual interactive element 697 if ((formFieldElement.tagName.toLowerCase() === 'input' && 698 formFieldElement.getAttribute('type') !== 'hidden') || 699 formFieldElement.tagName.toLowerCase() === 'textarea' || 700 formFieldElement.tagName.toLowerCase() === 'select') { 701 formFieldElement.setAttribute('aria-invalid', isInvalid ? 'true' : 'false'); 702 break; 703 } 704 705 if (formFieldElement.classList.contains("radiobuttonlist") || 706 formFieldElement.classList.contains("checkboxlist")) { 707 // For checkbox/radio lists, set aria-invalid on the container 708 formFieldElement.setAttribute('aria-invalid', isInvalid ? 'true' : 'false'); 709 break; 710 } 711 712 formFieldElement = formFieldElement.previousElementSibling; 713 } 714 } 715 716 /** 717 * Sets the focus to the form field input element associated with the provided validation message element. 718 * @param {Element} validationErrorEl the element of the validation error. 719 */
720 function setFocusOnFormField(validationErrorEl) { 721 var formFieldElement = validationErrorEl.previousElementSibling; 722 while (formFieldElement) { 723 if (formFieldElement.tagName.toLowerCase() === 'input' || 724 formFieldElement.tagName.toLowerCase() === 'textarea' || 725 formFieldElement.tagName.toLowerCase() === 'select') { 726 formFieldElement.focus(); 727 break; 728 } 729 730 if (formFieldElement.classList.contains("radiobuttonlist") || 731 formFieldElement.classList.contains("checkboxlist")) { 732 for (var i = 0; i < formFieldElement.children.length; i++) { 733 var formFieldChildElement = formFieldElement.children[i]; 734 if (formFieldChildElement.tagName.toLowerCase() === 'input') { 735 formFieldChildElement.focus(); 736 break; 737 } 738 } 739 740 break; 741 } 742 743 formFieldElement = formFieldElement.previousElementSibling; 744 } 745 } 746 747 /** 748 * Class to handle Umbraco Forms conditional statements 749 * @param {any} form a reference to the form 750 * @param {any} pageButtonConditions a reference to the page button conditions 751 * @param {any} fieldsetConditions a reference to the fieldset conditions 752 * @param {any} fieldConditions a reference to the field conditions 753 * @param {any} triggerConditionsCheckOn the client-side event on which condition checks should be triggered 754 * @param {any} values the form values 755 * @param {any} formElementHtmlIdPrefix the HTML ID prefix used when rendering the form 756 * @return {Object} reference to the created class 757 */ 758 function UmbracoFormsConditions(form, pageButtonConditions, fieldsetConditions, fieldConditions, triggerConditionsCheckOn, values, formElementHtmlIdPrefix) { 759 760 //our conditions "class" - must always be newed to work as it uses a form instance to operate on 761 //load all the information from the dom and serverside info and then the class will take care of the rest 762 763 var self = {}; 764 self.form = form; 765 self.pageButtonConditions = pageButtonConditions; 766 self.fieldsetConditions = fieldsetConditions; 767 self.fieldConditions = fieldConditions; 768 self.triggerConditionsCheckOn = triggerConditionsCheckOn; 769 self.values = values; 770 self.formElementHtmlIdPrefix = formElementHtmlIdPrefix; 771 self.dataTypes = {}; 772 773 //Iterates through all the form elements found on the page to update the registered value 774 function populateFieldValues(page, formValues, dataTypes) { 775 776 var selectFields = page.querySelectorAll("select"); 777 for (let i = 0; i < selectFields.length; i++) { 778 const field = selectFields[i]; 779 formValues[field.getAttribute("id")] = field.value; 780 dataTypes[field.getAttribute("id")] = "select"; 781 } 782 783 var textareaFields = page.querySelectorAll("textarea"); 784 for (let i = 0; i < textareaFields.length; i++) { 785 const field = textareaFields[i]; 786 formValues[field.getAttribute("id")] = field.value; 787 dataTypes[field.getAttribute("id")] = "textarea"; 788 } 789 790 var getKeyForRadioOrCheckBoxListElement = function (field) { 791 const idParts = field.getAttribute("id").split("_"); 792 return idParts.slice(0, idParts.length - 1).join("_"); 793 } 794 795 // clear out all saved checkbox values to we can safely append 796 var checkboxFields = page.querySelectorAll("input[type=checkbox]"); 797 for (let i = 0; i < checkboxFields.length; i++) { 798 const field = checkboxFields[i]; 799 800 // Need to handle both a single checkbox, and a checkbox list. We can distinguish by comparing the field's 801 // id and name attributes. If they are the same, accounting for a custom prefix, then we have a single checkbox. 802 if (field.getAttribute("id") !== self.formElementHtmlIdPrefix + field.getAttribute("name")) { 803 // Handle checkbox list. 804 // For a checkbox in a list we need to remove the suffix from the Id (which will be '_0', '_1' etc.). 805 const checkboxKey = getKeyForRadioOrCheckBoxListElement(field); 806 formValues[checkboxKey] = null; 807 dataTypes[checkboxKey] = "checkbox"; 808 } else { 809 // Handle single checkbox. 810 formValues[field.getAttribute("id")] = null; 811 dataTypes[field.getAttribute("id")] = "checkbox"; 812 } 813 } 814 815 var inputFields = page.querySelectorAll("input"); 816 for (let i = 0; i < inputFields.length; i++) { 817 const field = inputFields[i]; 818 819 if (field.getAttribute('type') === "text" || 820 field.getAttribute('type') === "number" || 821 field.getAttribute('type') === "email" || 822 field.getAttribute('type') === "url" || 823 field.getAttribute('type') === "tel" || 824 field.getAttribute('type') === "time" || 825 field.getAttribute('type') === "date" || 826 field.getAttribute('type') === "datetime-local" || 827 field.getAttribute("type") === "hidden") { 828 formValues[field.getAttribute("id")] = field.value; 829 dataTypes[field.getAttribute("id")] = "text"; 830 } 831 832 if (field.getAttribute('type') === "radio") { 833 if (field.matches(':checked')) { 834 // For a radio button in a list we need to remove the suffix from the Id (which will be '_0', '_1' etc.). 835 const radioKey = getKeyForRadioOrCheckBoxListElement(field); 836 formValues[radioKey] = field.value; 837 dataTypes[radioKey] = "radio"; 838 } 839 } 840 841 if (field.getAttribute("type") === "checkbox") {
842 // Again, need to handle both a single checkbox, and a checkbox list. 843 if (field.getAttribute("id") !== self.formElementHtmlIdPrefix + field.getAttribute("name")) { 844 // Handle checkbox list. 845 // For a checkbox in a list we need to remove the suffix from the Id (which will be '_0', '_1' etc.). 846 const checkboxKey = getKeyForRadioOrCheckBoxListElement(field); 847 if (field.matches(":checked")) { 848 if (formValues[checkboxKey] === null) { 849 formValues[checkboxKey] = field.value; 850 } 851 else { 852 formValues[checkboxKey] += ";;" + field.value; 853 } 854 } 855 } 856 else { 857 // Handle single checkbox. 858 formValues[field.getAttribute("id")] = (field.matches(":checked") ? "true" : "false"); 859 } 860 } 861 } 862 } 863 864 /* Public api */ 865 866 self.operators = { 867 Is: function (value, expected) { 868 if ((value || "") === expected) { 869 return true; 870 } 871 if (value == null) { 872 return (expected == value); 873 } 874 875 // Handle special cases where we case insensitively compare for expected values of "true" and "on", which we 876 // expect to have come from checkbox conditions. 877 if (expected.toUpperCase() === "TRUE" || expected.toUpperCase() === "ON") { 878 expected = "true" 879 } else if (expected.toUpperCase() === "FALSE" || expected.toUpperCase() === "OFF") { 880 expected = "false" 881 } 882 883 // Handle single checked checkbox, where we'll get two values provided. 884 if (value === "true;;false") { 885 value = "true"; 886 } 887 888 var values = value.split(';;'); 889 890 var matchingExpected = values.filter( 891 function (o) { 892 return o === expected; 893 }); 894 return matchingExpected.length === values.length; // If there are multiple values (e.g. for a checkbox list), "is" only matches if 895 // a single value only matches the expected. Contains should be used if matching any value. 896 }, 897 IsNot: function (value, unexpected, dataType) { 898 if (value == null) { 899 return (unexpected != value); 900 } 901 902 // Handle single checked checkbox, where we'll get two values provided. 903 if (value === "true;;false") { 904 value = "true"; 905 } 906 907 var values = value.split(';;'); 908 var matchingUnexpected = values.filter( 909 function (o) { 910 return o === unexpected; 911 }); 912 913 if (dataType === "checkbox") { 914 if (unexpected.toUpperCase() === "TRUE" || unexpected.toUpperCase() === "ON") { 915 unexpected = "true" 916 } else if (unexpected.toUpperCase() === "FALSE" || unexpected.toUpperCase() === "OFF") { 917 unexpected = "false" 918 } 919 } 920 return (value || "") !== unexpected && matchingUnexpected.length != values.length; 921 }, 922 GreaterThen: function (value, limit) { 923 if (isNaN(value) || isNaN(limit)) { 924 return value > limit; 925 } 926 927 return parseInt(value) > parseInt(limit); 928 }, 929 LessThen: function (value, limit) { 930 if (isNaN(value) || isNaN(limit)) { 931 return value < limit; 932 } 933 934 return parseInt(value) < parseInt(limit); 935 }, 936 StartsWith: function (value, criteria) { 937 return value && value.startsWith(criteria); 938 }, 939 EndsWith: function (value, criteria) { 940 return value && value.endsWith(criteria); 941 }, 942 Contains: function (value, criteria) { 943 return value && value.indexOf(criteria) > -1; 944 }, 945 ContainsIgnoreCase: function (value, criteria) { 946 return value && criteria && value.toUpperCase().indexOf(criteria.toUpperCase()) >= 0; 947 }, 948 StartsWithIgnoreCase: function (value, criteria) { 949 return value && criteria && value.toUpperCase().startsWith(criteria.toUpperCase()); 950 }, 951 EndsWithIgnoreCase: function (value, criteria) { 952 return value && criteria && value.toUpperCase().endsWith(criteria.toUpperCase()); 953 }, 954 NotContains: function (value, criteria) { 955 return !value || (criteria && value.indexOf(criteria) < 0); 956 }, 957 NotStartsWith: function (value, criteria) { 958 return !value || (criteria && !value.startsWith(criteria)); 959 }, 960 NotEndsWith: function (value, criteria) { 961 return !value || (criteria && !value.endsWith(criteria)); 962 }, 963 NotContainsIgnoreCase: function (value, criteria) { 964 return !value || (criteria && value.toUpperCase().indexOf(criteria.toUpperCase()) < 0); 965 }, 966 NotStartsWithIgnoreCase: function (value, criteria) { 967 return !value || (criteria && !value.toUpperCase().startsWith(criteria.toUpperCase())); 968 }, 969 NotEndsWithIgnoreCase: function (value, criteria) { 970 return !value || (criteria && !value.toUpperCase().endsWith(criteria.toUpperCase())); 971 }, 972 }; 973 974 self.watch = function () { 975 // This is a special case for pikaday 976 // The only way around to pickup the value, for now, is to 977 // subscribe to blur events 978 var datepickerfields = self.form.querySelectorAll('.datepickerfield'); 979 for (let i = 0; i < datepickerfields.length; i++) { 980 const field = datepickerfields[i]; 981 field.addEventListener('blur', function () { 982 if (this.value === "") { 983 // Here comes the hack 984 // Force the hidden datepicker field the datepicker field 985 var id = this.getAttribute("id"); 986 var hiddenDatePickerField = id + "_1"; 987 self.values[hiddenDatePickerField] = "";
988 document.getElementById(hiddenDatePickerField).value = "";// sadly we cant use querySelector with current mark-up (would need to prefix IDs) 989 } 990 991 populateFieldValues(self.form, self.values, self.dataTypes); 992 //process the conditions 993 self.run(); 994 }.bind(field)); 995 } 996 //subscribe to change or input event as per configuration 997 var changeablefields = self.form.querySelectorAll("input, textarea, select"); 998 for (let i = 0; i < changeablefields.length; i++) { 999 const field = changeablefields[i]; 1000 field.addEventListener(self.triggerConditionsCheckOn, function () { 1001 populateFieldValues(self.form, self.values, self.dataTypes); 1002 //process the conditions 1003 self.run(); 1004 }.bind(field)); 1005 } 1006 1007 //register all values from the current fields on the page 1008 populateFieldValues(self.form, self.values, self.dataTypes); 1009 1010 //the initial run-through of all the conditions 1011 self.run(); 1012 }; 1013 1014 self.run = function () { 1015 var pageId, 1016 htmlFieldId, 1017 fsId, 1018 fieldId, 1019 1020 /* 1021 fsConditions = params.fsConditions || {}, 1022 fieldConditions = params.fieldConditions || {}, 1023 values = params.values || {},*/ 1024 1025 cachedResults = {}; 1026 1027 function evaluateRuleInstance(rule) { 1028 var value = self.values[self.formElementHtmlIdPrefix + rule.field], 1029 func = self.operators[rule.operator], 1030 result = value !== null && func(value, rule.value); 1031 return result; 1032 } 1033 1034 function evaluateRule(rule) { 1035 var dependencyIsVisible = true; 1036 1037 if (self.fieldConditions[rule.field]) { 1038 dependencyIsVisible = isVisible(rule.field, self.fieldConditions[rule.field]); 1039 } 1040 1041 if (dependencyIsVisible) { 1042 return evaluateRuleInstance(rule); 1043 } 1044 else { 1045 return false; 1046 } 1047 } 1048 1049 function evaluateCondition(id, condition) { 1050 // This was once pretty. Now it needs refactoring again. :) 1051 1052 var any = condition.logicType === "Any", 1053 all = condition.logicType === "All", 1054 fieldsetVisibilities = {}, 1055 hasHiddenFieldset = false, 1056 success = true, 1057 rule, 1058 i; 1059 1060 // If we don't have any rules defined, we must return false (as neither 'any' nor 'all' of the conditions 1061 // can be considered passing). 1062 if (condition.rules.length === 0) { 1063 return false; 1064 } 1065 1066 for (i = 0; i < condition.rules.length; i++) { 1067 rule = condition.rules[i]; 1068 1069 if (id === rule.field || id === rule.fieldsetId) { 1070 throw new Error("Field or fieldset " + id + " has a condition on itself."); 1071 } 1072 1073 if (fieldsetVisibilities[rule.fieldsetId] !== undefined) { 1074 continue; 1075 } 1076 1077 if (self.fieldsetConditions[rule.fieldsetId]) { 1078 1079 fieldsetVisibilities[rule.fieldsetId] = 1080 isVisible(rule.fieldsetId, self.fieldsetConditions[rule.fieldsetId]); 1081 1082 if (!fieldsetVisibilities[rule.fieldsetId]) { 1083 hasHiddenFieldset = true; 1084 } 1085 } 1086 else { 1087 fieldsetVisibilities[rule.fieldsetId] = true; 1088 } 1089 } 1090 1091 if (all && hasHiddenFieldset) { 1092 return false; 1093 } 1094 1095 for (i = 0; i < condition.rules.length; i++) { 1096 rule = condition.rules[i]; 1097 1098 if (fieldsetVisibilities[rule.fieldsetId]) { 1099 success = evaluateRule(condition.rules[i]); 1100 } 1101 else { 1102 success = false; 1103 } 1104 1105 if (any && success) { 1106 break; 1107 } 1108 if (all && !success) { 1109 break; 1110 } 1111 } 1112 return success; 1113 } 1114 1115 function evaluateConditionVisibility(id, condition) { 1116 var show = condition.actionType === "Show", 1117 cachedResult = cachedResults[id]; 1118 1119 var success; 1120 if (cachedResult === undefined) { 1121 cachedResults[id] = show; // set default value to avoid circular issues 1122 success = (cachedResults[id] = evaluateCondition(id, condition)); 1123 } else { 1124 success = cachedResult; 1125 } 1126 1127 var visible = !(success ^ show); 1128 return visible; 1129 } 1130 1131 function isVisible(id, condition) { 1132 if (condition) { 1133 return evaluateConditionVisibility(id, condition); 1134 } 1135 return true; 1136 } 1137 1138 function handleCondition(element, id, condition) { 1139 var shouldShow = isVisible(id, condition); 1140 if (element) { 1141 if (shouldShow) { 1142 element.classList.remove("umbraco-forms-hidden"); 1143 } 1144 else { 1145 element.classList.add("umbraco-forms-hidden"); 1146 } 1147 } 1148 } 1149 1150 for (pageId in self.pageButtonConditions) { 1151 if (Object.prototype.hasOwnProperty.call(self.pageButtonConditions, pageId)) { 1152 var pageElem = document.getElementById(pageId); 1153 if (pageElem) { 1154 // Exclude the hidden helper submit button (rendered first on non-first pages to fix 1155 // return-key/mobile-go navigation, see issue #1343) so the condition is applied to the 1156 // real, visible next/submit button rather than the invisible helper (issue #1705). 1157 handleCondition(pageElem.querySelector("input[name='__next']:not([hidden]), button[name='__next']:not([hidden]), input[data-form-navigate='next']:not([hidden]), button[data-form-navigate='next']:not([hidden])"), pageId, self.pageButtonConditions[pageId], "Page"); 1158 } 1159 } 1160 } 1161 1162 for (fsId in self.fieldsetConditions) { 1163 if (Object.prototype.hasOwnProperty.call(self.fieldsetConditions, fsId)) { 1164 htmlFieldId = self.formElementHtmlIdPrefix + fsId; 1165 handleCondition(document.getElementById(htmlFieldId), fsId, self.fieldsetConditions[fsId], "Fieldset");// sadly we cant use querySelector with current mark-up (would need to prefix IDs) 1166 } 1167 } 1168 1169 for (fieldId in self.fieldConditions) { 1170 if (Object.prototype.hasOwnProperty.call(self.fieldConditions, fieldId)) { 1171 htmlFieldId = self.formElementHtmlIdPrefix + fieldId; 1172 if (document.getElementById(htmlFieldId)) { 1173 handleCondition(document.getElementById(htmlFieldId).closest(".umbraco-forms-field"),// sadly we cant use querySelector with current mark-up (would need to prefix IDs) 1174 fieldId, 1175 self.fieldConditions[fieldId], 1176 "Field"); 1177 } 1178 } 1179 } 1180 }; 1181 1182 return self; 1183 } 1184})();
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.