You are on page 1of 11

'use strict';

var $ = require('jquery');
var UI = require('./core');

var Validator = function(element, options) {


this.options = $.extend({}, Validator.DEFAULTS, options);
this.options.patterns = $.extend({}, Validator.patterns,
this.options.patterns);
var locales = this.options.locales;
!Validator.validationMessages[locales] && (this.options.locales = 'zh_CN');
this.$element = $(element);
this.init();
};

Validator.DEFAULTS = {
debug: false,
locales: 'zh_CN',
H5validation: false,
H5inputType: ['email', 'url', 'number'],
patterns: {},
patternClassPrefix: 'js-pattern-',
activeClass: 'am-active',
inValidClass: 'am-field-error',
validClass: 'am-field-valid',

validateOnSubmit: true,
alwaysRevalidate: false,
// Elements to validate with allValid (only validating visible elements)
// :input: selects all input, textarea, select and button elements.
// @since 2.5: move `:visible` to `ignore` option (became to `:hidden`)
allFields: ':input:not(:submit, :button, :disabled, .am-novalidate)',

// ignored elements
// @since 2.5
ignore: ':hidden:not([data-am-selected], .am-validate)',

// Custom events
customEvents: 'validate',

// Keyboard events
keyboardFields: ':input:not(:submit, :button, :disabled, .am-novalidate)',
keyboardEvents: 'focusout, change', // keyup, focusin

// bind `keyup` event to active field


activeKeyup: false,
textareaMaxlenthKeyup: true,

// Mouse events
pointerFields: 'input[type="range"]:not(:disabled, .am-novalidate), ' +
'input[type="radio"]:not(:disabled, .am-novalidate), ' +
'input[type="checkbox"]:not(:disabled, .am-novalidate), ' +
'select:not(:disabled, .am-novalidate), ' +
'option:not(:disabled, .am-novalidate)',
pointerEvents: 'click',

onValid: function(validity) {
},
onInValid: function(validity) {
},

markValid: function(validity) {
// this is Validator instance
var options = this.options;
var $field = $(validity.field);
var $parent = $field.closest('.am-form-group');

$field.addClass(options.validClass).removeClass(options.inValidClass);
$parent.addClass('am-form-success').removeClass('am-form-error');
options.onValid.call(this, validity);
},

markInValid: function(validity) {
var options = this.options;
var $field = $(validity.field);
var $parent = $field.closest('.am-form-group');

$field.addClass(options.inValidClass + ' ' + options.activeClass).


removeClass(options.validClass);
$parent.addClass('am-form-error').removeClass('am-form-success');
options.onInValid.call(this, validity);
},

validate: function(validity) {
// return validity;
},

submit: null
};

Validator.VERSION = '{{VERSION}}';

/* jshint -W101 */
Validator.patterns = {
email: /^((([a-zA-Z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\
uFDCF\uFDF0-\uFFEF])+(\.([a-zA-Z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\
uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?
(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\
uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\
uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-zA-
Z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-zA-Z]|\d|[\u00A0-\uD7FF\
uF900-\uFDCF\uFDF0-\uFFEF])([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\
uFDF0-\uFFEF])*([a-zA-Z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-
zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-zA-Z]|[\u00A0-\uD7FF\uF900-\
uFDCF\uFDF0-\uFFEF])([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\
uFFEF])*([a-zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/,

url: /^(https?|ftp):\/\/(((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\
uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|
25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-
5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-zA-Z]|\d|[\u00A0-\uD7FF\uF900-\
uFDCF\uFDF0-\uFFEF])|(([a-zA-Z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-
zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-zA-Z]|\d|[\u00A0-\
uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\
uFDF0-\uFFEF])|(([a-zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-zA-Z]|\
d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-zA-Z]|[\u00A0-\uD7FF\
uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\
uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-zA-Z]|\
d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\
+,;=]|:|@)*)*)?)?(\?((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\
uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-zA-
Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\
*\+,;=]|:|@)|\/|\?)*)?$/,

// Number, including positive, negative, and floating decimal


number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/,
dateISO: /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/,
integer: /^-?\d+$/
};
/* jshint +W101 */

Validator.validationMessages = {
zh_CN: {
valueMissing: '请填写(选择)此字段',
customError: {
tooShort: '至少填写 %s 个字符',
checkedOverflow: '至多选择 %s 项',
checkedUnderflow: '至少选择 %s 项'
},
patternMismatch: '请按照要求的格式填写',
rangeOverflow: '请填写小于等于 %s 的值',
rangeUnderflow: '请填写大于等于 %s 的值',
stepMismatch: '',
tooLong: '至多填写 %s 个字符',
typeMismatch: '请按照要求的类型填写'
}
};

Validator.ERROR_MAP = {
tooShort: 'minlength',
checkedOverflow: 'maxchecked',
checkedUnderflow: 'minchecked',
rangeOverflow: 'max',
rangeUnderflow: 'min',
tooLong: 'maxlength'
};

// TODO: 考虑表单元素不是 form 子元素的情形


// TODO: change/click/focusout 同时触发时处理重复
// TODO: 显示提示信息

Validator.prototype.init = function() {
var _this = this;
var $element = this.$element;
var options = this.options;

// using H5 form validation if option set and supported


if (options.H5validation && UI.support.formValidation) {
return false;
}

// disable HTML5 form validation


$element.attr('novalidate', 'novalidate');

function regexToPattern(regex) {
var pattern = regex.toString();
return pattern.substring(1, pattern.length - 1);
}

// add pattern to H5 input type


$.each(options.H5inputType, function(i, type) {
var $field = $element.find('input[type=' + type + ']');
if (!$field.attr('pattern') &&
!$field.is('[class*=' + options.patternClassPrefix + ']')) {
$field.attr('pattern', regexToPattern(options.patterns[type]));
}
});

// add pattern to .js-pattern-xx


$.each(options.patterns, function(key, value) {
var $field = $element.find('.' + options.patternClassPrefix + key);
!$field.attr('pattern') && $field.attr('pattern', regexToPattern(value));
});

$element.on('submit.validator.amui', function(e) {
// user custom submit handler
if (typeof options.submit === 'function') {
return options.submit.call(_this, e);
}

if (options.validateOnSubmit) {
var formValidity = _this.isFormValid();

// sync validate, return result


if ($.type(formValidity) === 'boolean') {
return formValidity;
}

if ($element.data('amui.checked')) {
return true;
} else {
$.when(formValidity).then(function() {
// done, submit form
$element.data('amui.checked', true).submit();
}, function() {
// fail
$element.data('amui.checked', false).
find('.' + options.inValidClass).eq(0).focus();
});
return false;
}
}
});

function bindEvents(fields, eventFlags, debounce) {


var events = eventFlags.split(',');
var validate = function(e) {
// console.log(e.type);
_this.validate(this);
};

if (debounce) {
validate = UI.utils.debounce(validate, debounce);
}
$.each(events, function(i, event) {
$element.on(event + '.validator.amui', fields, validate);
});
}

bindEvents(':input', options.customEvents);
bindEvents(options.keyboardFields, options.keyboardEvents);
bindEvents(options.pointerFields, options.pointerEvents);

if (options.textareaMaxlenthKeyup) {
bindEvents('textarea[maxlength]', 'keyup', 50);
}

if (options.activeKeyup) {
bindEvents('.am-active', 'keyup', 50);
}

/*if (options.errorMessage === 'tooltip') {


this.$tooltip = $('<div></div>', {
'class': 'am-validator-message',
id: UI.utils.generateGUID('am-validator-message')
});

$(document.body).append(this.$tooltip);
}*/
};

Validator.prototype.isValid = function(field) {
var $field = $(field);
var options = this.options;
// valid field not has been validated
if ($field.data('validity') === undefined || options.alwaysRevalidate) {
this.validate(field);
}

return $field.data('validity') && $field.data('validity').valid;


};

Validator.prototype.validate = function(field) {
var _this = this;
var $element = this.$element;
var options = this.options;
var $field = $(field);

// Validate equal, e.g. confirm password


var equalTo = $field.data('equalTo');
if (equalTo) {
$field.attr('pattern', '^' + $element.find(equalTo).val() + '$');
}

var pattern = $field.attr('pattern') || false;


var re = new RegExp(pattern);
var $radioGroup = null;
var $checkboxGroup = null;
// if checkbox, return `:chcked` length
// NOTE: checkbox and radio should have name attribute
var value = ($field.is('[type=checkbox]')) ?
($checkboxGroup = $element.find('input[name="' + field.name + '"]')).
filter(':checked').length : ($field.is('[type=radio]') ?
($radioGroup = this.$element.find('input[name="' + field.name + '"]')).
filter(':checked').length > 0 : $field.val());

// if checkbox, valid the first input of checkbox group


$field = ($checkboxGroup && $checkboxGroup.length) ?
$checkboxGroup.first() : $field;

var required = ($field.attr('required') !== undefined) &&


($field.attr('required') !== 'false');
var maxLength = parseInt($field.attr('maxlength'), 10);
var minLength = parseInt($field.attr('minlength'), 10);
var min = Number($field.attr('min'));
var max = Number($field.attr('max'));
var validity = this.createValidity({field: $field[0], valid: true});

// Debug
if (options.debug && window.console) {
console.log('Validate: value -> [' + value + ', regex -> [' + re +
'], required -> ' + required);
console.log('Regex test: ' + re.test(value) + ', Pattern: ' + pattern);
}

// check value length


if (!isNaN(maxLength) && value.length > maxLength) {
validity.valid = false;
validity.tooLong = true;
}

if (!isNaN(minLength) && value.length < minLength) {


validity.valid = false;
validity.customError = 'tooShort';
}

// check minimum and maximum


// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input
// TODO: 日期验证最小值和最大值 min/max
if (!isNaN(min) && Number(value) < min) {
validity.valid = false;
validity.rangeUnderflow = true;
}

if (!isNaN(max) && Number(value) > max) {


validity.valid = false;
validity.rangeOverflow = true;
}

// check required
if (required && !value) {
validity.valid = false;
validity.valueMissing = true;
} else if (($checkboxGroup || $field.is('select[multiple="multiple"]')) &&
value) {
// check checkboxes / multiple select with `minchecked`/`maxchecked` attr
// var $multipleField = $checkboxGroup ? $checkboxGroup.first() : $field;

// if is select[multiple="multiple"], return selected length


value = $checkboxGroup ? value : value.length;
// at least checked
var minChecked = parseInt($field.attr('minchecked'), 10);
// at most checked
var maxChecked = parseInt($field.attr('maxchecked'), 10);

if (!isNaN(minChecked) && value < minChecked) {


// console.log('At least [%d] items checked!', maxChecked);
validity.valid = false;
validity.customError = 'checkedUnderflow';
}

if (!isNaN(maxChecked) && value > maxChecked) {


// console.log('At most [%d] items checked!', maxChecked);
validity.valid = false;
validity.customError = 'checkedOverflow';
}
} else if (pattern && !re.test(value) && value) { // check pattern
validity.valid = false;
validity.patternMismatch = true;
}

var validateComplete = function(validity) {


this.markField(validity);

var event = $.Event('validated.field.validator.amui');


event.validity = validity;

$field.trigger(event).data('validity', validity);

// validate the radios/checkboxes with the same name


var $fields = $radioGroup || $checkboxGroup;
if ($fields) {
$fields.not($field).data('validity', validity).each(function() {
validity.field = this;
_this.markField(validity);
});
}

return validity;
};

// Run custom validate


// NOTE: async custom validate should return Deferred project
var customValidate;
(typeof options.validate === 'function') &&
(customValidate = options.validate.call(this, validity));

// Deferred
if (customValidate) {
var dfd = new $.Deferred();
$field.data('amui.dfdValidity', dfd.promise());
return $.when(customValidate).always(function(validity) {
dfd[validity.valid ? 'resolve' : 'reject'](validity);
validateComplete.call(_this, validity);
});
}

validateComplete.call(this, validity);
};
Validator.prototype.markField = function(validity) {
var options = this.options;
var flag = 'mark' + (validity.valid ? '' : 'In') + 'Valid';
options[flag] && options[flag].call(this, validity);
};

// check all fields in the form are valid


Validator.prototype.validateForm = function() {
var _this = this;
var $element = this.$element;
var options = this.options;
var $allFields = $element.find(options.allFields).not(options.ignore);
var radioNames = [];
var valid = true;
var formValidity = [];
var $inValidFields = $([]);
var promises = [];
// for async validate
var async = false;

$element.trigger('validate.form.validator.amui');

// Filter radio with the same name and keep only one,
// since they will be checked as a group by validate()
var $filteredFields = $allFields.filter(function(index) {
var name;
if (this.tagName === 'INPUT' && this.type === 'radio') {
name = this.name;
if (radioNames[name] === true) {
return false;
}
radioNames[name] = true;
}
return true;
});

$filteredFields.each(function() {
var $this = $(this);
var fieldValid = _this.isValid(this);
var fieldValidity = $this.data('validity');

valid = !!fieldValid && valid;


formValidity.push(fieldValidity);

if (!fieldValid) {
$inValidFields = $inValidFields.add($(this), $element);
}

// async validity
var promise = $this.data('amui.dfdValidity');

if (promise) {
promises.push(promise);
async = true;
} else {
// convert sync validity to Promise
var dfd = new $.Deferred();
promises.push(dfd.promise());
dfd[fieldValid ? 'resolve' : 'reject'](fieldValidity);
}
});

// NOTE: If there are async validity, the valid may be not exact result.
var validity = {
valid: valid,
$invalidFields: $inValidFields,
validity: formValidity,
promises: promises,
async: async
};

$element.trigger('validated.form.validator.amui', validity);

return validity;
};

Validator.prototype.isFormValid = function() {
var _this = this;
var formValidity = this.validateForm();
var triggerValid = function(type) {
_this.$element.trigger(type + '.validator.amui');
};

if (formValidity.async) {
var masterDfd = new $.Deferred();

$.when.apply(null, formValidity.promises).then(function() {
masterDfd.resolve();
triggerValid('valid');
}, function() {
masterDfd.reject();
triggerValid('invalid');
});

return masterDfd.promise();
} else {
if (!formValidity.valid) {
var $first = formValidity.$invalidFields.first();

// Selected plugin support


// @since 2.5
if ($first.is('[data-am-selected]')) {
$first = $first.next('.am-selected').find('.am-selected-btn');
}

$first.focus();
triggerValid('invalid');
return false;
}

triggerValid('valid');
return true;
}
};

// customErrors:
// 1. tooShort
// 2. checkedOverflow
// 3. checkedUnderflow
Validator.prototype.createValidity = function(validity) {
return $.extend({
customError: validity.customError || false,
patternMismatch: validity.patternMismatch || false,
rangeOverflow: validity.rangeOverflow || false, // higher than maximum
rangeUnderflow: validity.rangeUnderflow || false, // lower than minimum
stepMismatch: validity.stepMismatch || false,
tooLong: validity.tooLong || false,
// value is not in the correct syntax
typeMismatch: validity.typeMismatch || false,
valid: validity.valid || true,
// Returns true if the element has no value but is a required field
valueMissing: validity.valueMissing || false
}, validity);
};

Validator.prototype.getValidationMessage = function(validity) {
var messages = Validator.validationMessages[this.options.locales];
var error;
var message;
var placeholder = '%s';
var $field = $(validity.field);

if ($field.is('[type="checkbox"]') || $field.is('[type="radio"]')) {
$field = this.$element.find('[name=' + $field.attr('name') + ']').first();
}

// get error name


$.each(validity, function(key, val) {
// skip `field` and `valid`
if (key === 'field' || key === 'valid') {
return key;
}

// Amaze UI custom error type


if (key === 'customError' && val) {
error = val;
messages = messages.customError;
return false;
}

// W3C specs error type


if (val === true) {
error = key;
return false;
}
});

message = messages[error] || undefined;

if (message && Validator.ERROR_MAP[error]) {


message = message.replace(placeholder,
$field.attr(Validator.ERROR_MAP[error]) || '规定的');
}

return message;
};
// remove valid mark
Validator.prototype.removeMark = function() {
this.$element
.find('.am-form-success, .am-form-error, .' + this.options.inValidClass +
', .' + this.options.validClass)
.removeClass([
'am-form-success',
'am-form-error',
this.options.inValidClass,
this.options.validClass
].join(' '));
};

// @since 2.5
Validator.prototype.destroy = function() {
this.removeMark();

// Remove data
// - Validator.prototype.init -> $element.data('amui.checked')
// - Validator.prototype.validateForm
// - Validator.prototype.isValid
this.$element.removeData('amui.validator amui.checked')
.off('.validator.amui')
.find(this.options.allFields).removeData('validity amui.dfdValidity');
};

UI.plugin('validator', Validator);

// init code
UI.ready(function(context) {
$('[data-am-validator]', context).validator();
});

module.exports = Validator;

You might also like