From 1670cbab8f9508a6095318fbc4984685ba2da4e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B4=A4=E5=BF=83?=
<3277200+sentsim@users.noreply.github.com>
Date: Mon, 25 Nov 2024 11:33:32 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20dropdown=20?=
=?UTF-8?q?=E6=89=93=E5=BC=80=E4=B8=8E=E5=85=B3=E9=97=AD=E9=80=BB=E8=BE=91?=
=?UTF-8?q?=20(#2349)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor: 重构 dropdown 打开与关闭逻辑
* chore: 优化变量
* refactor: 保留采用 elem 的 jQuery Data 进行面板打开状态的判断
* fix: 优化延时移除面板时的实例不一致的问题
---
docs/dropdown/examples/base.md | 3 +-
docs/dropdown/examples/contextmenu.md | 12 +-
docs/table/examples/demo.md | 10 +-
examples/dropdown.html | 35 ++++-
src/modules/dropdown.js | 195 +++++++++++++-------------
5 files changed, 147 insertions(+), 108 deletions(-)
diff --git a/docs/dropdown/examples/base.md b/docs/dropdown/examples/base.md
index ea69a818..a2ee51e6 100644
--- a/docs/dropdown/examples/base.md
+++ b/docs/dropdown/examples/base.md
@@ -46,6 +46,7 @@ layui.use(function(){
// 绑定输入框
dropdown.render({
elem: '#ID-dropdown-demo-base-input',
+ closeOnClick: false, // 不开启“打开与关闭的自动切换”,即点击输入框时始终为打开状态
data: [{
title: 'menu item 1',
id: 101
@@ -118,4 +119,4 @@ layui.use(function(){
});
});
-
\ No newline at end of file
+
diff --git a/docs/dropdown/examples/contextmenu.md b/docs/dropdown/examples/contextmenu.md
index 556afb71..a4f77f84 100644
--- a/docs/dropdown/examples/contextmenu.md
+++ b/docs/dropdown/examples/contextmenu.md
@@ -65,20 +65,22 @@ layui.use(function(){
// 其他操作
util.event('lay-on', {
- // 全局右键菜单
+ // 改变触发右键菜单的目标元素
contextmenu: function(othis){
var ID = 'ID-dropdown-demo-contextmenu';
- if(!othis.data('open')){
+ if (!othis.data('open')) {
dropdown.reload(ID, {
- elem: document // 将事件直接绑定到 document
+ elem: document // 设置全局元素右键
});
+
layer.msg('已开启全局右键菜单,请尝试在页面任意处单击右键。')
othis.html('取消全局右键菜单');
othis.data('open', true);
} else {
dropdown.reload(ID, {
- elem: '#'+ ID // 重新绑定到指定元素上
+ elem: '#'+ ID // 设置局部元素右键
});
+
layer.msg('已取消全局右键菜单,恢复默认右键菜单')
othis.html('开启全局右键菜单');
othis.data('open', false);
@@ -86,4 +88,4 @@ layui.use(function(){
}
});
});
-
\ No newline at end of file
+
diff --git a/docs/table/examples/demo.md b/docs/table/examples/demo.md
index 0501e8d1..ed18c312 100644
--- a/docs/table/examples/demo.md
+++ b/docs/table/examples/demo.md
@@ -316,11 +316,19 @@ layui.use(['table', 'dropdown'], function(){
});
}
},
+ id: 'dropdown-table-tool',
align: 'right', // 右对齐弹出
style: 'box-shadow: 1px 1px 10px rgb(0 0 0 / 12%);' // 设置额外样式
- })
+ });
}
});
+
+ // table 滚动时移除内部弹出的元素
+ var tableInst = table.getOptions('test');
+ tableInst.elem.next().find('.layui-table-main').on('scroll', function() {
+ dropdown.close('dropdown-table-tool');
+ });
+
// 触发表格复选框选择
table.on('checkbox(test)', function(obj){
diff --git a/examples/dropdown.html b/examples/dropdown.html
index 4ed28b38..32b4a2d1 100644
--- a/examples/dropdown.html
+++ b/examples/dropdown.html
@@ -35,9 +35,13 @@
-
+
+
+
@@ -258,9 +262,9 @@ layui.use('dropdown', function () {
}
});
- //右键
+ // 右键
dropdown.render({
- elem: document, //'#demo20' //也可绑定到 document,从而重置整个右键
+ elem: '#ID-dropdown-demo-contextmenu', // 也可绑定到 document,从而重置整个右键
trigger: 'contextmenu', //contextmenu
isAllowSpread: false,
//,style: 'width: 200px'
@@ -316,6 +320,28 @@ layui.use('dropdown', function () {
}
});
+ util.on({
+ // 改变触发右键菜单的目标元素
+ contextmenu: function(othis){
+ var ID = 'ID-dropdown-demo-contextmenu';
+ if (!othis.data('open')) {
+ dropdown.reload(ID, {
+ elem: document // 设置全局元素右键
+ });
+
+ othis.html('取消全局右键菜单');
+ othis.data('open', true);
+ } else {
+ dropdown.reload(ID, {
+ elem: '#'+ ID // 设置局部元素右键
+ });
+
+ othis.html('开启全局右键菜单');
+ othis.data('open', false);
+ }
+ }
+ });
+
dropdown.render({
elem: '#testopen',
id: 'testopen',
@@ -330,8 +356,7 @@ layui.use('dropdown', function () {
close: function(){
dropdown.close('testopen')
}
- },
- {trigger: 'mouseenter'});
+ });
return;
diff --git a/src/modules/dropdown.js b/src/modules/dropdown.js
index b059ab00..f525813b 100644
--- a/src/modules/dropdown.js
+++ b/src/modules/dropdown.js
@@ -16,6 +16,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
// 模块名
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';
// 外部接口
@@ -48,8 +49,6 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
var options = that.config;
var id = options.id;
- thisModule.that[id] = that; // 记录当前实例对象
-
return {
config: options,
// 重置实例
@@ -87,6 +86,8 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
var STR_GROUP_TITLE = '.'+ STR_ITEM_GROUP + '>.'+ STR_MENU_TITLE;
+ var bodyElem = $('body');
+
// 构造器
var Class = function(options){
var that = this;
@@ -108,7 +109,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
delay: [200, 300], // 延时显示或隐藏的毫秒数,若为 number 类型,则表示显示和隐藏的延迟时间相同,trigger 为 hover 时才生效
shade: 0, // 遮罩
accordion: false, // 手风琴效果,仅菜单组生效。基础菜单需要在容器上追加 'lay-accordion' 属性。
- closeOnClick: false // 面板打开后,再次点击目标元素时是否关闭面板。行为取决于所使用的触发事件类型
+ closeOnClick: true // 面板打开后,再次点击目标元素时是否关闭面板。行为取决于所使用的触发事件类型
};
// 重载实例
@@ -138,10 +139,9 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
$.extend(options, lay.options(elem[0]));
// 若重复执行 render,则视为 reload 处理
- if(!rerender && elem[0] && elem.attr(MOD_ID)){
+ if(!rerender && elem.attr(MOD_ID)){
var newThat = thisModule.getThis(elem.attr(MOD_ID));
if(!newThat) return;
-
return newThat.reload(options, type);
}
@@ -152,21 +152,29 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
elem.attr('id') || that.index
);
- elem.attr(MOD_ID, options.id);
+ thisModule.that[options.id] = that; // 记录当前实例对象
+ elem.attr(MOD_ID, options.id); // 目标元素已渲染过的标记
// 初始化自定义字段名
options.customName = $.extend({}, dropdown.config.customName, options.customName);
- if(options.show || (type === 'reloadData' && that.elemView && $('body').find(that.elemView.get(0)).length)) that.render(rerender, type); //初始即显示或者面板弹出之后执行了刷新数据
- that.events(); // 事件
+ // 若传入 hover,则解析为 mouseenter
+ if (options.trigger === 'hover') {
+ options.trigger = 'mouseenter';
+ }
+
+ // 初始即显示或者面板弹出之后执行了刷新数据
+ if(options.show || (type === 'reloadData' && that.mainElem && bodyElem.find(that.mainElem.get(0)).length)) that.render(type);
+
+ // 事件
+ that.events();
};
// 渲染
- Class.prototype.render = function(rerender, type){
+ Class.prototype.render = function(type) {
var that = this;
var options = that.config;
var customName = options.customName;
- var elemBody = $('body');
// 默认菜单内容
var getDefaultView = function(){
@@ -276,42 +284,37 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
};
// 主模板
- var TPL_MAIN = [''
- ,'
'].join('');
-
- // 如果是右键事件,则每次触发事件时,将允许重新渲染
- if(options.trigger === 'contextmenu' || lay.isTopElem(options.elem[0])) rerender = true;
-
- // 判断是否已经打开了下拉菜单面板
- if(!rerender && options.elem.data(MOD_INDEX +'_opened')) return;
+ var TPL_MAIN = [
+ '',
+ '
'
+ ].join('');
- // 记录模板对象
- that.elemView = $('.' + STR_ELEM + '[' + MOD_ID + '="' + options.id + '"]');
- if (type === 'reloadData' && that.elemView.length) {
- that.elemView.html(options.content || getDefaultView());
- } else {
- that.elemView = $(TPL_MAIN);
- that.elemView.append(options.content || getDefaultView());
+ // 重载或插入面板内容
+ var content = options.content || getDefaultView();
+ var mainElemExisted = thisModule.findMainElem(options.id);
+ if (type === 'reloadData' && mainElemExisted.length) { // 是否仅重载数据
+ var mainElem = that.mainElem = mainElemExisted;
+ mainElemExisted.html(content);
+ } else { // 常规渲染
+ var mainElem = that.mainElem = $(TPL_MAIN);
+ mainElem.append(content);
// 初始化某些属性
- if(options.className) that.elemView.addClass(options.className);
- if(options.style) that.elemView.attr('style', options.style);
+ mainElem.addClass(options.className);
+ mainElem.attr('style', options.style);
- // 记录当前执行的实例索引
- dropdown.thisId = options.id;
-
- // 插入视图
- that.remove(); // 移除非当前绑定元素的面板
- elemBody.append(that.elemView);
- options.elem.data(MOD_INDEX +'_opened', true);
+ // 辞旧迎新
+ that.remove(dropdown.thisId);
+ bodyElem.append(mainElem);
+ options.elem.data(MOD_INDEX_OPENED, true); // 面板已打开的标记
// 遮罩
- var shade = options.shade ? ('') : '';
- that.elemView.before(shade);
+ var shade = options.shade ? ('') : '';
+ mainElem.before(shade);
// 如果是鼠标移入事件,则鼠标移出时自动关闭
if(options.trigger === 'mouseenter'){
- that.elemView.on('mouseenter', function(){
+ mainElem.on('mouseenter', function(){
clearTimeout(thisModule.timer);
}).on('mouseleave', function(){
that.delayRemove();
@@ -319,18 +322,16 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
}
}
- // 坐标定位
- that.position();
- thisModule.prevElem = that.elemView; // 记录当前打开的元素,以便在下次关闭
- thisModule.prevElem.data('prevElem', options.elem); // 将当前绑定的元素,记录在打开元素的 data 对象中
-
+ that.position(); // 定位坐标
+ dropdown.thisId = options.id; // 当前打开的面板 id
+
// 阻止全局事件
- that.elemView.find('.layui-menu').on(clickOrMousedown, function(e){
+ mainElem.find('.layui-menu').on(clickOrMousedown, function(e){
layui.stope(e);
});
// 触发菜单列表事件
- that.elemView.find('.layui-menu li').on('click', function(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;
@@ -350,7 +351,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
});
// 触发菜单组展开收缩
- that.elemView.find(STR_GROUP_TITLE).on('click', function(e){
+ mainElem.find(STR_GROUP_TITLE).on('click', function(e){
var othis = $(this);
var elemGroup = othis.parent();
var data = elemGroup.data('item') || {};
@@ -361,10 +362,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
});
// 组件打开完毕的事件
- typeof options.ready === 'function' && options.ready(
- that.elemView,
- options.elem
- );
+ typeof options.ready === 'function' && options.ready(mainElem, options.elem);
};
// 位置定位
@@ -372,7 +370,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
var that = this;
var options = that.config;
- lay.position(options.elem[0], that.elemView[0], {
+ lay.position(options.elem[0], that.mainElem[0], {
position: options.position,
e: that.e,
clickType: options.trigger === 'contextmenu' ? 'right' : null,
@@ -380,25 +378,23 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
});
};
- // 删除视图
- Class.prototype.remove = function(){
- var that = this;
+ // 移除面板
+ Class.prototype.remove = function(id) {
+ id = id || this.config.id;
+ var that = thisModule.getThis(id); // 根据 id 查找对应的实例
+ if (!that) return;
+
var options = that.config;
- var prevContentElem = thisModule.prevElem;
-
+ var mainElem = thisModule.findMainElem(id);
+
// 若存在已打开的面板元素,则移除
- if(prevContentElem){
- var prevId = prevContentElem.attr(MOD_ID);
- var prevTriggerElem = prevContentElem.data('prevElem');
- var prevInstance = thisModule.getThis(prevId);
- var prevOnClose = prevInstance.config.close;
-
- prevTriggerElem && prevTriggerElem.data(MOD_INDEX +'_opened', false);
- prevContentElem.remove();
- delete thisModule.prevElem;
- typeof prevOnClose === 'function' && prevOnClose.call(prevInstance.config, prevTriggerElem);
+ if (mainElem[0]) {
+ mainElem.prev('.' + STR_ELEM_SHADE).remove(); // 先移除遮罩
+ mainElem.remove();
+ options.elem.removeData(MOD_INDEX_OPENED);
+ delete dropdown.thisId;
+ typeof options.close === 'function' && options.close(options.elem);
}
- lay('.' + STR_ELEM_SHADE).remove();
};
Class.prototype.normalizedDelay = function(){
@@ -412,7 +408,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
}
}
- // 延迟删除视图
+ // 延迟移除面板
Class.prototype.delayRemove = function(){
var that = this;
var options = that.config;
@@ -427,40 +423,41 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
Class.prototype.events = function(){
var that = this;
var options = that.config;
-
- // 若传入 hover,则解析为 mouseenter
- if(options.trigger === 'hover') options.trigger = 'mouseenter';
-
- // 解除上一个事件
- if(that.prevElem) that.prevElem.off(options.trigger, that.prevElemCallback);
// 是否鼠标移入时触发
var isMouseEnter = options.trigger === 'mouseenter';
-
- // 记录被绑定的元素及回调
- that.prevElem = options.elem;
- that.prevElemCallback = function(e){
+ 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(thisModule.timer);
that.e = e;
+ // 主面板是否已打开
+ var opened = options.elem.data(MOD_INDEX_OPENED);
+
// 若为鼠标移入事件,则延迟触发
- if(isMouseEnter){
- thisModule.timer = setTimeout(function(){
- that.render();
- }, that.normalizedDelay().show)
- }else{
- if(options.closeOnClick && options.elem.data(MOD_INDEX +'_opened') && options.trigger === 'click'){
+ if (isMouseEnter) {
+ if (!opened) {
+ thisModule.timer = setTimeout(function(){
+ that.render();
+ }, that.normalizedDelay().show);
+ }
+ } else {
+ // 若为 click 事件,则根据主面板状态,自动切换打开与关闭
+ if (options.closeOnClick && opened && options.trigger === 'click') {
that.remove();
- }else{
+ } else {
that.render();
}
}
-
- e.preventDefault();
- };
- // 触发元素事件
- options.elem.on(options.trigger, that.prevElemCallback);
+ e.preventDefault();
+ });
// 如果是鼠标移入事件
if (isMouseEnter) {
@@ -475,10 +472,16 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
thisModule.that = {}; // 记录所有实例对象
// 获取当前实例对象
- thisModule.getThis = function(id){
- var that = thisModule.that[id];
- if(!that) hint.error(id ? (MOD_NAME +' instance with ID \''+ id +'\' not found') : 'ID argument required');
- return 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 + '"]');
};
// 设置菜单组展开和收缩状态
@@ -523,7 +526,7 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
var that = thisModule.getThis(dropdown.thisId);
if(!that) return;
- if((that.elemView && !that.elemView[0]) || !$('.'+ STR_ELEM)[0]){
+ if((that.mainElem && !that.mainElem[0]) || !$('.'+ STR_ELEM)[0]){
return false;
}
@@ -551,10 +554,10 @@ layui.define(['jquery', 'laytpl', 'lay', 'util'], function(exports){
// 若触发的是绑定的元素,或者属于绑定元素的子元素,则不关闭
// 满足条件:当前绑定的元素是 body document,或者是鼠标右键事件时,忽略绑定元素
var isTriggerTarget = !(isTopElem || isCtxMenu) && (options.elem[0] === e.target || options.elem.find(e.target)[0]);
- var isPanelTarget = that.elemView && (e.target === that.elemView[0] || that.elemView.find(e.target)[0]);
+ var isPanelTarget = that.mainElem && (e.target === that.mainElem[0] || that.mainElem.find(e.target)[0]);
if(isTriggerTarget || isPanelTarget) return;
// 处理移动端点击穿透问题
- if(e.type === 'touchstart' && options.elem.data(MOD_INDEX +'_opened')){
+ if(e.type === 'touchstart' && options.elem.data(MOD_INDEX_OPENED)){
$(e.target).hasClass(STR_ELEM_SHADE) && e.preventDefault();
}