UNPKG

hyperform

Version:

Capture form validation back from the browser

1,794 lines (1,455 loc) 90.4 kB
// hyperform.js.org 'use strict'; var instances = new WeakMap(); /** * wrap <form>s, window or document, that get treated with the global * hyperform() */ function Wrapper(form, settings) { /* do not allow more than one instance per form. Otherwise we'd end * up with double event handlers, polyfills re-applied, ... */ var existing = instances.get(form); if (existing) { existing.settings = settings; return existing; } this.form = form; this.settings = settings; this.observer = null; instances.set(form, this); } Wrapper.prototype = { destroy: function destroy() { instances["delete"](this.form); if (this._destruct) { this._destruct(); } } }; /** * try to get the appropriate wrapper for a specific element by looking up * its parent chain * * @return Wrapper | undefined */ function get_wrapper(element) { var wrapped; if (element.form) { /* try a shortcut with the element's <form> */ wrapped = instances.get(element.form); } /* walk up the parent nodes until document (including) */ while (!wrapped && element) { wrapped = instances.get(element); element = element.parentNode; } if (!wrapped) { /* try the global instance, if exists. This may also be undefined. */ wrapped = instances.get(window); } return wrapped; } /** * filter a form's elements for the ones needing validation prior to * a submit * * Returns an array of form elements. */ function get_validated_elements(form) { var wrapped_form = get_wrapper(form); return Array.prototype.filter.call(form.elements, function (element) { /* it must have a name (or validating nameless inputs is allowed) */ if (element.getAttribute('name') || wrapped_form && wrapped_form.settings.validateNameless) { return true; } return false; }); } var registry = Object.create(null); /** * run all actions registered for a hook * * Every action gets called with a state object as `this` argument and with the * hook's call arguments as call arguments. * * @return mixed the returned value of the action calls or undefined */ function call_hook(hook) { var result; var call_args = Array.prototype.slice.call(arguments, 1); if (hook in registry) { result = registry[hook].reduce(function (args) { return function (previousResult, currentAction) { var interimResult = currentAction.apply({ state: previousResult, hook: hook }, args); return interimResult !== undefined ? interimResult : previousResult; }; }(call_args), result); } return result; } /** * Filter a value through hooked functions * * Allows for additional parameters: * js> do_filter('foo', null, current_element) */ function do_filter(hook, initial_value) { var result = initial_value; var call_args = Array.prototype.slice.call(arguments, 1); if (hook in registry) { result = registry[hook].reduce(function (previousResult, currentAction) { call_args[0] = previousResult; var interimResult = currentAction.apply({ state: previousResult, hook: hook }, call_args); return interimResult !== undefined ? interimResult : previousResult; }, result); } return result; } /** * remove an action again */ function remove_hook(hook, action) { if (hook in registry) { for (var i = 0; i < registry[hook].length; i++) { if (registry[hook][i] === action) { registry[hook].splice(i, 1); break; } } } } /** * add an action to a hook */ function add_hook(hook, action, position) { if (!(hook in registry)) { registry[hook] = []; } if (position === undefined) { position = registry[hook].length; } registry[hook].splice(position, 0, action); } /** * return either the data of a hook call or the result of action, if the * former is undefined * * @return function a function wrapper around action */ function return_hook_or (hook, action) { return function () { var data = call_hook(hook, Array.prototype.slice.call(arguments)); if (data !== undefined) { return data; } return action.apply(this, arguments); }; } /* the following code is borrowed from the WebComponents project, licensed * under the BSD license. Source: * <https://github.com/webcomponents/webcomponentsjs/blob/5283db1459fa2323e5bfc8b9b5cc1753ed85e3d0/src/WebComponents/dom.js#L53-L78> */ // defaultPrevented is broken in IE. // https://connect.microsoft.com/IE/feedback/details/790389/event-defaultprevented-returns-false-after-preventdefault-was-called var workingDefaultPrevented = function () { var e = document.createEvent('Event'); e.initEvent('foo', true, true); e.preventDefault(); return e.defaultPrevented; }(); if (!workingDefaultPrevented) { var origPreventDefault = window.Event.prototype.preventDefault; window.Event.prototype.preventDefault = function () { if (!this.cancelable) { return; } origPreventDefault.call(this); Object.defineProperty(this, 'defaultPrevented', { get: function get() { return true; }, configurable: true }); }; } /* end of borrowed code */ function create_event(name) { var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref$bubbles = _ref.bubbles, bubbles = _ref$bubbles === void 0 ? true : _ref$bubbles, _ref$cancelable = _ref.cancelable, cancelable = _ref$cancelable === void 0 ? false : _ref$cancelable; var event = document.createEvent('Event'); event.initEvent(name, bubbles, cancelable); return event; } function trigger_event (element, event) { var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, _ref2$bubbles = _ref2.bubbles, bubbles = _ref2$bubbles === void 0 ? true : _ref2$bubbles, _ref2$cancelable = _ref2.cancelable, cancelable = _ref2$cancelable === void 0 ? false : _ref2$cancelable; var payload = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; if (!(event instanceof window.Event)) { event = create_event(event, { bubbles: bubbles, cancelable: cancelable }); } for (var key in payload) { if (payload.hasOwnProperty(key)) { event[key] = payload[key]; } } element.dispatchEvent(event); return event; } /* and datetime-local? Spec says “Nah!” */ var dates = ['datetime', 'date', 'month', 'week', 'time']; var plain_numbers = ['number', 'range']; /* everything that returns something meaningful for valueAsNumber and * can have the step attribute */ var numbers = dates.concat(plain_numbers, 'datetime-local'); /* the spec says to only check those for syntax in validity.typeMismatch. * ¯\_(ツ)_/¯ */ var type_checked = ['email', 'url']; /* check these for validity.badInput */ var input_checked = ['email', 'date', 'month', 'week', 'time', 'datetime', 'datetime-local', 'number', 'range', 'color']; var text = ['text', 'search', 'tel', 'password'].concat(type_checked); /* input element types, that are candidates for the validation API. * Missing from this set are: button, hidden, menu (from <button>), reset and * the types for non-<input> elements. */ var validation_candidates = ['checkbox', 'color', 'file', 'image', 'radio', 'submit'].concat(numbers, text); /* all known types of <input> */ var inputs = ['button', 'hidden', 'reset'].concat(validation_candidates); /* apparently <select> and <textarea> have types of their own */ var non_inputs = ['select-one', 'select-multiple', 'textarea']; /** * get the element's type in a backwards-compatible way */ function get_type (element) { if (element instanceof window.HTMLTextAreaElement) { return 'textarea'; } else if (element instanceof window.HTMLSelectElement) { return element.hasAttribute('multiple') ? 'select-multiple' : 'select-one'; } else if (element instanceof window.HTMLButtonElement) { return (element.getAttribute('type') || 'submit').toLowerCase(); } else if (element instanceof window.HTMLInputElement) { var attr = (element.getAttribute('type') || '').toLowerCase(); if (attr && inputs.indexOf(attr) > -1) { return attr; } else { /* perhaps the DOM has in-depth knowledge. Take that before returning * 'text'. */ return element.type || 'text'; } } return ''; } /** * check if an element should be ignored due to any of its parents * * Checks <fieldset disabled> and <datalist>. */ function is_in_disallowed_parent(element) { var p = element.parentNode; while (p && p.nodeType === 1) { if (p instanceof window.HTMLFieldSetElement && p.hasAttribute('disabled')) { /* quick return, if it's a child of a disabled fieldset */ return true; } else if (p.nodeName.toUpperCase() === 'DATALIST') { /* quick return, if it's a child of a datalist * Do not use HTMLDataListElement to support older browsers, * too. * @see https://html.spec.whatwg.org/multipage/forms.html#the-datalist-element:barred-from-constraint-validation */ return true; } else if (p === element.form) { /* the outer boundary. We can stop looking for relevant elements. */ break; } p = p.parentNode; } return false; } /** * check if an element is a candidate for constraint validation * * @see https://html.spec.whatwg.org/multipage/forms.html#barred-from-constraint-validation */ function is_validation_candidate (element) { /* allow a shortcut via filters, e.g. to validate type=hidden fields */ var filtered = do_filter('is_validation_candidate', null, element); if (filtered !== null) { return !!filtered; } /* it must be any of those elements */ if (element instanceof window.HTMLSelectElement || element instanceof window.HTMLTextAreaElement || element instanceof window.HTMLButtonElement || element instanceof window.HTMLInputElement) { var type = get_type(element); /* its type must be in the whitelist */ if (non_inputs.indexOf(type) > -1 || validation_candidates.indexOf(type) > -1) { /* it mustn't be disabled or readonly */ if (!element.hasAttribute('disabled') && !element.hasAttribute('readonly')) { var wrapped_form = get_wrapper(element); if ( /* the parent form doesn't allow non-standard "novalidate" attributes... */ wrapped_form && !wrapped_form.settings.novalidateOnElements || /* ...or it doesn't have such an attribute/property */ !element.hasAttribute('novalidate') && !element.noValidate) { /* it isn't part of a <fieldset disabled> */ if (!is_in_disallowed_parent(element)) { /* then it's a candidate */ return true; } } } } } /* this is no HTML5 validation candidate... */ return false; } function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function () {}; return { s: F, n: function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function (e) { throw e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function () { it = it.call(o); }, n: function () { var step = it.next(); normalCompletion = step.done; return step; }, e: function (e) { didErr = true; err = e; }, f: function () { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function mark (obj) { if (['object', 'function'].indexOf(_typeof(obj)) > -1) { delete obj.__hyperform; Object.defineProperty(obj, '__hyperform', { configurable: true, enumerable: false, value: true }); } return obj; } function format_date (date) { var part = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; switch (part) { case 'date': return (date.toLocaleDateString || date.toDateString).call(date); case 'time': return (date.toLocaleTimeString || date.toTimeString).call(date); case 'month': return 'toLocaleDateString' in date ? date.toLocaleDateString(undefined, { year: 'numeric', month: '2-digit' }) : date.toDateString(); // case 'week': // TODO default: return (date.toLocaleString || date.toString).call(date); } } function sprintf (str) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } var args_length = args.length; var global_index = 0; return str.replace(/%([0-9]+\$)?([sl])/g, function (match, position, type) { var local_index = global_index; if (position) { local_index = Number(position.replace(/\$$/, '')) - 1; } global_index += 1; var arg = ''; if (args_length > local_index) { arg = args[local_index]; } if (arg instanceof Date || typeof arg === 'number' || arg instanceof Number) { /* try getting a localized representation of dates and numbers, if the * browser supports this */ if (type === 'l') { arg = (arg.toLocaleString || arg.toString).call(arg); } else { arg = arg.toString(); } } return arg; }); } /* For a given date, get the ISO week number * * Source: http://stackoverflow.com/a/6117889/113195 * * Based on information at: * * http://www.merlyn.demon.co.uk/weekcalc.htm#WNR * * Algorithm is to find nearest thursday, it's year * is the year of the week number. Then get weeks * between that date and the first day of that year. * * Note that dates in one year can be weeks of previous * or next year, overlap is up to 3 days. * * e.g. 2014/12/29 is Monday in week 1 of 2015 * 2012/1/1 is Sunday in week 52 of 2011 */ function get_week_of_year (d) { /* Copy date so don't modify original */ d = new Date(+d); d.setUTCHours(0, 0, 0); /* Set to nearest Thursday: current date + 4 - current day number * Make Sunday's day number 7 */ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); /* Get first day of year */ var yearStart = new Date(d.getUTCFullYear(), 0, 1); /* Calculate full weeks to nearest Thursday */ var weekNo = Math.ceil(((d - yearStart) / 86400000 + 1) / 7); /* Return array of year and week number */ return [d.getUTCFullYear(), weekNo]; } function pad(num) { var size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; var s = num + ''; while (s.length < size) { s = '0' + s; } return s; } /** * calculate a string from a date according to HTML5 */ function date_to_string(date, element_type) { if (!(date instanceof Date)) { return null; } switch (element_type) { case 'datetime': return date_to_string(date, 'date') + 'T' + date_to_string(date, 'time'); case 'datetime-local': return sprintf('%s-%s-%sT%s:%s:%s.%s', date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate()), pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds()), pad(date.getMilliseconds(), 3)).replace(/(:00)?\.000$/, ''); case 'date': return sprintf('%s-%s-%s', date.getUTCFullYear(), pad(date.getUTCMonth() + 1), pad(date.getUTCDate())); case 'month': return sprintf('%s-%s', date.getUTCFullYear(), pad(date.getUTCMonth() + 1)); case 'week': { var params = get_week_of_year(date); return sprintf.call(null, '%s-W%s', params[0], pad(params[1])); } case 'time': return sprintf('%s:%s:%s.%s', pad(date.getUTCHours()), pad(date.getUTCMinutes()), pad(date.getUTCSeconds()), pad(date.getUTCMilliseconds(), 3)).replace(/(:00)?\.000$/, ''); } return null; } /** * return a new Date() representing the ISO date for a week number * * @see http://stackoverflow.com/a/16591175/113195 */ function get_date_from_week (week, year) { var date = new Date(Date.UTC(year, 0, 1 + (week - 1) * 7)); if (date.getUTCDay() <= 4 /* thursday */ ) { date.setUTCDate(date.getUTCDate() - date.getUTCDay() + 1); } else { date.setUTCDate(date.getUTCDate() + 8 - date.getUTCDay()); } return date; } /** * calculate a date from a string according to HTML5 */ function string_to_date (string, element_type) { var date; switch (element_type) { case 'datetime': if (!/^([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:\.([0-9]{1,3}))?)?$/.test(string)) { return null; } date = new Date(string + 'z'); return isNaN(date.valueOf()) ? null : date; case 'date': if (!/^([0-9]{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/.test(string)) { return null; } date = new Date(string); return isNaN(date.valueOf()) ? null : date; case 'month': if (!/^([0-9]{4})-(0[1-9]|1[012])$/.test(string)) { return null; } date = new Date(string); return isNaN(date.valueOf()) ? null : date; case 'week': if (!/^([0-9]{4})-W(0[1-9]|[1234][0-9]|5[0-3])$/.test(string)) { return null; } return get_date_from_week(Number(RegExp.$2), Number(RegExp.$1)); case 'time': if (!/^([01][0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:\.([0-9]{1,3}))?)?$/.test(string)) { return null; } date = new Date('1970-01-01T' + string + 'z'); return date; } return null; } /** * calculate a number from a string according to HTML5 */ function string_to_number (string, element_type) { var rval = string_to_date(string, element_type); if (rval !== null) { return +rval; } /* not parseFloat, because we want NaN for invalid values like "1.2xxy" */ return Number(string); } /** * the following validation messages are from Firefox source, * http://mxr.mozilla.org/mozilla-central/source/dom/locales/en-US/chrome/dom/dom.properties * released under MPL license, http://mozilla.org/MPL/2.0/. */ var catalog = { en: { TextTooLong: 'Please shorten this text to %l characters or less (you are currently using %l characters).', ValueMissing: 'Please fill out this field.', CheckboxMissing: 'Please check this box if you want to proceed.', RadioMissing: 'Please select one of these options.', FileMissing: 'Please select a file.', SelectMissing: 'Please select an item in the list.', InvalidEmail: 'Please enter an email address.', InvalidURL: 'Please enter a URL.', PatternMismatch: 'Please match the requested format.', PatternMismatchWithTitle: 'Please match the requested format: %l.', NumberRangeOverflow: 'Please select a value that is no more than %l.', DateRangeOverflow: 'Please select a value that is no later than %l.', TimeRangeOverflow: 'Please select a value that is no later than %l.', NumberRangeUnderflow: 'Please select a value that is no less than %l.', DateRangeUnderflow: 'Please select a value that is no earlier than %l.', TimeRangeUnderflow: 'Please select a value that is no earlier than %l.', StepMismatch: 'Please select a valid value. The two nearest valid values are %l and %l.', StepMismatchOneValue: 'Please select a valid value. The nearest valid value is %l.', BadInputNumber: 'Please enter a number.' } }; /** * the global language Hyperform will use */ var language = 'en'; /** * the base language according to BCP47, i.e., only the piece before the first hyphen */ var base_lang = 'en'; /** * set the language for Hyperform’s messages */ function set_language(newlang) { language = newlang; base_lang = newlang.replace(/[-_].*/, ''); } /** * add a lookup catalog "string: translation" for a language */ function add_translation(lang, new_catalog) { if (!(lang in catalog)) { catalog[lang] = {}; } for (var key in new_catalog) { if (new_catalog.hasOwnProperty(key)) { catalog[lang][key] = new_catalog[key]; } } } /** * return `s` translated into the current language * * Defaults to the base language and then English if the former has no * translation for `s`. */ function _ (s) { if (language in catalog && s in catalog[language]) { return catalog[language][s]; } else if (base_lang in catalog && s in catalog[base_lang]) { return catalog[base_lang][s]; } else if (s in catalog.en) { return catalog.en[s]; } return s; } var default_step = { 'datetime-local': 60, datetime: 60, time: 60 }; var step_scale_factor = { 'datetime-local': 1000, datetime: 1000, date: 86400000, week: 604800000, time: 1000 }; var default_step_base = { week: -259200000 }; var default_min = { range: 0 }; var default_max = { range: 100 }; /** * get previous and next valid values for a stepped input element */ function get_next_valid (element) { var n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var type = get_type(element); var aMin = element.getAttribute('min'); var min = default_min[type] || NaN; if (aMin) { var pMin = string_to_number(aMin, type); if (!isNaN(pMin)) { min = pMin; } } var aMax = element.getAttribute('max'); var max = default_max[type] || NaN; if (aMax) { var pMax = string_to_number(aMax, type); if (!isNaN(pMax)) { max = pMax; } } var aStep = element.getAttribute('step'); var step = default_step[type] || 1; if (aStep && aStep.toLowerCase() === 'any') { /* quick return: we cannot calculate prev and next */ return [_('any value'), _('any value')]; } else if (aStep) { var pStep = string_to_number(aStep, type); if (!isNaN(pStep)) { step = pStep; } } var default_value = string_to_number(element.getAttribute('value'), type); var value = string_to_number(element.value || element.getAttribute('value'), type); if (isNaN(value)) { /* quick return: we cannot calculate without a solid base */ return [_('any valid value'), _('any valid value')]; } var step_base = !isNaN(min) ? min : !isNaN(default_value) ? default_value : default_step_base[type] || 0; var scale = step_scale_factor[type] || 1; var prev = step_base + Math.floor((value - step_base) / (step * scale)) * (step * scale) * n; var next = step_base + (Math.floor((value - step_base) / (step * scale)) + 1) * (step * scale) * n; if (prev < min) { prev = null; } else if (prev > max) { prev = max; } if (next > max) { next = null; } else if (next < min) { next = min; } /* convert to date objects, if appropriate */ if (dates.indexOf(type) > -1) { prev = date_to_string(new Date(prev), type); next = date_to_string(new Date(next), type); } return [prev, next]; } /** * patch String.length to account for non-BMP characters * * @see https://mathiasbynens.be/notes/javascript-unicode * We do not use the simple [...str].length, because it needs a ton of * polyfills in older browsers. */ function unicode_string_length (str) { return str.match(/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g).length; } /** * internal storage for custom error messages */ var store = new WeakMap(); /** * register custom error messages per element */ var custom_messages = { set: function set(element, validator, message) { var messages = store.get(element) || {}; messages[validator] = message; store.set(element, messages); return custom_messages; }, get: function get(element, validator) { var _default = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined; var messages = store.get(element); if (messages === undefined || !(validator in messages)) { var data_id = 'data-' + validator.replace(/[A-Z]/g, '-$&').toLowerCase(); if (element.hasAttribute(data_id)) { /* if the element has a data-validator attribute, use this as fallback. * E.g., if validator == 'valueMissing', the element can specify a * custom validation message like this: * <input data-value-missing="Oh noes!"> */ return element.getAttribute(data_id); } return _default; } return messages[validator]; }, "delete": function _delete(element) { var validator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; if (!validator) { return store["delete"](element); } var messages = store.get(element) || {}; if (validator in messages) { delete messages[validator]; store.set(element, messages); return true; } return false; } }; /** * get all radio buttons (including `element`) that belong to element's * radio group */ function get_radiogroup(element) { if (element.form) { return Array.prototype.filter.call(element.form.elements, function (radio) { return radio.type === 'radio' && radio.name === element.name; }); } return [element]; } /** * the internal storage for messages */ var store$1 = new WeakMap(); /** * radio buttons store the combined message on the first element */ function get_message_element(element) { if (element.type === 'radio') { return get_radiogroup(element)[0]; } return element; } /** * handle validation messages * * Falls back to browser-native errors, if any are available. The messages * are String objects so that we can mark() them. */ var message_store = { set: function set(element, message) { var is_custom = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; element = get_message_element(element); if (element instanceof window.HTMLFieldSetElement) { var wrapped_form = get_wrapper(element); if (wrapped_form && !wrapped_form.settings.extendFieldset) { /* make this a no-op for <fieldset> in strict mode */ return message_store; } } if (typeof message === 'string') { /* jshint -W053 */ /* allow new String() */ message = new String(message); } if (is_custom) { message.is_custom = true; } mark(message); store$1.set(element, message); /* allow the :invalid selector to match */ if ('_original_setCustomValidity' in element) { element._original_setCustomValidity(message.toString()); } return message_store; }, get: function get(element) { element = get_message_element(element); var message = store$1.get(element); if (message === undefined && '_original_validationMessage' in element) { /* get the browser's validation message, if we have none. Maybe it * knows more than we. */ /* jshint -W053 */ /* allow new String() */ message = new String(element._original_validationMessage); } /* jshint -W053 */ /* allow new String() */ return message ? message : new String(''); }, "delete": function _delete(element) { var is_custom = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; element = get_message_element(element); if ('_original_setCustomValidity' in element) { element._original_setCustomValidity(''); } var message = store$1.get(element); if (message && is_custom && !message.is_custom) { /* do not delete "native" messages, if asked */ return false; } return store$1["delete"](element); } }; var internal_registry = new WeakMap(); /** * A registry for custom validators * * slim wrapper around a WeakMap to ensure the values are arrays * (hence allowing > 1 validators per element) */ var custom_validator_registry = { set: function set(element, validator) { var current = internal_registry.get(element) || []; current.push(validator); internal_registry.set(element, current); return custom_validator_registry; }, get: function get(element) { return internal_registry.get(element) || []; }, "delete": function _delete(element) { return internal_registry["delete"](element); } }; /** * test whether the element suffers from bad input */ function test_bad_input (element) { var type = get_type(element); if (input_checked.indexOf(type) === -1) { /* we're not interested, thanks! */ return true; } /* the browser hides some bad input from the DOM, e.g. malformed numbers, * email addresses with invalid punycode representation, ... We try to resort * to the original method here. The assumption is, that a browser hiding * bad input will hopefully also always support a proper * ValidityState.badInput */ if (!element.value) { if ('_original_validity' in element && !element._original_validity.__hyperform) { return !element._original_validity.badInput; } /* no value and no original badInput: Assume all's right. */ return true; } var result = true; switch (type) { case 'color': result = /^#[a-f0-9]{6}$/.test(element.value); break; case 'number': case 'range': result = !isNaN(Number(element.value)); break; case 'datetime': case 'date': case 'month': case 'week': case 'time': result = string_to_date(element.value, type) !== null; break; case 'datetime-local': result = /^([0-9]{4,})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9])(?::([0-5][0-9])(?:\.([0-9]{1,3}))?)?$/.test(element.value); break; } return result; } /** * test the max attribute * * we use Number() instead of parseFloat(), because an invalid attribute * value like "123abc" should result in an error. */ function test_max (element) { var type = get_type(element); if (!element.value || !element.hasAttribute('max')) { /* we're not responsible here */ return true; } var value, max; if (dates.indexOf(type) > -1) { value = string_to_date(element.value, type); value = value === null ? NaN : +value; max = string_to_date(element.getAttribute('max'), type); max = max === null ? NaN : +max; } else { value = Number(element.value); max = Number(element.getAttribute('max')); } /* we cannot validate invalid values and trust on badInput, if isNaN(value) */ return isNaN(max) || isNaN(value) || value <= max; } /** * test the maxlength attribute */ function test_maxlength (element) { if (!element.value || text.indexOf(get_type(element)) === -1 || !element.hasAttribute('maxlength') || !element.getAttribute('maxlength') // catch maxlength="" ) { return true; } var maxlength = parseInt(element.getAttribute('maxlength'), 10); /* check, if the maxlength value is usable at all. * We allow maxlength === 0 to basically disable input (Firefox does, too). */ if (isNaN(maxlength) || maxlength < 0) { return true; } return unicode_string_length(element.value) <= maxlength; } /** * test the min attribute * * we use Number() instead of parseFloat(), because an invalid attribute * value like "123abc" should result in an error. */ function test_min (element) { var type = get_type(element); if (!element.value || !element.hasAttribute('min')) { /* we're not responsible here */ return true; } var value, min; if (dates.indexOf(type) > -1) { value = string_to_date(element.value, type); value = value === null ? NaN : +value; min = string_to_date(element.getAttribute('min'), type); min = min === null ? NaN : +min; } else { value = Number(element.value); min = Number(element.getAttribute('min')); } /* we cannot validate invalid values and trust on badInput, if isNaN(value) */ return isNaN(min) || isNaN(value) || value >= min; } /** * test the minlength attribute */ function test_minlength (element) { if (!element.value || text.indexOf(get_type(element)) === -1 || !element.hasAttribute('minlength') || !element.getAttribute('minlength') // catch minlength="" ) { return true; } var minlength = parseInt(element.getAttribute('minlength'), 10); /* check, if the minlength value is usable at all. */ if (isNaN(minlength) || minlength < 0) { return true; } return unicode_string_length(element.value) >= minlength; } /** * test the pattern attribute */ function test_pattern (element) { return !element.value || !element.hasAttribute('pattern') || new RegExp('^(?:' + element.getAttribute('pattern') + ')$').test(element.value); } function has_submittable_option(select) { /* Definition of the placeholder label option: * https://www.w3.org/TR/html5/sec-forms.html#element-attrdef-select-required * Being required (the first constraint in the spec) is trivially true, since * this function is only called for such selects. */ var has_placeholder_option = !select.multiple && select.size <= 1 && select.options.length > 0 && select.options[0].parentNode == select && select.options[0].value === ''; return ( /* anything selected at all? That's redundant with the .some() call below, * but more performant in the most probable error case. */ select.selectedIndex > -1 && Array.prototype.some.call(select.options, function (option) { return ( /* it isn't the placeholder option */ (!has_placeholder_option || option.index !== 0) && /* it isn't disabled */ !option.disabled && /* and it is, in fact, selected */ option.selected ); }) ); } /** * test the required attribute */ function test_required (element) { if (element.type === 'radio') { /* the happy (and quick) path for radios: */ if (element.hasAttribute('required') && element.checked) { return true; } var radiogroup = get_radiogroup(element); /* if any radio in the group is required, we need any (not necessarily the * same) radio to be checked */ if (radiogroup.some(function (radio) { return radio.hasAttribute('required'); })) { return radiogroup.some(function (radio) { return radio.checked; }); } /* not required, validation passes */ return true; } if (!element.hasAttribute('required')) { /* nothing to do */ return true; } if (element instanceof window.HTMLSelectElement) { return has_submittable_option(element); } return element.type === 'checkbox' ? element.checked : !!element.value; } /** * test the step attribute */ function test_step (element) { var type = get_type(element); if (!element.value || numbers.indexOf(type) === -1 || (element.getAttribute('step') || '').toLowerCase() === 'any') { /* we're not responsible here. Note: If no step attribute is given, we * need to validate against the default step as per spec. */ return true; } var step = element.getAttribute('step'); if (step) { step = string_to_number(step, type); } else { step = default_step[type] || 1; } if (step <= 0 || isNaN(step)) { /* error in specified "step". We cannot validate against it, so the value * is true. */ return true; } var scale = step_scale_factor[type] || 1; var value = string_to_number(element.value, type); var min = string_to_number(element.getAttribute('min') || element.getAttribute('value') || '', type); if (isNaN(value)) { /* we cannot compare an invalid value and trust that the badInput validator * takes over from here */ return true; } if (isNaN(min)) { min = default_step_base[type] || 0; } if (type === 'month') { /* type=month has month-wide steps. See * https://html.spec.whatwg.org/multipage/forms.html#month-state-%28type=month%29 */ min = new Date(min).getUTCFullYear() * 12 + new Date(min).getUTCMonth(); value = new Date(value).getUTCFullYear() * 12 + new Date(value).getUTCMonth(); } var result = Math.abs(min - value) % (step * scale); return result < 0.00000001 || /* crappy floating-point arithmetics! */ result > step * scale - 0.00000001; } var ws_on_start_or_end = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; /** * trim a string of whitespace * * We don't use String.trim() to remove the need to polyfill it. */ function trim (str) { return str.replace(ws_on_start_or_end, ''); } /** * split a string on comma and trim the components * * As specified at * https://html.spec.whatwg.org/multipage/infrastructure.html#split-a-string-on-commas * plus removing empty entries. */ function comma_split (str) { return str.split(',').map(function (item) { return trim(item); }).filter(function (b) { return b; }); } /* we use a dummy <a> where we set the href to test URL validity * The definition is out of the "global" scope so that JSDOM can be instantiated * after loading Hyperform for tests. */ var url_canary; /* see https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address */ var email_pattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; /** * test the type-inherent syntax */ function test_type (element) { var type = get_type(element); if (type !== 'file' && !element.value || type !== 'file' && type_checked.indexOf(type) === -1) { /* we're not responsible for this element */ return true; } var is_valid = true; switch (type) { case 'url': { if (!url_canary) { url_canary = document.createElement('a'); } var value = trim(element.value); url_canary.href = value; is_valid = url_canary.href === value || url_canary.href === value + '/'; break; } case 'email': if (element.hasAttribute('multiple')) { is_valid = comma_split(element.value).every(function (value) { return email_pattern.test(value); }); } else { is_valid = email_pattern.test(trim(element.value)); } break; case 'file': if ('files' in element && element.files.length && element.hasAttribute('accept')) { var patterns = comma_split(element.getAttribute('accept')).map(function (pattern) { if (/^(audio|video|image)\/\*$/.test(pattern)) { pattern = new RegExp('^' + RegExp.$1 + '/.+$'); } return pattern; }); if (!patterns.length) { break; } fileloop: for (var i = 0; i < element.files.length; i++) { /* we need to match a whitelist, so pre-set with false */ var file_valid = false; patternloop: for (var j = 0; j < patterns.length; j++) { var file = element.files[i]; var pattern = patterns[j]; var fileprop = file.type; if (typeof pattern === 'string' && pattern.substr(0, 1) === '.') { if (file.name.search('.') === -1) { /* no match with any file ending */ continue patternloop; } fileprop = file.name.substr(file.name.lastIndexOf('.')); } if (fileprop.search(pattern) === 0) { /* we found one match and can quit looking */ file_valid = true; break patternloop; } } if (!file_valid) { is_valid = false; break fileloop; } } } } return is_valid; } /** * boilerplate function for all tests but customError */ function check(test, react) { return function (element) { var invalid = !test(element); if (invalid) { react(element); } return invalid; }; } /** * create a common function to set error messages */ function set_msg(element, msgtype, _default) { message_store.set(element, custom_messages.get(element, msgtype, _default)); } var badInput = check(test_bad_input, function (element) { return set_msg(element, 'badInput', _('Please match the requested type.')); }); function customError(element) { /* prevent infinite loops when the custom validators call setCustomValidity(), * which in turn calls this code again. We check, if there is an already set * custom validity message there. */ if (element.__hf_custom_validation_running) { var msg = message_store.get(element); return msg && msg.is_custom; } /* check, if there are custom validators in the registry, and call * them. */ var custom_validators = custom_validator_registry.get(element); var cvl = custom_validators.length; var valid = true; if (cvl) { element.__hf_custom_validation_running = true; for (var i = 0; i < cvl; i++) { var result = custom_validators[i](element); if (result !== undefined && !result) { valid = false; /* break on first invalid response */ break; } } delete element.__hf_custom_validation_running; } /* check, if there are other validity messages already */ if (valid) { var _msg = message_store.get(element); valid = !(_msg.toString() && 'is_custom' in _msg); } return !valid; } var patternMismatch = check(test_pattern, function (element) { set_msg(element, 'patternMismatch', element.title ? sprintf(_('PatternMismatchWithTitle'), element.title) : _('PatternMismatch')); }); /** * TODO: when rangeOverflow and rangeUnderflow are both called directly and * successful, the inRange and outOfRange classes won't get removed, unless * element.validityState.valid is queried, too. */ var rangeOverflow = check(test_max, function (element) { var type = get_type(element); var wrapper = get_wrapper(element); var outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range'; var inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range'; var msg; switch (type) { case 'date': case 'datetime': case 'datetime-local': msg = sprintf(_('DateRangeOverflow'), format_date(string_to_date(element.getAttribute('max'), type), type)); break; case 'time': msg = sprintf(_('TimeRangeOverflow'), format_date(string_to_date(element.getAttribute('max'), type), type)); break; // case 'number': default: msg = sprintf(_('NumberRangeOverflow'), string_to_number(element.getAttribute('max'), type)); break; } set_msg(element, 'rangeOverflow', msg); element.classList.add(outOfRangeClass); element.classList.remove(inRangeClass); }); var rangeUnderflow = check(test_min, function (element) { var type = get_type(element); var wrapper = get_wrapper(element); var outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range'; var inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range'; var msg; switch (type) { case 'date': case 'datetime': case 'datetime-local': msg = sprintf(_('DateRangeUnderflow'), format_date(string_to_date(element.getAttribute('min'), type), type)); break; case 'time': msg = sprintf(_('TimeRangeUnderflow'), format_date(string_to_date(element.getAttribute('min'), type), type)); break; // case 'number': default: msg = sprintf(_('NumberRangeUnderflow'), string_to_number(element.getAttribute('min'), type)); break; } set_msg(element, 'rangeUnderflow', msg); element.classList.add(outOfRangeClass); element.classList.remove(inRangeClass); }); var stepMismatch = check(test_step, function (element) { var list = get_next_valid(element); var min = list[0]; var max = list[1]; var sole = false; var msg; if (min === null) { sole = max; } else if (max === null) { sole = min; } if (sole !== false) { msg = sprintf(_('StepMismatchOneValue'), sole); } else { msg = sprintf(_('StepMismatch'), min, max); } set_msg(element, 'stepMismatch', msg); }); var tooLong = check(test_maxlength, function (element) { set_msg(element, 'tooLong', sprintf(_('TextTooLong'), element.getAttribute('maxlength'), unicode_string_length(element.value))); }); var tooShort = check(test_minlength, function (element) { set_msg(element, 'tooShort', sprintf(_('Please lengthen this text to %l characters or more (you are currently using %l characters).'), element.getAttribute('minlength'), unicode_string_length(element.value))); }); var typeMismatch = check(test_type, function (element) { var msg = _('Please use the appropriate format.'); var type = get_type(element); if (type === 'email') { if (element.hasAttribute('multiple')) { msg = _('Please enter a comma separated list of email addresses.'); } else { msg = _('InvalidEmail'); } } else if (type === 'url') { msg = _('InvalidURL'); } else if (type === 'file') { msg = _('Please select a file of the correct type.'); } set_msg(element, 'typeMismatch', msg); }); var valueMissing = check(test_required, function (element) { var msg = _('ValueMissing'); var type = get_type(element); if (type === 'checkbox') { msg = _('CheckboxMissing'); } else if (type === 'radio') { msg = _('RadioMissing'); } else if (type === 'file') { if (element.hasAttribute('multiple')) { msg = _('Please select one or more files.'); } else { msg = _('FileMissing'); } } else if (element instanceof window.HTMLSelectElement) { msg = _('SelectMissing'); } set_msg(element, 'valueMissing', msg); }); /** * the "valid" property calls all other validity checkers and returns true, * if all those return false. * * This is the major access point for _all_ other API methods, namely * (check|report)Validity(). */ var valid = function valid(element) { var wrapper = get_wrapper(element); var validClass = wrapper && wrapper.settings.classes.valid || 'hf-valid'; var invalidClass = wrapper && wrapper.settings.classes.invalid || 'hf-invalid'; var userInvalidClass = wrapper && wrapper.settings.classes.userInvalid || 'hf-user-invalid'; var userValidClass = wrapper && wrapper.settings.classes.userValid || 'hf-user-valid'; var inRangeClass = wrapper && wrapper.settings.classes.inRange || 'hf-in-range'; var outOfRangeClass = wrapper && wrapper.settings.classes.outOfRange || 'hf-out-of-range'; var validatedClass = wrapper && wrapper.settings.classes.validated || 'hf-validated'; element.classList.add(validatedClass); for (var _i = 0, _arr = [badInput, customError, patternMismatch, rangeOverflow, rangeUnderflow, stepMismatch, tooLong, tooShort, typeMismatch, valueMissing]; _i < _arr.length; _i++) { var checker = _arr[_i]; if (checker(element)) { element.classList.add(invalidClass); element.classList.remove(validClass); element.classList.remove(userValidClass); if ((element.type === 'checkbox' || element.type === 'radio') && element.checked !== element.defaultChecked || /* the following test is trivially false for checkboxes/radios */ element.value !== element.defaultValue) { element.classList.add(userInvalidClass); } else { element.classList.remove(userInvalidClass); } element.setAttribute('aria-invalid', 'true'); return false; } } message_store["delete"](element); element.classList.remove(invalidClass); element.classList.remove(userInvalidClass); element.classList.remove(outOfRangeClass); element.classList.add(validClass); element.classList.add(inRangeClass); if (element.value !== element.defaultValue) { element.classList.add(userValidClass); } else { element.classList.remove(userValidClass); } element.setAttribute('aria-invalid', 'false'); return true; }; var validity_state_checkers = { badInput: badInput, customError: customError, patternMismatch: patternMismatch, rangeOverflow: rangeOverflow, rangeUnderflow: rangeUnderflow, stepMismatch: stepMismatch, tooLong: tooLong, tooShort: tooShort, typeMismatch: typeMismatch, valueMissing: valueMissing, valid: valid }; /** * the validity state constructor */ var ValidityState = function ValidityState(element) { if (!(element instanceof window.HTMLElement)) { throw new Error('cannot create a ValidityState for a non-element'); } var cached = ValidityState.cache.get(element);