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:
- extend the class
Dropdown
manually, or
- raise an Issue and create a pull-request to the related Bootstrap module
as 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:
Solution:
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/bootstrap@5.1.3/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/bootstrap@5.1.3/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)
Using "mousedown"
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/bootstrap@5.1.3/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/bootstrap@5.1.3/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.