UNPKG

@studiocms/ui

Version:

The UI library for StudioCMS. Includes the layouts & components we use to build StudioCMS.

202 lines (201 loc) 6.82 kB
class DropdownHelper { container; toggleEl; dropdown; alignment; triggerOn; fullWidth = false; focusIndex = -1; active = false; /** * A helper function to interact with dropdowns. * @param id The ID of the dropdown. * @param fullWidth Whether the dropdown should be full width. Not needed normally. */ constructor(id, fullWidth) { this.container = document.getElementById(`${id}-container`); if (!this.container) { throw new Error(`Unable to find dropdown with ID ${id}.`); } this.alignment = this.container.dataset.align; this.triggerOn = this.container.dataset.trigger; this.toggleEl = document.getElementById(`${id}-toggle-btn`); this.dropdown = document.getElementById(`${id}-dropdown`); if (fullWidth) this.fullWidth = true; this.hideOnClickOutside(this.container); this.initialBehaviorRegistration(); this.initialOptClickRegistration(); } /** * Registers a click callback for the dropdown options. Whenever one of the options * is clicked, the callback will be called with the value of the option. * @param func The callback function. */ registerClickCallback = (func) => { const dropdownOpts = this.dropdown.querySelectorAll("li"); for (const opt of dropdownOpts) { opt.removeEventListener("click", this.hide); opt.addEventListener("click", () => { func(opt.dataset.value || ""); this.hide(); }); } }; /** * Sets up all listeners for the dropdown. */ initialBehaviorRegistration = () => { window.addEventListener("scroll", this.hide); document.addEventListener("keydown", (e) => { if (e.key === "Escape") this.hide(); }); document.addEventListener("astro:before-preparation", () => { this.dropdown.classList.remove("initialized"); }); if (this.triggerOn === "left") { this.toggleEl.addEventListener("click", this.toggle); } else if (this.triggerOn === "both") { this.toggleEl.addEventListener("click", this.toggle); this.toggleEl.addEventListener("contextmenu", (e) => { e.preventDefault(); this.toggle(); }); } else { this.toggleEl.addEventListener("contextmenu", (e) => { e.preventDefault(); this.toggle(); }); } this.toggleEl.addEventListener("keydown", (e) => { if (!this.active) return; if (e.key === "Enter") { e.preventDefault(); const focused = this.dropdown.querySelector("li.focused"); if (!focused) { this.hide(); return; } focused.click(); } if (e.key === "ArrowDown") { e.preventDefault(); this.focusIndex = this.focusIndex === this.dropdown.children.length - 1 ? 0 : this.focusIndex + 1; } if (e.key === "ArrowUp") { e.preventDefault(); this.focusIndex = this.focusIndex === 0 ? this.dropdown.children.length - 1 : this.focusIndex - 1; } if (e.key === "ArrowDown" || e.key === "ArrowUp") { if (this.focusIndex > this.dropdown.children.length - 1) { this.focusIndex = 0; } this.dropdown.querySelector("li.focused")?.classList.remove("focused"); const newFocus = this.dropdown.children[this.focusIndex]; if (!newFocus) return; newFocus.classList.add("focused"); newFocus.focus(); } }); }; /** * Registers callbacks to hide the dropdown when an option is clicked. */ initialOptClickRegistration = () => { const dropdownOpts = this.dropdown.querySelectorAll("li"); for (const opt of dropdownOpts) { opt.addEventListener("click", this.hide); } }; /** * A function to toggle the dropdown. */ toggle = () => { if (this.active) { this.hide(); return; } this.show(); }; /** * A function to hide the dropdown. */ hide = () => { this.dropdown.classList.remove("active"); this.active = false; this.focusIndex = -1; this.dropdown.querySelector("li.focused")?.classList.remove("focused"); setTimeout(() => this.dropdown.classList.remove("above", "below"), 200); }; /** * A function to show the dropdown. */ show = () => { const isMobile = window.matchMedia("screen and (max-width: 840px)").matches; const { bottom, left, right, width: parentWidth, x, y, height } = this.toggleEl.getBoundingClientRect(); const { width: dropdownWidth } = this.dropdown.getBoundingClientRect(); const optionHeight = 43.28; const totalBorderSize = 2; const margin = 4; const dropdownHeight = this.dropdown.children.length * optionHeight + totalBorderSize + margin; const CustomRect = { top: bottom + margin, left, right, bottom: bottom + margin + dropdownHeight, width: isMobile || this.fullWidth ? parentWidth : dropdownWidth, // Account for scaling of animation height: dropdownHeight, x, y: y + height + margin }; this.active = true; if (isMobile || this.fullWidth) { this.dropdown.style.maxWidth = `${parentWidth}px`; this.dropdown.style.minWidth = "unset"; this.dropdown.style.width = `${parentWidth}px`; this.dropdown.style.left = `calc(${parentWidth / 2}px - ${CustomRect.width / 2}px)`; } else { if (this.alignment === "end") { this.dropdown.style.left = `calc(${parentWidth}px - ${CustomRect.width}px)`; } if (this.alignment === "center") { this.dropdown.style.left = `calc(${parentWidth / 2}px - ${CustomRect.width / 2}px)`; } } if (!this.dropdown.classList.contains("initialized")) { this.dropdown.classList.add("initialized"); } if (CustomRect.top >= 0 && CustomRect.left >= 0 && CustomRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && CustomRect.right <= (window.innerWidth || document.documentElement.clientWidth)) { this.dropdown.classList.add("active", "below"); this.focusIndex = -1; } else { this.dropdown.classList.add("active", "above"); this.focusIndex = this.dropdown.children.length; } }; /** * A jQuery-like function to hide the dropdown when clicking outside of it. * @param element The element to hide when clicking outside of it. */ hideOnClickOutside = (element) => { const outsideClickListener = (event) => { if (!event.target) return; if (!element.contains(event.target) && isVisible(element) && this.active === true) { this.hide(); } }; document.addEventListener("click", outsideClickListener); }; } const isVisible = (elem) => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); export { DropdownHelper };