refactor: 重构 dropdown 打开与关闭逻辑 (#2349)

* refactor: 重构 dropdown 打开与关闭逻辑

* chore: 优化变量

* refactor: 保留采用 elem 的 jQuery Data 进行面板打开状态的判断

* fix: 优化延时移除面板时的实例不一致的问题
This commit is contained in:
贤心 2024-11-25 11:33:32 +08:00 committed by GitHub
parent 636551547b
commit 1670cbab8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 147 additions and 108 deletions

View File

@ -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(){
});
});
</script>
</script>

View File

@ -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(){
}
});
});
</script>
</script>

View File

@ -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){

View File

@ -35,9 +35,13 @@
<button class="layui-btn" lay-on="close">close</button>
</div>
<div class="layui-bg-gray" style="margin-top: 30px; width: 100%; height: 300px; text-align: center;" id="demo20">
<div class="layui-bg-gray" style="margin-top: 30px; width: 100%; height: 300px; text-align: center;" id="ID-dropdown-demo-contextmenu">
<span class="layui-font-gray" style="position: relative; top:50%;">鼠标右键菜单</span>
</div>
<button class="layui-btn" style="margin-top: 15px;" lay-on="contextmenu">
开启全局右键菜单
</button>
</div>
@ -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;

View File

@ -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 = ['<div class="layui-dropdown layui-border-box layui-panel layui-anim layui-anim-downbit" ' + MOD_ID + '="' + options.id + '">'
,'</div>'].join('');
// 如果是右键事件,则每次触发事件时,将允许重新渲染
if(options.trigger === 'contextmenu' || lay.isTopElem(options.elem[0])) rerender = true;
// 判断是否已经打开了下拉菜单面板
if(!rerender && options.elem.data(MOD_INDEX +'_opened')) return;
var TPL_MAIN = [
'<div class="layui-dropdown layui-border-box layui-panel layui-anim layui-anim-downbit" ' + MOD_ID + '="' + options.id + '">',
'</div>'
].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 ? ('<div class="'+ STR_ELEM_SHADE +'" style="'+ ('z-index:'+ (that.elemView.css('z-index')-1) +'; background-color: ' + (options.shade[1] || '#000') + '; opacity: ' + (options.shade[0] || options.shade)) +'"></div>') : '';
that.elemView.before(shade);
var shade = options.shade ? ('<div class="'+ STR_ELEM_SHADE +'" style="'+ ('z-index:'+ (mainElem.css('z-index')-1) +'; background-color: ' + (options.shade[1] || '#000') + '; opacity: ' + (options.shade[0] || options.shade)) +'"></div>') : '';
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();
}