hyperform
Version:
Capture form validation back from the browser
1,715 lines (1,388 loc) • 95.7 kB
JavaScript
// hyperform.js.org
define(function () { '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 va