Bootstrap:单击 jQuery 时下拉菜单无法打开
我正在创建一个包含多行的表格,每行都有一个“选项”按钮,用于显示下拉上下文菜单。为了使代码更简短,我使用单个
div
以便将其重新用作上下文菜单的通用标记。
我使用的是 Bootstrap 5.1.3 和 jQuery 3.6.0。以下是我的代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Code</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</head>
<body>
<table id="myTable" class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Document</th>
<th>Reference</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>General Policies</td>
<td>GP-01-2022</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">Options</a>
</div>
</td>
</tr>
<tr>
<td>2</td>
<td>Training Material</td>
<td>GP-02-2022</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">Options</a>
</div>
</td>
</tr>
</tbody>
</table>
<ul id="contextMenu" class="dropdown-menu">
<li><a tabindex="-1" href="#" class="dropdown-item downloadLink">Download</a></li>
<li><a tabindex="-1" href="#" class="dropdown-item propertiesLink">Properties</a></li>
</ul>
<script>
//save the selector so you don't have to do the lookup everytime
var $dropdown = $('#contextMenu');
$('.optionsButton').click(function(event) {
//get document ID
var id = this.id;
//move dropdown menu
$(this).after($dropdown);
//update links
$dropdown.find(".downloadLink").attr("href", "/data/download?id=" + id);
$dropdown.find(".propertiesLink").attr("href", "/data/viewproperties?id=" + id);
//show dropdown
$(this).dropdown();
});
</script>
</body>
</html>
在此代码中,我面临两种类型的问题。首先,下拉菜单未打开。当我在开发人员模式下检查代码时,我可以看到 jQuery 脚本已成功将
contextmenu
DIV 传输到“选项”按钮下方,以便它按照 Bootstrap 的要求进行嵌套。但是
$(this).dropdown();
并没有打开菜单。
第二个错误是,在开发者模式控制台中,每次单击“选项”按钮时都会看到此错误:
dropdown.js:285 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
并且此错误的堆栈跟踪指向
dropdown.js
,并未指定错误位于我的代码中的哪个位置。
需要帮助来尝试诊断此处的问题。我对 Bootstrap 和 jQuery 还不太熟悉。谢谢。
TL;DR: 不要 移动 #contextMenu 到任何地方。阅读: 解决方案*
您收到的错误
dropdown.js:285 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
与 Bootstrap (v5.1.3) 相关: dropdown.js 代码:
// @l62:
const SELECTOR_MENU = '.dropdown-menu'
// @l103:
constructor(element, config) {
super(element);
//...
this._menu = this._getMenuElement()
//...
}
// @l269:
_getMenuElement() {
return SelectorEngine.next(this._element, SELECTOR_MENU)[0]
}
// @l285:
_getPlacement() {
// ...
const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'
// ...
}
此处
:
SelectorEngine.next(this._element,
如您所见,除了 BS 硬编码的元素
.menu-dropdown
之外,没有办法将另一个元素传递给构造函数,而这是方法中的
下一个兄弟元素
_getMenuElement()
~line285.
BS 使用
data-bs-toggle="dropdown"
为每个按钮分配一个“click”事件,并盲目地期望切换
下一个同级元素
下拉菜单 —
实际上还不存在(目前)!
我会:
-
手动扩展类
Dropdown
,或者 - 提出问题并创建 pull-request 到相关的 Bootstrap 模块
作为将任何所需元素作为
this._menu
传递的方式;类似于:
constructor(element, config) {
//...
// Fix: allow for custom reusable menu Element
this._menu = config.menuElement || this._getMenuElement()
//...
}
免责声明: 在 主分支 中,关于上述剥离的代码有一些变化,我不确定在撰写本文时这些问题是否已得到解决。
与此同时,您可以简单地做,不使用“mousedown”事件(领先BS的
“click”
事件一步 - 就像在这个
重复问题的答案
),也不使用愚蠢的
Event.stopPropagation()
(永远都不要使用,除非您真的非常清楚自己在做什么,或者仅用于调试目的)— 是:
解决方案:
不要使用
.after()
或(使用 JS)
.insertAdjacentElement()
移动
UL#contextMenu
,而是在扩展的 Popper 实例实例化时,更改预期的 Bootstrap
this._menu
属性以指向所需的可重用元素 — 您的
in-body
“#contextMenu”类似:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Code</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body>
<table id="myTable" class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Document</th>
<th>Reference</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>General Policies</td>
<td>GP-01-2022</td>
<td>
<div class="dropdown">
<button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">Options</button>
</div>
</td>
</tr>
<tr>
<td>2</td>
<td>Training Material</td>
<td>GP-02-2022</td>
<td>
<div class="dropdown">
<button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">Options</button>
</div>
</td>
</tr>
</tbody>
</table>
<ul id="contextMenu" class="dropdown-menu">
<li><button type="button" tabindex="-1" class="dropdown-item downloadLink">Download</button></li>
<li><button type="button" tabindex="-1" class="dropdown-item propertiesLink">Properties</button></li>
</ul>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script>
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const els = (sel, par) => (par || document).querySelectorAll(sel);
// Task: BS5 Popper fix for single static dropdown menu:
const elDropdown = el('#contextMenu');
const elsBtns = els(".optionsButton");
const dropdownList = [...elsBtns].map(function(elBtn) {
const instance = new bootstrap.Dropdown(elBtn);
instance._menu = elDropdown;
return instance;
});
// console.log(dropdownList);
</script>
</body>
</html>
上述
好处
在于 DOM 中没有任何会触发重排的更改。 Popper 代码将计算浮动 contextMenu 的最佳位置,然后完成任务。
不太好
的事情
是,在动态向 Table 添加 TR 元素的情况下,应特别小心;这意味着每个新添加的按钮都应在创建时实例化为
new bootstrap.Dropdown(elBtn)
使用“mousedown”
另一个(不太好的)解决方案是(
不必要地
)
在 DOM 中移动下拉菜单
。可以使用
“mousedown”
事件来实现,以便“提前”移动下拉菜单 — 在 BS 的
“click”
事件触发之前(如
此相关问题的答案
中建议的那样)。但这样无法正常工作。单击一个又一个按钮,可以看到内容闪烁/故障(实际下拉菜单)。可能有方法可以缓解此问题……但是,为什么呢。无论如何,FYEO 这里是代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Code</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body>
<table id="myTable" class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Document</th>
<th>Reference</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>General Policies</td>
<td>GP-01-2022</td>
<td>
<div class="dropdown">
<button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">Options</button>
</div>
</td>
</tr>
<tr>
<td>2</td>
<td>Training Material</td>
<td>GP-02-2022</td>
<td>
<div class="dropdown">
<button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">Options</button>
</div>
</td>
</tr>
</tbody>
</table>
<ul id="contextMenu" class="dropdown-menu">
<li><button type="button" tabindex="-1" class="dropdown-item downloadLink">Download</button></li>
<li><button type="button" tabindex="-1" class="dropdown-item propertiesLink">Properties</button></li>
</ul>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script>
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const els = (sel, par) => (par || document).querySelectorAll(sel);
// Task: BS5 Popper fix for single static dropdown menu:
const elDropdown = el('#contextMenu');
const elsBtns = els(".optionsButton");
const prepareDropdown = (evt) => {
const elBtn = evt.currentTarget;
elBtn.insertAdjacentElement("afterend", elDropdown);
};
elsBtns.forEach(elBtn => elBtn.addEventListener("mousedown", prepareDropdown));
</script>
</body>
</html>
PS:如果您不需要导航,而只是一个简单的 UI 交互按钮元素,请始终使用
<button type="button">
而不是 Anchors(如上例所示)。
BS 使用和实现弹出窗口、选择等的方式有点 无论如何都是坏的 。如果已经通过单击另一个按钮打开了弹出窗口(或模式),则应 立即 显示第二个(或相同)弹出窗口(而不是在第二次单击后)。这是设计中的 UI/UX 缺陷。 Bootstrap 经常会以相当奇怪的方式实现想法,但不要忘记,你始终可以通过提供 Pull Request 来帮助开源社区。
如果你对如何使用 JavaScript 从头开始创建(类似的)弹出窗口感兴趣 — — 你可以在这里找到更多信息: 在鼠标位置显示自定义弹出窗口 。
我已经注释了您对 bootstrap cdn 的加载,并且只处理了您的实际 Javascript。
您只有一个显示下拉菜单的逻辑,尽管我将其从
$(this).dropdown();
更改为
$dropdown.show()
,因为您的下拉菜单已使用此变量进行跟踪。
我还实现了切换效果,也就是说,如果您的下拉菜单已打开,那么我们将有以下场景:
- 您单击表中的其他位置,在这种情况下下拉菜单将被隐藏
- 您单击相同的按钮,在这种情况下下拉菜单将被隐藏
- 您单击另一行中的类似按钮,在这种情况下下拉菜单将使用适当的参数正确移动到适当的位置
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Code</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</head>
<body>
<table id="myTable" class="table table-hover" onclick="hideall()">
<thead>
<tr>
<th>#</th>
<th>Document</th>
<th>Reference</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>General Policies</td>
<td>GP-01-2022</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary optionsButton" aria-expanded="false" id="doc1">
Options
</a>
</div>
</td>
</tr>
<tr>
<td>2</td>
<td>Training Material</td>
<td>GP-02-2022</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary optionsButton" aria-expanded="false" id="doc2">
Options
</a>
</div>
</td>
</tr>
</tbody>
</table>
<ul id="contextMenu" class="dropdown-menu">
<li><a tabindex="-1" href="#" class="dropdown-item downloadLink">Download</a></li>
<li><a tabindex="-1" href="#" class="dropdown-item propertiesLink">Properties</a></li>
</ul>
<script>
//save the selector so you don't have to do the lookup everytime
var $dropdown = $('#contextMenu');
var activeOptionsButton;
$('.optionsButton').click(function(event) {
event.stopPropagation();
if (activeOptionsButton === this) {
hideall();
activeOptionsButton = undefined;
return;
}
activeOptionsButton = this;
//get document ID
var id = this.id;
//move dropdown menu
$(this).after($dropdown);
//update links
$dropdown.find(".downloadLink").attr("href", "/data/download?id="+id);
$dropdown.find(".propertiesLink").attr("href", "/data/viewproperties?id="+id);
//show dropdown
$dropdown.show();
});
function hideall() {
$dropdown.hide();
}
</script>
</body>
</html>
试试这个..
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Code</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2" crossorigin="anonymous"></script>
</head>
<body>
<table id="myTable" class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Document</th>
<th>Reference</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>General Policies</td>
<td>GP-01-2022</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">
Options
</a>
</div>
</td>
</tr>
<tr>
<td>2</td>
<td>Training Material</td>
<td>GP-02-2022</td>
<td>
<div class="dropdown">
<a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">
Options
</a>
</div>
</td>
</tr>
</tbody>
</table>
<ul id="contextMenu" class="dropdown-menu">
<li><a tabindex="-1" href="#" class="dropdown-item downloadLink">Download</a></li>
<li><a tabindex="-1" href="#" class="dropdown-item propertiesLink">Properties</a></li>
</ul>
<script>
//save the selector so you don't have to do the lookup everytime
var $dropdown = $('#contextMenu');
$('.optionsButton').click(function(event) {
//get document ID
var id = this.id;
//move dropdown menu
$(this).after($dropdown);
//update links
$dropdown.find(".downloadLink").attr("href", "/data/download?id="+id);
$dropdown.find(".propertiesLink").attr("href", "/data/viewproperties?id="+id);
//show dropdown
$(this).dropdown();
});
</script>
</body>
</html>