amazeui
Version:
Sleek, intuitive, and powerful front-end framework for faster and easier web development.
599 lines (497 loc) • 19.5 kB
JavaScript
;
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;