I'm creating a table with multiple rows, all having an "Options" button that is supposed to show a dropdown context menu. To keep the code shorter, I'm using a single div
in order to reuse it as a common markup for the context menu.
I'm using Bootstrap 5.1.3 and jQuery 3.6.0. Following is my code:
<!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>
In this code I'm facing two types of problems. Firstly, the dropdown menu isn't opening. When I inspect the code in Developer Mode, I can see that the jQuery script is successfully transferring the contextmenu
DIV underneath the "Options" button so that it becomes nested as required by Bootstrap. But then the $(this).dropdown();
isn't opening the menu.
Second error is that in Developer Mode console, I see this error every time I click the 'Options' button:
dropdown.js:285 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
And the stack trace of this error points to dropdown.js
, does not specify where the error lies in my code.
Need help in trying to diagnose the issue here. I'm fairly new to Bootstrap and jQuery. Thanks.
TL;DR: Don't move the #contextMenu anywhere. Read: Solution*
The error you're getting
dropdown.js:285 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
is related to the Bootstrap (v5.1.3): dropdown.js code:
// @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'
// ...
}
here: SelectorEngine.next(this._element,
as you can see, there's no way to pass to the constructor another Element .menu-dropdown
besides the one that BS hardcoded, and that's a next Sibling element in the Method _getMenuElement()
~line285.
BS assigns a "click" Event to every button with data-bs-toggle="dropdown"
and blindly expects to toggle a next sibling Element dropdown — which does not actually exists (yet)!
I would either:
Dropdown
manually, oras a way to pass any desired Element as the this._menu
; something like:
constructor(element, config) {
//...
// Fix: allow for custom reusable menu Element
this._menu = config.menuElement || this._getMenuElement()
//...
}
Disclaimer: There are some changes in the main branch regarding the above stripped-off code, I'm not sure if at the time of writing those issues were addressed.
In the meantime what you can simply do, without using the "mousedown" Event (to be one step ahead the BS's "click"
event - like in this duplicate question's answer), and without using the silly Event.stopPropagation()
(which should never be used, besides you really, really know what you're doing, or for debugging purpose only) — is:
Don't move the UL#contextMenu
using .after()
or (with JS) .insertAdjacentElement()
, rather, on instantiation of the extended Popper instances change the expected Bootstrap this._menu
property to point to the desired reusable Element — your in-body "#contextMenu" like:
<!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>
The nice thing of the above is that there are no changes in the DOM that would trigger a reflow. The Popper code will calculate the best position of your floating contextMenu and call it job-done.
The not so nice thing is that special care should be given in the case you dynamically add TR elements to the Table; in the means that every newly added Button should be instantiated upon creation as a new bootstrap.Dropdown(elBtn)
Another (not so good) solution to your original idea is to (unnecessarily) move the dropdown in DOM. It can be achieved using the "mousedown"
Event, in order to move the dropdown "ahead-of-time" — before the BS's "click"
event triggers (as suggested in this related question's answer). But such will not work correctly. Clicking one button after the other, a flash of content / glitch (of the actual dropdown) can be seen. There might be ways to mitigate the issue… but, why. Anyways, FYEO here's the code:
<!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: use always <button type="button">
instead of Anchors (as in the examples above) if you don't need to navigate, but just a plain UI interaction button Element.
The way that BS uses and implements popups, selects etc. is kind of broken anyways. If a popup (or modal) is already opened, by clicking another button — the second (or the same) popup should be shown immediately (not after a second click). It's a UI/UX flaw in the design. Bootstrap often implements ideas quite oddly, but don't forget you can always help the Open Source community by providing a Pull Request.
If you're interested on how to create a (similar) Popup from scratch using JavaScript — you can find out more here: Show custom popup on mouse location.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With