import { layui } from '../core/layui.js'; import { lay } from '../core/lay.js'; import { i18n } from '../core/i18n.js'; import $ from 'jquery'; import { laytpl } from '../core/laytpl.js'; import { util } from './util.js'; /** * dropdown * 下拉菜单组件 */ var device = layui.device(); var clickOrMousedown = device.mobile ? 'touchstart' : 'mousedown'; // 模块名 var MOD_NAME = 'dropdown'; var MOD_INDEX = 'layui_' + MOD_NAME + '_index'; // 模块索引名 var MOD_INDEX_OPENED = MOD_INDEX + '_opened'; var MOD_ID = 'lay-' + MOD_NAME + '-id'; var resizeObserver = lay.createSharedResizeObserver(MOD_NAME); // 外部接口 var dropdown = { config: { customName: { // 自定义 data 字段名 id: 'id', title: 'title', children: 'child' } }, // 设置全局项 set: function (options) { var that = this; that.config = $.extend({}, that.config, options); return that; }, // 事件 on: function (events, callback) { return layui.onevent.call(this, MOD_NAME, events, callback); } }; // 操作当前实例 var thisModule = function () { var that = this; var options = that.config; var id = options.id; return { config: options, // 重置实例 reload: function (options) { that.reload.call(that, options); }, reloadData: function (options) { dropdown.reloadData(id, options); }, close: function () { that.remove(); }, open: function () { that.render(); } }; }; // 字符常量 var STR_ELEM = 'layui-dropdown'; // var STR_HIDE = 'layui-hide'; var STR_DISABLED = 'layui-disabled'; // var STR_NONE = 'layui-none'; var STR_ITEM_UP = 'layui-menu-item-up'; var STR_ITEM_DOWN = 'layui-menu-item-down'; var STR_MENU_TITLE = 'layui-menu-body-title'; var STR_ITEM_GROUP = 'layui-menu-item-group'; var STR_ITEM_PARENT = 'layui-menu-item-parent'; var STR_ITEM_DIV = 'layui-menu-item-divider'; var STR_ITEM_CHECKED = 'layui-menu-item-checked'; var STR_ITEM_CHECKED2 = 'layui-menu-item-checked2'; var STR_MENU_PANEL = 'layui-menu-body-panel'; var STR_MENU_PANEL_L = 'layui-menu-body-panel-left'; var STR_ELEM_SHADE = 'layui-dropdown-shade'; var STR_GROUP_TITLE = '.' + STR_ITEM_GROUP + '>.' + STR_MENU_TITLE; // 构造器 var Class = function (options) { var that = this; that.index = dropdown.index = lay.autoIncrementer('dropdown'); that.config = $.extend({}, that.config, dropdown.config, options); that.stopClickOutsideEvent = $.noop; that.stopResizeEvent = $.noop; that.init(); }; // 默认配置 Class.prototype.config = { trigger: 'click', // 事件类型 content: '', // 自定义菜单内容 className: '', // 自定义样式类名 style: '', // 设置面板 style 属性 show: false, // 是否初始即显示菜单面板 isAllowSpread: true, // 是否允许菜单组展开收缩 isSpreadItem: true, // 是否初始展开子菜单 data: [], // 菜单数据结构 delay: [200, 300], // 延时显示或隐藏的毫秒数,若为 number 类型,则表示显示和隐藏的延迟时间相同,trigger 为 hover 时才生效 shade: 0, // 遮罩 accordion: false, // 手风琴效果,仅菜单组生效。基础菜单需要在容器上追加 'lay-accordion' 属性。 closeOnClick: true // 面板打开后,再次点击目标元素时是否关闭面板。行为取决于所使用的触发事件类型 }; // 重载实例 Class.prototype.reload = function (options, type) { var that = this; that.config = $.extend({}, that.config, options); that.init(true, type); }; // 初始化准备 Class.prototype.init = function (rerender, type) { var that = this; var options = that.config; // 若 elem 非唯一 var elem = $(options.elem); if (elem.length > 1) { layui.each(elem, function () { dropdown.render($.extend({}, options, { elem: this })); }); return that; } // 合并 lay-options 属性上的配置信息 $.extend(options, lay.options(elem[0])); // 若重复执行 render,则视为 reload 处理 if (!rerender && elem.attr(MOD_ID)) { var newThat = thisModule.getThis(elem.attr(MOD_ID)); if (!newThat) return; return newThat.reload(options, type); } options.elem = $(options.elem); options.target = $('body'); // 后续考虑开放 target 元素 // 初始化 id 属性 - 优先取 options > 元素 id > 自增索引 options.id = 'id' in options ? options.id : elem.attr('id') || that.index; thisModule.that[options.id] = that; // 记录当前实例对象 elem.attr(MOD_ID, options.id); // 目标元素已渲染过的标记 // 初始化自定义字段名 options.customName = $.extend({}, dropdown.config.customName, options.customName); // 若传入 hover,则解析为 mouseenter if (options.trigger === 'hover') { options.trigger = 'mouseenter'; } // 初始即显示或者面板弹出之后执行了刷新数据 if (options.show || type === 'reloadData' && that.mainElem && options.target.find(that.mainElem.get(0)).length) that.render(type); // 若面板已经打开,则无需再绑定目标元素事件,避免 render 重复执行 if (!elem.data(MOD_INDEX_OPENED)) { that.events(); // 事件 } }; // 渲染 Class.prototype.render = function (type) { var that = this; var options = that.config; var customName = options.customName; // 默认菜单内容 var getDefaultView = function () { var elemUl = $(''); if (options.data.length > 0) { eachItemView(elemUl, options.data); } else { elemUl.html('
  • ' + i18n.$t('dropdown.noData') + '
  • '); } return elemUl; }; // 遍历菜单项 var eachItemView = function (views, data) { // var views = []; layui.each(data, function (index, item) { // 是否存在子级 var isChild = item[customName.children] && item[customName.children].length > 0; var isSpreadItem = 'isSpreadItem' in item ? item.isSpreadItem : options.isSpreadItem; var title = function (title) { var templet = item.templet || options.templet; if (templet) { title = typeof templet === 'function' ? templet(item) : laytpl(templet).render(item); } return title; }(util.escape(item[customName.title])); // 初始类型 var type = function () { if (isChild) { item.type = item.type || 'parent'; } if (item.type) { return { group: 'group', parent: 'parent', '-': '-' }[item.type] || 'parent'; } return ''; }(); if (type !== '-' && !item[customName.title] && !item[customName.id] && !isChild) return; //列表元素 var viewLi = $(['', //标题区 function () { //是否超文本 var viewText = 'href' in item ? '' + title + '' : title; //是否存在子级 if (isChild) { return '
    ' + viewText + function () { if (type === 'parent') { return ''; } else if (type === 'group' && options.isAllowSpread) { return ''; } else { return ''; } }() + '
    '; } return '
    ' + viewText + '
    '; }(), ''].join('')); viewLi.data('item', item); //子级区 if (isChild) { var elemPanel = $('
    '); var elemUl = $(''); if (type === 'parent') { elemPanel.append(eachItemView(elemUl, item[customName.children])); viewLi.append(elemPanel); } else { viewLi.append(eachItemView(elemUl, item[customName.children])); } } views.append(viewLi); }); return views; }; // 主模板 var TPL_MAIN = ['
    ', '
    '].join(''); // 重载或插入面板内容 var mainElem; var content = options.content || getDefaultView(); var mainElemExisted = thisModule.findMainElem(options.id); if (type === 'reloadData' && mainElemExisted.length) { // 是否仅重载数据 mainElem = that.mainElem = mainElemExisted; mainElemExisted.html(content); } else { // 常规渲染 mainElem = that.mainElem = $(TPL_MAIN); mainElem.append(content); // 初始化某些属性 mainElem.addClass(options.className); mainElem.attr('style', options.style); // 辞旧迎新 that.remove(options.id); options.target.append(mainElem); options.elem.data(MOD_INDEX_OPENED, true); // 面板已打开的标记 // 遮罩 var shade = options.shade ? '
    ' : ''; var shadeElem = $(shade); // 处理移动端点击穿透问题 if (clickOrMousedown === 'touchstart') { shadeElem.on(clickOrMousedown, function (e) { e.preventDefault(); }); } mainElem.before(shadeElem); // 如果是鼠标移入事件,则鼠标移出时自动关闭 if (options.trigger === 'mouseenter') { mainElem.on('mouseenter', function () { clearTimeout(that.timer); }).on('mouseleave', function () { that.delayRemove(); }); } } that.position(); // 定位坐标 // 阻止全局事件 mainElem.find('.layui-menu').on(clickOrMousedown, function (e) { layui.stope(e); }); // 触发菜单列表事件 mainElem.find('.layui-menu li').on('click', function (e) { var othis = $(this); var data = othis.data('item') || {}; var isChild = data[customName.children] && data[customName.children].length > 0; var isClickAllScope = options.clickScope === 'all'; // 是否所有父子菜单均触发点击事件 if (data.disabled) return; // 菜单项禁用状态 // 普通菜单项点击后的回调及关闭面板 if ((!isChild || isClickAllScope) && data.type !== '-') { var ret = typeof options.click === 'function' ? options.click(data, othis, e) : null; ret === false || isChild || that.remove(); layui.stope(e); } }); // 触发菜单组展开收缩 mainElem.find(STR_GROUP_TITLE).on('click', function () { var othis = $(this); var elemGroup = othis.parent(); var data = elemGroup.data('item') || {}; if (data.type === 'group' && options.isAllowSpread) { thisModule.spread(elemGroup, options.accordion); } }); that.onClickOutside(); that.autoUpdatePosition(); // 组件打开完毕的事件 typeof options.ready === 'function' && options.ready(mainElem, options.elem); }; // 位置定位 Class.prototype.position = function () { var that = this; var options = that.config; lay.position(options.elem[0], that.mainElem[0], { position: options.position, e: that.e, clickType: options.trigger === 'contextmenu' ? 'right' : null, align: options.align || null }); }; // 移除面板 Class.prototype.remove = function (id) { id = id || this.config.id; var that = thisModule.getThis(id); // 根据 id 查找对应的实例 if (!that) return; var options = that.config; var mainElem = thisModule.findMainElem(id); that.stopClickOutsideEvent(); that.stopResizeEvent(); // 若存在已打开的面板元素,则移除 if (mainElem[0]) { mainElem.prev('.' + STR_ELEM_SHADE).remove(); // 先移除遮罩 mainElem.remove(); options.elem.removeData(MOD_INDEX_OPENED); typeof options.close === 'function' && options.close(options.elem); } }; Class.prototype.normalizedDelay = function () { var that = this; var options = that.config; var delay = [].concat(options.delay); return { show: delay[0], hide: delay[1] !== undefined ? delay[1] : delay[0] }; }; // 延迟移除面板 Class.prototype.delayRemove = function () { var that = this; // var options = that.config; clearTimeout(that.timer); that.timer = setTimeout(function () { that.remove(); }, that.normalizedDelay().hide); }; // 事件 Class.prototype.events = function () { var that = this; var options = that.config; // 是否鼠标移入时触发 var isMouseEnter = options.trigger === 'mouseenter'; var trigger = options.trigger + '.lay_dropdown_render'; // 始终先解除上一个触发元素的事件(如重载时改变 elem 的情况) if (that.thisEventElem) that.thisEventElem.off(trigger); that.thisEventElem = options.elem; // 触发元素事件 options.elem.off(trigger).on(trigger, function (e) { clearTimeout(that.timer); that.e = e; // 主面板是否已打开 var opened = options.elem.data(MOD_INDEX_OPENED); // 若为鼠标移入事件,则延迟触发 if (isMouseEnter) { if (!opened) { that.timer = setTimeout(function () { that.render(); }, that.normalizedDelay().show); } } else { // 若为 click 事件,则根据主面板状态,自动切换打开与关闭 if (options.closeOnClick && opened && options.trigger === 'click') { that.remove(); } else { that.render(); } } e.preventDefault(); }); // 如果是鼠标移入事件 if (isMouseEnter) { // 执行鼠标移出事件 options.elem.on('mouseleave', function () { that.delayRemove(); }); } }; /** * 点击面板外部时的事件 */ Class.prototype.onClickOutside = function () { var that = this; var options = that.config; var isCtxMenu = options.trigger === 'contextmenu'; var isTopElem = lay.isTopElem(options.elem[0]); that.stopClickOutsideEvent(); var stop = lay.onClickOutside(that.mainElem[0], function (e) { // 点击面板外部时的事件 if (typeof options.onClickOutside === 'function') { var shouldClose = options.onClickOutside(e); if (shouldClose === false) return; } that.remove(); }, { ignore: isCtxMenu || isTopElem ? null : [options.elem[0]], event: clickOrMousedown, capture: false, detectIframe: true }); that.stopClickOutsideEvent = function () { stop(); that.stopClickOutsideEvent = $.noop; }; }; /** * 窗口大小变化时自动更新位置 */ Class.prototype.autoUpdatePosition = function () { var that = this; var options = that.config; that.stopResizeEvent(); var windowResizeHandler = function () { if (that.mainElem && (!that.mainElem[0] || !that.mainElem.is(':visible'))) return; if (options.trigger === 'contextmenu') { that.remove(); } else { that.position(); } }; $(window).on('resize.lay_dropdown_resize', windowResizeHandler); var shouldObserveResize = resizeObserver && options.trigger !== 'contextmenu'; var triggerEl = options.elem[0]; var contentEl = that.mainElem[0]; if (shouldObserveResize) { resizeObserver.observe(triggerEl, $.proxy(that.position, that)); resizeObserver.observe(contentEl, $.proxy(that.position, that)); } that.stopResizeEvent = function () { $(window).off('resize.lay_dropdown_resize', windowResizeHandler); if (shouldObserveResize) { resizeObserver.unobserve(triggerEl); resizeObserver.unobserve(contentEl); } that.stopResizeEvent = $.noop; }; }; // 记录所有实例 thisModule.that = {}; // 记录所有实例对象 // 获取当前实例对象 thisModule.getThis = function (id) { if (id === undefined) { throw new Error('ID argument required'); } return thisModule.that[id]; }; // 根据 id 从页面查找组件主面板元素 thisModule.findMainElem = function (id) { return $('.' + STR_ELEM + '[' + MOD_ID + '="' + id + '"]'); }; // 设置菜单组展开和收缩状态 thisModule.spread = function (othis, isAccordion) { var contentElem = othis.children('ul'); var needSpread = othis.hasClass(STR_ITEM_UP); var ANIM_MS = 200; // 动画执行完成后的操作 var complete = function () { $(this).css({ display: '' }); // 剔除临时 style,以适配外部样式的状态重置; }; // 动画是否正在执行 if (contentElem.is(':animated')) return; // 展开 if (needSpread) { othis.removeClass(STR_ITEM_UP).addClass(STR_ITEM_DOWN); contentElem.hide().stop().slideDown(ANIM_MS, complete); } else { // 收缩 contentElem.stop().slideUp(ANIM_MS, complete); othis.removeClass(STR_ITEM_DOWN).addClass(STR_ITEM_UP); } // 手风琴 if (needSpread && isAccordion) { var groupSibs = othis.siblings('.' + STR_ITEM_DOWN); groupSibs.children('ul').stop().slideUp(ANIM_MS, complete); groupSibs.removeClass(STR_ITEM_DOWN).addClass(STR_ITEM_UP); } }; // 全局事件 (function () { var _WIN = $(window); var _DOC = $(document); // 基础菜单的静态元素事件 var ELEM_LI = '.layui-menu:not(.layui-dropdown-menu) li'; _DOC.on('click', ELEM_LI, function () { var othis = $(this); var parent = othis.parents('.layui-menu').eq(0); var isChild = othis.hasClass(STR_ITEM_GROUP) || othis.hasClass(STR_ITEM_PARENT); var filter = parent.attr('lay-filter') || parent.attr('id'); var options = lay.options(this); // 非触发元素 if (othis.hasClass(STR_ITEM_DIV)) return; // 非菜单组 if (!isChild) { // 选中 parent.find('.' + STR_ITEM_CHECKED).removeClass(STR_ITEM_CHECKED); // 清除选中样式 parent.find('.' + STR_ITEM_CHECKED2).removeClass(STR_ITEM_CHECKED2); // 清除父级菜单选中样式 othis.addClass(STR_ITEM_CHECKED); //添加选中样式 othis.parents('.' + STR_ITEM_PARENT).addClass(STR_ITEM_CHECKED2); // 添加父级菜单选中样式 options.title = options.title || $.trim(othis.children('.' + STR_MENU_TITLE).text()); // 触发事件 layui.event.call(this, MOD_NAME, 'click(' + filter + ')', options); } }); // 基础菜单的展开收缩事件 _DOC.on('click', ELEM_LI + STR_GROUP_TITLE, function () { var othis = $(this); var elemGroup = othis.parents('.' + STR_ITEM_GROUP + ':eq(0)'); var options = lay.options(elemGroup[0]); var isAccordion = typeof othis.parents('.layui-menu').eq(0).attr('lay-accordion') === 'string'; if ('isAllowSpread' in options ? options.isAllowSpread : true) { thisModule.spread(elemGroup, isAccordion); } }); // 判断子级菜单是否超出屏幕 var ELEM_LI_PAR = '.layui-menu .' + STR_ITEM_PARENT; _DOC.on('mouseenter', ELEM_LI_PAR, function () { var othis = $(this); var elemPanel = othis.find('.' + STR_MENU_PANEL); if (!elemPanel[0]) return; var rect = elemPanel[0].getBoundingClientRect(); // 是否超出右侧屏幕 if (rect.right > _WIN.width()) { elemPanel.addClass(STR_MENU_PANEL_L); // 不允许超出左侧屏幕 rect = elemPanel[0].getBoundingClientRect(); if (rect.left < 0) { elemPanel.removeClass(STR_MENU_PANEL_L); } } // 是否超出底部屏幕 if (rect.bottom > _WIN.height()) { elemPanel.eq(0).css('margin-top', -(rect.bottom - _WIN.height() + 5)); } }).on('mouseleave', ELEM_LI_PAR, function () { var othis = $(this); var elemPanel = othis.children('.' + STR_MENU_PANEL); elemPanel.removeClass(STR_MENU_PANEL_L); elemPanel.css('margin-top', 0); }); })(); // 关闭面板 dropdown.close = function (id) { var that = thisModule.getThis(id); if (!that) return this; that.remove(); return thisModule.call(that); }; // 打开面板 dropdown.open = function (id) { var that = thisModule.getThis(id); if (!that) return this; that.render(); return thisModule.call(that); }; // 重载实例 dropdown.reload = function (id, options, type) { var that = thisModule.getThis(id); if (!that) return this; that.reload(options, type); return thisModule.call(that); }; // 仅重载数据 dropdown.reloadData = function () { var args = $.extend([], arguments); args[2] = 'reloadData'; // 重载时,与数据相关的参数 var dataParams = new RegExp('^(' + ['data', 'templet', 'content'].join('|') + ')$'); // 过滤与数据无关的参数 layui.each(args[1], function (key) { if (!dataParams.test(key)) { delete args[1][key]; } }); return dropdown.reload.apply(null, args); }; // 核心入口 dropdown.render = function (options) { var inst = new Class(options); return thisModule.call(inst); }; export { dropdown };