/** * 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 = $('