diff --git a/docs/form/index.md b/docs/form/index.md index 108145c3..cac7d0d1 100644 --- a/docs/form/index.md +++ b/docs/form/index.md @@ -118,6 +118,8 @@ form 还可以借助*栅格*实现更灵活的响应式布局。 | lay-skin | [#详见](checkbox.html#default) | 设置 UI 风格。 ``,`` 元素 **私有属性** | | lay-search | 默认不区分大小写;设置`cs`区分大小写 | 给 `select` 组件开启搜索功能。`` 元素 **私有属性** | | lay-creatable 2.9.7+ | 无需值 | 是否允许创建新条目,需要配合 `lay-search` 使用。`` 元素 **私有属性** | +| lay-append-to 2.9.12+ 实验性 | `body` | 是否将 select 面板追加到 body 元素中。`` 元素 **私有属性** | +| lay-append-position 2.9.12+ 实验性 | `absolute` 绝对定位 (默认)`fixed` 固定定位 | 用于设置 select 面板开启 `lay-append-to` 属性后的定位方式。`` 元素 **私有属性** | | lay-submit | 无需值 | 设置元素(一般为`` 标签)触发 `submit` 提交事件 | | lay-ignore | 无需值 | 设置表单元素忽略渲染,即让元素保留系统原始 UI 风格 | diff --git a/docs/form/select.md b/docs/form/select.md index e71a4e13..c8150c6b 100644 --- a/docs/form/select.md +++ b/docs/form/select.md @@ -179,6 +179,62 @@ toc: true +独立选择框 2.9.12+ + +在 `` 元素中设置 `lay-append-to="body"` 属性,可将 select 面板插入到 `` 根节点下,以便让选择框从 form 结构中剥离,成为更灵活的独立选择框。借助该特性,可完美解决 select 在 table, layer 等组件中使用的若干问题。 + +### 1. 在 table 中使用 select + +参考 table 示例: [实现多样化编辑](/docs/2/table/#demo-editmodes) + +### 2. 在 layer 中使用 select + + + +弹出 layer+select + + + + + + 选择框事件 diff --git a/docs/table/examples/editModes.md b/docs/table/examples/editModes.md index bab78337..43132973 100644 --- a/docs/table/examples/editModes.md +++ b/docs/table/examples/editModes.md @@ -1,28 +1,19 @@ -{{! - - - - + @@ -45,6 +36,7 @@ layui.use(function(){ var dropdown = layui.dropdown; var laydate = layui.laydate; var colorpicker = layui.colorpicker; + var util = layui.util; // 渲染 table.render({ @@ -59,49 +51,40 @@ layui.use(function(){ ].join(''), cols: [[ // 表头 {field: 'id', title: 'ID', width:80, align: 'center', fixed: 'left'}, - {field: 'city', title: '原生 select', width:135, unresize: true, templet: '#TPL-select-primary'}, - //{field: 'city', title: 'layui select', width:150, templet: '#TPL-select-city'}, - {field: 'sex', title: 'dropdown', width:115, unresize: true, align: 'center', templet: '#TPL-dropdpwn-demo'}, - {field: 'date', title: 'laydate', width:150, templet: '#TPL-laydate-demo'}, - {field: 'color', title: 'color', width:80, unresize: true, align: 'center', templet: '#TPL-colorpicker-demo'}, + {field: 'city', title: 'select', minWidth: 150, templet: '#TPL-select-demo'}, + {field: 'sex', title: 'dropdown', width: 130, unresize: true, align: 'center', templet: '#TPL-dropdpwn-demo'}, + {field: 'date', title: 'laydate', minWidth: 150, templet: '#TPL-laydate-demo'}, + {field: 'color', title: 'color', width: 80, unresize: true, align: 'center', templet: '#TPL-colorpicker-demo'}, {field: 'sign', title: '文本', edit: 'textarea'} ]], done: function(res, curr, count){ var options = this; - // 获取当前行数据 + // 获取当前行数据 - 自定义方法 table.getRowData = function(tableId, elem){ var index = $(elem).closest('tr').data('index'); return table.cache[tableId][index] || {}; }; - - // 原生 select 事件 - var tableViewElem = this.elem.next(); - // 解除 tbSelect 命名空间下的所有 change 事件处理程序 - tableViewElem.off("change.tbSelect"); - // 将 '.select-demo-primary' 元素的 change 事件委托给 tableViewElem, 事件命名空间为 tbSelect - tableViewElem.on("change.tbSelect", ".select-demo-primary", function () { - var value = this.value; // 获取选中项 value - var data = table.getRowData(options.id, this); // 获取当前行数据(如 id 等字段,以作为数据修改的索引) - - // 更新数据中对应的字段 - data.city = value; - - // 显示 - 仅用于演示 - layer.msg('选中值: '+ value +'当前行数据:'+ JSON.stringify(data)); - }); + // 展示数据 - 仅用于演示 + var showData = function(data) { + return layer.msg('当前行最新数据:'+ util.escape(JSON.stringify(data)), { + offset: '16px', + anim: 'slideDown' + }); + }; // layui form select 事件 form.on('select(select-demo)', function(obj){ - console.log(obj); // 获取选中项数据 - + var value = obj.value; // 获取选中项 value // 获取当前行数据(如 id 等字段,以作为数据修改的索引) var data = table.getRowData(options.id, obj.elem); // 更新数据中对应的字段 data.city = value; - console.log(data); + + // 显示当前行最新数据 - 仅用于示例展示 + showData(data); }); // dropdown 方式的下拉选择 @@ -127,8 +110,8 @@ layui.use(function(){ // 更新数据中对应的字段 data.sex = obj.title; - // 显示 - 仅用于演示 - layer.msg('选中值: '+ obj.title +'当前行数据:'+ JSON.stringify(data)); + // 显示当前行最新数据 - 仅用于示例展示 + showData(data); } }); @@ -141,8 +124,8 @@ layui.use(function(){ // 更新数据中对应的字段 data.date = value; - // 显示 - 仅用于演示 - layer.msg('选中值: '+ value +'当前行数据:'+ JSON.stringify(data)); + // 显示当前行最新数据 - 仅用于示例展示 + showData(data); } }); @@ -155,8 +138,8 @@ layui.use(function(){ // 更新数据中对应的字段 data.color = value; - // 显示 - 仅用于演示 - layer.msg('选中值: '+ value +'当前行数据:'+ JSON.stringify(data)); + // 显示当前行最新数据 - 仅用于示例展示 + showData(data); } }); @@ -174,8 +157,8 @@ layui.use(function(){ // 编辑后续操作,如提交更新请求,以完成真实的数据更新 // … - // 显示 - 仅用于演示 - layer.msg('编辑值: '+ value +'当前行数据:'+ JSON.stringify(data)); + // 显示当前行最新数据 - 仅用于示例展示 + showData(data); }); // 更多编辑方式…… diff --git a/src/css/layui.css b/src/css/layui.css index c537aa78..45cff7d3 100644 --- a/src/css/layui.css +++ b/src/css/layui.css @@ -860,6 +860,8 @@ hr.layui-border-black{border-width: 0 0 1px;} :root .layui-form-selected .layui-edge{margin-top: -9px\0/IE9;} .layui-form-selectup dl{top: auto; bottom: 42px;} .layui-select-none{margin: 5px 0; text-align: center; color: #999;} +.layui-select-panel-wrap {position: absolute; z-index: 99999999;} +.layui-select-panel-wrap dl{position: relative; display: block; top:0;} .layui-select-disabled .layui-disabled{border-color: #eee !important;} .layui-select-disabled .layui-edge{border-top-color: #d2d2d2} diff --git a/src/modules/form.js b/src/modules/form.js index 2e3074e9..4f40897e 100644 --- a/src/modules/form.js +++ b/src/modules/form.js @@ -382,38 +382,29 @@ layui.define(['lay', 'layer', 'util'], function(exports){ var TITLE = 'layui-select-title'; var NONE = 'layui-select-none'; var CREATE_OPTION = 'layui-select-create-option'; - var initValue = ''; - var thatInput; + var PANEL_WRAP = 'layui-select-panel-wrap' + var PANEL_ELEM_DATA = 'layui-select-panel-elem-data'; var selects = elem || elemForm.find('select'); - // 隐藏 select - var hide = function(e, clear){ - if(!$(e.target).parent().hasClass(TITLE) || clear){ - var elem = $('.' + CLASS); - elem.removeClass(CLASS+'ed ' + CLASS+'up'); - if(elem.hasClass('layui-select-creatable')){ - elem.children('dl').children('.' + CREATE_OPTION).remove(); - } - thatInput && initValue && thatInput.val(initValue); - } - thatInput = null; - }; - // 各种事件 - var events = function(reElem, disabled, isSearch, isCreatable){ + var events = function(reElem, titleElem, disabled, isSearch, isCreatable, isAppendTo){ var select = $(this); - var title = reElem.find('.' + TITLE); + 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 nearElem; // select 组件当前选中的附近元素,用于辅助快捷键功能 + var initValue = ''; + var removeClickOutsideEvent; if(disabled) return; // 搜索项 var laySearch = 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 事件 @@ -422,15 +413,29 @@ layui.define(['lay', 'layer', 'util'], function(exports){ // 展开下拉 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 - reElem.addClass(CLASS+'ed'); + title.parent().addClass(CLASS+'ed'); dds.removeClass(HIDE); dts.removeClass(HIDE); - nearElem = null; // 初始选中样式 dds.removeClass(THIS); @@ -444,22 +449,35 @@ layui.define(['lay', 'layer', 'util'], function(exports){ followScroll(); if(needPlaceholderPatch){ - dl.off('mousedown.select.ieph').on('mousedown.select.ieph', function(){ + 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){ - reElem.removeClass(CLASS+'ed ' + CLASS+'up'); + title.parent().removeClass(CLASS+'ed ' + CLASS+'up'); input.blur(); - nearElem = null; 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){ @@ -503,10 +521,9 @@ layui.define(['lay', 'layer', 'util'], function(exports){ // 点击标题区域 title.on('click', function(e){ - reElem.hasClass(CLASS+'ed') ? ( + title.parent().hasClass(CLASS+'ed') ? ( hideDown() ) : ( - hide(e, true), showDown() ); dl.find('.'+NONE).remove(); @@ -668,7 +685,6 @@ layui.define(['lay', 'layer', 'util'], function(exports){ input.on('input propertychange', layui.debounce(search, 50)).on('blur', function(e){ var selectedIndex = select[0].selectedIndex; - thatInput = input; // 当前的 select 中的 input 元素 initValue = $(select[0].options[selectedIndex]).text(); // 重新获得初始选中值 // 如果是第一项,且文本值等于 placeholder,则清空初始值 @@ -721,40 +737,55 @@ layui.define(['lay', 'layer', 'util'], function(exports){ reElem.find('dl>dt').on('click', function(e){ return false; }); - - $(document).off('click', hide).on('click', hide); // 点击其它元素关闭 select + + if(isAppendTo){ + titleElem.on('_lay-select-destroy', function(){ + reElem.remove(); + }) + } } + + // 仅 appendTo 使用,移除触发元素时,自动移除面板元素 + $.event.special['_lay-select-destroy'] = { + remove: function( handleObj ) { + handleObj.handler(); + } + }; // 初始渲染 select 组件选项 selects.each(function(index, select){ - var othis = $(this) - ,hasRender = othis.next('.'+CLASS) - ,disabled = this.disabled - ,value = select.value - ,selected = $(select.options[select.selectedIndex]) // 获取当前选中项 - ,optionsFirst = select.options[0]; + var othis = $(this); + var hasRender = othis.next('.'+CLASS); + var disabled = this.disabled; + var value = select.value; + var selected = $(select.options[select.selectedIndex]); // 获取当前选中项 + var optionsFirst = select.options[0]; if(typeof othis.attr('lay-ignore') === 'string') return othis.show(); var isSearch = typeof othis.attr('lay-search') === 'string' - ,isCreatable = typeof othis.attr('lay-creatable') === 'string' && isSearch - ,placeholder = optionsFirst ? ( - optionsFirst.value ? TIPS : (optionsFirst.innerHTML || TIPS) - ) : TIPS; + var isCreatable = typeof othis.attr('lay-creatable') === 'string' && isSearch + var isAppendTo = typeof othis.attr('lay-append-to') === 'string' + var placeholder = optionsFirst + ? (optionsFirst.value ? TIPS : (optionsFirst.innerHTML || TIPS)) + : TIPS; // 替代元素 var reElem = $(['' - ,'' + ,(disabled ? ' layui-select-disabled' : '') + '">'].join('')); + + var triggerElem = $([ + '' ,('') // 禁用状态 - ,'' - ,'' + ,'' + ,''].join('')); + + var contentElem = $(['' ,function(options){ var arr = []; layui.each(options, function(index, item){ @@ -771,11 +802,27 @@ layui.define(['lay', 'layer', 'util'], function(exports){ arr.length === 0 && arr.push('没有选项'); return arr.join(''); }(othis.find('*')) +'' - ,''].join('')); + ].join('')); - hasRender[0] && hasRender.remove(); // 如果已经渲染,则Rerender - othis.after(reElem); - events.call(this, reElem, disabled, isSearch, isCreatable); + // 如果已经渲染,则Rerender + if(hasRender[0]){ + if(isAppendTo){ + var panelWrapElem = hasRender.data(PANEL_ELEM_DATA); + panelWrapElem && panelWrapElem.remove(); + } + hasRender.remove(); + } + if(isAppendTo){ + reElem.append(triggerElem); + othis.after(reElem); + var contentWrapElem = $('').append(contentElem); + reElem.data(PANEL_ELEM_DATA, contentWrapElem); // 将面板元素对象记录在触发元素 data 中,重新渲染时需要清理旧面板元素 + events.call(this, contentWrapElem, triggerElem, disabled, isSearch, isCreatable, isAppendTo); + }else{ + reElem.append(triggerElem).append(contentElem); + othis.after(reElem); + events.call(this, reElem, triggerElem, disabled, isSearch, isCreatable, isAppendTo); + } }); } diff --git a/src/modules/lay.js b/src/modules/lay.js index 41071b23..d9464f50 100644 --- a/src/modules/lay.js +++ b/src/modules/lay.js @@ -270,6 +270,7 @@ * @param {string | number} [opts.margin=5] - 边距 * @param {Event} [opts.e] - 事件对象,仅右键生效 * @param {boolean} [opts.SYSTEM_RELOAD] - 是否重载,用于出现滚动条时重新计算位置 + * @param {[offsetX:number, offsetY:number]} [opts.offset] - 相对于触发元素的额外偏移量[x,y] * @example * ```js * dropdown @@ -370,10 +371,12 @@ // 定位类型 var position = opts.position; if(position) elem.style.position = position; + var offsetX = opts.offset ? opts.offset[0] : 0; + var offsetY = opts.offset ? opts.offset[1] : 0; // 设置坐标 - elem.style.left = left + (position === 'fixed' ? 0 : scrollArea(1)) + 'px'; - elem.style.top = top + (position === 'fixed' ? 0 : scrollArea()) + 'px'; + elem.style.left = left + (position === 'fixed' ? 0 : scrollArea(1)) + offsetX + 'px'; + elem.style.top = top + (position === 'fixed' ? 0 : scrollArea()) + offsetY + 'px'; // 防止页面无滚动条时,又因为弹出面板而出现滚动条导致的坐标计算偏差 if(!lay.hasScrollbar()){ @@ -676,6 +679,99 @@ } }(); + /** + * 监听指定元素外部的点击 + * @param {HTMLElement} target - 被监听的元素 + * @param {(e: Event) => void} handler - 事件触发时执行的函数 + * @param {object} [options] - 选项 + * @param {string} [options.event="pointerdown"] - 监听的事件类型 + * @param {HTMLElement | Window} [options.scope=document] - 监听范围 + * @param {Array} [options.ignore] - 忽略监听的元素或选择器字符串 + * @param {boolean} [options.capture=true] - 对内部事件侦听器使用捕获阶段 + * @returns {() => void} - 返回一个停止事件监听的函数 + */ + lay.onClickOutside = function(target, handler, options){ + options = options || {}; + var eventType = options.event || ('onpointerdown' in window ? 'pointerdown' : 'mousedown'); + var scopeTarget = options.scope || document; + var ignore = options.ignore || []; + var useCapture = 'capture' in options ? options.capture : true; + + var listener = function(event){ + var el = target; + var eventTarget = event.target || event.srcElement; + var eventPath = getEventPath(event); + + if (!el || el === eventTarget || eventPath.indexOf(el) !== -1){ + return; + } + if(shouldIgnore(event, eventPath)){ + return; + } + + handler(event); + }; + + function shouldIgnore(event, eventPath){ + var eventTarget = event.target || event.srcElement; + for(var i = 0; i < ignore.length; i++){ + var target = ignore[i]; + if(typeof target === 'string'){ + var targetElements = document.querySelectorAll(target); + for(var j = 0; j < targetElements.length; j++){ + var targetEl = targetElements[i]; + if(targetEl === eventTarget || eventPath.indexOf(targetEl) !== -1){ + return true; + } + } + }else{ + if(target && (target === eventTarget || eventPath.indexOf(target) !== -1)){ + return true; + } + } + } + } + + function getEventPath(event){ + var path = (event.composedPath && event.composedPath()) || event.path; + var eventTarget = event.target || event.srcElement; + + if (path !== null && path !== undefined){ + return path; + } + + function getParents(node, memo){ + memo = memo || []; + var parentNode = node.parentNode; + + return parentNode + ? getParents(parentNode, memo.concat([parentNode])) + : memo; + } + + return [eventTarget].concat(getParents(eventTarget)); + } + + function bindEventListener(elem, eventName, handler, opts){ + elem.addEventListener + ? elem.addEventListener(eventName, handler, opts) + : elem.attachEvent('on' + eventName, handler); + + return function(){ + elem.removeEventListener + ? elem.removeEventListener(eventName, handler, opts) + : elem.detachEvent('on' + eventName, handler); + } + } + + return bindEventListener( + scopeTarget, + eventType, + listener, + lay.passiveSupported ? { passive: true, capture: useCapture } : useCapture + ); + }; + /* * lay 元素操作
+ +弹出 layer+select + + + + +