/**
* form 表单组件
*/
layui.define(['lay', 'layer', 'util'], function (exports) {
"use strict";
var $ = layui.$;
var layer = layui.layer;
var util = layui.util;
var hint = layui.hint();
var device = layui.device();
var MOD_NAME = 'form';
var ELEM = '.layui-form';
var THIS = 'layui-this';
var SHOW = 'layui-show';
var HIDE = 'layui-hide';
var DISABLED = 'layui-disabled';
var OUT_OF_RANGE = 'layui-input-number-out-of-range';
var Form = function () {
this.config = {
// 内置的验证规则
verify: {
required: function (value) {
if (!/[\S]+/.test(value)) {
return '必填项不能为空';
}
},
phone: function (value) {
var EXP = /^1\d{10}$/;
if (value && !EXP.test(value)) {
return '手机号格式不正确';
}
},
email: function (value) {
var EXP = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
if (value && !EXP.test(value)) {
return '邮箱格式不正确';
}
},
url: function (value) {
var EXP = /^(#|(http(s?)):\/\/|\/\/)[^\s]+\.[^\s]+$/;
if (value && !EXP.test(value)) {
return '链接格式不正确';
}
},
number: function (value) {
if (value && isNaN(value)) {
return '只能填写数字';
}
},
date: function (value) {
var EXP = /^(\d{4})[-\/](\d{1}|0\d{1}|1[0-2])([-\/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/;
if (value && !EXP.test(value)) {
return '日期格式不正确';
}
},
identity: function (value) {
var EXP = /(^\d{15}$)|(^\d{17}(x|X|\d)$)/;
if (value && !EXP.test(value)) {
return '身份证号格式不正确';
}
},
integer: function (value) {
var EXP = /^\d*$/;
if (value && !EXP.test(value)) {
return '只能填写整数';
}
}
},
autocomplete: null // 全局 autocomplete 状态。 null 表示不干预
};
};
// 全局设置
Form.prototype.set = function (options) {
var that = this;
$.extend(true, that.config, options);
return that;
};
// 验证规则设定
Form.prototype.verify = function (settings) {
var that = this;
$.extend(true, that.config.verify, settings);
return that;
};
// 获取指定表单对象
Form.prototype.getFormElem = function (filter) {
return $(ELEM + function () {
return filter ? ('[lay-filter="' + filter + '"]') : '';
}());
};
// 表单事件
Form.prototype.on = function (events, callback) {
return layui.onevent.call(this, MOD_NAME, events, callback);
};
// 赋值/取值
Form.prototype.val = function (filter, object) {
var that = this
, formElem = that.getFormElem(filter);
// 遍历
formElem.each(function (index, item) {
var itemForm = $(this);
// 赋值
layui.each(object, function (key, value) {
var itemElem = itemForm.find('[name="' + key + '"]')
, type;
// 如果对应的表单不存在,则不执行
if (!itemElem[0]) return;
type = itemElem[0].type;
// 如果为复选框
if (type === 'checkbox') {
itemElem[0].checked = value;
} else if (type === 'radio') { // 如果为单选框
itemElem.each(function () {
this.checked = this.value == value + '';
});
} else { // 其它类型的表单
itemElem.val(value);
}
});
});
form.render(null, filter);
// 返回值
return that.getValue(filter);
};
// 取值
Form.prototype.getValue = function (filter, itemForm) {
itemForm = itemForm || this.getFormElem(filter);
var nameIndex = {} // 数组 name 索引
, field = {}
, fieldElem = itemForm.find('input,select,textarea') // 获取所有表单域
layui.each(fieldElem, function (_, item) {
var othis = $(this)
, init_name; // 初始 name
item.name = (item.name || '').replace(/^\s*|\s*&/, '');
if (!item.name) return;
// 用于支持数组 name
if (/^.*\[\]$/.test(item.name)) {
var key = item.name.match(/^(.*)\[\]$/g)[0];
nameIndex[key] = nameIndex[key] | 0;
init_name = item.name.replace(/^(.*)\[\]$/, '$1[' + (nameIndex[key]++) + ']');
}
if (/^(checkbox|radio)$/.test(item.type) && !item.checked) return; // 复选框和单选框未选中,不记录字段
field[init_name || item.name] = item.value;
});
return field;
};
// 表单控件渲染
Form.prototype.render = function (type, filter) {
var that = this;
var options = that.config;
var elemForm = $(ELEM + function () {
return filter ? ('[lay-filter="' + filter + '"]') : '';
}());
var items = {
// 输入框
input: function (elem) {
var inputs = elem || elemForm.find('input,textarea');
// 初始化全局的 autocomplete
options.autocomplete && inputs.attr('autocomplete', options.autocomplete);
var handleInputNumber = function (elem, eventType) {
var that = this;
var rawValue = elem.val();
var value = Number(rawValue);
var step = Number(elem.attr('step')) || 1; // 加减的数字间隔
var min = Number(elem.attr('min'));
var max = Number(elem.attr('max'));
var precision = Number(elem.attr('lay-precision'));
var noAction = eventType !== 'click' && rawValue === ''; // 初始渲染和失焦时空值不作处理
var isInit = eventType === 'init';
if (isNaN(value)) return; // 若非数字,则不作处理
if (eventType === 'click') {
var isDecrement = !!$(that).index() // 0: icon-up, 1: icon-down
value = isDecrement ? value - step : value + step;
}
// 获取小数点后位数
var decimals = function (step) {
var decimals = (step.toString().match(/\.(\d+$)/) || [])[1] || '';
return decimals.length;
};
precision = precision >= 0 ? precision : Math.max(decimals(step), decimals(rawValue));
// 赋值
if (!noAction) {
// 初始渲染时只处理数字精度
if (!isInit) {
if (value <= min) value = min;
if (value >= max) value = max;
}
// 若 `lay-precision` 为 0, 则表示只保留整数
if (precision === 0) {
value = parseInt(value);
} else if (precision > 0) { // 小数位精度
value = value.toFixed(precision);
}
elem.val(value);
}
// 超出范围的样式
var outOfRange = value < min || value > max;
elem[outOfRange && !noAction ? 'addClass' : 'removeClass'](OUT_OF_RANGE);
if (isInit) return;
// 更新按钮状态
var controlBtn = {
increment: elem.next().find('.layui-icon-up'),
decrement: elem.next().find('.layui-icon-down')
}
controlBtn.increment[(value >= max && !noAction) ? 'addClass' : 'removeClass'](DISABLED)
controlBtn.decrement[(value <= min && !noAction) ? 'addClass' : 'removeClass'](DISABLED)
}
// 初始化输入框动态点缀
elemForm.find('input[lay-affix],textarea[lay-affix]').each(function () {
var othis = $(this);
var affix = othis.attr('lay-affix');
var CLASS_WRAP = 'layui-input-wrap';
var CLASS_SUFFIX = 'layui-input-suffix';
var CLASS_AFFIX = 'layui-input-affix';
var disabled = othis.is('[disabled]') || othis.is('[readonly]');
// 根据是否空值来显示或隐藏元素
var showAffix = function (elem, value) {
elem = $(elem);
if (!elem[0]) return;
elem[$.trim(value) ? 'removeClass' : 'addClass'](HIDE);
};
// 渲染动态点缀内容
var renderAffix = function (opts) {
opts = $.extend({}, (affixOptions[affix] || {
value: affix
}), opts, lay.options(othis[0]));
var elemAffix = $('
');
var value = layui.isArray(opts.value) ? opts.value : [opts.value];
var elemIcon = $(function () {
var arr = [];
layui.each(value, function (i, item) {
arr.push('
');
});
return arr.join('');
}());
elemAffix.append(elemIcon); // 插入图标元素
// 追加 className
if (opts.split) elemAffix.addClass('layui-input-split');
if (opts.className) elemAffix.addClass(opts.className);
// 移除旧的元素
var hasElemAffix = othis.next('.' + CLASS_AFFIX);
if (hasElemAffix[0]) hasElemAffix.remove();
// 是否在规定的容器中
if (!othis.parent().hasClass(CLASS_WRAP)) {
othis.wrap('
');
}
// 是否已经存在后缀元素
var hasElemSuffix = othis.next('.' + CLASS_SUFFIX);
if (hasElemSuffix[0]) {
hasElemAffix = hasElemSuffix.find('.' + CLASS_AFFIX);
if (hasElemAffix[0]) hasElemAffix.remove();
hasElemSuffix.prepend(elemAffix);
othis.css('padding-right', function () {
var paddingRight = othis.closest('.layui-input-group')[0]
? 0
: hasElemSuffix.outerWidth();
return paddingRight + elemAffix.outerWidth()
});
} else {
elemAffix.addClass(CLASS_SUFFIX);
othis.after(elemAffix);
}
opts.show === 'auto' && showAffix(elemAffix, othis.val());
typeof opts.init === 'function' && opts.init.call(this, othis, opts);
// 输入事件
othis.on('input propertychange', function () {
var value = this.value;
opts.show === 'auto' && showAffix(elemAffix, value);
});
// 失去焦点事件
othis.on('blur', function () {
typeof opts.blur === 'function' && opts.blur.call(this, othis, opts);
});
// 点击动态后缀事件
elemIcon.on('click', function () {
var inputFilter = othis.attr('lay-filter');
if ($(this).hasClass(DISABLED)) return;
typeof opts.click === 'function' && opts.click.call(this, othis, opts);
// 对外事件
layui.event.call(this, MOD_NAME, 'input-affix(' + inputFilter + ')', {
elem: othis[0],
affix: affix,
options: opts
});
});
};
// 动态点缀配置项
var affixOptions = {
eye: { // 密码显隐
value: 'eye-invisible',
click: function (elem, opts) { // 事件
var SHOW_NAME = 'LAY_FORM_INPUT_AFFIX_SHOW';
var isShow = elem.data(SHOW_NAME);
elem.attr('type', isShow ? 'password' : 'text').data(SHOW_NAME, !isShow);
renderAffix({
value: isShow ? 'eye-invisible' : 'eye'
});
}
},
clear: { // 内容清除
value: 'clear',
click: function (elem) {
elem.val('').focus();
showAffix($(this).parent(), null);
},
show: 'auto', // 根据输入框值是否存在来显示或隐藏点缀图标
disabled: disabled // 跟随输入框禁用状态
},
number: { // 数字输入框
value: ['up', 'down'],
split: true,
className: 'layui-input-number',
disabled: othis.is('[disabled]'), // 跟随输入框禁用状态
init: function (elem) {
handleInputNumber.call(this, elem, 'init')
},
click: function (elem) {
handleInputNumber.call(this, elem, 'click')
},
blur: function (elem) {
handleInputNumber.call(this, elem, 'blur')
},
}
};
renderAffix();
});
}
// 下拉选择框
, select: function (elem) {
var TIPS = '请选择';
var CLASS = 'layui-form-select';
var TITLE = 'layui-select-title';
var NONE = 'layui-select-none';
var CREATE_OPTION = 'layui-select-create-option';
var PANEL_WRAP = 'layui-select-panel-wrap'
var PANEL_ELEM_DATA = 'layui-select-panel-elem-data';
var selects = elem || elemForm.find('select');
// 各种事件
var events = function (reElem, titleElem, disabled, isSearch, isCreatable, isAppendTo) {
var select = $(this);
var title = titleElem;
var input = title.find('input');
var dl = reElem.find('dl');
var dds = dl.children('dd');
var dts = dl.children('dt'); // select 分组dt元素
var index = this.selectedIndex; // 当前选中的索引
var initValue = '';
var removeClickOutsideEvent;
if (disabled) return;
/**
* 搜索项
* @typedef searchOption
* @prop {boolean} [caseSensitive=false] 是否区分大小写
* @prop {boolean} [fuzzy=false] 是否开启模糊匹配,开启后将会忽略模式出现在字符串中的位置。
*/
/** @type {searchOption} */
var laySearch = select.attr('lay-search') === 'cs' ? { caseSensitive: true } : lay.options(select, { attr: 'lay-search' });
// 目前只支持 body
var appendTarget = select.attr('lay-append-to') || 'body';
var appendPosition = select.attr('lay-append-position');
// #1449
// IE10 和 11 中,带有占位符的 input 元素获得/失去焦点时,会触发 input 事件
// 当鼠标按下时,根据 input 元素上的 __ieph 标识忽略 input 事件
var needPlaceholderPatch = !!(lay.ie && (lay.ie === '10' || lay.ie === '11') && input.attr('placeholder'));
// 展开下拉
var showDown = function () {
if (isAppendTo) {
// 如果追加面板元素后出现滚动条,触发元素宽度可能会有变化,所以先追加面板元素
reElem.appendTo(appendTarget).css({ width: title.width() + 'px' });
var updatePosition = function () {
lay.position(title[0], reElem[0], {
position: appendPosition,
allowBottomOut: true,
offset: [0, 5]
});
}
updatePosition();
$(window).on('resize.lay_select_resize', updatePosition);
}
var top = reElem.offset().top + reElem.outerHeight() + 5 - $win.scrollTop();
var dlHeight = dl.outerHeight();
var dds = dl.children('dd');
index = select[0].selectedIndex; // 获取最新的 selectedIndex
title.parent().addClass(CLASS + 'ed');
dds.removeClass(HIDE);
dts.removeClass(HIDE);
// 初始选中样式
dds.removeClass(THIS);
index >= 0 && dds.eq(index).addClass(THIS);
// 上下定位识别
if (top + dlHeight > $win.height() && top >= dlHeight) {
reElem.addClass(CLASS + 'up');
}
followScroll();
if (needPlaceholderPatch) {
dl.off('mousedown.lay_select_ieph').on('mousedown.lay_select_ieph', function () {
input[0].__ieph = true;
setTimeout(function () {
input[0].__ieph = false;
}, 60)
});
}
removeClickOutsideEvent = lay.onClickOutside(
isAppendTo ? reElem[0] : dl[0],
function () {
hideDown();
initValue && input.val(initValue);
},
{ ignore: title }
);
};
// 隐藏下拉
var hideDown = function (choose) {
title.parent().removeClass(CLASS + 'ed ' + CLASS + 'up');
input.blur();
isCreatable && dl.children('.' + CREATE_OPTION).remove();
removeClickOutsideEvent && removeClickOutsideEvent();
if (isAppendTo) {
reElem.detach();
$(window).off('resize.lay_select_resize');
}
if (choose) return;
notOption(input.val(), function (none) {
var selectedIndex = select[0].selectedIndex;
// 未查询到相关值
if (none) {
initValue = $(select[0].options[selectedIndex]).html(); // 重新获得初始选中值
// 如果是第一项,且文本值等于 placeholder,则清空初始值
if (selectedIndex === 0 && initValue === input.attr('placeholder')) {
initValue = '';
}
// 如果有选中值,则将输入框纠正为该值。否则清空输入框
input.val(initValue || '');
}
});
};
// 定位下拉滚动条
var followScroll = function () {
var thisDd = dl.children('dd.' + THIS);
if (!thisDd[0]) return;
var posTop = thisDd.position().top;
var dlHeight = dl.height();
var ddHeight = thisDd.height();
// 若选中元素在滚动条不可见底部
if (posTop > dlHeight) {
dl.scrollTop(posTop + dl.scrollTop() - dlHeight + ddHeight - 5);
}
// 若选择元素在滚动条不可见顶部
if (posTop < 0) {
dl.scrollTop(posTop + dl.scrollTop() - 5);
}
};
// 点击标题区域
title.on('click', function (e) {
title.parent().hasClass(CLASS + 'ed') ? (
hideDown()
) : (
showDown()
);
dl.find('.' + NONE).remove();
});
// 点击箭头获取焦点
title.find('.layui-edge').on('click', function () {
input.focus();
});
// select 中 input 键盘事件
input.on('keyup', function (e) { // 键盘松开
var keyCode = e.keyCode;
// Tab键展开
if (keyCode === 9) {
showDown();
}
}).on('keydown', function (e) { // 键盘按下
var keyCode = e.keyCode;
// Tab键隐藏
if (keyCode === 9) {
hideDown();
}
// 标注 dd 的选中状态
var setThisDd = function (prevNext) {
e.preventDefault();
var allDisplayedElem = dl.children('dd:not(.' + HIDE + ',.' + DISABLED + ')');
if (!allDisplayedElem.length) return;
var firstIndex = 0;
var lastIndex = allDisplayedElem.length - 1;
var selectedIndex = -1;
layui.each(allDisplayedElem, function (index, el) {
if ($(el).hasClass(THIS)) {
selectedIndex = index;
return true;
}
})
var nextIndex = prevNext === 'prev'
? (selectedIndex - 1 < firstIndex ? lastIndex : selectedIndex - 1)
: (selectedIndex + 1 > lastIndex ? firstIndex : selectedIndex + 1)
var selectedElem = allDisplayedElem.eq(nextIndex);
selectedElem.addClass(THIS).siblings().removeClass(THIS); // 标注样式
followScroll(); // 定位滚动条
};
if (keyCode === 38) setThisDd('prev'); // Up 键
if (keyCode === 40) setThisDd('next'); // Down 键
// Enter 键
if (keyCode === 13) {
e.preventDefault();
dl.children('dd.' + THIS).trigger('click');
}
}).on('paste', function () {
showDown();
});
// 检测值是否不属于 select 项
var notOption = function (value, callback, origin) {
var num = 0;
var dds = dl.children('dd');
var hasEquals = false;
var rawValue = value;
var fuzzyMatchRE;
if (!laySearch.caseSensitive) {
value = value.toLowerCase();
}
if (laySearch.fuzzy) {
fuzzyMatchRE = fuzzyMatchRegExp(value, laySearch.caseSensitive);
}
layui.each(dds, function () {
var othis = $(this);
var text = othis.text();
var isCreateOption = isCreatable && othis.hasClass(CREATE_OPTION);
// 需要区分大小写
if (isCreatable && !isCreateOption && text === rawValue) {
hasEquals = true;
}
// 是否区分大小写
if (!laySearch.caseSensitive) {
text = text.toLowerCase();
}
// 匹配
var not = laySearch.fuzzy ? !fuzzyMatchRE.test(text) : text.indexOf(value) === -1;
if (value === '' || (origin === 'blur') ? value !== text : not) num++;
origin === 'keyup' && othis[(isCreatable ? (not && !isCreateOption) : not) ? 'addClass' : 'removeClass'](HIDE);
});
// 处理 select 分组元素
origin === 'keyup' && layui.each(dts, function () {
var othis = $(this);
var thisDds = othis.nextUntil('dt').filter('dd'); // 当前分组下的dd元素
if (isCreatable) thisDds = thisDds.not('.' + CREATE_OPTION);
var allHide = thisDds.length == thisDds.filter('.' + HIDE).length; // 当前分组下所有dd元素都隐藏了
othis[allHide ? 'addClass' : 'removeClass'](HIDE);
});
var none = num === dds.length;
return callback(none, hasEquals), none;
};
// 搜索匹配
var search = function (e) {
var value = this.value, keyCode = e.keyCode;
if (keyCode === 9 || keyCode === 13
|| keyCode === 37 || keyCode === 38
|| keyCode === 39 || keyCode === 40
) {
return false;
}
if (needPlaceholderPatch && e.target.__ieph) {
e.target.__ieph = false;
return false;
}
notOption(value, function (none, hasEquals) {
if (isCreatable) {
if (hasEquals) {
dl.children('.' + CREATE_OPTION).remove();
} else {
var createOptionElem = dl.children('.' + CREATE_OPTION);
if (createOptionElem[0]) {
createOptionElem.attr('lay-value', value).html(util.escape(value));
} else {
// 临时显示在顶部
var ddElem = $('
').addClass(CREATE_OPTION).attr('lay-value', value).html(util.escape(value));
var firstOptionELem = dl.children().eq(0);
var hasTips = firstOptionELem.hasClass('layui-select-tips');
firstOptionELem[hasTips ? 'after' : 'before'](ddElem);
}
}
} else {
if (none) {
dl.find('.' + NONE)[0] || dl.append('无匹配项
');
} else {
dl.find('.' + NONE).remove();
}
}
}, 'keyup');
// 当搜索值清空时
if (value === '') {
// 取消选中项
select.val('');
dl.find('.' + THIS).removeClass(THIS);
(select[0].options[0] || {}).value || dl.children('dd:eq(0)').addClass(THIS);
dl.find('.' + NONE).remove();
isCreatable && dl.children('.' + CREATE_OPTION).remove();
}
followScroll(); // 定位滚动条
};
if (isSearch) {
input.on('input propertychange', layui.debounce(search, 50)).on('blur', function (e) {
var selectedIndex = select[0].selectedIndex;
initValue = $(select[0].options[selectedIndex]).text(); // 重新获得初始选中值
// 如果是第一项,且文本值等于 placeholder,则清空初始值
if (selectedIndex === 0 && initValue === input.attr('placeholder')) {
initValue = '';
}
setTimeout(function () {
notOption(input.val(), function (none) {
initValue || input.val(''); // none && !initValue
}, 'blur');
}, 200);
});
}
// 选择
dl.on('click', 'dd', function () {
var othis = $(this), value = othis.attr('lay-value');
var filter = select.attr('lay-filter'); // 获取过滤器
if (othis.hasClass(DISABLED)) return false;
if (othis.hasClass('layui-select-tips')) {
input.val('');
} else {
input.val(othis.text());
othis.addClass(THIS);
}
// 将新增的 option 元素添加到末尾
if (isCreatable && othis.hasClass(CREATE_OPTION)) {
dl.append(othis.removeClass(CREATE_OPTION));
var optionElem = $('