Files
mal/static/dropdown.ts

199 lines
5.5 KiB
TypeScript

import { closestFocusable, onHtmxLoad } from "./utils";
class UIDropdown extends HTMLElement {
isOpen = false;
triggerEl: HTMLElement | null = null;
contentEl: HTMLElement | null = null;
previouslyFocused: HTMLElement | null = null;
constructor() {
super();
this.onTriggerClick = this.onTriggerClick.bind(this);
this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
}
connectedCallback(): void {
this.triggerEl = this.querySelector("[data-trigger]");
this.contentEl = this.querySelector("[data-content]");
if (this.contentEl) {
this.contentEl.classList.add("hidden");
this.contentEl.setAttribute("aria-hidden", "true");
}
const triggerButton = this.triggerButton();
triggerButton?.setAttribute("aria-expanded", "false");
this.triggerEl?.addEventListener("click", this.onTriggerClick);
document.addEventListener("click", this.handleDocumentClick);
document.addEventListener("keydown", this.handleKeydown);
}
disconnectedCallback(): void {
this.triggerEl?.removeEventListener("click", this.onTriggerClick);
document.removeEventListener("click", this.handleDocumentClick);
document.removeEventListener("keydown", this.handleKeydown);
}
triggerButton(): HTMLButtonElement | null {
const button = this.triggerEl?.querySelector("button");
return button instanceof HTMLButtonElement ? button : null;
}
open(): void {
if (!this.contentEl || this.isOpen) {
return;
}
document.querySelectorAll<UIDropdown>("ui-dropdown").forEach((dropdown) => {
if (dropdown !== this) {
dropdown.close();
}
});
this.isOpen = true;
this.previouslyFocused =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
this.contentEl.classList.remove("hidden");
this.contentEl.setAttribute("aria-hidden", "false");
this.triggerButton()?.setAttribute("aria-expanded", "true");
closestFocusable(this.contentEl)?.focus();
}
close(options: { restoreFocus?: boolean } = {}): void {
if (!this.contentEl || !this.isOpen) {
return;
}
this.isOpen = false;
this.contentEl.classList.add("hidden");
this.contentEl.setAttribute("aria-hidden", "true");
this.triggerButton()?.setAttribute("aria-expanded", "false");
if (options.restoreFocus !== false) {
this.previouslyFocused?.focus();
}
}
toggle(): void {
if (this.isOpen) {
this.close();
return;
}
this.open();
}
onTriggerClick(event: Event): void {
event.preventDefault();
this.toggle();
}
handleDocumentClick(event: MouseEvent): void {
if (!this.isOpen) {
return;
}
if (!(event.target instanceof Node)) {
return;
}
if (this.contains(event.target)) {
return;
}
this.close({ restoreFocus: false });
}
handleKeydown(event: KeyboardEvent): void {
if (!this.isOpen) {
return;
}
if (event.key !== "Escape") {
return;
}
event.preventDefault();
this.close();
}
}
customElements.define("ui-dropdown", UIDropdown);
const initStudioDropdown = (): void => {
document.addEventListener("click", (e) => {
const { target } = e;
if (!(target instanceof Element)) {
return;
}
const btn = target.closest<HTMLButtonElement>("button[data-studio-select]");
if (!btn) {
return;
}
const input = document.querySelector("#studio-input");
const form = document.querySelector("#browse-search-form");
if (!(input instanceof HTMLInputElement) || !(form instanceof HTMLFormElement)) {
return;
}
input.value = btn.dataset.studioSelect ?? "";
form.requestSubmit();
const dropdown = btn.closest("ui-dropdown");
if (dropdown instanceof UIDropdown) {
dropdown.close({ restoreFocus: false });
}
});
};
const initCheckboxVisuals = (): void => {
const syncCheckboxVisual = (input: HTMLInputElement): void => {
const box = input.nextElementSibling;
if (!(box instanceof HTMLElement)) {
return;
}
const icon = box.querySelector("svg");
icon?.classList.toggle("hidden", !input.checked);
if (input.matches("[data-genre-visual]")) {
box.classList.toggle("border-accent", input.checked);
box.classList.toggle("bg-foreground-muted/12", input.checked);
box.classList.toggle("border-white/45", !input.checked);
box.classList.toggle("bg-transparent", !input.checked);
return;
}
if (input.matches("[data-sfw-checkbox]")) {
box.classList.toggle("border-accent", input.checked);
box.classList.toggle("bg-foreground-muted/12", input.checked);
box.classList.toggle("border-white/45", !input.checked);
box.classList.toggle("bg-transparent", !input.checked);
document
.querySelectorAll<HTMLInputElement>('input[type="hidden"][name="sfw"]')
.forEach((value) => {
value.value = String(input.checked);
});
}
};
document.addEventListener("change", (event) => {
const { target } = event;
if (!(target instanceof HTMLInputElement)) {
return;
}
if (!target.matches("[data-checkbox-visual], [data-sfw-checkbox], [data-genre-visual]")) {
return;
}
syncCheckboxVisual(target);
});
onHtmxLoad((root) => {
root
.querySelectorAll<HTMLInputElement>(
"[data-checkbox-visual], [data-sfw-checkbox], [data-genre-visual]",
)
.forEach(syncCheckboxVisual);
});
};
initStudioDropdown();
initCheckboxVisuals();