news 2026/10/10 8:20:34

Bootstrap 4 多级下拉菜单(Multi-Level Dropdown)Hack 实现指南:以 flask-admin 导航为例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bootstrap 4 多级下拉菜单(Multi-Level Dropdown)Hack 实现指南:以 flask-admin 导航为例
  • 后端

【免费下载链接】flask-admin

Simple and extensible administrative interface framework for Flask

项目地址:https://gitcode.com/gh_mirrors/fl/flask-admin
点击查看免费下载

本文围绕 flask-admin 仓库内置的第三方组件multi-level-dropdowns-bootstrap展开,讲解如何在不修改 Bootstrap 4 官方 CSS/JS 的前提下,让下拉菜单支持无限层级展开。文章既覆盖组件的完整用法(基础引入、Hover 触发、事件处理),也结合 flask-admin 源码说明其在后台导航菜单中的真实集成方式与工作原理,读完即可在自己的 Bootstrap 4 项目中复刻同款多级菜单。

背景:Bootstrap 4 原生下拉为何做不到多级

Bootstrap 4 的官方下拉组件(dropdown.js)在设计上只支持“一层下拉”:

  • 点击/悬停[data-toggle="dropdown"]元素后,dropdown.js只会切换当前菜单的show类;
  • 当二级菜单里再嵌套一个.dropdown时,官方组件既不会在点击时阻止事件冒泡,也不会在关闭时区分“子菜单是否仍处于打开状态”;
  • 结果就是:子菜单一点开,父菜单就被关闭,深层菜单永远无法稳定显示。

multi-level-dropdowns-bootstrap正是针对这一缺口设计的 hack:它复用 Bootstrap 官方的dropdown()方法、事件与数据结构,只通过一层轻量包装就能实现“无限级”下拉,且不引入任何额外 CSS 类来覆盖官方样式——正如其 README 所说“Using official HTML without adding extra CSS styles and classes, it's just like native support”。

该组件对应的核心文件位于仓库 flask_admin/static/vendor/multi-level-dropdowns-bootstrap/ 目录:

文件作用
bootstrap4-dropdown-ml-hack.js核心多级下拉 hack(点击触发)
bootstrap4-dropdown-ml-hack-hover.js带 Hover 触发版本(包含 hack + 鼠标悬停逻辑)
bootstrap4-dropdown-ml-hack-hover.css配合 Hover 使用的间距修正样式
README.md官方使用说明(本文分析主体)

基础用法:一行<script>开启多级下拉

根据 README 的 “Base” 部分,多级下拉的开启方式极其简单:在 jQuery 与 Bootstrap 的 JS 文件之后引入 hack 脚本即可:

<script src="https://raw.githubusercontent.com/dallaslu/bootstrap-4-multi-level-dropdown/master/bootstrap4-dropdown-ml-hack.js"></script>

注意事项:

  • 加载顺序必须为jquery → bootstrap(含 dropdown.js)→ 本 hack 脚本,因为 hack 内部会读取并包装$.fn.dropdown,依赖官方组件已就绪;
  • 引入后无需修改任何 HTML 结构,官方文档 getbootstrap.com/docs/4.4/components/dropdowns 中列出的所有功能不受影响——CSS 类、JS 方法、事件与选项均可照常使用;
  • 在 flask-admin 中,该脚本并非由用户手工引入,而是由后台基础模板统一加载(详见下文“flask-admin 的集成方式”)。

原理剖析:hack 到底改了什么

阅读仓库内的 bootstrap4-dropdown-ml-hack.js 源码,可以发现它的全部逻辑只有 26 行,核心是“包装官方方法 + 接管事件”:

$.fn.dropdown = (function() { var $bsDropdown = $.fn.dropdown; // 1. 保存官方 $.fn.dropdown return function(config) { if (typeof config === 'string' && config === 'toggle') { // 2. 拦截 toggle 调用 $('.has-child-dropdown-show').removeClass('has-child-dropdown-show'); $(this).closest('.dropdown').parents('.dropdown').addClass('has-child-dropdown-show'); } var ret = $bsDropdown.call($(this), config); // 3. 调用官方方法完成实际开关 $(this).off('click.bs.dropdown'); // 4. 关闭官方内置的 click 绑定,防止重复 toggle return ret; } })(); $(function() { $('.dropdown [data-toggle="dropdown"]').on('click', function(e) { $(this).dropdown('toggle'); e.stopPropagation(); // 5. 阻止冒泡,避免事件传给父级 dropdown }); $('.dropdown').on('hide.bs.dropdown', function(e) { if ($(this).is('.has-child-dropdown-show')) { // 6. 子菜单打开时,阻止父菜单被隐藏 $(this).removeClass('has-child-dropdown-show'); e.preventDefault(); } e.stopPropagation(); // 7. 多级模式下不再向上传播 hide 事件 }); });

逐段拆解其工作机制:

  1. 包装$.fn.dropdown:先把官方实现保存为$bsDropdown,再返回新函数。这样外部调用$(...).dropdown('toggle')时仍会执行官方逻辑,hack 只是在其前后追加行为。
  2. 标记祖先菜单:触发toggle时,先清除全局的has-child-dropdown-show标记,再给当前.dropdown的所有祖先.dropdown加上该标记,用于记录“哪些父菜单下有正在打开的子菜单”。
  3. 移除官方 click 绑定:Bootstrap 官方dropdown.js会给[data-toggle="dropdown"]绑定click.bs.dropdown,内部会再次调用toggle();hack 用$(this).off('click.bs.dropdown')将其关闭,避免一次点击触发两次开关导致菜单闪动。
  4. 点击事件自行接管:在 document ready 后统一为所有.dropdown [data-toggle="dropdown"]绑定 click,显式调用dropdown('toggle')并e.stopPropagation()——这是多级菜单最关键的一步:点击子菜单按钮时事件不会冒泡到父级 dropdown 的处理器,父菜单因此不会被意外关闭。
  5. 拦截hide.bs.dropdown:当某个 dropdown 触发隐藏事件时,如果它带有has-child-dropdown-show标记(说明它的子菜单还开着),就preventDefault()阻止其关闭,并移除标记;同时stopPropagation()阻止隐藏事件继续向上传播,保证深层菜单关闭时不会连坐关闭上层菜单。

这套设计保证了两个核心体验:点开深层子菜单时父级保持打开,关闭深层子菜单时父级不受影响。

Hover 触发:鼠标悬停即展开

README 的 “Hover” 部分提供了两种实现方式。

方式一:手动加样式 + 事件

先加一行 CSS,把菜单与触发元素之间的间距归零,避免鼠标从按钮移向菜单时触发mouseleave而被误关:

.dropdown-hover-all .dropdown-menu, .dropdown-hover > .dropdown-menu { margin:0 }

再为指定容器绑定mouseenter/mouseleave事件,用dropdown('toggle')(README 建议使用'toggle'以获得最佳体验)控制开关:

$('.dropdown-hover').on('mouseenter',function() { if(!$(this).hasClass('show')){ $('>[data-toggle="dropdown"]', this).dropdown('toggle'); } }); $('.dropdown-hover').on('mouseleave',function() { if($(this).hasClass('show')){ $('>[data-toggle="dropdown"]', this).dropdown('toggle'); } }); $('.dropdown-hover-all').on('mouseenter', '.dropdown', function() { if(!$(this).hasClass('show')){ $('>[data-toggle="dropdown"]', this).dropdown('toggle'); } }); $('.dropdown-hover-all').on('mouseleave', '.dropdown', function() { if($(this).hasClass('show')){ $('>[data-toggle="dropdown"]', this).dropdown('toggle'); } });

两类选择器的区别:

  • .dropdown-hover:针对单个dropdown 元素,只监听其自身的鼠标进出;
  • .dropdown-hover-all:针对容器内所有.dropdown,利用事件委托(on('mouseenter', '.dropdown', ...))一次绑定、全局生效,适合整条导航栏整体启用 Hover。

方式二:直接引入配套文件

如果不想手写事件,仓库已经提供了现成的组合文件,README 给出的完整用法如下:

<link rel="stylesheet" href="https://raw.githubusercontent.com/dallaslu/bootstrap-4-multi-level-dropdown/master/bootstrap4-dropdown-ml-hack-hover.css" /> ... <div class="dropdown-hover-all"> <!-- .dropdown elements --> </div> <div class="dropdown dropdown-hover"> <!-- toggle and menu elements --> </div> ... <script src="https://raw.githubusercontent.com/dallaslu/bootstrap-4-multi-level-dropdown/master/bootstrap4-dropdown-ml-hack-hover.js"></script>

对照仓库文件可以确认:

  • bootstrap4-dropdown-ml-hack-hover.css 内容与 README 中的样式完全一致(margin:0一条规则);
  • bootstrap4-dropdown-ml-hack-hover.js 前 26 行与bootstrap4-dropdown-ml-hack.js完全相同,其后追加了.dropdown-hover/.dropdown-hover-all的鼠标事件绑定。

因此,若只需点击触发,引入bootstrap4-dropdown-ml-hack.js一个文件即可;需要 Hover 时则换成-hover后缀的 JS + CSS 组合。

flask-admin 的集成方式:后台导航栏的多级菜单

flask-admin 使用 Bootstrap 4 渲染整个管理后台界面,其左侧顶部导航由Admin.menu()生成的菜单树驱动。为了让分类(category)可以嵌套为多级下拉,仓库直接内置了该 hack 组件。

1. 模板层:统一加载 hack 脚本

所有 Bootstrap 4 后台页面共用的基础模板 flask_admin/templates/bootstrap4/admin/base.html 在tail_js块中按顺序加载脚本(见 base.html 第 81-98 行):

<script src="{{ admin_static.url(filename='vendor/jquery.min.js', v='3.5.1') }}" ...></script> <script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/bootstrap.min.js', v='4.6.2') }}" ...></script> <script src="{{ admin_static.url(filename='vendor/bootstrap4/util.js', v='4.3.1') }}" ...></script> <script src="{{ admin_static.url(filename='vendor/bootstrap4/dropdown.js', v='4.3.1') }}" ...></script> <script src="{{ admin_static.url(filename='vendor/multi-level-dropdowns-bootstrap/bootstrap4-dropdown-ml-hack.js') }}" ...></script>

可以观察到 flask-admin 在加载顺序上的刻意安排:

  • jquery.min.js(v3.5.1)与bootstrap.min.js(v4.6.2)在前;
  • 之后是官方util.js与dropdown.js(v4.3.1)——dropdown.js是 hack 包装的目标对象,必须先就位;
  • 最后才是bootstrap4-dropdown-ml-hack.js,且未启用 Hover 版本,即后台菜单默认使用“点击展开”交互。

这正好印证了 README 中“Just add js after jquery and bootstrap js files”的要求:hack 脚本必须后于官方组件加载。

2. 菜单渲染:递归生成嵌套.dropdown

后台菜单由 flask_admin/templates/bootstrap4/admin/layout.html 中的menu()宏渲染(见 layout.html 第 21-83 行)。其关键逻辑是递归:

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 8:12:35

【MT32F006】MT32F006之systick定时器延时

本文最后修改时间&#xff1a;2026年09月10日一、本节简介本文介绍如何使用MT32F006的定时器做us、ms级的延时。二、实验平台库版本&#xff1a;V1.0.0编译软件&#xff1a;MDK5.37硬件平台&#xff1a;MT32F006开发板&#xff08;主芯片MT32F006&#xff09;仿真器&#xff1a…

作者头像 李华
网站建设 2026/10/10 8:12:13

CPU 飙到 100%,先别急着翻代码

CPU 飙到 100%&#xff0c;先别急着翻代码 Java 线上排查的四步法与三个坑 经典排查流程整理与修订 作者观点&#xff0c;仅供讨论 线上告警&#xff1a;某台机器 CPU 打满。很多人的第一反应是翻代码、猜哪里有死循环。我的观点是&#xff1a;**先定位&#xff0c;再推理&…

作者头像 李华
网站建设 2026/10/10 8:10:46

PCA9422与MKV42F256VLH16协同实现μA级嵌入式电源管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 8:08:42

基于PCA9422与PIC32MX的电源管理及低功耗设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 8:07:42

幼儿手工活动:创造力与动手能力培养的底层逻辑与实操指南

很多家长可能都遇到过这样的场景&#xff1a;孩子兴冲冲地举着一幅歪歪扭扭的剪纸或者一个看不出原型的黏土作品跑过来&#xff0c;满脸期待地问“好不好看”。我们嘴上夸着“真棒”&#xff0c;心里却在犯嘀咕&#xff1a;这到底有什么意义&#xff1f;半天时间就折腾出这么个…

作者头像 李华