UNPKG

preline

Version:

Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.

1,035 lines (861 loc) 27.2 kB
/* * HSDropdown * @version: 3.1.0 * @author: Preline Labs Ltd. * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) * Copyright 2024 Preline Labs Ltd. */ import { afterTransition, dispatch, getClassProperty, getClassPropertyAlt, isIOS, isIpadOS, menuSearchHistory, stringToBoolean, } from "../../utils"; import { IMenuSearchHistory } from "../../utils/interfaces"; import { autoUpdate, computePosition, flip, offset, type Placement, type Strategy, VirtualElement, } from "@floating-ui/dom"; import { IDropdown, IHTMLElementFloatingUI } from "../dropdown/interfaces"; import HSBasePlugin from "../base-plugin"; import { ICollectionItem } from "../../interfaces"; import { DROPDOWN_ACCESSIBILITY_KEY_SET, POSITIONS } from "../../constants"; class HSDropdown extends HSBasePlugin<{}, IHTMLElementFloatingUI> implements IDropdown { private static history: IMenuSearchHistory; private readonly toggle: HTMLElement | null; private readonly closers: HTMLElement[] | null; public menu: HTMLElement | null; private eventMode: string; private closeMode: string; private hasAutofocus: boolean; private animationInProcess: boolean; private longPressTimer: number | null = null; private onElementMouseEnterListener: () => void | null; private onElementMouseLeaveListener: () => void | null; private onToggleClickListener: (evt: Event) => void | null; private onToggleContextMenuListener: (evt: Event) => void | null; private onTouchStartListener: ((evt: TouchEvent) => void) | null = null; private onTouchEndListener: ((evt: TouchEvent) => void) | null = null; private onCloserClickListener: | { el: HTMLButtonElement; fn: () => void; }[] | null; constructor(el: IHTMLElementFloatingUI, options?: {}, events?: {}) { super(el, options, events); this.toggle = this.el.querySelector(":scope > .hs-dropdown-toggle") || this.el.querySelector( ":scope > .hs-dropdown-toggle-wrapper > .hs-dropdown-toggle", ) || (this.el.children[0] as HTMLElement); this.closers = Array.from(this.el.querySelectorAll(":scope .hs-dropdown-close")) || null; this.menu = this.el.querySelector(":scope > .hs-dropdown-menu"); this.eventMode = getClassProperty(this.el, "--trigger", "click"); this.closeMode = getClassProperty(this.el, "--auto-close", "true"); this.hasAutofocus = stringToBoolean( getClassProperty(this.el, "--has-autofocus", "true") || "true", ); this.animationInProcess = false; this.onCloserClickListener = []; if (this.toggle && this.menu) this.init(); } private elementMouseEnter() { this.onMouseEnterHandler(); } private elementMouseLeave() { this.onMouseLeaveHandler(); } private toggleClick(evt: Event) { this.onClickHandler(evt); } private toggleContextMenu(evt: MouseEvent) { evt.preventDefault(); this.onContextMenuHandler(evt); } private handleTouchStart(evt: TouchEvent): void { this.longPressTimer = window.setTimeout(() => { evt.preventDefault(); const touch = evt.touches[0]; const contextMenuEvent = new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, clientX: touch.clientX, clientY: touch.clientY, }); if (this.toggle) this.toggle.dispatchEvent(contextMenuEvent); }, 400); } private handleTouchEnd(evt: TouchEvent): void { if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } } private closerClick() { this.close(); } private init() { this.createCollection(window.$hsDropdownCollection, this); if ((this.toggle as HTMLButtonElement).disabled) return false; if (this.toggle) this.buildToggle(); if (this.menu) this.buildMenu(); if (this.closers) this.buildClosers(); if (!isIOS() && !isIpadOS()) { this.onElementMouseEnterListener = () => this.elementMouseEnter(); this.onElementMouseLeaveListener = () => this.elementMouseLeave(); this.el.addEventListener("mouseenter", this.onElementMouseEnterListener); this.el.addEventListener("mouseleave", this.onElementMouseLeaveListener); } } resizeHandler() { this.eventMode = getClassProperty(this.el, "--trigger", "click"); this.closeMode = getClassProperty(this.el, "--auto-close", "true"); } private buildToggle() { if (this?.toggle?.ariaExpanded) { if (this.el.classList.contains("open")) this.toggle.ariaExpanded = "true"; else this.toggle.ariaExpanded = "false"; } if (this.eventMode === "contextmenu") { this.onToggleContextMenuListener = (evt: MouseEvent) => this.toggleContextMenu(evt); this.onTouchStartListener = this.handleTouchStart.bind(this); this.onTouchEndListener = this.handleTouchEnd.bind(this); this.toggle.addEventListener( "contextmenu", this.onToggleContextMenuListener, ); this.toggle.addEventListener("touchstart", this.onTouchStartListener, { passive: false, }); this.toggle.addEventListener("touchend", this.onTouchEndListener); this.toggle.addEventListener("touchmove", this.onTouchEndListener); } else { this.onToggleClickListener = (evt) => this.toggleClick(evt); this.toggle.addEventListener("click", this.onToggleClickListener); } } private buildMenu() { this.menu.role = this.menu.getAttribute("role") || "menu"; const checkboxes = this.menu.querySelectorAll('[role="menuitemcheckbox"]'); const radiobuttons = this.menu.querySelectorAll('[role="menuitemradio"]'); checkboxes.forEach((el: HTMLElement) => el.addEventListener("click", () => this.selectCheckbox(el)) ); radiobuttons.forEach((el: HTMLElement) => el.addEventListener("click", () => this.selectRadio(el)) ); } private buildClosers() { this.closers.forEach((el: HTMLButtonElement) => { this.onCloserClickListener.push({ el, fn: () => this.closerClick(), }); el.addEventListener( "click", this.onCloserClickListener.find((closer) => closer.el === el).fn, ); }); } private getScrollbarSize() { let div = document.createElement("div"); div.style.overflow = "scroll"; div.style.width = "100px"; div.style.height = "100px"; document.body.appendChild(div); let scrollbarSize = div.offsetWidth - div.clientWidth; document.body.removeChild(div); return scrollbarSize; } private onContextMenuHandler(evt: MouseEvent) { const virtualElement: VirtualElement = { getBoundingClientRect: () => new DOMRect(), }; virtualElement.getBoundingClientRect = () => new DOMRect(evt.clientX, evt.clientY, 0, 0); HSDropdown.closeCurrentlyOpened(); if ( this.el.classList.contains("open") && !this.menu.classList.contains("hidden") ) { this.close(); document.body.style.overflow = ""; document.body.style.paddingRight = ""; } else { document.body.style.overflow = "hidden"; document.body.style.paddingRight = `${this.getScrollbarSize()}px`; this.open(virtualElement); } } private onClickHandler(evt: Event) { if ( this.el.classList.contains("open") && !this.menu.classList.contains("hidden") ) { this.close(); } else { this.open(); } } private onMouseEnterHandler() { if (this.eventMode !== "hover") return false; if ( !this.el._floatingUI || ( this.el._floatingUI && !this.el.classList.contains("open") ) ) this.forceClearState(); if ( !this.el.classList.contains("open") && this.menu.classList.contains("hidden") ) { this.open(); } } private onMouseLeaveHandler() { if (this.eventMode !== "hover") return false; if ( this.el.classList.contains("open") && !this.menu.classList.contains("hidden") ) { this.close(); } } private destroyFloatingUI() { const scope = (window.getComputedStyle(this.el).getPropertyValue("--scope") || "") .trim(); this.menu.classList.remove("block"); this.menu.classList.add("hidden"); this.menu.style.inset = null; this.menu.style.position = null; if (this.el && this.el._floatingUI) { this.el._floatingUI.destroy(); this.el._floatingUI = null; } if (scope === "window") this.el.appendChild(this.menu); this.animationInProcess = false; } private focusElement() { const input: HTMLInputElement = this.menu.querySelector("[autofocus]"); if (!input) return false; else input.focus(); } private setupFloatingUI(target?: VirtualElement | HTMLElement) { const _target = target || this.el; const computedStyle = window.getComputedStyle(this.el); const placementCss = (computedStyle.getPropertyValue("--placement") || "") .trim(); const flipCss = (computedStyle.getPropertyValue("--flip") || "true").trim(); const strategyCss = (computedStyle.getPropertyValue("--strategy") || "fixed").trim(); const offsetCss = (computedStyle.getPropertyValue("--offset") || "10") .trim(); const gpuAccelerationCss = (computedStyle.getPropertyValue("--gpu-acceleration") || "true").trim(); const adaptive = (window.getComputedStyle(this.el).getPropertyValue("--adaptive") || "adaptive").replace(" ", ""); const strategy = strategyCss as Strategy; const offsetValue = parseInt(offsetCss, 10); const placement: Placement = POSITIONS[placementCss] || "bottom-start"; const middleware = [ ...(flipCss === "true" ? [flip()] : []), offset(offsetValue), ]; const options = { placement, strategy, middleware, }; const checkSpaceAndAdjust = (x: number) => { const menuRect = this.menu.getBoundingClientRect(); const viewportWidth = window.innerWidth; const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; const availableWidth = viewportWidth - scrollbarWidth; if (x + menuRect.width > availableWidth) { x = availableWidth - menuRect.width; } if (x < 0) x = 0; return x; }; const update = () => { computePosition(_target, this.menu, options).then( ({ x, y, placement: computedPlacement }) => { const adjustedX = checkSpaceAndAdjust(x); if (strategy === "absolute" && adaptive === "none") { Object.assign(this.menu.style, { position: strategy, margin: "0", }); } else if (strategy === "absolute") { Object.assign(this.menu.style, { position: strategy, transform: `translate3d(${x}px, ${y}px, 0px)`, margin: "0", }); } else { if (gpuAccelerationCss === "true") { Object.assign(this.menu.style, { position: strategy, left: "", top: "", inset: "0px auto auto 0px", margin: "0", transform: `translate3d(${ adaptive === "adaptive" ? adjustedX : 0 }px, ${y}px, 0)`, }); } else { Object.assign(this.menu.style, { position: strategy, left: `${x}px`, top: `${y}px`, transform: "", }); } } this.menu.setAttribute("data-placement", computedPlacement); }, ); }; update(); const cleanup = autoUpdate(_target, this.menu, update); return { update, destroy: cleanup, }; } private selectCheckbox(target: HTMLElement) { target.ariaChecked = target.ariaChecked === "true" ? "false" : "true"; } private selectRadio(target: HTMLElement) { if (target.ariaChecked === "true") return false; const group = target.closest(".group"); const items = group.querySelectorAll('[role="menuitemradio"]'); const otherItems = Array.from(items).filter((el) => el !== target); otherItems.forEach((el) => { el.ariaChecked = "false"; }); target.ariaChecked = "true"; } // Public methods // TODO:: rename "Popper" to "FLoatingUI" public calculatePopperPosition(target?: VirtualElement | HTMLElement) { const floatingUIInstance = this.setupFloatingUI(target); const floatingUIPosition = this.menu.getAttribute("data-placement"); floatingUIInstance.update(); floatingUIInstance.destroy(); return floatingUIPosition; } public open(target?: VirtualElement | HTMLElement) { if (this.el.classList.contains("open") || this.animationInProcess) { return false; } this.animationInProcess = true; this.menu.style.cssText = ""; const _target = target || this.el; const computedStyle = window.getComputedStyle(this.el); const scope = (computedStyle.getPropertyValue("--scope") || "").trim(); const strategyCss = (computedStyle.getPropertyValue("--strategy") || "fixed").trim(); const strategy = strategyCss as Strategy; if (scope === "window") document.body.appendChild(this.menu); if (strategy !== ("static" as Strategy)) { this.el._floatingUI = this.setupFloatingUI(_target); } this.menu.style.margin = null; this.menu.classList.remove("hidden"); this.menu.classList.add("block"); setTimeout(() => { if (this?.toggle?.ariaExpanded) this.toggle.ariaExpanded = "true"; this.el.classList.add("open"); if (scope === "window") this.menu.classList.add("open"); this.animationInProcess = false; if (this.hasAutofocus) this.focusElement(); this.fireEvent("open", this.el); dispatch("open.hs.dropdown", this.el, this.el); }); } public close(isAnimated = true) { if (this.animationInProcess || !this.el.classList.contains("open")) { return false; } const scope = (window.getComputedStyle(this.el).getPropertyValue("--scope") || "") .trim(); const clearAfterClose = () => { this.menu.style.margin = null; if (this?.toggle?.ariaExpanded) this.toggle.ariaExpanded = "false"; this.el.classList.remove("open"); this.fireEvent("close", this.el); dispatch("close.hs.dropdown", this.el, this.el); }; this.animationInProcess = true; if (scope === "window") this.menu.classList.remove("open"); if (isAnimated) { const el: HTMLElement = this.el.querySelector("[data-hs-dropdown-transition]") || this.menu; afterTransition(el, () => this.destroyFloatingUI()); } else { this.destroyFloatingUI(); } clearAfterClose(); } public forceClearState() { this.destroyFloatingUI(); this.menu.style.margin = null; this.el.classList.remove("open"); this.menu.classList.add("hidden"); } public destroy() { // Remove listeners if (!isIOS() && !isIpadOS()) { this.el.removeEventListener( "mouseenter", this.onElementMouseEnterListener, ); this.el.removeEventListener( "mouseleave", () => this.onElementMouseLeaveListener, ); this.onElementMouseEnterListener = null; this.onElementMouseLeaveListener = null; } if (this.eventMode === "contextmenu") { if (this.toggle) { this.toggle.removeEventListener( "contextmenu", this.onToggleContextMenuListener, ); this.toggle.removeEventListener( "touchstart", this.onTouchStartListener, ); this.toggle.removeEventListener("touchend", this.onTouchEndListener); this.toggle.removeEventListener("touchmove", this.onTouchEndListener); } this.onToggleContextMenuListener = null; this.onTouchStartListener = null; this.onTouchEndListener = null; } else { if (this.toggle) { this.toggle.removeEventListener("click", this.onToggleClickListener); } this.onToggleClickListener = null; } if (this.closers.length) { this.closers.forEach((el: HTMLButtonElement) => { el.removeEventListener( "click", this.onCloserClickListener.find((closer) => closer.el === el).fn, ); }); this.onCloserClickListener = null; } // Remove classes this.el.classList.remove("open"); this.destroyFloatingUI(); window.$hsDropdownCollection = window.$hsDropdownCollection.filter(( { element }, ) => element.el !== this.el); } // Static methods private static findInCollection( target: HSDropdown | HTMLElement | string, ): ICollectionItem<HSDropdown> | null { return window.$hsDropdownCollection.find((el) => { if (target instanceof HSDropdown) return el.element.el === target.el; else if (typeof target === "string") { return el.element.el === document.querySelector(target); } else return el.element.el === target; }) || null; } static getInstance(target: HTMLElement | string, isInstance?: boolean) { const elInCollection = window.$hsDropdownCollection.find( (el) => el.element.el === (typeof target === "string" ? document.querySelector(target) : target), ); return elInCollection ? isInstance ? elInCollection : elInCollection.element : null; } static autoInit() { if (!window.$hsDropdownCollection) { window.$hsDropdownCollection = []; document.addEventListener( "keydown", (evt) => HSDropdown.accessibility(evt), ); window.addEventListener("click", (evt) => { const evtTarget = evt.target; HSDropdown.closeCurrentlyOpened(evtTarget as HTMLElement); }); let prevWidth = window.innerWidth; window.addEventListener("resize", () => { if (window.innerWidth !== prevWidth) { prevWidth = innerWidth; HSDropdown.closeCurrentlyOpened(null, false); } }); } if (window.$hsDropdownCollection) { window.$hsDropdownCollection = window.$hsDropdownCollection.filter( ({ element }) => document.contains(element.el), ); } document .querySelectorAll(".hs-dropdown:not(.--prevent-on-load-init)") .forEach((el: IHTMLElementFloatingUI) => { if ( !window.$hsDropdownCollection.find( (elC) => (elC?.element?.el as HTMLElement) === el, ) ) { new HSDropdown(el); } }); } static open(target: HSDropdown | HTMLElement | string) { const instance = HSDropdown.findInCollection(target); if ( instance && instance.element.menu.classList.contains("hidden") ) instance.element.open(); } static close(target: HSDropdown | HTMLElement | string) { const instance = HSDropdown.findInCollection(target); if ( instance && !instance.element.menu.classList.contains("hidden") ) instance.element.close(); } // Accessibility methods static accessibility(evt: KeyboardEvent) { this.history = menuSearchHistory; const target: ICollectionItem<HSDropdown> | null = window .$hsDropdownCollection.find((el) => el.element.el.classList.contains("open") ); if ( target && (DROPDOWN_ACCESSIBILITY_KEY_SET.includes(evt.code) || (evt.code.length === 4 && evt.code[evt.code.length - 1].match(/^[A-Z]*$/))) && !evt.metaKey && !target.element.menu.querySelector("input:focus") && !target.element.menu.querySelector("textarea:focus") ) { switch (evt.code) { case "Escape": if (!target.element.menu.querySelector(".hs-select.active")) { evt.preventDefault(); this.onEscape(evt); } break; case "Enter": if ( !target.element.menu.querySelector(".hs-select button:focus") && !target.element.menu.querySelector(".hs-collapse-toggle:focus") ) { this.onEnter(evt); } break; case "ArrowUp": evt.preventDefault(); evt.stopImmediatePropagation(); this.onArrow(); break; case "ArrowDown": evt.preventDefault(); evt.stopImmediatePropagation(); this.onArrow(false); break; case "ArrowRight": evt.preventDefault(); evt.stopImmediatePropagation(); this.onArrowX(evt, "right"); break; case "ArrowLeft": evt.preventDefault(); evt.stopImmediatePropagation(); this.onArrowX(evt, "left"); break; case "Home": evt.preventDefault(); evt.stopImmediatePropagation(); this.onStartEnd(); break; case "End": evt.preventDefault(); evt.stopImmediatePropagation(); this.onStartEnd(false); break; default: evt.preventDefault(); this.onFirstLetter(evt.key); break; } } } static onEscape(evt: KeyboardEvent) { const dropdown = (evt.target as HTMLElement).closest(".hs-dropdown.open"); if (window.$hsDropdownCollection.find((el) => el.element.el === dropdown)) { const target = window.$hsDropdownCollection.find( (el) => el.element.el === dropdown, ); if (target) { target.element.close(); target.element.toggle.focus(); } } else { this.closeCurrentlyOpened(); } } static onEnter(evt: KeyboardEvent) { const target = evt.target as HTMLElement; const { element } = window.$hsDropdownCollection.find( (el) => el.element.el === target.closest(".hs-dropdown"), ) ?? null; if (element && target.classList.contains("hs-dropdown-toggle")) { evt.preventDefault(); element.open(); } else if (element && target.getAttribute("role") === "menuitemcheckbox") { element.selectCheckbox(target); element.close(); } else if (element && target.getAttribute("role") === "menuitemradio") { element.selectRadio(target); element.close(); } else { return false; } } static onArrow(isArrowUp = true) { const target = window.$hsDropdownCollection.find((el) => el.element.el.classList.contains("open") ); if (target) { const menu = target.element.menu; if (!menu) return false; const preparedLinks = isArrowUp ? Array.from( menu.querySelectorAll( 'a:not([hidden]), :scope button:not([hidden]), [role="button"]:not([hidden]), [role^="menuitem"]:not([hidden])', ), ).reverse() : Array.from( menu.querySelectorAll( 'a:not([hidden]), :scope button:not([hidden]), [role="button"]:not([hidden]), [role^="menuitem"]:not([hidden])', ), ); const visiblePreparedLinks = Array.from(preparedLinks).filter((item) => { const el = item as HTMLElement; return el.closest("[hidden]") === null && el.offsetParent !== null; }); const links = visiblePreparedLinks.filter( (el: any) => !el.classList.contains("disabled"), ); const current = menu.querySelector( 'a:focus, button:focus, [role="button"]:focus, [role^="menuitem"]:focus', ); let currentInd = links.findIndex((el: any) => el === current); if (currentInd + 1 < links.length) { currentInd++; } (links[currentInd] as HTMLButtonElement | HTMLAnchorElement).focus(); } } static onArrowX(evt: KeyboardEvent, direction: "right" | "left") { const toggle = evt.target as HTMLElement; const closestDropdown = toggle.closest(".hs-dropdown.open"); const isRootDropdown = !!closestDropdown && !closestDropdown?.parentElement.closest(".hs-dropdown"); const menuToOpen = (HSDropdown.getInstance( toggle.closest(".hs-dropdown") as HTMLElement, true, ) as ICollectionItem<HSDropdown>) ?? null; const firstLink = menuToOpen.element.menu.querySelector( 'a, button, [role="button"], [role^="menuitem"]', ) as HTMLButtonElement; if (isRootDropdown && !toggle.classList.contains("hs-dropdown-toggle")) { return false; } const menuToClose = (HSDropdown.getInstance( toggle.closest(".hs-dropdown.open") as HTMLElement, true, ) as ICollectionItem<HSDropdown>) ?? null; if ( menuToOpen.element.el.classList.contains("open") && menuToOpen.element.el._floatingUI.state.placement.includes(direction) ) { firstLink.focus(); return false; } // TODO:: rename "Popper" to "FLoatingUI" const futurePosition = menuToOpen.element.calculatePopperPosition(); if (isRootDropdown && !futurePosition.includes(direction)) return false; if ( futurePosition.includes(direction) && toggle.classList.contains("hs-dropdown-toggle") ) { menuToOpen.element.open(); firstLink.focus(); } else { menuToClose.element.close(false); menuToClose.element.toggle.focus(); } } static onStartEnd(isStart = true) { const target = window.$hsDropdownCollection.find((el) => el.element.el.classList.contains("open") ); if (target) { const menu = target.element.menu; if (!menu) return false; const preparedLinks = isStart ? Array.from( menu.querySelectorAll( 'a, button, [role="button"], [role^="menuitem"]', ), ) : Array.from( menu.querySelectorAll( 'a, button, [role="button"], [role^="menuitem"]', ), ).reverse(); const links = preparedLinks.filter( (el: any) => !el.classList.contains("disabled"), ); if (links.length) { (links[0] as HTMLButtonElement).focus(); } } } static onFirstLetter(code: string) { const target = window.$hsDropdownCollection.find((el) => el.element.el.classList.contains("open") ); if (target) { const menu = target.element.menu; if (!menu) return false; const links = Array.from( menu.querySelectorAll('a, [role="button"], [role^="menuitem"]'), ); const getCurrentInd = () => links.findIndex( (el, i) => (el as HTMLElement).innerText.toLowerCase().charAt(0) === code.toLowerCase() && this.history.existsInHistory(i), ); let currentInd = getCurrentInd(); if (currentInd === -1) { this.history.clearHistory(); currentInd = getCurrentInd(); } if (currentInd !== -1) { (links[currentInd] as HTMLElement).focus(); this.history.addHistory(currentInd); } } } static closeCurrentlyOpened( evtTarget: HTMLElement | null = null, isAnimated = true, ) { const parent = evtTarget && evtTarget.closest(".hs-dropdown") && evtTarget.closest(".hs-dropdown").parentElement.closest(".hs-dropdown") ? evtTarget .closest(".hs-dropdown") .parentElement.closest(".hs-dropdown") : null; let currentlyOpened = parent ? window.$hsDropdownCollection.filter( (el) => el.element.el.classList.contains("open") && el.element.menu .closest(".hs-dropdown") .parentElement.closest(".hs-dropdown") === parent, ) : window.$hsDropdownCollection.filter((el) => el.element.el.classList.contains("open") ); if ( evtTarget && evtTarget.closest(".hs-dropdown") && getClassPropertyAlt(evtTarget.closest(".hs-dropdown"), "--auto-close") === "inside" ) { currentlyOpened = currentlyOpened.filter( (el) => el.element.el !== evtTarget.closest(".hs-dropdown"), ); } if (currentlyOpened) { currentlyOpened.forEach((el) => { if ( el.element.closeMode === "false" || el.element.closeMode === "outside" ) { return false; } el.element.close(isAnimated); }); } if (currentlyOpened) { currentlyOpened.forEach((el) => { if (getClassPropertyAlt(el.element.el, "--trigger") !== "contextmenu") { return false; } document.body.style.overflow = ""; document.body.style.paddingRight = ""; }); } } // Backward compatibility static on( evt: string, target: HSDropdown | HTMLElement | string, cb: Function, ) { const instance = HSDropdown.findInCollection(target); if (instance) instance.element.events[evt] = cb; } } declare global { interface Window { HSDropdown: Function; $hsDropdownCollection: ICollectionItem<HSDropdown>[]; } } window.addEventListener("load", () => { HSDropdown.autoInit(); // Uncomment for debug // console.log('Dropdown collection:', window.$hsDropdownCollection); }); window.addEventListener("resize", () => { if (!window.$hsDropdownCollection) window.$hsDropdownCollection = []; window.$hsDropdownCollection.forEach((el) => el.element.resizeHandler()); }); if (typeof window !== "undefined") { window.HSDropdown = HSDropdown; } export default HSDropdown;