lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
1,316 lines (1,132 loc) • 78.3 kB
JavaScript
/**
* @Validate.js
* @author zhangxinxu
* @version
* Created: 15-08-19
* @edited: 20-08-17 edit by wanghao
*/
/**
* 基于HTML5规范的表单验证方法
* 根据原生属性:type, required, min, max, minlength, maxlength, step, pattern等属性进行验证
* 使用原生checkValidity方法名,以及{
badInput: false
customError: false
patternMismatch: false
rangeOverflow: false
rangeUnderflow: false
stepMismatch: false
tooLong: false
tooShort: false
typeMismatch: false
valid: true
valueMissing: false
}
等参数名。
* 支持new实例构造,例如:
* new Validate(el, options);
* 很多验证规则基于其他UI组件特征
*/
import './ErrorTip.js';
const Validate = (() => {
// 全角转半角方法
window.DBC2SBC = (str) => {
let result = '';
let i, code;
for (i = 0; i < str.length; i++) {
code = str.charCodeAt(i);
if (code >= 65281 && code <= 65373) {
result += String.fromCharCode(str.charCodeAt(i) - 65248);
} else if (code == 12288) {
result += String.fromCharCode(str.charCodeAt(i) - 12288 + 32);
} else {
result += str.charAt(i);
}
}
return result;
};
// 滚动到顶部缓动实现
// rate表示缓动速率,默认是2
window.scrollTopTo = (top, callback) => {
let scrollTop = document.scrollingElement.scrollTop;
const rate = 2;
const funTop = () => {
scrollTop = scrollTop + (top - scrollTop) / rate;
// 临界判断,终止动画
if (Math.abs(scrollTop - top) <= 1) {
document.scrollingElement.scrollTop = top;
callback && callback();
return;
}
document.scrollingElement.scrollTop = scrollTop;
// 动画gogogo!
requestAnimationFrame(funTop);
};
funTop();
};
// 一些全局的属性和方法
document.validate = (function () {
return {
reg: {
email: '^[a-z0-9._%-]+@([a-z0-9-]+\\.)+[a-z]{2,4}$',
number: '^\\-?\\d+(\\.\\d+)?$',
url: "^(http|https)\\:\\/\\/[a-z0-9\\-\\.]+\\.[a-z]{2,3}(:[a-z0-9]*)?\\/?([a-z0-9\\-\\._\\:\\?\\,\\'\\/\\\\\\+&%\\$#\\=~])*$",
tel: '^1\\d{10}$',
zipcode: '^\\d{6}$',
date: '^\\d{4}\\-(0\\d|1[0-2])\\-([0-2]\\d|3[0-1])$',
time: '^[0-2]\\d\\:[0-5]\\d$',
hour: '^[0-2]\\d\\:00$',
minute: '^[0-2]\\d\\:[0-5]\\d$',
'date-range': '^\\d{4}(\\-\\d{2}){2}\\s至\\s\\d{4}(\\-\\d{2}){2}$',
'month-range': '^\\d{4}\\-\\d{2}\\s至\\s\\d{4}\\-\\d{2}$'
},
name: {
email: '邮箱',
tel: '手机号码',
url: '网址',
zipcode: '邮编',
password: '密码',
number: '数值',
range: '数值',
date: '日期',
year: '年份',
month: '月份',
hour: '小时',
minute: '分钟',
time: '时间',
datetime: '日期时间',
'date-range': '日期范围',
'month-range': '月份范围'
},
// 选中某范围文字内容的拓展方法
selectRange (element, start, end) {
if (!element) {
return;
}
if (element.createTextRange) {
const objRange = element.createTextRange();
objRange.collapse(true);
objRange.moveEnd('character', end);
objRange.moveStart('character', start);
objRange.select();
} else if (element.focus) {
element.focus();
element.setSelectionRange(start, end);
}
},
// 电话号码过滤成符合要求的号码,省去用户自己处理
// 例如我们从某个地方复制电话号码,可能是短横线相连
// 或者前面带+86
getTel (tel) {
let strTel = tel || '';
strTel = strTel.replace('+86', '');
// 如果此时剩余所有数字正好11位
if (strTel.match(/\d/) && strTel.match(/\d/g).length == 11) {
strTel = strTel.replace(/\D/g, '');
}
return strTel;
},
// 获得字符长度
// @element Object input/textarea表单控件元素
// @max Number 返回满足最大值时候的真实字符长度
getLength (element, max) {
if (element.type == 'password') {
return max ? max : element.value.length;
}
// 语言属性和trim后的值
const strAttrLang = element.getAttribute('lang');
const strValue = element.value.trim();
if (!strAttrLang) {
return max ? max : strValue.length;
}
if (strValue == '') {
return 0;
}
// 中文和英文计算字符个数的比例
let numRatioCh = 1;
let numRatioEn = 1;
if (/zh/i.test(strAttrLang)) {
// 1个英文半个字符
numRatioEn = 0.5;
} else if (/en/i.test(strAttrLang)) {
// 1个中文2个字符
numRatioCh = 2;
}
// 下面是中文或全角等字符正则
if (!max) {
const lenOriginCh = strValue.replace(/[\x00-\xff]/g, '').length;
const lenOriginEn = strValue.length - lenOriginCh;
// 根据比例返回最终长度
return Math.ceil(lenOriginEn * numRatioEn) + Math.ceil(lenOriginCh * numRatioCh);
}
let numStart = 0;
let lenMatch = max;
strValue.split('').forEach((letter, index) => {
if (numStart >= max) {
return;
}
if (/[\x00-\xff]/.test(letter)) {
// 如果字符是中文或全角
numStart += numRatioEn;
} else {
// 如果字符是英文
numStart += numRatioCh;
}
if (numStart >= max) {
lenMatch = index + 1;
}
});
return lenMatch;
},
// 获得并重置type
getType (element) {
// 控件的类型
// 这里不能直接element.type获取,否则,类似email类型直接就是text类型
// 但是,有些元素的type是隐式的,例如textarea,因此element.type还是需要的
const strAttrType = element.getAttribute('type');
let strType = strAttrType || element.type || '';
if (strType == 'select-one') {
strType = 'select';
}
return strType;
},
/**
* 返回对应的提示信息
* @param {[type]} element 提示元素
* @return {String} 返回对应的提示信息
*/
getReportText (element) {
const defaultPrompt = {
name: this.name,
badInput: '值无效',
customError: '包含错误',
patternMismatch: {
pattern: '内容格式不符合要求',
multiple: '某项内容格式不符合要求'
},
valueMissing: {
radio: '请选择一个选项',
checkbox: '如果要继续,请选中此框',
select: '请选择列表中的一项',
'select-one': '请选择列表中的一项',
empty: '请填写此字段'
},
rangeOverflow: '值偏大',
rangeUnderflow: '值偏小',
stepMismatch: '值不在间隔要求范围内',
tooLong: '内容长度偏大',
tooShort: '内容长度偏小',
typeMismatch: '值和输入框类型不匹配'
};
if (!element) {
return '';
}
// 验证信息
const objValidateState = element.validity;
// 如果没有错误,没有任何返回
if (objValidateState.valid == true) {
return '';
}
// 最终的提示文字
let strFinalText = '';
// 元素的id, type等
const strId = element.id;
const strType = this.getType(element);
// 元素上自定义的提示信息
// 元素存储的提示信息,主要是自定义方法返回的自定义提示
const customValidate = element.customValidate || {};
const optionPrompt = customValidate.report || {};
// 错误提示关键字
const strName = defaultPrompt.name[strType] || (function () {
if (!customValidate.label || !strId || /checkbox|radio/.test(strType)) {
return;
}
let strTextLabel = '';
// 从label动态获取提示数据
document.querySelectorAll('label[for="' + strId + '"]').forEach(function (eleLabel) {
const eleLabelClone = eleLabel.cloneNode(true);
// 只使用裸露的文字作为提示关键字
[].slice.call(eleLabelClone.children).forEach(function (eleChild) {
eleChild.remove();
});
// 去除数字和冒号
const strLabelCloneText = eleLabelClone.innerHTML.trim().replace(/\d/g, '').replace(':', '');
// 内容最长的那个元素作为提示文字
if (strLabelCloneText.length > strTextLabel.length) {
strTextLabel = strLabelCloneText;
}
});
// 提示关键字显然不会小于2个字
// 不会是纯数字
if (strTextLabel.length >= 2) {
return strTextLabel;
}
})();
// 开始一个一个确认提示
if (objValidateState.valueMissing) {
strFinalText = optionPrompt.valueMissing;
// 如果没有对应的自定义提示
if (!strFinalText) {
// 则使用默认的提示:
// 1. 根据类型关键字组装的提示
if (strType && strName) {
if (strType != 'select') {
strFinalText = strName + '不能为空';
} else {
strFinalText = '您尚未选择' + strName;
}
} else {
// 2. 默认的missing几个错误提示
strFinalText = defaultPrompt.valueMissing[strType];
}
strFinalText = strFinalText || defaultPrompt.valueMissing.empty;
}
} else if (objValidateState.patternMismatch) {
// 首先,优先使用自定义提示是一样的
strFinalText = optionPrompt.patternMismatch;
if (!strFinalText) {
// 然后确定是否是多值
const isMultiple = element.hasAttribute('multiple') && element.value.split(',').length > 1;
strFinalText = defaultPrompt.patternMismatch[isMultiple ? 'multiple' : 'pattern'];
// 然后
if (strType && strName) {
// 1. 试试关键字提示
strFinalText = strName + strFinalText;
}
}
} else if (objValidateState.badInput) {
// 优先使用自定义提示是一样的
strFinalText = optionPrompt.badInput;
if (!strFinalText) {
strFinalText = (strName || '') + defaultPrompt.badInput;
}
} else if (objValidateState.typeMismatch) {
// 优先使用自定义提示是一样的
strFinalText = optionPrompt.typeMismatch;
if (!strFinalText) {
strFinalText = defaultPrompt.typeMismatch;
if (strName) {
strFinalText = strFinalText.replace('输入框', strName);
}
}
} else if (objValidateState.rangeUnderflow || objValidateState.rangeOverflow) {
// 先看看有没有自定义的提示
strFinalText = optionPrompt.rangeUnderflow || optionPrompt.rangeOverflow;
if (!strFinalText && strType && strName) {
let strMin = element.getAttribute('min');
let strMax = element.getAttribute('max');
if (strType == 'month-range') {
strMin = strMin.slice(0, 7);
strMax = strMax.slice(0, 7);
}
const strTextBigger = '必须要大于或等于' + strMin;
const strTextSmall = '必须要小于或等于' + strMax;
// 同时范围超出,一定是日期范围
if (objValidateState.rangeUnderflow && objValidateState.rangeOverflow) {
strFinalText = '起始日期' + strTextBigger + ',结束日期' + strTextSmall;
} else if (objValidateState.rangeUnderflow) {
strFinalText = strName + strTextBigger;
// 如果是日期范围
if (strType.slice(-6) == '-range') {
strFinalText = '起始日期' + strTextBigger;
}
} else {
strFinalText = strName + strTextSmall;
// 如果是日期范围
if (strType.slice(-6) == '-range') {
strFinalText = '结束日期' + strTextSmall;
}
}
}
strFinalText = strFinalText || '值不在要求的范围内';
} else if (objValidateState.stepMismatch) {
// 优先使用自定义提示是一样的
strFinalText = optionPrompt.stepMismatch;
if (!strFinalText) {
const numMin = element.getAttribute('min') * 1;
const numMax = element.getAttribute('max') * 1;
const numStep = element.getAttribute('step') * 1 || 1;
if (strType == 'number' || strType == 'range') {
const numStart = numMin || element.getAttribute('value') * 1 || 0;
const numEnd = numMax || 999999;
strFinalText = '请输入有效的值。两个最接近的有效值是' + (function () {
// 百倍计算,避免浮点数计算不准确
const numValue = element.value.trim() * 100;
let numClosest = numStart * 100;
for (let start = numStart * 100; start < numEnd * 100; start += numStep * 100) {
if (start < numValue && (start + numStep * 100) > numValue) {
numClosest = start / 100;
break;
}
}
return [numClosest, (numClosest * 100 + numStep * 100) / 100].join('和');
})();
} else {
strFinalText = '请' + (element.hasAttribute('readonly') ? '选择' : '输入') + '有效的值。' + (strName || '') + '间隔是' + numStep;
}
}
strFinalText = strFinalText || defaultPrompt.stepMismatch;
} else if (objValidateState.tooLong || objValidateState.tooShort) {
// 输入内容太多或太少
const strAttrLang = element.getAttribute('lang');
let strTextCharLength = '';
if (/zh/i.test(strAttrLang)) {
strTextCharLength = '个汉字(2字母=1汉字)';
} else if (/en/i.test(strAttrLang)) {
strTextCharLength = '个字符(1汉字=2字符)';
}
if (objValidateState.tooLong) {
// 先看看有没有自定义的提示
strFinalText = optionPrompt.tooLong;
if (!strFinalText) {
const strMaxLength = element.maxlength || element.getAttribute('maxlength');
strFinalText = (strName || '') + '内容长度不能大于' + strMaxLength.replace(/\D/g, '') + strTextCharLength;
}
} else {
// 先看看有没有自定义的提示
strFinalText = optionPrompt.tooShort;
if (!strFinalText) {
const strMinLength = element.getAttribute('minlength');
strFinalText = '内容长度不能小于' + strMinLength + strTextCharLength;
}
}
} else if (objValidateState.customError) {
// 先看看有没有自定义的提示
strFinalText = optionPrompt.customError || defaultPrompt.customError;
}
if (typeof strFinalText == 'function') {
strFinalText = strFinalText.call(element, element);
}
return strFinalText;
},
/*
** 验证一般包括下面几个个方向:
** 1. 是否required同时值为空
** 2. 是否数据匹配错误(pattern, type)
** 3. 是否超出范围(min/max/step)
** 4. 内容超出限制(minlength, maxlength)
** 下面的这些方法就是上面3个方向判断
** 其中,参数element为DOM对象
** 返回数据规则如下:
** 如果没有把柄,返回false;
** 如果真的有误,则返回错误类别:{
type: ''
}
*/
/**
* 判断元素是否为空的验证
* @param Element element 验证的DOM元素
* @return Object 返回验证的状态对象
*/
getMissingState (element) {
const objValidateState = {
valueMissing: false
};
// 主要针对required必填或必选属性的验证
if (!element || element.disabled) {
return objValidateState;
}
// 类型
const strType = this.getType(element);
// 此时的值才是准确的值(浏览器可能会忽略文本框中的值,例如number类型输入框中的不合法数值)
let strValue = element.value;
if (element.hasAttribute('required')) {
// 根据控件类型进行验证
// 1. 单复选框比较特殊,先处理下
if (strType == 'radio') {
// 单选框,只需验证是否必选,同一name单选组只有要一个设置required即可
let eleRadios = [element];
const eleForm = element.closest('form') || element.parentElement.parentElement;
if (element.name && eleForm) {
eleRadios = eleForm.querySelectorAll('input[type="radio"][name="' + element.name + '"]');
}
// 如果单选框们没有一个选中,则认为无视了required属性
const isAtLeastOneRadioChecked = [].slice.call(eleRadios).some(function (eleRadio) {
return eleRadio.checked;
});
if (isAtLeastOneRadioChecked == false) {
// 没有一个单选框选中
objValidateState.valueMissing = true;
}
return objValidateState;
} else if (strType == 'checkbox') {
// 复选框是每一个都必须选中
if (element.checked == false) {
objValidateState.valueMissing = true;
}
return objValidateState;
} else if (strType != 'password') {
strValue = strValue.trim();
}
if (strValue == '') {
// 重新赋值
element.value = '';
// 返回验证结果
objValidateState.valueMissing = true;
}
}
return objValidateState;
},
/**
* 判断元素是否为空的验证
* @param {Element} element 验证的DOM元素
* @return {Boolean|Object} 返回验证通过布尔值或者出错信息对象
*/
isMissing (element) {
return this.getMissingState(element).valueMissing;
},
/**
* 返回元素值的合法状态
* @param {Element} element 验证的DOM元素
* @param {RegExp} regex 验证的正则
* @return {Boolean|Object} 返回验证通过布尔值或者出错信息对象
*/
getMismatchState (element, regex, params) {
const objValidateState = {
patternMismatch: false,
typeMismatch: false
};
// 禁用元素不参与验证
if (!element || element.disabled) {
return false;
}
// 原始值和处理值
const strInputValue = element.value;
let strDealValue = strInputValue;
// 类型
const strType = this.getType(element);
// 特殊控件不参与验证
if (/^radio|checkbox|select|file$/i.test(strType)) {
return objValidateState;
}
// 全角半角以及前后空格的处理
// 适用于type=url/email/tel/zipcode/number/date/time等
if (strType != 'password') {
strDealValue = strInputValue.trim();
}
if (/^text|textarea|password$/i.test(strType) == false) {
strDealValue = window.DBC2SBC(strDealValue);
}
if (strType == 'tel') {
strDealValue = this.getTel(strDealValue);
}
// 文本框值改变,重新赋值
// 即时验证的时候,不赋值,否则,无法输入空格
if (document.validate.focusable !== false && document.validate.focusable !== 0 && strDealValue != strInputValue) {
element.value = strDealValue;
}
// 如果没有值,则认为通过,没有错误
if (strDealValue == '') {
return objValidateState;
}
// 获取正则表达式,pattern属性获取优先,然后通过type类型匹配。
// 注意,不处理为空的情况
regex = regex ||
(function () {
return element.getAttribute('pattern');
})() ||
(function () {
// 文本框类型处理,可能有管道符——多类型重叠,如手机或邮箱
return strType && strType.split('|').map(function (strTypeSplit) {
const regMatch = document.validate.reg[strTypeSplit];
if (regMatch) {
return regMatch;
}
}).join('|');
})();
// 如果没有正则匹配的表达式,就没有验证的说法,认为出错为false
if (!regex) {
return objValidateState;
}
// multiple多数据的处理
const isMultiple = element.hasAttribute('multiple');
const regNew = new RegExp(regex, params || 'i');
// 正则验证标志量
let isAllPass = true;
// number,range等类型下multiple是无效的
if (isMultiple && /^number|range$/i.test(strType) == false) {
strDealValue.split(',').forEach(function (partValue) {
partValue = partValue.trim();
if (isAllPass && !regNew.test(partValue)) {
isAllPass = false;
}
});
} else {
isAllPass = regNew.test(strDealValue);
}
// 根据pattern判断类型
if (isAllPass == false) {
if (element.hasAttribute('pattern')) {
objValidateState.patternMismatch = true;
} else {
objValidateState.typeMismatch = true;
}
}
return objValidateState;
},
/**
* 判断元素值的合法性
* @param {Element} element 验证的DOM元素
* @param {RegExp} regex 验证的正则
* @return {Boolean|Object} 返回验证通过布尔值或者出错信息对象
*/
isMismatch (element, regex, params) {
const objValidateState = this.getMismatchState(element, regex, params);
return objValidateState.patternMismatch || objValidateState.typeMismatch;
},
/**
* 判断数值或日期范围超出
* @param {Element} element 验证的DOM元素
* @return {Boolean|Object} 返回验证状态对象
*/
getRangeState (element) {
const objValidateState = {
badInput: false,
rangeOverflow: false,
rangeUnderflow: false,
stepMismatch: false
};
// 是否数值或日期范围超出
if (!element || element.disabled) {
return objValidateState;
}
// 类型和值
const strType = this.getType(element);
let strValue = element.value.trim();
const attrValue = element.getAttribute('value');
if (/radio|checkbox|select|textarea/i.test(strType) || strValue == '') {
return objValidateState;
}
let strAttrMin = element.getAttribute('min');
let strAttrMax = element.getAttribute('max');
const strAttrStep = Number(element.getAttribute('step')) || 1;
if ((strType == 'number' || strType == 'range') && !/[-+]?[0-9]/.test(strValue)) {
objValidateState.badInput = true;
}
if (strType.slice(-6) != '-range') {
if (strValue == '0' || Number(strValue) == strValue) {
strValue = strValue * 1;
}
// 如果是日期选择
if (strType.includes('datetime')) {
strValue = strValue.replace('T', ' ');
if (strAttrMin) {
strAttrMin.replaceAll('/', '-').replace('T', ' ');
}
if (strAttrMax) {
strAttrMax.replaceAll('/', '-').replace('T', ' ');
}
}
if (strAttrMin && strValue < strAttrMin) {
objValidateState.rangeUnderflow = true;
}
if (strAttrMax && strValue > strAttrMax) {
objValidateState.rangeOverflow = true;
}
// number, range类型的范围
if ((strType == 'number' || strType == 'range') && strAttrStep) {
const startValue = (strAttrMin ? strAttrMin : attrValue) || 0;
if (!/^\d+$/.test(Math.abs(strValue - startValue) / strAttrStep) && element.matches(':invalid')) {
objValidateState.stepMismatch = true;
}
}
// hour, minute, time类型的范围
if ((strType == 'hour' || strType == 'minute' || strType == 'time') && strAttrMin && strAttrStep) {
const minuteValue = strValue.split(':')[1];
const minuteMin = strAttrMin.split(':')[1];
if (strType == 'hour' && (minuteValue != minuteMin || (strValue.split(':')[0] - strAttrMin.split(':')[0]) % strAttrStep != 0)) {
// 小时,则是分钟要一致,小时数正好step
objValidateState.stepMismatch = true;
} else if ((minuteValue - minuteMin) % strAttrStep !== 0) {
objValidateState.stepMismatch = true;
}
}
} else {
// 时间范围
const arrSplitValue = strValue.split(' ');
// 防止month-range时候,min/max设置的是完整时间,
// 如2017-07-07,而不是2017-07
if (strType == 'month-range') {
strAttrMin = strAttrMin && strAttrMin.slice(0, 7);
strAttrMax = strAttrMax && strAttrMax.slice(0, 7);
}
if (arrSplitValue.length == 3) {
if (strAttrMin && arrSplitValue[0] < strAttrMin) {
objValidateState.rangeUnderflow = true;
}
if (strAttrMax && arrSplitValue[2] > strAttrMax) {
objValidateState.rangeOverflow = true;
}
}
}
return objValidateState;
},
/**
* 判断数值或日期范围超出
* @param {Element} element 验证的DOM元素
* @return {Boolean|Object} 返回验证通过布尔值或者出错信息对象
*/
isOut (element) {
const objValidateState = this.getRangeState(element);
return objValidateState.badInput || objValidateState.rangeOverflow || objValidateState.rangeUnderflow || objValidateState.stepMismatch;
},
/**
* 内容是否超出长度限制的判断
* @param {Element} element DOM元素对象
* @return {Boolean|Object} 返回验证通过布尔值或者出错信息对象
*/
getLengthState (element) {
const objValidateState = {
tooLong: false,
tooShort: false
};
// 是否内容长度溢出的判断
if (!element || element.disabled || /^radio|checkbox|select$/i.test(element.type)) {
return objValidateState;
}
// 大小限制
const strAttrMinLength = element.getAttribute('minlength');
let strAttrMaxLength = element.maxlength || element.getAttribute('maxlength');
// 值
const strValue = element.value;
if (strValue == '') {
return objValidateState;
}
const numLength = this.getLength(element);
if (strAttrMinLength && numLength < strAttrMinLength) {
objValidateState.tooShort = true;
}
if (strAttrMaxLength) {
strAttrMaxLength = strAttrMaxLength.replace(/\D/g, '');
if (numLength > strAttrMaxLength) {
objValidateState.tooLong = true;
}
}
return objValidateState;
},
/**
* 是否范围超出
* @param {[type]} element [description]
* @return {Boolean} [description]
*/
isOverflow (element) {
const objValidateState = this.getLengthState(element);
return objValidateState.tooLong || objValidateState.tooShort;
},
/**
* 自定义验证状态
* @return {[type]} [description]
*/
getCustomState (element) {
const objValidateState = {
customError: false
};
const customValidate = element.customValidate;
if (customValidate && typeof customValidate.method == 'function') {
const dataResult = customValidate.method.call(customValidate.owner, element);
if (dataResult) {
objValidateState.customError = true;
// 记住自定义提示内容
if (typeof dataResult == 'object' && dataResult.customError) {
this.setCustomValidity(element, dataResult.customError);
} else {
this.setCustomValidity(element, dataResult);
}
}
}
return objValidateState;
},
/**
* 设置自定义提示内容
*/
setCustomValidity (element, content) {
if (!content) {
return;
}
if (!element.customValidate) {
element.customValidate = {
report: {}
};
}
if (!element.customValidate.report) {
element.customValidate.report = {};
}
element.customValidate.report.customError = content;
},
/**
* 判断元素验证通过与否
* @param Element element 输入框元素或者表单元素
* @return {[type]} [description]
*/
checkValidity (element) {
// 1. 元素不存在不验证
// 2. 禁用态表单元素不参与验证
if (!element || element.disabled) {
return true;
}
// 3. 提交、重置等元素不参与验证
const strType = element.getAttribute('type') || element.type;
const strTag = element.tagName.toLowerCase();
if (/^button|submit|reset|file|image$/.test(strType) == true || strTag == 'button') {
return true;
}
if (element.matches('input, select, textarea') == false && element.children) {
element = element.querySelectorAll('input, select, textarea');
}
if (element.nodeType == 1) {
return this.getValidity(element).valid;
}
if (element.length) {
return [].slice.call(element).every(function (ele) {
return this.checkValidity(ele);
}.bind(this));
}
return true;
},
/**
* 获取元素的验证状态
* @return {[type]} [description]
*/
getValidity (element) {
// 性能优化
// 如果值不变,且之前已经验证过,
// 且不是单复选框,则直接返回
// 避免同一元素短时间多次验证
if (element.lastValidateState && element.lastValue === element.value && /radio|checkbox/.test(element.type) == false) {
return element.lastValidateState;
}
// 浏览器原生的状态
let objValidateState = {
badInput: false,
customError: false,
patternMismatch: false,
rangeOverflow: false,
rangeUnderflow: false,
stepMismatch: false,
tooLong: false,
tooShort: false,
typeMismatch: false,
valid: true,
valueMissing: false
};
// 但是,不足以满足实际需求
objValidateState = Object.assign({},
objValidateState,
this.getMissingState(element),
this.getMismatchState(element),
this.getRangeState(element),
this.getLengthState(element),
this.getCustomState(element)
);
let isSomeInvalid = false;
for (let keyValidate in objValidateState) {
if (keyValidate != 'valid' && objValidateState[keyValidate] == true) {
isSomeInvalid = true;
}
}
objValidateState.valid = !isSomeInvalid;
element.lastValue = element.value;
element.lastValidateState = objValidateState;
setTimeout(function () {
delete element.lastValidateState;
}, 1);
return objValidateState;
},
/**
* 验证并进行错误提示
* @param {Element} element 需要显示提示信息的元素
* @param {Object} options 可选参数,主要包括label,以及自定义提示和验证方法
* @return {[type]} [description]
*/
reportValidity (element, content) {
// 验证并设置结果状态
content = content || this.getReportText(element);
// 提示
this.errorTip(element, content);
// 认为是成功,隐藏提示
if (content === '' && element.data && element.data.errorTip) {
element.data.errorTip.hide();
}
const isPass = !content;
this.styleError(element, isPass);
// 根据提示内容判断验证结果
return isPass;
},
/**
* 出错还是成功的样式处理
* @param {[type]} element [description]
* @param {[type]} valid [description]
* @return {[type]} [description]
*/
styleError (element, valid) {
// 是否有错
if (!element) {
return this;
}
if (typeof valid == 'undefined') {
valid = element.validity.valid;
} else {
// 触发验证通过与否的自定义事件
element.dispatchEvent(new CustomEvent(valid ? 'valid' : 'invalid'));
}
// 获取提示应该显示的元素
const eleTarget = this.getTarget(element);
if (!eleTarget) {
return valid;
}
const eleForm = element.form || element.closest('form') || (element.customValidate && element.customValidate.owner);
if (element.type == 'radio' && element.name && eleForm) {
// 单选框组是整体去除高亮
eleForm.querySelectorAll('input[type=radio][name=' + element.name + ']').forEach(function (eleRadio) {
const eleTargetRadio = this.getTarget(eleRadio);
if (valid) {
eleTargetRadio.removeAttribute('is-error');
eleTargetRadio.removeAttribute('aria-label');
} else {
eleTargetRadio.setAttribute('is-error', '');
}
}.bind(this));
} else if (valid) {
eleTarget.removeAttribute('is-error');
eleTarget.removeAttribute('aria-label');
} else {
eleTarget.setAttribute('is-error', '');
}
return valid;
},
/**
* 显示出错信息处理
* @param {Object} element 出错提示原始元素(这里可能会进行转移)
* @param {String} content 出错提示内容
* @return {Object} 返回当前上下文
*/
errorTip (element, content) {
const eleTarget = this.getTarget(element);
if (!eleTarget || !content) {
return this;
}
// 如果元素隐藏,也不提示
const objStyle = window.getComputedStyle(eleTarget);
if (objStyle.display == 'none' || objStyle.visibility == 'hidden') {
return this;
}
// 出错显示逻辑
const funShow = function () {
const eleControl = document.validate.errorTip.control;
const eleTipTarget = document.validate.errorTip.target;
eleTipTarget.errorTip(content, {
onShow (eleTrigger, eleTips) {
// 如果tips宽度比较小,居左定位
// 否则,使用默认的居中
const numOffsetX = 0.5 * (eleTips.clientWidth - eleTipTarget.clientWidth);
if (numOffsetX < 0) {
eleTips.style.marginLeft = 0.5 * (eleTips.clientWidth - eleTipTarget.clientWidth) + 'px';
} else {
eleTips.style.marginLeft = 0;
}
// 使提示出现时候不会闪
if (document.validate.focusable === false) {
eleTips.classList.add('none');
} else {
eleTips.classList.remove('none');
}
},
onHide (eleTrigger, eleTips) {
const eleForm = eleControl.form || eleControl.closest('form');
if (!eleForm || !eleForm.isImmediated) {
return;
}
// margin偏移还原,否则其他的出错提示会有定位问题
eleTips.style.marginLeft = '';
// 即时验证的时候,如果出错了,提示隐藏时候,红色仍然保留
document.validate.styleError(eleControl);
}
});
// 即时验证不需要focus与选中
// input输入的时候focusable是false
// focus时候focusable是0
if (document.validate.focusable === false || document.validate.focusable === 0) {
return;
}
// focus状态还原
document.validate.focusable = null;
if (!document.validate.getType(eleControl)) {
return;
}
// 如果是内容超出,选中超出的文字内容
if (content.indexOf('内容长度') != -1 && content.indexOf('大') != -1) {
const strValue = eleControl.value;
const numLength = strValue.length;
// var lang = element.getAttribute('lang');
// 中文和英文计算字符个数的比例
// var numRatioCh = 1;
// var numRatioEn = 1;
// if (/zh/i.test(lang)) {
// // 1个英文半个字符
// numRatioEn = 0.5;
// } else if (/en/i.test(lang)) {
// // 1个中文2个字符
// numRatioCh = 2;
// }
// 获得最大长度的索引位置
const strAttrMaxLength = eleControl.maxlength || eleControl.getAttribute('maxlength').replace(/\D/g, '');
if (numLength && strAttrMaxLength) {
document.validate.selectRange(element, document.validate.getLength(element, strAttrMaxLength), numLength);
}
} else if (eleControl.focus && eleControl.select) {
eleControl.focus();
eleControl.select();
}
};
document.validate.errorTip.control = element;
document.validate.errorTip.target = eleTarget;
// 1. 首先,确保el在屏幕内,且需要预留tips的显示的足够距离,这里使用50像素
const objRect = eleTarget.getBoundingClientRect();
let numScrollTop = -1;
if (objRect.top < 50) {
numScrollTop = window.pageYOffset - (50 - objRect.top);
} else if (objRect.bottom > window.innerHeight) {
numScrollTop = window.pageYOffset + (objRect.bottom - window.innerHeight);
}
if (numScrollTop >= 0) {
window.scrollTopTo(numScrollTop, funShow);
} else {
funShow();
}
return this;
},
/**
* 获得对应展示的元素
* @param {Object} el 元素
* @return {Object} 返回对应的展示元素(可能就是自身)
*/
getTarget (element) {
if (!element) {
return null;
}
let eleTarget = element;
// 根据原生控件元素,确定真实的自定义控件元素
// 这里的很多处理跟其他表单UI组件想耦合、关联
const strType = element.getAttribute('type') || element.type;
const strId = element.id;
const strTag = element.tagName.toLowerCase();
const objStyle = window.getComputedStyle(element);
// 1. 单复选框
if (strType == 'radio') {
if (objStyle.opacity != '1') {
eleTarget = element.parentElement.querySelector('label.ui-radio[for="' + strId + '"]');
}
} else if (strType == 'checkbox') {
if (objStyle.opacity != '1') {
eleTarget = element.parentElement.querySelector('label.ui-checkbox[for="' + strId + '"]');
}
// 下拉框
} else if (strType == 'select' || strTag == 'select') {
if (objStyle.opacity != '1') {
eleTarget = element.nextElementSibling;
}
// range范围选择框
} else if (strType == 'range') {
if (objStyle.display == 'none') {
eleTarget = element.nextElementSibling;
}
// 隐藏元素的目标提示元素的转移
} else if (strType == 'hidden' || objStyle.display == 'none' || objStyle.visibility == 'hidden') {
const eleTargetRel = document.getElementById(eleTarget.getAttribute('data-target')) || element.dataTarget;
if (eleTargetRel) {
eleTarget = document.validate.getTarget(eleTargetRel);
}
} else if (strType == 'textarea' || strTag == 'textarea') {
if (element.classList.contains('ui-textarea') == false && element.parentElement.querySelector('.ui-textarea')) {
eleTarget = element.parentElement.querySelector('.ui-textarea');
}
} else if (strTag == 'input') {
if (element.classList.contains('ui-input') == false && element.parentElement.querySelector('.ui-input')) {
eleTarget = element.parentElement.querySelector('.ui-input');
}
}
return eleTarget;
}
};
})();
// 重置原生的validity属性和checkValidity方法
[HTMLInputElement.prototype, HTMLSelectElement.prototype, HTMLTextAreaElement.prototype].forEach(function (prop) {
Object.defineProperty(prop, 'validity', {
get () {
return document.validate.getValidity(this);
},
configurable: true
});
Object.defineProperty(prop, 'validationMessage', {
get () {
return document.validate.getReportText(this);
},
configurable: true
});
Object.defineProperty(prop, 'checkValidity', {
value () {
return this.validity.valid;
},
configurable: true
});
Object.defineProperty(prop, 'reportValidity', {
value (content) {
return document.validate.reportValidity(this, content);
},
configurable: true
});
Object.defineProperty(prop, 'setCustomValidity', {
value (content) {
if (!content) {
return;
}
// 浏览器原生的状态
const arrValidateKey = [
'badInput',
'customError',
'patternMismatch',
'rangeOverflow',
'rangeUnderflow',
'stepMismatch',
'tooLong',
'tooShort',
'typeMismatch',
'valueMissing'
];
if (!this.customValidate) {
this.customValidate = {};
}
if (!this.customValidate.report) {
this.customValidate.report = {};
}
// 如果content是字符串,则重置所有提示
if (typeof content == 'string') {
arrValidateKey.forEach(function (key) {
this.customValidate.report[key] = content;
});
} else if (typeof content == 'object') {
Object.assign(this.customValidate.report, content);
}
},
configurable: true
});
});
// 表单元素的checkValidity()方法支持
Object.defineProperty(HTMLFormElement.prototype, 'checkValidity', {
value () {
return document.validate.checkValidity(this);
},
configurable: true
});
class Component {
/**
* 验证实例方法主体
* @param {Object} el 通常值验证的表单元素
* @param {Function} callback 可选,表示验证成功的回调,可以使用自定义 DOM 事件代替
* @param {Object} options 可选参数
*/
constructor (element, callback, options) {
if (typeof element == 'string') {
element = document.getElementById(element) || document.querySelector(element);
}
// el一般指的是form元素
if (!elemen