You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 01:09:26