diff --git a/src/assets/netteForms.js b/src/assets/netteForms.js new file mode 100644 index 00000000..428cf385 --- /dev/null +++ b/src/assets/netteForms.js @@ -0,0 +1,573 @@ +/*! + * NetteForms - simple form validation. + * + * This file is part of the Nette Framework (https://nette.org) + * Copyright (c) 2004 David Grudl (https://davidgrudl.com) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Nette?.noInit ? (global.Nette = factory()) : (global.Nette = factory()).initOnLoad()); +})(this, (function () { 'use strict'; + + class Validators { + filled(elem, arg, val) { + return val !== '' && val !== false && val !== null + && (!Array.isArray(val) || val.length > 0) + && (!(val instanceof FileList) || val.length > 0); + } + blank(elem, arg, val) { + return !this.filled(elem, arg, val); + } + valid(elem, arg) { + return arg.validateControl(elem, undefined, true); + } + equal(elem, arg, val) { + if (arg === undefined) { + return null; + } + const toString = (val) => { + if (typeof val === 'number' || typeof val === 'string') { + return '' + val; + } + else { + return val === true ? '1' : ''; + } + }; + let vals = Array.isArray(val) ? val : [val]; + let args = Array.isArray(arg) ? arg : [arg]; + loop: for (let a of vals) { + for (let b of args) { + if (toString(a) === toString(b)) { + continue loop; + } + } + return false; + } + return vals.length > 0; + } + notEqual(elem, arg, val) { + return arg === undefined ? null : !this.equal(elem, arg, val); + } + minLength(elem, arg, val) { + val = typeof val === 'number' ? val.toString() : val; + return val.length >= arg; + } + maxLength(elem, arg, val) { + val = typeof val === 'number' ? val.toString() : val; + return val.length <= arg; + } + length(elem, arg, val) { + val = typeof val === 'number' ? val.toString() : val; + arg = Array.isArray(arg) ? arg : [arg, arg]; + return ((arg[0] === null || val.length >= arg[0]) + && (arg[1] === null || val.length <= arg[1])); + } + email(elem, arg, val) { + return (/^("([ !#-[\]-~]|\\[ -~])+"|[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*)@([0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)+[a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF])?$/i).test(val); + } + url(elem, arg, val, newValue) { + if (!(/^[a-z\d+.-]+:/).test(val)) { + val = 'https://' + val; + } + if ((/^https?:\/\/((([-_0-9a-z\u00C0-\u02FF\u0370-\u1EFF]+\.)*[0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)?[a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[0-9a-f:]{3,39}\])(:\d{1,5})?(\/\S*)?$/i).test(val)) { + newValue.value = val; + return true; + } + return false; + } + regexp(elem, arg, val) { + let parts = typeof arg === 'string' ? arg.match(/^\/(.*)\/([imu]*)$/) : false; + try { + return parts && (new RegExp(parts[1], parts[2].replace('u', ''))).test(val); + } + catch { + return null; + } + } + pattern(elem, arg, val, newValue, caseInsensitive) { + if (typeof arg !== 'string') { + return null; + } + try { + let regExp; + try { + regExp = new RegExp('^(?:' + arg + ')$', caseInsensitive ? 'ui' : 'u'); + } + catch { + regExp = new RegExp('^(?:' + arg + ')$', caseInsensitive ? 'i' : ''); + } + return val instanceof FileList + ? Array.from(val).every((file) => regExp.test(file.name)) + : regExp.test(val); + } + catch { + return null; + } + } + patternCaseInsensitive(elem, arg, val) { + return this.pattern(elem, arg, val, null, true); + } + numeric(elem, arg, val) { + return (/^[0-9]+$/).test(val); + } + integer(elem, arg, val, newValue) { + if ((/^-?[0-9]+$/).test(val)) { + newValue.value = parseFloat(val); + return true; + } + return false; + } + float(elem, arg, val, newValue) { + val = val.replace(/ +/g, '').replace(/,/g, '.'); + if ((/^-?[0-9]*\.?[0-9]+$/).test(val)) { + newValue.value = parseFloat(val); + return true; + } + return false; + } + min(elem, arg, val) { + if (Number.isFinite(arg)) { + val = parseFloat(val); + } + return val >= arg; + } + max(elem, arg, val) { + if (Number.isFinite(arg)) { + val = parseFloat(val); + } + return val <= arg; + } + range(elem, arg, val) { + if (!Array.isArray(arg)) { + return null; + } + else if (elem.type === 'time' && arg[0] > arg[1]) { + return val >= arg[0] || val <= arg[1]; + } + return (arg[0] === null || this.min(elem, arg[0], val)) + && (arg[1] === null || this.max(elem, arg[1], val)); + } + submitted(elem) { + return elem.form['nette-submittedBy'] === elem; + } + fileSize(elem, arg, val) { + return Array.from(val).every((file) => file.size <= arg); + } + mimeType(elem, args, val) { + let parts = []; + args = Array.isArray(args) ? args : [args]; + args.forEach((arg) => parts.push('^' + arg.replace(/([^\w])/g, '\\$1').replace('\\*', '.*') + '$')); + let re = new RegExp(parts.join('|')); + return Array.from(val).every((file) => !file.type || re.test(file.type)); + } + image(elem, arg, val) { + return this.mimeType(elem, arg ?? ['image/gif', 'image/png', 'image/jpeg', 'image/webp'], val); + } + static(elem, arg) { + return arg; + } + } + + class FormValidator { + formErrors = []; + validators = new Validators; + #preventFiltering = {}; + #formToggles = {}; + #toggleListeners = new WeakMap; + #getFormElement(form, name) { + let res = form.elements.namedItem(name); + return (res instanceof RadioNodeList ? res[0] : res); + } + #expandRadioElement(elem) { + let res = elem.form.elements.namedItem(elem.name); + return (res instanceof RadioNodeList ? Array.from(res) : [res]); + } + /** + * Function to execute when the DOM is fully loaded. + */ + #onDocumentReady(callback) { + if (document.readyState !== 'loading') { + callback.call(this); + } + else { + document.addEventListener('DOMContentLoaded', callback); + } + } + /** + * Returns the value of form element. + */ + getValue(elem) { + if (elem instanceof HTMLInputElement) { + if (elem.type === 'radio') { + return this.#expandRadioElement(elem) + .find((input) => input.checked) + ?.value ?? null; + } + else if (elem.type === 'file') { + return elem.files; + } + else if (elem.type === 'checkbox') { + return elem.name.endsWith('[]') // checkbox list + ? this.#expandRadioElement(elem) + .filter((input) => input.checked) + .map((input) => input.value) + : elem.checked; + } + else { + return elem.value.trim(); + } + } + else if (elem instanceof HTMLSelectElement) { + return elem.multiple + ? Array.from(elem.selectedOptions, (option) => option.value) + : elem.selectedOptions[0]?.value ?? null; + } + else if (elem instanceof HTMLTextAreaElement) { + return elem.value; + } + else if (elem instanceof RadioNodeList) { + return this.getValue(elem[0]); + } + else { + return null; + } + } + /** + * Returns the effective value of form element. + */ + getEffectiveValue(elem, filter = false) { + let val = this.getValue(elem); + if (val === elem.getAttribute('data-nette-empty-value')) { + val = ''; + } + if (filter && this.#preventFiltering[elem.name] === undefined) { + this.#preventFiltering[elem.name] = true; + let ref = { value: val }; + this.validateControl(elem, undefined, true, ref); + val = ref.value; + delete this.#preventFiltering[elem.name]; + } + return val; + } + /** + * Validates form element against given rules. + */ + validateControl(elem, rules, onlyCheck = false, value, emptyOptional) { + rules ??= JSON.parse(elem.getAttribute('data-nette-rules') ?? '[]'); + value ??= { value: this.getEffectiveValue(elem) }; + emptyOptional ??= !this.validateRule(elem, ':filled', null, value); + for (let rule of rules) { + let op = rule.op.match(/(~)?([^?]+)/), curElem = rule.control ? this.#getFormElement(elem.form, rule.control) : elem; + rule.neg = !!op[1]; + rule.op = op[2]; + rule.condition = !!rule.rules; + if (!curElem) { + continue; + } + else if (emptyOptional && !rule.condition && rule.op !== ':filled') { + continue; + } + let success = this.validateRule(curElem, rule.op, rule.arg, elem === curElem ? value : undefined); + if (success === null) { + continue; + } + else if (rule.neg) { + success = !success; + } + if (rule.condition && success) { + if (!this.validateControl(elem, rule.rules, onlyCheck, value, rule.op === ':blank' ? false : emptyOptional)) { + return false; + } + } + else if (!rule.condition && !success) { + if (this.isDisabled(curElem)) { + continue; + } + if (!onlyCheck) { + let arr = Array.isArray(rule.arg) ? rule.arg : [rule.arg], message = rule.msg.replace(/%(value|\d+)/g, (foo, m) => this.getValue(m === 'value' ? curElem : elem.form.elements.namedItem(arr[m].control))); + this.addError(curElem, message); + } + return false; + } + } + return true; + } + /** + * Validates whole form. + */ + validateForm(sender, onlyCheck = false) { + let form = sender.form ?? sender, scope; + this.formErrors = []; + if (form['nette-submittedBy'] && form['nette-submittedBy'].getAttribute('formnovalidate') !== null) { + let scopeArr = JSON.parse(form['nette-submittedBy'].getAttribute('data-nette-validation-scope') ?? '[]'); + if (scopeArr.length) { + scope = new RegExp('^(' + scopeArr.join('-|') + '-)'); + } + else { + this.showFormErrors(form, []); + return true; + } + } + for (let elem of form.elements) { + if (elem.willValidate && elem.validity.badInput) { + elem.reportValidity(); + return false; + } + } + for (let elem of form.elements) { + if (elem.getAttribute('data-nette-rules') + && (!scope || elem.name.replace(/]\[|\[|]|$/g, '-').match(scope)) + && !this.isDisabled(elem) + && !this.validateControl(elem, undefined, onlyCheck) + && !this.formErrors.length) { + return false; + } + } + let success = !this.formErrors.length; + this.showFormErrors(form, this.formErrors); + return success; + } + /** + * Check if input is disabled. + */ + isDisabled(elem) { + if (elem.type === 'radio') { + return this.#expandRadioElement(elem) + .every((input) => input.disabled); + } + return elem.disabled; + } + /** + * Adds error message to the queue. + */ + addError(elem, message) { + this.formErrors.push({ + element: elem, + message: message, + }); + } + /** + * Display error messages. + */ + showFormErrors(form, errors) { + let messages = [], focusElem; + for (let error of errors) { + if (messages.indexOf(error.message) < 0) { + messages.push(error.message); + focusElem ??= error.element; + } + } + if (messages.length) { + this.showModal(messages.join('\n'), () => { + focusElem?.focus(); + }); + } + } + /** + * Display modal window. + */ + showModal(message, onclose) { + let dialog = document.createElement('dialog'); + if (!dialog.showModal) { + alert(message); + onclose(); + return; + } + let style = document.createElement('style'); + style.innerText = '.netteFormsModal { text-align: center; margin: auto; border: 2px solid black; padding: 1rem } .netteFormsModal button { padding: .1em 2em }'; + let button = document.createElement('button'); + button.innerText = 'OK'; + button.onclick = () => { + dialog.remove(); + onclose(); + }; + dialog.setAttribute('class', 'netteFormsModal'); + dialog.innerText = message + '\n\n'; + dialog.append(style, button); + document.body.append(dialog); + dialog.showModal(); + } + /** + * Validates single rule. + */ + validateRule(elem, op, arg, value) { + if (elem.validity.badInput) { + return op === ':filled'; + } + value ??= { value: this.getEffectiveValue(elem, true) }; + let method = op.charAt(0) === ':' ? op.substring(1) : op; + method = method.replace('::', '_').replaceAll('\\', ''); + let args = Array.isArray(arg) ? arg : [arg]; + args = args.map((arg) => { + if (arg?.control) { + let control = this.#getFormElement(elem.form, arg.control); + return control === elem ? value.value : this.getEffectiveValue(control, true); + } + return arg; + }); + if (method === 'valid') { + args[0] = this; // todo + } + return this.validators[method] + ? this.validators[method](elem, Array.isArray(arg) ? args : args[0], value.value, value) + : null; + } + /** + * Process all toggles in form. + */ + toggleForm(form, event) { + this.#formToggles = {}; + for (let elem of Array.from(form.elements)) { + if (elem.getAttribute('data-nette-rules')) { + this.toggleControl(elem, undefined, null, !event); + } + } + for (let i in this.#formToggles) { + this.toggle(i, this.#formToggles[i].state, this.#formToggles[i].elem, event); + } + } + /** + * Process toggles on form element. + */ + toggleControl(elem, rules, success = null, firsttime = false, value, emptyOptional) { + rules ??= JSON.parse(elem.getAttribute('data-nette-rules') ?? '[]'); + value ??= { value: this.getEffectiveValue(elem) }; + emptyOptional ??= !this.validateRule(elem, ':filled', null, value); + let has = false, curSuccess; + for (let rule of rules) { + let op = rule.op.match(/(~)?([^?]+)/), curElem = rule.control ? this.#getFormElement(elem.form, rule.control) : elem; + rule.neg = !!op[1]; + rule.op = op[2]; + rule.condition = !!rule.rules; + if (!curElem) { + continue; + } + else if (emptyOptional && !rule.condition && rule.op !== ':filled') { + continue; + } + curSuccess = success; + if (success !== false) { + curSuccess = this.validateRule(curElem, rule.op, rule.arg, elem === curElem ? value : undefined); + if (curSuccess === null) { + continue; + } + else if (rule.neg) { + curSuccess = !curSuccess; + } + if (!rule.condition) { + success = curSuccess; + } + } + if ((rule.condition && this.toggleControl(elem, rule.rules, curSuccess, firsttime, value, rule.op === ':blank' ? false : emptyOptional)) || rule.toggle) { + has = true; + if (firsttime) { + this.#expandRadioElement(curElem) + .filter((el) => !this.#toggleListeners.has(el)) + .forEach((el) => { + el.addEventListener('change', (e) => this.toggleForm(elem.form, e)); + this.#toggleListeners.set(el, null); + }); + } + for (let id in rule.toggle ?? {}) { + this.#formToggles[id] ??= { elem: elem, state: false }; + this.#formToggles[id].state ||= rule.toggle[id] ? !!curSuccess : !curSuccess; + } + } + } + return has; + } + /** + * Displays or hides HTML element. + */ + toggle(selector, visible, srcElement, event) { + if (/^\w[\w.:-]*$/.test(selector)) { // id + selector = '#' + selector; + } + Array.from(document.querySelectorAll(selector)) + .forEach((elem) => elem.hidden = !visible); + } + /** + * Compact checkboxes + */ + compactCheckboxes(form, formData) { + let values = {}; + for (let elem of form.elements) { + if (elem instanceof HTMLInputElement && elem.type === 'checkbox' && elem.name.endsWith('[]') && elem.checked && !elem.disabled) { + formData.delete(elem.name); + values[elem.name] ??= []; + values[elem.name].push(elem.value); + } + } + for (let name in values) { + formData.set(name.substring(0, name.length - 2), values[name].join(',')); + } + } + /** + * Setup handlers. + */ + initForm(form) { + if (form.method === 'get' && form.hasAttribute('data-nette-compact')) { + form.addEventListener('formdata', (e) => this.compactCheckboxes(form, e.formData)); + } + if (!Array.from(form.elements).some((elem) => elem.getAttribute('data-nette-rules'))) { + return; + } + this.toggleForm(form); + if (form.noValidate) { + return; + } + form.noValidate = true; + form.addEventListener('submit', (e) => { + if (!this.validateForm(form)) { + e.stopPropagation(); + e.preventDefault(); + } + }); + form.addEventListener('reset', () => { + setTimeout(() => this.toggleForm(form)); + }); + } + initOnLoad() { + this.#onDocumentReady(() => { + Array.from(document.forms) + .forEach((form) => this.initForm(form)); + document.body.addEventListener('click', (e) => { + let target = e.target; + while (target) { + if (target.form && target.type in { submit: 1, image: 1 }) { + target.form['nette-submittedBy'] = target; + break; + } + target = target.parentNode; + } + }); + }); + } + } + + let webalizeTable = { \u00e1: 'a', \u00e4: 'a', \u010d: 'c', \u010f: 'd', \u00e9: 'e', \u011b: 'e', \u00ed: 'i', \u013e: 'l', \u0148: 'n', \u00f3: 'o', \u00f4: 'o', \u0159: 'r', \u0161: 's', \u0165: 't', \u00fa: 'u', \u016f: 'u', \u00fd: 'y', \u017e: 'z' }; + /** + * Converts string to web safe characters [a-z0-9-] text. + * @param {string} s + * @return {string} + */ + function webalize(s) { + s = s.toLowerCase(); + let res = ''; + for (let i = 0; i < s.length; i++) { + let ch = webalizeTable[s.charAt(i)]; + res += ch ? ch : s.charAt(i); + } + return res.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + } + + var version = "3.5.0"; + + let nette = new FormValidator; + nette.version = version; + nette.webalize = webalize; + + return nette; + +})); diff --git a/src/assets/netteForms.min.js b/src/assets/netteForms.min.js index 8d84f28f..f0bfad19 100644 --- a/src/assets/netteForms.min.js +++ b/src/assets/netteForms.min.js @@ -1,7 +1,7 @@ -"use strict"; -/**! +/*! * NetteForms - simple form validation. * * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) - */!function(e,t){if(e.JSON)if("function"==typeof define&&define.amd)define((()=>t(e)));else if("object"==typeof module&&"object"==typeof module.exports)module.exports=t(e);else{var n;let r=!(null!==(n=e.Nette)&&void 0!==n&&n.noInit);e.Nette=t(e),r&&e.Nette.initOnLoad()}}("undefined"!=typeof window?window:void 0,(e=>{const t={};let n={},r={},o=new e.WeakMap;function l(e,t){let n=e.elements.namedItem(t);return n instanceof RadioNodeList?n[0]:n}function a(e){let t=e.form.elements.namedItem(e.name);return t instanceof RadioNodeList?Array.from(t):[t]}return t.formErrors=[],t.version="3.3.0",t.onDocumentReady=function(e){"loading"!==document.readyState?e.call(this):document.addEventListener("DOMContentLoaded",e)},t.getValue=function(e){var n,r;return e instanceof HTMLInputElement?"radio"===e.type?(null===(n=a(e).find((e=>e.checked)))||void 0===n?void 0:n.value)??null:"file"===e.type?e.files:"checkbox"===e.type?e.name.endsWith("[]")?a(e).filter((e=>e.checked)).map((e=>e.value)):e.checked:e.value.trim():e instanceof HTMLSelectElement?e.multiple?Array.from(e.selectedOptions,(e=>e.value)):(null===(r=e.selectedOptions[0])||void 0===r?void 0:r.value)??null:e instanceof HTMLTextAreaElement?e.value:e instanceof RadioNodeList?t.getValue(e[0]):null},t.getEffectiveValue=function(e,r=!1){let o=t.getValue(e);if(o===e.getAttribute("data-nette-empty-value")&&(o=""),r&&void 0===n[e.name]){n[e.name]=!0;let r={value:o};t.validateControl(e,null,!0,r),o=r.value,delete n[e.name]}return o},t.validateControl=function(e,n,r=!1,o=null,a=null){n??=JSON.parse(e.getAttribute("data-nette-rules")??"[]"),o??={value:t.getEffectiveValue(e)},a??=!t.validateRule(e,":filled",null,o);for(let i of n){let n=i.op.match(/(~)?([^?]+)/),u=i.control?l(e.form,i.control):e;if(i.neg=n[1],i.op=n[2],i.condition=!!i.rules,!u)continue;if(a&&!i.condition&&":filled"!==i.op)continue;let f=t.validateRule(u,i.op,i.arg,e===u?o:void 0);if(null!==f)if(i.neg&&(f=!f),i.condition&&f){if(!t.validateControl(e,i.rules,r,o,":blank"!==i.op&&a))return!1}else if(!i.condition&&!f){if(t.isDisabled(u))continue;if(!r){let n=Array.isArray(i.arg)?i.arg:[i.arg],r=i.msg.replace(/%(value|\d+)/g,((r,o)=>t.getValue("value"===o?u:e.form.elements.namedItem(n[o].control))));t.addError(u,r)}return!1}}return!0},t.validateForm=function(e,n=!1){let r,o=e.form??e;if(t.formErrors=[],o["nette-submittedBy"]&&null!==o["nette-submittedBy"].getAttribute("formnovalidate")){let e=JSON.parse(o["nette-submittedBy"].getAttribute("data-nette-validation-scope")??"[]");if(!e.length)return t.showFormErrors(o,[]),!0;r=new RegExp("^("+e.join("-|")+"-)")}for(let e of o.elements)if(e.willValidate&&e.validity.badInput)return e.reportValidity(),!1;for(let e of Array.from(o.elements))if(e.getAttribute("data-nette-rules")&&(!r||e.name.replace(/]\[|\[|]|$/g,"-").match(r))&&!t.isDisabled(e)&&!t.validateControl(e,null,n)&&!t.formErrors.length)return!1;let l=!t.formErrors.length;return t.showFormErrors(o,t.formErrors),l},t.isDisabled=function(e){return"radio"===e.type?a(e).every((e=>e.disabled)):e.disabled},t.addError=function(e,n){t.formErrors.push({element:e,message:n})},t.showFormErrors=function(e,n){let r,o=[];for(let e of n)o.indexOf(e.message)<0&&(o.push(e.message),!r&&e.element.focus&&(r=e.element));o.length&&t.showModal(o.join("\n"),(()=>{r&&r.focus()}))},t.showModal=function(e,t){let n=document.createElement("dialog");if(!n.showModal)return alert(e),void t();let r=document.createElement("style");r.innerText=".netteFormsModal { text-align: center; margin: auto; border: 2px solid black; padding: 1rem } .netteFormsModal button { padding: .1em 2em }";let o=document.createElement("button");o.innerText="OK",o.onclick=()=>{n.remove(),t()},n.setAttribute("class","netteFormsModal"),n.innerText=e+"\n\n",n.append(r,o),document.body.append(n),n.showModal()},t.validateRule=function(e,n,r,o){if(e.validity.badInput)return":filled"===n;o??={value:t.getEffectiveValue(e,!0)};let a=":"===n.charAt(0)?n.substring(1):n;a=a.replace("::","_").replaceAll("\\","");let i=Array.isArray(r)?r:[r];return i=i.map((n=>{if(null!=n&&n.control){let r=l(e.form,n.control);return r===e?o.value:t.getEffectiveValue(r,!0)}return n})),t.validators[a]?t.validators[a](e,Array.isArray(r)?i:i[0],o.value,o):null},t.validators={filled:function(e,t,n){return""!==n&&!1!==n&&null!==n&&(!Array.isArray(n)||!!n.length)&&(!(n instanceof FileList)||n.length)},blank:function(e,n,r){return!t.validators.filled(e,n,r)},valid:function(e){return t.validateControl(e,null,!0)},equal:function(e,t,n){if(void 0===t)return null;let r=e=>"number"==typeof e||"string"==typeof e?""+e:!0===e?"1":"";n=Array.isArray(n)?n:[n],t=Array.isArray(t)?t:[t];e:for(let e of n){for(let n of t)if(r(e)===r(n))continue e;return!1}return n.length>0},notEqual:function(e,n,r){return void 0===n?null:!t.validators.equal(e,n,r)},minLength:function(e,t,n){return(n="number"==typeof n?n.toString():n).length>=t},maxLength:function(e,t,n){return(n="number"==typeof n?n.toString():n).length<=t},length:function(e,t,n){return n="number"==typeof n?n.toString():n,(null===(t=Array.isArray(t)?t:[t,t])[0]||n.length>=t[0])&&(null===t[1]||n.length<=t[1])},email:function(e,t,n){return/^("([ !#-[\]-~]|\\[ -~])+"|[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*)@([0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)+[a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF])?$/i.test(n)},url:function(e,t,n,r){return/^[a-z\d+.-]+:/.test(n)||(n="https://"+n),!!/^https?:\/\/((([-_0-9a-z\u00C0-\u02FF\u0370-\u1EFF]+\.)*[0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)?[a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[0-9a-f:]{3,39}\])(:\d{1,5})?(\/\S*)?$/i.test(n)&&(r.value=n,!0)},regexp:function(e,t,n){let r="string"==typeof t&&t.match(/^\/(.*)\/([imu]*)$/);try{return r&&new RegExp(r[1],r[2].replace("u","")).test(n)}catch{}},pattern:function(e,t,n,r,o){if("string"!=typeof t)return null;try{let e;try{e=new RegExp("^(?:"+t+")$",o?"ui":"u")}catch{e=new RegExp("^(?:"+t+")$",o?"i":"")}return n instanceof FileList?Array.from(n).every((t=>e.test(t.name))):e.test(n)}catch{}},patternCaseInsensitive:function(e,n,r){return t.validators.pattern(e,n,r,null,!0)},numeric:function(e,t,n){return/^[0-9]+$/.test(n)},integer:function(e,t,n,r){return!!/^-?[0-9]+$/.test(n)&&(r.value=parseFloat(n),!0)},float:function(e,t,n,r){return n=n.replace(/ +/g,"").replace(/,/g,"."),!!/^-?[0-9]*\.?[0-9]+$/.test(n)&&(r.value=parseFloat(n),!0)},min:function(e,t,n){return Number.isFinite(t)&&(n=parseFloat(n)),n>=t},max:function(e,t,n){return Number.isFinite(t)&&(n=parseFloat(n)),n<=t},range:function(e,n,r){return Array.isArray(n)?"time"===e.type&&n[0]>n[1]?r>=n[0]||r<=n[1]:(null===n[0]||t.validators.min(e,n[0],r))&&(null===n[1]||t.validators.max(e,n[1],r)):null},submitted:function(e){return e.form["nette-submittedBy"]===e},fileSize:function(e,t,n){return Array.from(n).every((e=>e.size<=t))},mimeType:function(e,t,n){let r=[];return(t=Array.isArray(t)?t:[t]).forEach((e=>r.push("^"+e.replace(/([^\w])/g,"\\$1").replace("\\*",".*")+"$"))),r=new RegExp(r.join("|")),Array.from(n).every((e=>!e.type||r.test(e.type)))},image:function(e,n,r){return t.validators.mimeType(e,n??["image/gif","image/png","image/jpeg","image/webp"],r)},static:function(e,t){return t}},t.toggleForm=function(e,n=null){r={};for(let r of Array.from(e.elements))r.getAttribute("data-nette-rules")&&t.toggleControl(r,null,null,!n);for(let e in r)t.toggle(e,r[e].state,r[e].elem,n)},t.toggleControl=function(e,n,i,u,f=null,d=null){n??=JSON.parse(e.getAttribute("data-nette-rules")??"[]"),f??={value:t.getEffectiveValue(e)},d??=!t.validateRule(e,":filled",null,f);let s,c=!1;for(let m of n){let n=m.op.match(/(~)?([^?]+)/),g=m.control?l(e.form,m.control):e;if(m.neg=n[1],m.op=n[2],m.condition=!!m.rules,g&&(!d||m.condition||":filled"===m.op)){if(s=i,!1!==i){if(s=t.validateRule(g,m.op,m.arg,e===g?f:void 0),null===s)continue;m.neg&&(s=!s),m.condition||(i=s)}if(m.condition&&t.toggleControl(e,m.rules,s,u,f,":blank"!==m.op&&d)||m.toggle){c=!0,u&&a(g).filter((e=>!o.has(e))).forEach((n=>{n.addEventListener("change",(n=>t.toggleForm(e.form,n))),o.set(n,null)}));for(let t in m.toggle??[])r[t]??={elem:e},r[t].state||=m.toggle[t]?s:!s}}}return c},t.toggle=function(e,t,n,r){/^\w[\w.:-]*$/.test(e)&&(e="#"+e),Array.from(document.querySelectorAll(e)).forEach((e=>e.hidden=!t))},t.compactCheckboxes=function(e,t){let n={};for(let r of e.elements)r instanceof HTMLInputElement&&"checkbox"===r.type&&r.name.endsWith("[]")&&r.checked&&!r.disabled&&(t.delete(r.name),n[r.name]??=[],n[r.name].push(r.value));for(let e in n)t.set(e.substring(0,e.length-2),n[e].join(","))},t.initForm=function(e){"get"===e.method&&e.hasAttribute("data-nette-compact")&&e.addEventListener("formdata",(n=>t.compactCheckboxes(e,n.formData))),Array.from(e.elements).some((e=>e.getAttribute("data-nette-rules")))&&(t.toggleForm(e),e.noValidate||(e.noValidate=!0,e.addEventListener("submit",(n=>{t.validateForm(e)||(n.stopPropagation(),n.preventDefault())})),e.addEventListener("reset",(()=>{setTimeout((()=>t.toggleForm(e)))}))))},t.initOnLoad=function(){t.onDocumentReady((()=>{Array.from(document.forms).forEach((e=>t.initForm(e))),document.body.addEventListener("click",(e=>{let t=e.target;for(;t;){if(t.form&&t.type in{submit:1,image:1}){t.form["nette-submittedBy"]=t;break}t=t.parentNode}}))}))},t.webalize=function(e){e=e.toLowerCase();let n,r="";for(let o=0;o0)&&(!(r instanceof FileList)||r.length>0)}blank(e,t,r){return!this.filled(e,t,r)}valid(e,t){return t.validateControl(e,void 0,!0)}equal(e,t,r){if(void 0===t)return null;const n=e=>"number"==typeof e||"string"==typeof e?""+e:!0===e?"1":"";let i=Array.isArray(r)?r:[r],l=Array.isArray(t)?t:[t];e:for(let e of i){for(let t of l)if(n(e)===n(t))continue e;return!1}return i.length>0}notEqual(e,t,r){return void 0===t?null:!this.equal(e,t,r)}minLength(e,t,r){return(r="number"==typeof r?r.toString():r).length>=t}maxLength(e,t,r){return(r="number"==typeof r?r.toString():r).length<=t}length(e,t,r){return r="number"==typeof r?r.toString():r,(null===(t=Array.isArray(t)?t:[t,t])[0]||r.length>=t[0])&&(null===t[1]||r.length<=t[1])}email(e,t,r){return/^("([ !#-[\]-~]|\\[ -~])+"|[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*)@([0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)+[a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF])?$/i.test(r)}url(e,t,r,n){return/^[a-z\d+.-]+:/.test(r)||(r="https://"+r),!!/^https?:\/\/((([-_0-9a-z\u00C0-\u02FF\u0370-\u1EFF]+\.)*[0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)?[a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[0-9a-f:]{3,39}\])(:\d{1,5})?(\/\S*)?$/i.test(r)&&(n.value=r,!0)}regexp(e,t,r){let n="string"==typeof t&&t.match(/^\/(.*)\/([imu]*)$/);try{return n&&new RegExp(n[1],n[2].replace("u","")).test(r)}catch{return null}}pattern(e,t,r,n,i){if("string"!=typeof t)return null;try{let e;try{e=new RegExp("^(?:"+t+")$",i?"ui":"u")}catch{e=new RegExp("^(?:"+t+")$",i?"i":"")}return r instanceof FileList?Array.from(r).every((t=>e.test(t.name))):e.test(r)}catch{return null}}patternCaseInsensitive(e,t,r){return this.pattern(e,t,r,null,!0)}numeric(e,t,r){return/^[0-9]+$/.test(r)}integer(e,t,r,n){return!!/^-?[0-9]+$/.test(r)&&(n.value=parseFloat(r),!0)}float(e,t,r,n){return r=r.replace(/ +/g,"").replace(/,/g,"."),!!/^-?[0-9]*\.?[0-9]+$/.test(r)&&(n.value=parseFloat(r),!0)}min(e,t,r){return Number.isFinite(t)&&(r=parseFloat(r)),r>=t}max(e,t,r){return Number.isFinite(t)&&(r=parseFloat(r)),r<=t}range(e,t,r){return Array.isArray(t)?"time"===e.type&&t[0]>t[1]?r>=t[0]||r<=t[1]:(null===t[0]||this.min(e,t[0],r))&&(null===t[1]||this.max(e,t[1],r)):null}submitted(e){return e.form["nette-submittedBy"]===e}fileSize(e,t,r){return Array.from(r).every((e=>e.size<=t))}mimeType(e,t,r){let n=[];(t=Array.isArray(t)?t:[t]).forEach((e=>n.push("^"+e.replace(/([^\w])/g,"\\$1").replace("\\*",".*")+"$")));let i=new RegExp(n.join("|"));return Array.from(r).every((e=>!e.type||i.test(e.type)))}image(e,t,r){return this.mimeType(e,t??["image/gif","image/png","image/jpeg","image/webp"],r)}static(e,t){return t}}let t={"á":"a","ä":"a","č":"c","ď":"d","é":"e","ě":"e","í":"i","ľ":"l","ň":"n","ó":"o","ô":"o","ř":"r","š":"s","ť":"t","ú":"u","ů":"u","ý":"y","ž":"z"};let r=new class{formErrors=[];validators=new e;#e={};#t={};#r=new WeakMap;#n(e,t){let r=e.elements.namedItem(t);return r instanceof RadioNodeList?r[0]:r}#i(e){let t=e.form.elements.namedItem(e.name);return t instanceof RadioNodeList?Array.from(t):[t]}#l(e){"loading"!==document.readyState?e.call(this):document.addEventListener("DOMContentLoaded",e)}getValue(e){return e instanceof HTMLInputElement?"radio"===e.type?this.#i(e).find((e=>e.checked))?.value??null:"file"===e.type?e.files:"checkbox"===e.type?e.name.endsWith("[]")?this.#i(e).filter((e=>e.checked)).map((e=>e.value)):e.checked:e.value.trim():e instanceof HTMLSelectElement?e.multiple?Array.from(e.selectedOptions,(e=>e.value)):e.selectedOptions[0]?.value??null:e instanceof HTMLTextAreaElement?e.value:e instanceof RadioNodeList?this.getValue(e[0]):null}getEffectiveValue(e,t=!1){let r=this.getValue(e);if(r===e.getAttribute("data-nette-empty-value")&&(r=""),t&&void 0===this.#e[e.name]){this.#e[e.name]=!0;let t={value:r};this.validateControl(e,void 0,!0,t),r=t.value,delete this.#e[e.name]}return r}validateControl(e,t,r=!1,n,i){t??=JSON.parse(e.getAttribute("data-nette-rules")??"[]"),n??={value:this.getEffectiveValue(e)},i??=!this.validateRule(e,":filled",null,n);for(let l of t){let t=l.op.match(/(~)?([^?]+)/),a=l.control?this.#n(e.form,l.control):e;if(l.neg=!!t[1],l.op=t[2],l.condition=!!l.rules,!a)continue;if(i&&!l.condition&&":filled"!==l.op)continue;let o=this.validateRule(a,l.op,l.arg,e===a?n:void 0);if(null!==o)if(l.neg&&(o=!o),l.condition&&o){if(!this.validateControl(e,l.rules,r,n,":blank"!==l.op&&i))return!1}else if(!l.condition&&!o){if(this.isDisabled(a))continue;if(!r){let t=Array.isArray(l.arg)?l.arg:[l.arg],r=l.msg.replace(/%(value|\d+)/g,((r,n)=>this.getValue("value"===n?a:e.form.elements.namedItem(t[n].control))));this.addError(a,r)}return!1}}return!0}validateForm(e,t=!1){let r,n=e.form??e;if(this.formErrors=[],n["nette-submittedBy"]&&null!==n["nette-submittedBy"].getAttribute("formnovalidate")){let e=JSON.parse(n["nette-submittedBy"].getAttribute("data-nette-validation-scope")??"[]");if(!e.length)return this.showFormErrors(n,[]),!0;r=new RegExp("^("+e.join("-|")+"-)")}for(let e of n.elements)if(e.willValidate&&e.validity.badInput)return e.reportValidity(),!1;for(let e of n.elements)if(e.getAttribute("data-nette-rules")&&(!r||e.name.replace(/]\[|\[|]|$/g,"-").match(r))&&!this.isDisabled(e)&&!this.validateControl(e,void 0,t)&&!this.formErrors.length)return!1;let i=!this.formErrors.length;return this.showFormErrors(n,this.formErrors),i}isDisabled(e){return"radio"===e.type?this.#i(e).every((e=>e.disabled)):e.disabled}addError(e,t){this.formErrors.push({element:e,message:t})}showFormErrors(e,t){let r,n=[];for(let e of t)n.indexOf(e.message)<0&&(n.push(e.message),r??=e.element);n.length&&this.showModal(n.join("\n"),(()=>{r?.focus()}))}showModal(e,t){let r=document.createElement("dialog");if(!r.showModal)return alert(e),void t();let n=document.createElement("style");n.innerText=".netteFormsModal { text-align: center; margin: auto; border: 2px solid black; padding: 1rem } .netteFormsModal button { padding: .1em 2em }";let i=document.createElement("button");i.innerText="OK",i.onclick=()=>{r.remove(),t()},r.setAttribute("class","netteFormsModal"),r.innerText=e+"\n\n",r.append(n,i),document.body.append(r),r.showModal()}validateRule(e,t,r,n){if(e.validity.badInput)return":filled"===t;n??={value:this.getEffectiveValue(e,!0)};let i=":"===t.charAt(0)?t.substring(1):t;i=i.replace("::","_").replaceAll("\\","");let l=Array.isArray(r)?r:[r];return l=l.map((t=>{if(t?.control){let r=this.#n(e.form,t.control);return r===e?n.value:this.getEffectiveValue(r,!0)}return t})),"valid"===i&&(l[0]=this),this.validators[i]?this.validators[i](e,Array.isArray(r)?l:l[0],n.value,n):null}toggleForm(e,t){this.#t={};for(let r of Array.from(e.elements))r.getAttribute("data-nette-rules")&&this.toggleControl(r,void 0,null,!t);for(let e in this.#t)this.toggle(e,this.#t[e].state,this.#t[e].elem,t)}toggleControl(e,t,r=null,n=!1,i,l){t??=JSON.parse(e.getAttribute("data-nette-rules")??"[]"),i??={value:this.getEffectiveValue(e)},l??=!this.validateRule(e,":filled",null,i);let a,o=!1;for(let s of t){let t=s.op.match(/(~)?([^?]+)/),u=s.control?this.#n(e.form,s.control):e;if(s.neg=!!t[1],s.op=t[2],s.condition=!!s.rules,u&&(!l||s.condition||":filled"===s.op)){if(a=r,!1!==r){if(a=this.validateRule(u,s.op,s.arg,e===u?i:void 0),null===a)continue;s.neg&&(a=!a),s.condition||(r=a)}if(s.condition&&this.toggleControl(e,s.rules,a,n,i,":blank"!==s.op&&l)||s.toggle){o=!0,n&&this.#i(u).filter((e=>!this.#r.has(e))).forEach((t=>{t.addEventListener("change",(t=>this.toggleForm(e.form,t))),this.#r.set(t,null)}));for(let t in s.toggle??{})this.#t[t]??={elem:e,state:!1},this.#t[t].state||=s.toggle[t]?!!a:!a}}}return o}toggle(e,t,r,n){/^\w[\w.:-]*$/.test(e)&&(e="#"+e),Array.from(document.querySelectorAll(e)).forEach((e=>e.hidden=!t))}compactCheckboxes(e,t){let r={};for(let n of e.elements)n instanceof HTMLInputElement&&"checkbox"===n.type&&n.name.endsWith("[]")&&n.checked&&!n.disabled&&(t.delete(n.name),r[n.name]??=[],r[n.name].push(n.value));for(let e in r)t.set(e.substring(0,e.length-2),r[e].join(","))}initForm(e){"get"===e.method&&e.hasAttribute("data-nette-compact")&&e.addEventListener("formdata",(t=>this.compactCheckboxes(e,t.formData))),Array.from(e.elements).some((e=>e.getAttribute("data-nette-rules")))&&(this.toggleForm(e),e.noValidate||(e.noValidate=!0,e.addEventListener("submit",(t=>{this.validateForm(e)||(t.stopPropagation(),t.preventDefault())})),e.addEventListener("reset",(()=>{setTimeout((()=>this.toggleForm(e)))}))))}initOnLoad(){this.#l((()=>{Array.from(document.forms).forEach((e=>this.initForm(e))),document.body.addEventListener("click",(e=>{let t=e.target;for(;t;){if(t.form&&t.type in{submit:1,image:1}){t.form["nette-submittedBy"]=t;break}t=t.parentNode}}))}))}};return r.version="3.5.0",r.webalize=function(e){e=e.toLowerCase();let r="";for(let n=0;n