hyperform
Version:
Capture form validation back from the browser
3,168 lines • 90.4 kB
JavaScript
// 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);
if (cached) {
return cached;
}
if (!(this instanceof ValidityState)) {
/* working around a forgotten `new` */
return new ValidityState(element);
}
this.element = element;
ValidityState.cache.set(element, this);
};
/**
* the prototype for new validityState instances
*/
var ValidityStatePrototype = {};
ValidityState.prototype = ValidityStatePrototype;
ValidityState.cache = new WeakMap();
/* small wrapper around the actual validator to check if the validator
* should actually be called. `this` refers to the ValidityState object. */
var checker_getter = function checker_getter(prop, func) {
return function () {
if (!is_validation_candidate(this.element)) {
/* not being validated == valid by default
* return value == false for all props except "valid", because we test
* problems like badInput here */
return prop === 'valid';
}
return func(this.element);
};
};
/**
* copy functionality from the validity checkers to the ValidityState
* prototype
*/
for (var prop in validity_state_checkers) {
Object.defineProperty(ValidityStatePrototype, prop, {
configurable: true,
enumerable: true,
get: checker_getter(prop, validity_state_checkers[prop]),
set: undefined
});
}
/**
* mark the validity prototype, because that is what the client-facing
* code deals with mostly, not the property descriptor thing */
mark(ValidityStatePrototype);
/**
* check an element's validity with respect to it's form
*/
var checkValidity = return_hook_or('checkValidity', function (element) {
/* if this is a <form>, check validity of all child inputs */
if (element instanceof window.HTMLFormElement) {
return get_validated_elements(element).map(checkValidity).every(function (b) {
return b;
});
}
/* default is true, also for elements that are no validation candidates */
var valid = ValidityState(element).valid;
if (valid) {
var wrapped_form = get_wrapper(element);
if (wrapped_form && wrapped_form.settings.validEvent) {
trigger_event(element, 'valid');
}
} else {
trigger_event(element, 'invalid', {
cancelable: true
});
}
return valid;
});
/**
* counter that will be incremented with every call
*
* Will enforce uniqueness, as long as no more than 1 hyperform scripts
* are loaded. (In that case we still have the "random" part below.)
*/
var uid = 0;
/**
* generate a random ID
*
* @see https://gist.github.com/gordonbrander/2230317
*/
function generate_id () {
var prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'hf_';
return prefix + uid++ + Math.random().toString(36).substr(2);
}
var warningsCache = new WeakMap();
var DefaultRenderer = {
/**
* called when a warning should become visible
*/
attachWarning: function attachWarning(warning, element) {
/* should also work, if element is last,
* http://stackoverflow.com/a/4793630/113195 */
element.parentNode.insertBefore(warning, element.nextSibling);
},
/**
* called when a warning should vanish
*/
detachWarning: function detachWarning(warning, element) {
/* be conservative here, since an overwritten attachWarning() might not
* actually have attached the warning. */
if (warning.parentNode) {
warning.parentNode.removeChild(warning);
}
},
/**
* called when feedback to an element's state should be handled
*
* i.e., showing and hiding warnings
*/
showWarning: function showWarning(element) {
var whole_form_validated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var warning;
var warning_element = element;
var referring_elements = [element];
if (element.type === 'radio') {
var radiogroup = get_radiogroup(element);
if (whole_form_validated && radiogroup[0] !== element) {
/* don't render error messages on subsequent radio buttons of the
* same group. This assumes, that element.validity.valueMissing is the only
* possible validation failure for radio buttons, i.e., that there are
* no individual validation problems on a single input. */
return;
} else {
/* an existing warning should be removed. We need to look it up in
* the cache with the right element, that is, with the first element
* from the current radiogroup. */
warning_element = radiogroup[0];
referring_elements = radiogroup;
}
}
warning = warningsCache.get(warning_element);
var msg = message_store.get(warning_element).toString();
if (msg) {
if (!warning) {
var wrapper = get_wrapper(warning_element);
warning = document.createElement('div');
warning.className = wrapper && wrapper.settings.classes.warning || 'hf-warning';
warning.id = generate_id();
warning.setAttribute('aria-live', 'polite');
warningsCache.set(warning_element, warning);
}
referring_elements.forEach(function (el) {
el.setAttribute('aria-errormessage', warning.id);
if (!el.hasAttribute('aria-describedby')) {
el.setAttribute('aria-describedby', warning.id);
}
});
Renderer.setMessage(warning, msg, warning_element);
Renderer.attachWarning(warning, warning_element);
} else if (warning && warning.parentNode) {
referring_elements.forEach(function (el) {
if (el.getAttribute('aria-describedby') === warning.id) {
el.removeAttribute('aria-describedby');
}
el.removeAttribute('aria-errormessage');
});
Renderer.detachWarning(warning, warning_element);
}
},
/**
* set the warning's content
*
* Overwrite this method, if you want, e.g., to allow HTML in warnings
* or preprocess the content.
*/
setMessage: function setMessage(warning, message, element) {
warning.textContent = message;
}
};
var Renderer = {
attachWarning: DefaultRenderer.attachWarning,
detachWarning: DefaultRenderer.detachWarning,
showWarning: DefaultRenderer.showWarning,
setMessage: DefaultRenderer.setMessage,
set: function set(renderer, action) {
if (!action) {
action = DefaultRenderer[renderer];
}
Renderer[renderer] = action;
},
getWarning: function getWarning(element) {
return warningsCache.get(element);
}
};
/**
* check element's validity and report an error back to the user
*/
function reportValidity(element) {
/* if this is a <form>, report validity of all child inputs */
if (element instanceof window.HTMLFormElement) {
element.__hf_form_validation = true;
var form_valid = get_validated_elements(element).map(reportValidity).every(function (b) {
return b;
});
delete element.__hf_form_validation;
return form_valid;
}
/* we copy checkValidity() here, b/c we have to check if the "invalid"
* event was canceled. */
var valid = ValidityState(element).valid;
var event;
if (valid) {
var wrapped_form = get_wrapper(element);
if (wrapped_form && wrapped_form.settings.validEvent) {
event = trigger_event(element, 'valid', {
cancelable: true
});
}
} else {
event = trigger_event(element, 'invalid', {
cancelable: true
});
}
if (!event || !event.defaultPrevented) {
Renderer.showWarning(element, element.form && element.form.__hf_form_validation);
}
return valid;
}
/**
* set a custom validity message or delete it with an empty string
*/
function setCustomValidity(element, msg) {
if (!msg) {
message_store["delete"](element, true);
} else {
message_store.set(element, msg, true);
}
/* live-update the warning */
var warning = Renderer.getWarning(element);
if (warning) {
Renderer.setMessage(warning, msg, element);
}
/* update any classes if the validity state changes */
validity_state_checkers.valid(element);
}
/**
* implement the valueAsDate functionality
*
* @see https://html.spec.whatwg.org/multipage/forms.html#dom-input-valueasdate
*/
function valueAsDate(element) {
var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
var type = get_type(element);
if (dates.indexOf(type) > -1) {
if (value !== undefined) {
/* setter: value must be null or a Date() */
if (value === null) {
element.value = '';
} else if (value instanceof Date) {
if (isNaN(value.getTime())) {
element.value = '';
} else {
element.value = date_to_string(value, type);
}
} else {
throw new window.DOMException('valueAsDate setter encountered invalid value', 'TypeError');
}
return;
}
var value_date = string_to_date(element.value, type);
return value_date instanceof Date ? value_date : null;
} else if (value !== undefined) {
/* trying to set a date on a not-date input fails */
throw new window.DOMException('valueAsDate setter cannot set date on this element', 'InvalidStateError');
}
return null;
}
/**
* implement the valueAsNumber functionality
*
* @see https://html.spec.whatwg.org/multipage/forms.html#dom-input-valueasnumber
*/
function valueAsNumber(element) {
var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
var type = get_type(element);
if (numbers.indexOf(type) > -1) {
if (type === 'range' && element.hasAttribute('multiple')) {
/* @see https://html.spec.whatwg.org/multipage/forms.html#do-not-apply */
return NaN;
}
if (value !== undefined) {
/* setter: value must be NaN or a finite number */
if (isNaN(value)) {
element.value = '';
} else if (typeof value === 'number' && window.isFinite(value)) {
try {
/* try setting as a date, but... */
valueAsDate(element, new Date(value));
} catch (e) {
/* ... when valueAsDate is not responsible, ... */
if (!(e instanceof window.DOMException)) {
throw e;
}
/* ... set it via Number.toString(). */
element.value = value.toString();
}
} else {
throw new window.DOMException('valueAsNumber setter encountered invalid value', 'TypeError');
}
return;
}
return string_to_number(element.value, type);
} else if (value !== undefined) {
/* trying to set a number on a not-number input fails */
throw new window.DOMException('valueAsNumber setter cannot set number on this element', 'InvalidStateError');
}
return NaN;
}
/**
*
*/
function stepDown(element) {
var n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
if (numbers.indexOf(get_type(element)) === -1) {
throw new window.DOMException('stepDown encountered invalid type', 'InvalidStateError');
}
if ((element.getAttribute('step') || '').toLowerCase() === 'any') {
throw new window.DOMException('stepDown encountered step "any"', 'InvalidStateError');
}
var prev = get_next_valid(element, n)[0];
if (prev !== null) {
valueAsNumber(element, prev);
}
}
/**
*
*/
function stepUp(element) {
var n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
if (numbers.indexOf(get_type(element)) === -1) {
throw new window.DOMException('stepUp encountered invalid type', 'InvalidStateError');
}
if ((element.getAttribute('step') || '').toLowerCase() === 'any') {
throw new window.DOMException('stepUp encountered step "any"', 'InvalidStateError');
}
var next = get_next_valid(element, n)[1];
if (next !== null) {
valueAsNumber(element, next);
}
}
/**
* get the validation message for an element, empty string, if the element
* satisfies all constraints.
*/
function validationMessage(element) {
var msg = message_store.get(element);
if (!msg) {
return '';
}
/* make it a primitive again, since message_store returns String(). */
return msg.toString();
}
/**
* check, if an element will be subject to HTML5 validation at all
*/
function willValidate(element) {
return is_validation_candidate(element);
}
/* shim layer for the Element.matches method */
var ep = window.Element.prototype;
var native_matches = ep.matches || ep.matchesSelector || ep.msMatchesSelector || ep.webkitMatchesSelector;
function matches (element, selector) {
return native_matches.call(element, selector);
}
/**
* submit a form, because `element` triggered it
*
* This method also dispatches a submit event on the form prior to the
* submission. The event contains the trigger element as `submittedVia`.
*
* If the element is a button with a name, the name=value pair will be added
* to the submitted data.
*/
function submit_form_via(element) {
/* apparently, the submit event is not triggered in most browsers on
* the submit() method, so we do it manually here to model a natural
* submit as closely as possible.
* Now to the fun fact: If you trigger a submit event from a form, what
* do you think should happen?
* 1) the form will be automagically submitted by the browser, or
* 2) nothing.
* And as you already suspected, the correct answer is: both! Firefox
* opts for 1), Chrome for 2). Yay! */
var event_got_cancelled;
var submit_event = create_event('submit', {
cancelable: true
});
/* force Firefox to not submit the form, then fake preventDefault() */
submit_event.preventDefault();
Object.defineProperty(submit_event, 'defaultPrevented', {
value: false,
writable: true
});
Object.defineProperty(submit_event, 'preventDefault', {
value: function value() {
return submit_event.defaultPrevented = event_got_cancelled = true;
},
writable: true
});
trigger_event(element.form, submit_event, {}, {
submittedVia: element
});
if (!event_got_cancelled) {
add_submit_field(element);
window.HTMLFormElement.prototype.submit.call(element.form);
window.setTimeout(function () {
return remove_submit_field(element);
});
}
}
/**
* if a submit button was clicked, add its name=value by means of a type=hidden
* input field
*/
function add_submit_field(button) {
if (['image', 'submit'].indexOf(button.type) > -1 && button.name) {
var wrapper = get_wrapper(button.form) || {};
var submit_helper = wrapper.submit_helper;
if (submit_helper) {
if (submit_helper.parentNode) {
submit_helper.parentNode.removeChild(submit_helper);
}
} else {
submit_helper = document.createElement('input');
submit_helper.type = 'hidden';
wrapper.submit_helper = submit_helper;
}
submit_helper.name = button.name;
submit_helper.value = button.value;
button.form.appendChild(submit_helper);
}
}
/**
* remove a possible helper input, that was added by `add_submit_field`
*/
function remove_submit_field(button) {
if (['image', 'submit'].indexOf(button.type) > -1 && button.name) {
var wrapper = get_wrapper(button.form) || {};
var submit_helper = wrapper.submit_helper;
if (submit_helper && submit_helper.parentNode) {
submit_helper.parentNode.removeChild(submit_helper);
}
}
}
/**
* check a form's validity and submit it
*
* The method triggers a cancellable `validate` event on the form. If the
* event is cancelled, form submission will be aborted, too.
*
* If the form is found to contain invalid fields, focus the first field.
*/
function check$1(button) {
/* trigger a "validate" event on the form to be submitted */
var val_event = trigger_event(button.form, 'validate', {
cancelable: true
});
if (val_event.defaultPrevented) {
/* skip the whole submit thing, if the validation is canceled. A user
* can still call form.submit() afterwards. */
return;
}
var valid = true;
var first_invalid;
button.form.__hf_form_validation = true;
get_validated_elements(button.form).map(function (element) {
if (!reportValidity(element)) {
valid = false;
if (!first_invalid && 'focus' in element) {
first_invalid = element;
}
}
});
delete button.form.__hf_form_validation;
if (valid) {
submit_form_via(button);
} else if (first_invalid) {
/* focus the first invalid element, if validation went south */
first_invalid.focus();
/* tell the tale, if anyone wants to react to it */
trigger_event(button.form, 'forminvalid');
}
}
/**
* test if node is a submit button
*/
function is_submit_button(node) {
return (
/* must be an input or button element... */
(node.nodeName === 'INPUT' || node.nodeName === 'BUTTON') && (
/* ...and have a submitting type */
node.type === 'image' || node.type === 'submit')
);
}
/**
* test, if the click event would trigger a submit
*/
function is_submitting_click(event, button) {
return (
/* prevented default: won't trigger a submit */
!event.defaultPrevented && (
/* left button or middle button (submits in Chrome) */
!('button' in event) || event.button < 2) &&
/* must be a submit button... */
is_submit_button(button) &&
/* the button needs a form, that's going to be submitted */
button.form &&
/* again, if the form should not be validated, we're out of the game */
!button.form.hasAttribute('novalidate')
);
}
/**
* test, if the keypress event would trigger a submit
*/
function is_submitting_keypress(event) {
return (
/* prevented default: won't trigger a submit */
!event.defaultPrevented && (
/* ...and <Enter> was pressed... */
event.keyCode === 13 &&
/* ...on an <input> that is... */
event.target.nodeName === 'INPUT' &&
/* ...a standard text input field (not checkbox, ...) */
text.indexOf(event.target.type) > -1 ||
/* or <Enter> or <Space> was pressed... */
(event.keyCode === 13 || event.keyCode === 32) &&
/* ...on a submit button */
is_submit_button(event.target)) &&
/* there's a form... */
event.target.form &&
/* ...and the form allows validation */
!event.target.form.hasAttribute('novalidate')
);
}
/**
* catch clicks to children of <button>s
*/
function get_clicked_button(element) {
if (is_submit_button(element)) {
return element;
} else if (matches(element, 'button:not([type]) *, button[type="submit"] *')) {
return get_clicked_button(element.parentNode);
} else {
return null;
}
}
/**
* return event handler to catch explicit submission by click on a button
*/
function get_click_handler() {
var ignore = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
return function (event) {
var button = get_clicked_button(event.target);
if (button && is_submitting_click(event, button)) {
event.preventDefault();
if (ignore || button.hasAttribute('formnovalidate')) {
/* if validation should be ignored, we're not interested in any checks */
submit_form_via(button);
} else {
check$1(button);
}
}
};
}
var click_handler = get_click_handler();
var ignored_click_handler = get_click_handler(true);
/**
* catch implicit submission by pressing <Enter> in some situations
*/
function get_keypress_handler(ignore) {
return function keypress_handler(event) {
if (is_submitting_keypress(event)) {
event.preventDefault();
var wrapper = get_wrapper(event.target.form) || {
settings: {}
};
if (wrapper.settings.preventImplicitSubmit) {
/* user doesn't want an implicit submit. Cancel here. */
return;
}
/* check, that there is no submit button in the form. Otherwise
* that should be clicked. */
var el = event.target.form.elements.length;
var submit;
for (var i = 0; i < el; i++) {
if (['image', 'submit'].indexOf(event.target.form.elements[i].type) > -1) {
submit = event.target.form.elements[i];
break;
}
}
/* trigger an "implicit_submit" event on the form to be submitted */
var implicit_event = trigger_event(event.target.form, 'implicit_submit', {
cancelable: true
}, {
trigger: event.target,
submittedVia: submit || event.target
});
if (implicit_event.defaultPrevented) {
/* skip the submit, if implicit submit is canceled */
return;
}
if (submit) {
submit.click();
} else if (ignore) {
submit_form_via(event.target);
} else {
check$1(event.target);
}
}
};
}
var keypress_handler = get_keypress_handler();
var ignored_keypress_handler = get_keypress_handler(true);
/**
* catch all relevant events _prior_ to a form being submitted
*
* @param bool ignore bypass validation, when an attempt to submit the
* form is detected. True, when the wrapper's revalidate
* setting is 'never'.
*/
function catch_submit(listening_node) {
var ignore = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (ignore) {
listening_node.addEventListener('click', ignored_click_handler);
listening_node.addEventListener('keypress', ignored_keypress_handler);
} else {
listening_node.addEventListener('click', click_handler);
listening_node.addEventListener('keypress', keypress_handler);
}
}
/**
* decommission the event listeners from catch_submit() again
*/
function uncatch_submit(listening_node) {
listening_node.removeEventListener('click', ignored_click_handler);
listening_node.removeEventListener('keypress', ignored_keypress_handler);
listening_node.removeEventListener('click', click_handler);
listening_node.removeEventListener('keypress', keypress_handler);
}
/**
* add `property` to an element
*
* ATTENTION! This function will search for an equally named property on the
* *prototype* of an element, if element is a concrete DOM node. Do not use
* it as general-purpose property installer.
*
* js> installer(element, 'foo', { value: 'bar' });
* js> assert(element.foo === 'bar');
*/
function install_property (element, property, descriptor) {
descriptor.configurable = true;
descriptor.enumerable = true;
if ('value' in descriptor) {
descriptor.writable = true;
}
/* on concrete instances, i.e., <input> elements, the naive lookup
* yields undefined. We have to look on its prototype then. On elements
* like the actual HTMLInputElement object the first line works. */
var original_descriptor = Object.getOwnPropertyDescriptor(element, property);
if (original_descriptor === undefined) {
original_descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), property);
}
if (original_descriptor) {
if (original_descriptor.configurable === false) {
/* Safari <= 9 and PhantomJS will end up here :-( Nothing to do except
* warning */
var wrapper = get_wrapper(element);
if (wrapper && wrapper.settings.debug) {
/* global console */
console.log('[hyperform] cannot install custom property ' + property);
}
return false;
}
/* we already installed that property... */
if (original_descriptor.get && original_descriptor.get.__hyperform || original_descriptor.value && original_descriptor.value.__hyperform) {
return;
}
/* publish existing property under new name, if it's not from us */
Object.defineProperty(element, '_original_' + property, original_descriptor);
}
delete element[property];
Object.defineProperty(element, property, descriptor);
return true;
}
function is_field (element) {
return element instanceof window.HTMLButtonElement || element instanceof window.HTMLInputElement || element instanceof window.HTMLSelectElement || element instanceof window.HTMLTextAreaElement || element instanceof window.HTMLFieldSetElement || element === window.HTMLButtonElement.prototype || element === window.HTMLInputElement.prototype || element === window.HTMLSelectElement.prototype || element === window.HTMLTextAreaElement.prototype || element === window.HTMLFieldSetElement.prototype;
}
/**
* remove `property` from element and restore _original_property, if present
*/
function uninstall_property (element, property) {
try {
delete element[property];
} catch (e) {
/* Safari <= 9 and PhantomJS will end up here :-( Nothing to do except
* warning */
var wrapper = get_wrapper(element);
if (wrapper && wrapper.settings.debug) {
/* global console */
console.log('[hyperform] cannot uninstall custom property ' + property);
}
return false;
}
var original_descriptor = Object.getOwnPropertyDescriptor(element, '_original_' + property);
if (original_descriptor) {
Object.defineProperty(element, property, original_descriptor);
}
}
var gA = function gA(prop) {
return function () {
return do_filter('attr_get_' + prop, this.getAttribute(prop), this);
};
};
var sA = function sA(prop) {
return function (value) {
this.setAttribute(prop, do_filter('attr_set_' + prop, value, this));
};
};
var gAb = function gAb(prop) {
return function () {
return do_filter('attr_get_' + prop, this.hasAttribute(prop), this);
};
};
var sAb = function sAb(prop) {
return function (value) {
if (do_filter('attr_set_' + prop, value, this)) {
this.setAttribute(prop, prop);
} else {
this.removeAttribute(prop);
}
};
};
var gAn = function gAn(prop) {
return function () {
return do_filter('attr_get_' + prop, Math.max(0, Number(this.getAttribute(prop))), this);
};
};
var sAn = function sAn(prop) {
return function (value) {
value = do_filter('attr_set_' + prop, value, this);
if (/^[0-9]+$/.test(value)) {
this.setAttribute(prop, value);
}
};
};
function install_properties(element) {
for (var _i = 0, _arr = ['accept', 'max', 'min', 'pattern', 'placeholder', 'step']; _i < _arr.length; _i++) {
var prop = _arr[_i];
install_property(element, prop, {
get: gA(prop),
set: sA(prop)
});
}
for (var _i2 = 0, _arr2 = ['multiple', 'required', 'readOnly']; _i2 < _arr2.length; _i2++) {
var _prop = _arr2[_i2];
install_property(element, _prop, {
get: gAb(_prop.toLowerCase()),
set: sAb(_prop.toLowerCase())
});
}
for (var _i3 = 0, _arr3 = ['minLength', 'maxLength']; _i3 < _arr3.length; _i3++) {
var _prop2 = _arr3[_i3];
install_property(element, _prop2, {
get: gAn(_prop2.toLowerCase()),
set: sAn(_prop2.toLowerCase())
});
}
}
function uninstall_properties(element) {
for (var _i4 = 0, _arr4 = ['accept', 'max', 'min', 'pattern', 'placeholder', 'step', 'multiple', 'required', 'readOnly', 'minLength', 'maxLength']; _i4 < _arr4.length; _i4++) {
var prop = _arr4[_i4];
uninstall_property(element, prop);
}
}
var polyfills = {
checkValidity: {
value: mark(function () {
return checkValidity(this);
})
},
reportValidity: {
value: mark(function () {
return reportValidity(this);
})
},
setCustomValidity: {
value: mark(function (msg) {
return setCustomValidity(this, msg);
})
},
stepDown: {
value: mark(function () {
var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
return stepDown(this, n);
})
},
stepUp: {
value: mark(function () {
var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
return stepUp(this, n);
})
},
validationMessage: {
get: mark(function () {
return validationMessage(this);
})
},
validity: {
get: mark(function () {
return ValidityState(this);
})
},
valueAsDate: {
get: mark(function () {
return valueAsDate(this);
}),
set: mark(function (value) {
valueAsDate(this, value);
})
},
valueAsNumber: {
get: mark(function () {
return valueAsNumber(this);
}),
set: mark(function (value) {
valueAsNumber(this, value);
})
},
willValidate: {
get: mark(function () {
return willValidate(this);
})
}
};
function polyfill (element) {
if (is_field(element)) {
for (var prop in polyfills) {
install_property(element, prop, polyfills[prop]);
}
install_properties(element);
} else if (element instanceof window.HTMLFormElement || element === window.HTMLFormElement.prototype) {
install_property(element, 'checkValidity', polyfills.checkValidity);
install_property(element, 'reportValidity', polyfills.reportValidity);
}
}
function polyunfill (element) {
if (is_field(element)) {
uninstall_property(element, 'checkValidity');
uninstall_property(element, 'reportValidity');
uninstall_property(element, 'setCustomValidity');
uninstall_property(element, 'stepDown');
uninstall_property(element, 'stepUp');
uninstall_property(element, 'validationMessage');
uninstall_property(element, 'validity');
uninstall_property(element, 'valueAsDate');
uninstall_property(element, 'valueAsNumber');
uninstall_property(element, 'willValidate');
uninstall_properties(element);
} else if (element instanceof window.HTMLFormElement) {
uninstall_property(element, 'checkValidity');
uninstall_property(element, 'reportValidity');
}
}
var element_prototypes = [window.HTMLButtonElement.prototype, window.HTMLInputElement.prototype, window.HTMLSelectElement.prototype, window.HTMLTextAreaElement.prototype, window.HTMLFieldSetElement.prototype];
/**
* get the appropriate function to revalidate form elements
*/
function get_revalidator() {
var method = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'hybrid';
return function (event) {
if (event.target instanceof window.HTMLButtonElement || event.target instanceof window.HTMLTextAreaElement || event.target instanceof window.HTMLSelectElement || event.target instanceof window.HTMLInputElement) {
if (event.target.form && event.target.form.hasAttribute('novalidate')) {
/* do nothing, if the form forbids it. This still allows manual
* validation via, e.g., input.reportValidity(), but mirrors browser
* behavior, that are also completely silent in this case. */
return;
}
if (method === 'hybrid') {
/* "hybrid" somewhat simulates what browsers do. See for example
* Firefox's :-moz-ui-invalid pseudo-class:
* https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-ui-invalid */
if (event.type === 'blur' && event.target.value !== event.target.defaultValue || ValidityState(event.target).valid) {
/* on blur, update the report when the value has changed from the
* default or when the element is valid (possibly removing a still
* standing invalidity report). */
reportValidity(event.target);
} else if (event.type === 'keyup' && event.keyCode !== 9 || event.type === 'change') {
if (ValidityState(event.target).valid) {
// report instantly, when an element becomes valid,
// postpone report to blur event, when an element is invalid
reportValidity(event.target);
}
}
} else if (event.type !== 'keyup' || event.keyCode !== 9) {
/* do _not_ validate, when the user "tabbed" into the field initially,
* i.e., a keyup event with keyCode 9 */
reportValidity(event.target);
}
}
};
}
/**
* run a function on all found elements
*/
function execute_on_elements(fn, elements) {
if (elements instanceof window.Element) {
elements = [elements];
}
var elements_length = elements.length;
for (var i = 0; i < elements_length; i++) {
fn(elements[i]);
}
}
/**
* get a function, that removes hyperform behavior again
*/
function get_destructor(hform) {
var form = hform.form;
return function () {
uncatch_submit(form);
form.removeEventListener('keyup', hform.revalidator);
form.removeEventListener('change', hform.revalidator);
form.removeEventListener('blur', hform.revalidator, true);
if (form === window || form.nodeType === 9) {
hform.uninstall(element_prototypes);
polyunfill(window.HTMLFormElement);
} else if (form instanceof window.HTMLFormElement || form instanceof window.HTMLFieldSetElement) {
hform.uninstall(form.elements);
if (form instanceof window.HTMLFormElement) {
polyunfill(form);
}
} else if (form instanceof window.HTMLElement) {
hform.observer.disconnect();
var _iterator = _createForOfIteratorHelper(Array.prototype.slice.call(form.getElementsByTagName('form'))),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var subform = _step.value;
hform.uninstall(subform.elements);
polyunfill(subform);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
};
}
/**
* add hyperform behavior to a freshly initialized wrapper
*/
function add_behavior(hform) {
var form = hform.form;
var settings = hform.settings;
hform.revalidator = get_revalidator(settings.revalidate);
hform.observer = {
disconnect: function disconnect() {}
};
hform.install = function (elements) {
return execute_on_elements(polyfill, elements);
};
hform.uninstall = function (elements) {
return execute_on_elements(polyunfill, elements);
};
hform._destruct = get_destructor(hform);
catch_submit(form, settings.revalidate === 'never');
if (form === window || form.nodeType === 9) {
/* install on the prototypes, when called for the whole document */
hform.install(element_prototypes);
polyfill(window.HTMLFormElement);
} else if (form instanceof window.HTMLFormElement || form instanceof window.HTMLFieldSetElement) {
hform.install(form.elements);
if (form instanceof window.HTMLFormElement) {
polyfill(form);
}
} else if (form instanceof window.HTMLElement) {
var _iterator2 = _createForOfIteratorHelper(Array.prototype.slice.call(hform.form.getElementsByTagName('form'))),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var subform = _step2.value;
hform.install(subform.elements);
polyfill(subform);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
hform.observer = new window.MutationObserver(function (mutationsList) {
var _iterator3 = _createForOfIteratorHelper(mutationsList),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var mutation = _step3.value;
if (mutation.type === 'childList') {
var _iterator4 = _createForOfIteratorHelper(Array.prototype.slice.call(mutation.addedNodes)),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var subform = _step4.value;
if (subform instanceof window.HTMLFormElement) {
hform.install(subform.elements);
polyfill(subform);
}
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
var _iterator5 = _createForOfIteratorHelper(Array.prototype.slice.call(mutation.removedNodes)),
_step5;
try {
for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
var _subform = _step5.value;
if (_subform instanceof window.HTMLFormElement) {
hform.uninstall(_subform.elements);
polyunfill(_subform);
}
}
} catch (err) {
_iterator5.e(err);
} finally {
_iterator5.f();
}
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
});
hform.observer.observe(form, {
subtree: true,
childList: true
});
} else {
throw new Error('Hyperform must be used with a node or window.');
}
if (settings.revalidate === 'oninput' || settings.revalidate === 'hybrid') {
/* in a perfect world we'd just bind to "input", but support here is
* abysmal: http://caniuse.com/#feat=input-event */
form.addEventListener('keyup', hform.revalidator);
form.addEventListener('change', hform.revalidator);
}
if (settings.revalidate === 'onblur' || settings.revalidate === 'hybrid') {
/* useCapture=true, because `blur` doesn't bubble. See
* https://developer.mozilla.org/en-US/docs/Web/Events/blur#Event_delegation
* for a discussion */
form.addEventListener('blur', hform.revalidator, true);
}
}
var version = '0.12.1';
/**
* public hyperform interface:
*/
function hyperform(form) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
classes = _ref.classes,
_ref$debug = _ref.debug,
debug = _ref$debug === void 0 ? false : _ref$debug,
extendFieldset = _ref.extendFieldset,
novalidateOnElements = _ref.novalidateOnElements,
_ref$preventImplicitS = _ref.preventImplicitSubmit,
preventImplicitSubmit = _ref$preventImplicitS === void 0 ? false : _ref$preventImplicitS,
revalidate = _ref.revalidate,
_ref$strict = _ref.strict,
strict = _ref$strict === void 0 ? false : _ref$strict,
validEvent = _ref.validEvent,
_ref$validateNameless = _ref.validateNameless,
validateNameless = _ref$validateNameless === void 0 ? false : _ref$validateNameless;
if (!classes) {
classes = {};
}
if (extendFieldset === undefined) {
extendFieldset = !strict;
}
if (novalidateOnElements === undefined) {
novalidateOnElements = !strict;
}
if (preventImplicitSubmit === undefined) {
preventImplicitSubmit = false;
}
if (revalidate === undefined) {
/* other recognized values: 'oninput', 'onblur', 'onsubmit' and 'never' */
revalidate = strict ? 'onsubmit' : 'hybrid';
}
if (validEvent === undefined) {
validEvent = !strict;
}
var settings = {
debug: debug,
strict: strict,
preventImplicitSubmit: preventImplicitSubmit,
revalidate: revalidate,
validEvent: validEvent,
extendFieldset: extendFieldset,
classes: classes,
novalidateOnElements: novalidateOnElements,
validateNameless: validateNameless
};
if (form instanceof window.NodeList || form instanceof window.HTMLCollection || form instanceof Array) {
return Array.prototype.map.call(form, function (element) {
return hyperform(element, settings);
});
}
var wrapper = new Wrapper(form, settings);
add_behavior(wrapper);
return wrapper;
}
hyperform.version = version;
hyperform.checkValidity = checkValidity;
hyperform.reportValidity = reportValidity;
hyperform.setCustomValidity = setCustomValidity;
hyperform.stepDown = stepDown;
hyperform.stepUp = stepUp;
hyperform.validationMessage = validationMessage;
hyperform.ValidityState = ValidityState;
hyperform.valueAsDate = valueAsDate;
hyperform.valueAsNumber = valueAsNumber;
hyperform.willValidate = willValidate;
hyperform.setLanguage = function (lang) {
set_language(lang);
return hyperform;
};
hyperform.addTranslation = function (lang, catalog) {
add_translation(lang, catalog);
return hyperform;
};
hyperform.setRenderer = function (renderer, action) {
Renderer.set(renderer, action);
return hyperform;
};
hyperform.addValidator = function (element, validator) {
custom_validator_registry.set(element, validator);
return hyperform;
};
hyperform.setMessage = function (element, validator, message) {
custom_messages.set(element, validator, message);
return hyperform;
};
hyperform.addHook = function (hook, action, position) {
add_hook(hook, action, position);
return hyperform;
};
hyperform.removeHook = function (hook, action) {
remove_hook(hook, action);
return hyperform;
};
if (document.currentScript && document.currentScript.hasAttribute('data-hf-autoload')) {
hyperform(window);
}
module.exports = hyperform;