var validatorRepo = require('./validator-repo');
var stateEnum = require('./state-enum');
var ValidationState = require('./validation-state');
var InputState = require('./input-state');
var constants = require('./constants');
var $$ = require('./dom-helpers');
var validInputTagNames = constants.validInputTagNames;
var keyStrokedInputTypes = constants.keyStrokedInputTypes;
/**
* creates a new Input object wrapping around a DOM object.
* @memberof! vivalid
* @class
* @example new Input(document.getElementById('Name'), [['required',{msg: 'custom required message'}],['max',{max: 10}]])
* @param {HTMLElement} el the DOM object to wrap. For radios and checkboxes, pass only 1 element- the class will find it's siblings with the same name attribute.
* @param {_internal.validatorsNameOptionsTuple[]} validatorsNameOptionsTuples <b> the order matters- the input's state is the first {@link _internal.validatorsNameOptionsTuple validatorsNameOptionsTuple} that evulates to a non-valid (pending or invalid) state. </b>
* @param {function} [onInputValidationResult] Signature of {@link _internal.onInputValidationResult onInputValidationResult}. A function to handle an input state or message change. If not passed, {@link _internal.defaultOnInputValidationResult defaultOnInputValidationResult} will be used.
* @param {boolean} isBlurOnly if true, doesn't not trigger validation on 'input' or 'change' events.
*/
function Input(el, validatorsNameOptionsTuples, onInputValidationResult, isBlurOnly) {
if (validInputTagNames.indexOf(el.nodeName.toLowerCase()) === -1) {
throw 'only operates on the following html tags: ' + validInputTagNames.toString();
}
this._el = el;
this._validatorsNameOptionsTuples = validatorsNameOptionsTuples;
this._onInputValidationResult = onInputValidationResult || defaultOnInputValidationResult;
this._isBlurOnly = isBlurOnly;
this._validators = buildValidators();
this._inputState = new InputState();
this._elName = el.nodeName.toLowerCase();
this._elType = el.type;
this._isKeyed = (this._elName === 'textarea' || keyStrokedInputTypes.indexOf(this._elType) > -1);
this._runValidatorsBounded = this._runValidators.bind(this);
this._initListeners();
function buildValidators() {
var result = [];
validatorsNameOptionsTuples.forEach(function(validatorsNameOptionsTuple) {
var validatorName = validatorsNameOptionsTuple[0];
var validatorOptions = validatorsNameOptionsTuple[1];
result.push({
name: validatorName,
run: validatorRepo.build(validatorName, validatorOptions)
});
});
return result;
}
/** The default {@link _internal.onInputValidationResult onInputValidationResult} used when {@link vivalid.Input} is initiated without a 3rd parameter
* @name defaultOnInputValidationResult
* @function
* @memberof! _internal
*/
function defaultOnInputValidationResult(el, validationsResult, validatorName, stateEnum) {
var errorDiv;
// for radio buttons and checkboxes: get the last element in group by name
if ((el.nodeName.toLowerCase() === 'input' && (el.type === 'radio' || el.type === 'checkbox'))) {
var getAllByName = el.parentNode.querySelectorAll('input[name="' + el.name + '"]');
el = getAllByName.item(getAllByName.length - 1);
}
if (validationsResult.stateEnum === stateEnum.invalid) {
errorDiv = getExistingErrorDiv(el);
if (errorDiv) {
errorDiv.textContent = validationsResult.message;
} else {
appendNewErrorDiv(el, validationsResult.message);
}
el.style.borderStyle = "solid";
el.style.borderColor = "#ff0000";
$$.addClass(el, "vivalid-error-input");
} else {
errorDiv = getExistingErrorDiv(el);
if (errorDiv) {
errorDiv.parentNode.removeChild(errorDiv);
el.style.borderStyle = "";
el.style.borderColor = "";
$$.removeClass(el, "vivalid-error-input");
}
}
function getExistingErrorDiv(el) {
if (el.nextElementSibling && el.nextElementSibling.className === "vivalid-error") {
return el.nextElementSibling;
}
}
function appendNewErrorDiv(el, message) {
errorDiv = document.createElement("DIV");
errorDiv.className = "vivalid-error";
errorDiv.style.color = "#ff0000";
var t = document.createTextNode(validationsResult.message);
errorDiv.appendChild(t);
el.parentNode.insertBefore(errorDiv, el.nextElementSibling);
}
}
}
Input.prototype = (function() {
return {
triggerValidation: triggerValidation,
setGroup: setGroup,
reset: reset,
getDomElement: getDomElement,
_reBindCheckedElement: _reBindCheckedElement,
_runValidators: _runValidators,
_changeEventType: _changeEventType,
_initListeners: _initListeners,
_addChangeListener: _addChangeListener,
_addEventType: _addEventType,
_removeActiveEventType: _removeActiveEventType,
_getUpdateInputValidationResultAsync: _getUpdateInputValidationResultAsync,
_updateInputValidationResult: _updateInputValidationResult
};
function _reBindCheckedElement() {
// reBind only radio and checkbox buttons
if (!(this._el.nodeName.toLowerCase() === 'input' && (this._el.type === 'radio' || this._el.type === 'checkbox'))) {
return;
}
var checkedElement = document.querySelector('input[name="' + this._el.name + '"]:checked');
if (checkedElement) {
this._el = checkedElement;
this._inputState.isNoneChecked = false;
} else {
this._inputState.isNoneChecked = true;
}
}
function triggerValidation() {
if (this._inputState.validationCycle === 0 || this._inputState.isChanged) {
this._runValidatorsBounded();
}
}
function _changeEventType(eventType) {
if (!this._isKeyed) return;
if (eventType === this._inputState.activeEventType) return;
this._removeActiveEventType();
this._addEventType(eventType);
}
function setGroup(value) {
this._group = value;
}
function getDomElement(){
return this._el;
}
function _initListeners() {
this._addChangeListener();
if (this._isKeyed) {
this._addEventType('blur');
} else {
this._addEventType('change');
}
}
function _runValidators(event, fromIndex) {
this._inputState.validationCycle++;
this._reBindCheckedElement();
if (typeof this._group.getOnBeforeValidation() === 'function') {
this._group.getOnBeforeValidation()(this._el);
}
var validationsResult, validatorName;
var i = fromIndex || 0;
for (; i < this._validators.length; i++) {
var validator = this._validators[i];
var elementValue = this._inputState.isNoneChecked ? '' : this._el.value;
// if async, then return a pending enum with empty message and call the callback with result once ready
var validatorResult = validator.run(elementValue, this._getUpdateInputValidationResultAsync(validator.name, i, this._inputState.validationCycle));
if (validatorResult.stateEnum !== stateEnum.valid) {
validationsResult = validatorResult;
validatorName = validator.name;
if (!this._isBlurOnly) {
this._changeEventType('input'); //TODO: call only once?
}
break;
}
}
validationsResult = validationsResult || new ValidationState('', stateEnum.valid);
this._updateInputValidationResult(validationsResult, validatorName);
this._inputState.isChanged = false; // TODO: move to top of function?
if (typeof this._group.getOnAfterValidation() === 'function') {
this._group.getOnAfterValidation()(this._el);
}
}
function reset() {
this._removeActiveEventType();
this._initListeners();
this._inputState = new InputState();
this._onInputValidationResult(this._el, stateEnum.valid, '', stateEnum); // called with valid state to clear any previous errors UI
}
function _addChangeListener() {
var self = this;
if (this._isKeyed) {
if (this._isBlurOnly) {
return;
} else {
this._el.addEventListener('input', function() {
self._inputState.isChanged = true;
}, false);
}
} else if (this._elName === 'input' && (this._elType === 'radio' || this._elType === 'checkbox')) {
var groupElements = document.querySelectorAll('input[name="' + this._el.name + '"]');
var i = 0;
for (; i < groupElements.length; i++) {
groupElements[i].addEventListener('change', function() {
self._inputState.isChanged = true;
}, false);
}
} else if (this._elName === 'select') {
this._el.addEventListener('change', function() {
self._inputState.isChanged = true;
}, false);
}
}
function _addEventType(eventType) {
if (this._isKeyed) {
this._el.addEventListener(eventType, this._runValidatorsBounded, false);
} else if (this._elName === 'input' && (this._elType === 'radio' || this._elType === 'checkbox')) {
var groupElements = document.querySelectorAll('input[name="' + this._el.name + '"]');
var i = 0;
for (; i < groupElements.length; i++) {
groupElements[i].addEventListener(eventType, this._runValidatorsBounded, false);
}
} else if (this._elName === 'select') {
this._el.addEventListener(eventType, this._runValidatorsBounded, false);
}
this._inputState.activeEventType = eventType;
}
function _removeActiveEventType() {
this._el.removeEventListener(this._inputState.activeEventType, this._runValidatorsBounded, false);
}
function _getUpdateInputValidationResultAsync(validatorName, validatorIndex, asyncValidationCycle) {
var self = this;
return function(validatorResult) {
// guard against updating async validations from old cycles
if (asyncValidationCycle && asyncValidationCycle !== self._inputState.validationCycle) {
return;
}
// if pending turned to be valid, and there are more validation to run, run them:
if (validatorResult.stateEnum === stateEnum.valid && validatorIndex + 1 < self._validators.length) {
self._runValidatorsBounded(null, validatorIndex + 1);
} else {
self._updateInputValidationResult(validatorResult, validatorName);
}
};
}
function _updateInputValidationResult(validationsResult, validatorName) {
this._group.updateGroupStates(this._inputState.validationState, validationsResult); // filter equal state at caller
this._group.updateGroupListeners();
this._inputState.validationState = validationsResult;
this._onInputValidationResult(this._el, validationsResult, validatorName, stateEnum);
}
})();
module.exports = Input;
/** An Array where Array[0] is the validator {string} name, and Array[1] is the validator {object} options
* @name validatorsNameOptionsTuple
* @type {array}
* @memberof! _internal
* @example ['required',{msg: 'custom required message'}]
*/
/** A function to handle an input state or message change
* @name onInputValidationResult
* @function
* @memberof! _internal
* @param {HTMLElement} el the input's DOM object.
* @param {object} validationsResult A {@link _internal.ValidationState ValidationState} instance containing the state and validation message.
* @param {string} validatorName The name of validator that triggered an 'invalid' state.
* @param {object} stateEnum {@link _internal.stateEnum stateEnum}
*/