2024-10-14 23:09:27 +02:00
|
|
|
import {
|
|
|
|
autoUpdate,
|
|
|
|
computePosition,
|
|
|
|
flip,
|
|
|
|
offset,
|
|
|
|
shift,
|
|
|
|
} from "@floating-ui/dom";
|
2024-04-03 17:32:27 -04:00
|
|
|
import { Controller } from "@hotwired/stimulus";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A "menu" can contain arbitrary content including non-clickable items, links, buttons, and forms.
|
|
|
|
*/
|
|
|
|
export default class extends Controller {
|
2024-08-27 17:06:41 -04:00
|
|
|
static targets = ["button", "content"];
|
2024-04-04 17:29:50 -04:00
|
|
|
|
|
|
|
static values = {
|
2024-08-27 17:06:41 -04:00
|
|
|
show: Boolean,
|
|
|
|
placement: { type: String, default: "bottom-end" },
|
2024-09-13 15:43:16 -04:00
|
|
|
offset: { type: Number, default: 6 },
|
2024-04-04 17:29:50 -04:00
|
|
|
};
|
|
|
|
|
2024-08-27 17:06:41 -04:00
|
|
|
connect() {
|
2024-04-04 17:29:50 -04:00
|
|
|
this.show = this.showValue;
|
2024-08-27 17:06:41 -04:00
|
|
|
this.boundUpdate = this.update.bind(this);
|
|
|
|
this.addEventListeners();
|
|
|
|
this.startAutoUpdate();
|
2024-04-04 17:29:50 -04:00
|
|
|
}
|
2024-04-03 17:32:27 -04:00
|
|
|
|
2024-08-27 17:06:41 -04:00
|
|
|
disconnect() {
|
|
|
|
this.removeEventListeners();
|
|
|
|
this.stopAutoUpdate();
|
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
addEventListeners() {
|
2024-04-03 17:32:27 -04:00
|
|
|
this.buttonTarget.addEventListener("click", this.toggle);
|
|
|
|
this.element.addEventListener("keydown", this.handleKeydown);
|
|
|
|
document.addEventListener("click", this.handleOutsideClick);
|
|
|
|
document.addEventListener("turbo:load", this.handleTurboLoad);
|
|
|
|
}
|
|
|
|
|
2024-08-27 17:06:41 -04:00
|
|
|
removeEventListeners() {
|
2024-04-03 17:32:27 -04:00
|
|
|
this.buttonTarget.removeEventListener("click", this.toggle);
|
2024-08-27 17:06:41 -04:00
|
|
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
2024-04-03 17:32:27 -04:00
|
|
|
document.removeEventListener("click", this.handleOutsideClick);
|
|
|
|
document.removeEventListener("turbo:load", this.handleTurboLoad);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleTurboLoad = () => {
|
|
|
|
if (!this.show) this.close();
|
|
|
|
};
|
|
|
|
|
|
|
|
handleOutsideClick = (event) => {
|
2024-08-27 17:06:41 -04:00
|
|
|
if (this.show && !this.element.contains(event.target)) this.close();
|
2024-04-03 17:32:27 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
handleKeydown = (event) => {
|
2024-08-27 17:06:41 -04:00
|
|
|
if (event.key === "Escape") {
|
|
|
|
this.close();
|
|
|
|
this.buttonTarget.focus();
|
2024-04-03 17:32:27 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
toggle = () => {
|
|
|
|
this.show = !this.show;
|
|
|
|
this.contentTarget.classList.toggle("hidden", !this.show);
|
|
|
|
if (this.show) {
|
2024-08-27 17:06:41 -04:00
|
|
|
this.update();
|
2024-04-03 17:32:27 -04:00
|
|
|
this.focusFirstElement();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
close() {
|
|
|
|
this.show = false;
|
|
|
|
this.contentTarget.classList.add("hidden");
|
|
|
|
}
|
|
|
|
|
|
|
|
focusFirstElement() {
|
2024-10-14 23:09:27 +02:00
|
|
|
const focusableElements =
|
|
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
|
|
const firstFocusableElement =
|
|
|
|
this.contentTarget.querySelectorAll(focusableElements)[0];
|
2024-04-03 17:32:27 -04:00
|
|
|
if (firstFocusableElement) {
|
|
|
|
firstFocusableElement.focus();
|
|
|
|
}
|
|
|
|
}
|
2024-08-27 17:06:41 -04:00
|
|
|
|
|
|
|
startAutoUpdate() {
|
|
|
|
if (!this._cleanup) {
|
2024-10-14 23:09:27 +02:00
|
|
|
this._cleanup = autoUpdate(
|
|
|
|
this.buttonTarget,
|
|
|
|
this.contentTarget,
|
|
|
|
this.boundUpdate,
|
|
|
|
);
|
2024-08-27 17:06:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
stopAutoUpdate() {
|
|
|
|
if (this._cleanup) {
|
|
|
|
this._cleanup();
|
|
|
|
this._cleanup = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
update() {
|
|
|
|
computePosition(this.buttonTarget, this.contentTarget, {
|
|
|
|
placement: this.placementValue,
|
2024-10-14 23:09:27 +02:00
|
|
|
middleware: [offset(this.offsetValue), flip(), shift({ padding: 5 })],
|
2024-08-27 17:06:41 -04:00
|
|
|
}).then(({ x, y }) => {
|
|
|
|
Object.assign(this.contentTarget.style, {
|
2024-10-14 23:09:27 +02:00
|
|
|
position: "fixed",
|
2024-08-27 17:06:41 -04:00
|
|
|
left: `${x}px`,
|
|
|
|
top: `${y}px`,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2024-04-03 17:32:27 -04:00
|
|
|
}
|