(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Pristine = factory());
}(this, (function () { 'use strict';
var lang = {
en: {
required: "This field is required",
email: "This field requires a valid e-mail address",
number: "This field requires a number",
integer: "This field requires an integer value",
url: "This field requires a valid website URL",
tel: "This field requires a valid telephone number",
maxlength: "This fields length must be < ${1}",
minlength: "This fields length must be > ${1}",
min: "Minimum value for this field is ${1}",
max: "Maximum value for this field is ${1}",
pattern: "Please match the requested format",
equals: "The two fields do not match",
default: "Please enter a correct value"
}
};
function findAncestor(el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls)) {}
return el;
}
function tmpl(o) {
var _arguments = arguments;
return this.replace(/\${([^{}]*)}/g, function (a, b) {
return _arguments[b];
});
}
function groupedElemCount(input) {
return input.pristine.self.form.querySelectorAll('input[name="' + input.getAttribute('name') + '"]:checked').length;
}
function mergeConfig(obj1, obj2) {
for (var attr in obj2) {
if (!(attr in obj1)) {
obj1[attr] = obj2[attr];
}
}
return obj1;
}
var defaultConfig = {
classTo: 'form-group',
errorClass: 'has-danger',
successClass: 'has-success',
errorTextParent: 'form-group',
errorTextTag: 'div',
errorTextClass: 'text-help'
};
var PRISTINE_ERROR = 'pristine-error';
var SELECTOR = "input:not([type^=hidden]):not([type^=submit]), select, textarea";
var ALLOWED_ATTRIBUTES = ["required", "min", "max", 'minlength', 'maxlength', 'pattern'];
var EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var MESSAGE_REGEX = /-message(?:-([a-z]{2}(?:_[A-Z]{2})?))?/; // matches, -message, -message-en, -message-en_US
var currentLocale = 'en';
var validators = {};
var _ = function _(name, validator) {
validator.name = name;
if (validator.priority === undefined) validator.priority = 1;
validators[name] = validator;
};
_('text', { fn: function fn(val) {
return true;
}, priority: 0 });
_('required', { fn: function fn(val) {
return this.type === 'radio' || this.type === 'checkbox' ? groupedElemCount(this) : val !== undefined && val.trim() !== '';
}, priority: 99, halt: true });
_('email', { fn: function fn(val) {
return !val || EMAIL_REGEX.test(val);
} });
_('number', { fn: function fn(val) {
return !val || !isNaN(parseFloat(val));
}, priority: 2 });
_('integer', { fn: function fn(val) {
return !val || /^\d+$/.test(val);
} });
_('minlength', { fn: function fn(val, length) {
return !val || val.length >= parseInt(length);
} });
_('maxlength', { fn: function fn(val, length) {
return !val || val.length <= parseInt(length);
} });
_('min', { fn: function fn(val, limit) {
return !val || (this.type === 'checkbox' ? groupedElemCount(this) >= parseInt(limit) : parseFloat(val) >= parseFloat(limit));
} });
_('max', { fn: function fn(val, limit) {
return !val || (this.type === 'checkbox' ? groupedElemCount(this) <= parseInt(limit) : parseFloat(val) <= parseFloat(limit));
} });
_('pattern', { fn: function fn(val, pattern) {
var m = pattern.match(new RegExp('^/(.*?)/([gimy]*)$'));return !val || new RegExp(m[1], m[2]).test(val);
} });
_('equals', { fn: function fn(val, otherFieldSelector) {
var other = document.querySelector(otherFieldSelector);return other && (!val && !other.value || other.value === val);
} });
function Pristine(form, config, live) {
var self = this;
init(form, config, live);
function init(form, config, live) {
form.setAttribute("novalidate", "true");
self.form = form;
self.config = mergeConfig(config || {}, defaultConfig);
self.live = !(live === false);
self.fields = Array.from(form.querySelectorAll(SELECTOR)).map(function (input) {
var fns = [];
var params = {};
var messages = {};
[].forEach.call(input.attributes, function (attr) {
if (/^data-pristine-/.test(attr.name)) {
var name = attr.name.substr(14);
var messageMatch = name.match(MESSAGE_REGEX);
if (messageMatch !== null) {
var locale = messageMatch[1] === undefined ? 'en' : messageMatch[1];
if (!messages.hasOwnProperty(locale)) messages[locale] = {};
messages[locale][name.slice(0, name.length - messageMatch[0].length)] = attr.value;
return;
}
if (name === 'type') name = attr.value;
_addValidatorToField(fns, params, name, attr.value);
} else if (~ALLOWED_ATTRIBUTES.indexOf(attr.name)) {
_addValidatorToField(fns, params, attr.name, attr.value);
} else if (attr.name === 'type') {
_addValidatorToField(fns, params, attr.value);
}
});
fns.sort(function (a, b) {
return b.priority - a.priority;
});
self.live && input.addEventListener(!~['radio', 'checkbox'].indexOf(input.getAttribute('type')) ? 'input' : 'change', function (e) {
self.validate(e.target);
}.bind(self));
return input.pristine = { input: input, validators: fns, params: params, messages: messages, self: self };
}.bind(self));
}
function _addValidatorToField(fns, params, name, value) {
var validator = validators[name];
if (validator) {
fns.push(validator);
if (value) {
var valueParams = name === "pattern" ? [value] : value.split(',');
valueParams.unshift(null); // placeholder for input's value
params[name] = valueParams;
}
}
}
/***
* Checks whether the form/input elements are valid
* @param input => input element(s) or a jquery selector, null for full form validation
* @param silent => do not show error messages, just return true/false
* @returns {boolean} return true when valid false otherwise
*/
self.validate = function (input, silent) {
silent = input && silent === true || input === true;
var fields = self.fields;
if (input !== true && input !== false) {
if (input instanceof HTMLElement) {
fields = [input.pristine];
} else if (input instanceof NodeList || input instanceof (window.$ || Array) || input instanceof Array) {
fields = Array.from(input).map(function (el) {
return el.pristine;
});
}
}
var valid = true;
for (var i = 0; fields[i]; i++) {
var field = fields[i];
if (_validateField(field)) {
!silent && _showSuccess(field);
} else {
valid = false;
!silent && _showError(field);
}
}
return valid;
};
/***
* Get errors of a specific field or the whole form
* @param input
* @returns {Array|*}
*/
self.getErrors = function (input) {
if (!input) {
var erroneousFields = [];
for (var i = 0; i < self.fields.length; i++) {
var field = self.fields[i];
if (field.errors.length) {
erroneousFields.push({ input: field.input, errors: field.errors });
}
}
return erroneousFields;
}
if (input.tagName && input.tagName.toLowerCase() === "select") {
return input.pristine.errors;
}
return input.length ? input[0].pristine.errors : input.pristine.errors;
};
/***
* Validates a single field, all validator functions are called and error messages are generated
* when a validator fails
* @param field
* @returns {boolean}
* @private
*/
function _validateField(field) {
var errors = [];
var valid = true;
for (var i = 0; field.validators[i]; i++) {
var validator = field.validators[i];
var params = field.params[validator.name] ? field.params[validator.name] : [];
params[0] = field.input.value;
if (!validator.fn.apply(field.input, params)) {
valid = false;
if (typeof validator.msg === "function") {
errors.push(validator.msg(field.input.value, params));
} else if (typeof validator.msg === "string") {
errors.push(tmpl.apply(validator.msg, params));
} else if (validator.msg === Object(validator.msg) && validator.msg[currentLocale]) {
// typeof generates unnecessary babel code
errors.push(tmpl.apply(validator.msg[currentLocale], params));
} else if (field.messages[currentLocale] && field.messages[currentLocale][validator.name]) {
errors.push(tmpl.apply(field.messages[currentLocale][validator.name], params));
} else if (lang[currentLocale] && lang[currentLocale][validator.name]) {
errors.push(tmpl.apply(lang[currentLocale][validator.name], params));
} else {
errors.push(tmpl.apply(lang[currentLocale].default, params));
}
if (validator.halt === true) {
break;
}
}
}
field.errors = errors;
return valid;
}
/***
* Add a validator to a specific dom element in a form
* @param elem => The dom element where the validator is applied to
* @param fn => validator function
* @param msg => message to show when validation fails. Supports templating. ${0} for the input's value, ${1} and
* so on are for the attribute values
* @param priority => priority of the validator function, higher valued function gets called first.
* @param halt => whether validation should stop for this field after current validation function
*/
self.addValidator = function (elem, fn, msg, priority, halt) {
if (elem instanceof HTMLElement) {
elem.pristine.validators.push({ fn: fn, msg: msg, priority: priority, halt: halt });
elem.pristine.validators.sort(function (a, b) {
return b.priority - a.priority;
});
} else {
console.warn("The parameter elem must be a dom element");
}
};
/***
* An utility function that returns a 2-element array, first one is the element where error/success class is
* applied. 2nd one is the element where error message is displayed. 2nd element is created if doesn't exist and cached.
* @param field
* @returns {*}
* @private
*/
function _getErrorElements(field) {
if (field.errorElements) {
return field.errorElements;
}
var errorClassElement = findAncestor(field.input, self.config.classTo);
var errorTextParent = null,
errorTextElement = null;
if (self.config.classTo === self.config.errorTextParent) {
errorTextParent = errorClassElement;
} else {
errorTextParent = errorClassElement.querySelector('.' + self.config.errorTextParent);
}
if (errorTextParent) {
errorTextElement = errorTextParent.querySelector('.' + PRISTINE_ERROR);
if (!errorTextElement) {
errorTextElement = document.createElement(self.config.errorTextTag);
errorTextElement.className = PRISTINE_ERROR + ' ' + self.config.errorTextClass;
errorTextParent.appendChild(errorTextElement);
errorTextElement.pristineDisplay = errorTextElement.style.display;
}
}
return field.errorElements = [errorClassElement, errorTextElement];
}
function _showError(field) {
var errorElements = _getErrorElements(field);
var errorClassElement = errorElements[0],
errorTextElement = errorElements[1];
var input = field.input;
var inputId = input.id || Math.floor(new Date().valueOf() * Math.random());
var errorId = 'error-' + inputId;
if (errorClassElement) {
errorClassElement.classList.remove(self.config.successClass);
errorClassElement.classList.add(self.config.errorClass);
input.setAttribute('aria-describedby', errorId);
input.setAttribute('aria-invalid', 'true');
}
if (errorTextElement) {
errorTextElement.setAttribute('id', errorId);
errorTextElement.setAttribute('role', 'alert');
errorTextElement.innerHTML = field.errors.join('
');
errorTextElement.style.display = errorTextElement.pristineDisplay || '';
}
}
/***
* Adds error to a specific field
* @param input
* @param error
*/
self.addError = function (input, error) {
input = input.length ? input[0] : input;
input.pristine.errors.push(error);
_showError(input.pristine);
};
function _removeError(field) {
var errorElements = _getErrorElements(field);
var errorClassElement = errorElements[0],
errorTextElement = errorElements[1];
var input = field.input;
if (errorClassElement) {
// IE > 9 doesn't support multiple class removal
errorClassElement.classList.remove(self.config.errorClass);
errorClassElement.classList.remove(self.config.successClass);
input.removeAttribute('aria-describedby');
input.removeAttribute('aria-invalid');
}
if (errorTextElement) {
errorTextElement.removeAttribute('id');
errorTextElement.removeAttribute('role');
errorTextElement.innerHTML = '';
errorTextElement.style.display = 'none';
}
return errorElements;
}
function _showSuccess(field) {
var errorClassElement = _removeError(field)[0];
errorClassElement && errorClassElement.classList.add(self.config.successClass);
}
/***
* Resets the errors
*/
self.reset = function () {
for (var i = 0; self.fields[i]; i++) {
self.fields[i].errorElements = null;
}
Array.from(self.form.querySelectorAll('.' + PRISTINE_ERROR)).map(function (elem) {
elem.parentNode.removeChild(elem);
});
Array.from(self.form.querySelectorAll('.' + self.config.classTo)).map(function (elem) {
elem.classList.remove(self.config.successClass);
elem.classList.remove(self.config.errorClass);
});
};
/***
* Resets the errors and deletes all pristine fields
*/
self.destroy = function () {
self.reset();
self.fields.forEach(function (field) {
delete field.input.pristine;
});
self.fields = [];
};
self.setGlobalConfig = function (config) {
defaultConfig = config;
};
return self;
}
/***
*
* @param name => Name of the global validator
* @param fn => validator function
* @param msg => message to show when validation fails. Supports templating. ${0} for the input's value, ${1} and
* so on are for the attribute values
* @param priority => priority of the validator function, higher valued function gets called first.
* @param halt => whether validation should stop for this field after current validation function
*/
Pristine.addValidator = function (name, fn, msg, priority, halt) {
_(name, { fn: fn, msg: msg, priority: priority, halt: halt });
};
Pristine.addMessages = function (locale, messages) {
var langObj = lang.hasOwnProperty(locale) ? lang[locale] : lang[locale] = {};
Object.keys(messages).forEach(function (key, index) {
langObj[key] = messages[key];
});
};
Pristine.setLocale = function (locale) {
currentLocale = locale;
};
return Pristine;
})));