如何用JavaScript实现Bootstrap下拉菜单再次点击关闭
问题背景
现有Bootstrap多级下拉菜单结构如下:
<li class="dropdown"> <a class="nav-link" href="#" id="shop_submenu" role="button" data-bs-toggle="dropdown" aria-expanded="false"> Rent </a> <ul class="submenu dropdown-menu" aria-labelledby="shop_submenu"> <li><a href="">Short Gowns</a></li> <li><a href="">Couple Combo Sherwani</a></li> </ul> </li>
当前使用的下拉交互逻辑已支持多级下拉展开、hover触发功能,但展开后再次点击触发按钮无法关闭菜单,原有逻辑代码如下:
(function($bs) { const CLASS_NAME = 'has-child-dropdown-show'; $bs.Dropdown.prototype.toggle = function(_orginal) { return function() { document.querySelectorAll('.' + CLASS_NAME).forEach(function(e) { e.classList.remove(CLASS_NAME); }); let dd = this._element.closest('.dropdown').parentNode.closest('.dropdown'); for (; dd && dd !== document; dd = dd.parentNode.closest('.dropdown')) { dd.classList.add(CLASS_NAME); } return _orginal.call(this); } }($bs.Dropdown.prototype.toggle); document.querySelectorAll('.dropdown').forEach(function(dd) { dd.addEventListener('hide.bs.dropdown', function(e) { if (this.classList.contains(CLASS_NAME)) { this.classList.remove(CLASS_NAME); e.preventDefault(); } e.stopPropagation(); // do not need pop in multi level mode }); }); // for hover document.querySelectorAll('.dropdown-hover, .dropdown-hover-all .dropdown').forEach(function(dd) { dd.addEventListener('mouseenter', function(e) { let toggle = e.target.querySelector(':scope>[data-bs-toggle="dropdown"]'); if (!toggle.classList.contains('show')) { $bs.Dropdown.getOrCreateInstance(toggle).toggle(); dd.classList.add(CLASS_NAME); $bs.Dropdown.clearMenus(); } }); dd.addEventListener('mouseleave', function(e) { let toggle = e.target.querySelector(':scope>[data-bs-toggle="dropdown"]'); if (toggle.classList.contains('show')) { $bs.Dropdown.getOrCreateInstance(toggle).toggle(); } }); }); })(bootstrap);
具体修复方案
问题根源在hide.bs.dropdown事件的拦截逻辑:原有代码只要下拉元素带有has-child-dropdown-show类就无差别阻止关闭,没有区分「子菜单展开导致父菜单需要保持展开」和「用户主动点击当前触发按钮要关闭菜单」两种场景,同时重写的toggle方法没有处理当前点击的是已展开菜单的场景。
替换原有JS代码为以下版本即可实现点击触发按钮切换展开/关闭的效果,同时保留原有多级下拉、hover触发的正常功能:
(function($bs) { const CLASS_NAME = 'has-child-dropdown-show'; // 重写toggle方法 $bs.Dropdown.prototype.toggle = function(_original) { return function() { // 提前判断当前点击的菜单是否已经处于展开状态 const isCurrentOpen = this._element.classList.contains('show'); document.querySelectorAll('.' + CLASS_NAME).forEach(function(e) { e.classList.remove(CLASS_NAME); }); // 若当前点击的是已展开菜单,不给父级加标记,允许正常关闭 if (!isCurrentOpen) { let dd = this._element.closest('.dropdown').parentNode.closest('.dropdown'); for (; dd && dd !== document; dd = dd.parentNode.closest('.dropdown')) { dd.classList.add(CLASS_NAME); } } return _original.call(this); } }($bs.Dropdown.prototype.toggle); document.querySelectorAll('.dropdown').forEach(function(dd) { dd.addEventListener('hide.bs.dropdown', function(e) { // 只有非用户主动点击触发的关闭事件(比如子菜单联动的父级关闭)才拦截 if (this.classList.contains(CLASS_NAME) && !e.clickEvent) { this.classList.remove(CLASS_NAME); e.preventDefault(); } e.stopPropagation(); }); }); // hover触发逻辑保持原有功能不变 document.querySelectorAll('.dropdown-hover, .dropdown-hover-all .dropdown').forEach(function(dd) { dd.addEventListener('mouseenter', function(e) { let toggle = e.target.querySelector(':scope>[data-bs-toggle="dropdown"]'); if (!toggle.classList.contains('show')) { $bs.Dropdown.getOrCreateInstance(toggle).toggle(); dd.classList.add(CLASS_NAME); $bs.Dropdown.clearMenus(); } }); dd.addEventListener('mouseleave', function(e) { let toggle = e.target.querySelector(':scope>[data-bs-toggle="dropdown"]'); if (toggle.classList.contains('show')) { $bs.Dropdown.getOrCreateInstance(toggle).toggle(); } }); }); })(bootstrap);
修改点说明
- 在重写的
toggle方法中新增当前菜单展开状态判断:如果点击的是已经展开的菜单触发按钮,就不给父级下拉添加has-child-dropdown-show标记,允许关闭流程正常执行 - 调整
hide.bs.dropdown的拦截条件:只有非用户主动点击触发的关闭事件(比如子菜单展开联动的父级关闭)才会被拦截,用户主动点击触发按钮的关闭事件会正常执行 - 原有多级下拉联动、hover触发的逻辑完全保留,不会影响现有功能
内容的提问来源于stack exchange,提问作者Zubair Nazer Oliyat
相关产品推荐
相关产品推荐

