UNPKG

flyonui

Version:

The easiest, free and open-source Tailwind CSS component library with semantic classes.

921 lines (727 loc) 28.8 kB
/* * HSDropdown * @version: 3.2.2 * @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, stringToBoolean } from '../../utils' import { autoUpdate, computePosition, flip, offset, type Placement, type Strategy, VirtualElement } from '@floating-ui/dom' import { IDropdown, IHTMLElementFloatingUI } from './interfaces' import HSBasePlugin from '../base-plugin' import HSAccessibilityObserver from '../accessibility-manager' import { ICollectionItem } from '../../interfaces' import { IAccessibilityComponent } from '../accessibility-manager/interfaces' import { POSITIONS } from '../../constants' class HSDropdown extends HSBasePlugin<{}, IHTMLElementFloatingUI> implements IDropdown { private accessibilityComponent: IAccessibilityComponent private readonly toggle: HTMLElement | null private readonly closers: HTMLElement[] | null public menu: HTMLElement | null private eventMode: string private closeMode: string private hasAutofocus: boolean private autofocusOnKeyboardOnly: boolean private animationInProcess: boolean private longPressTimer: number | null = null private openedViaKeyboard: boolean = false 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 > .dropdown-toggle') || this.el.querySelector(':scope > .dropdown-toggle-wrapper > .dropdown-toggle') || (this.el.children[0] as HTMLElement) this.closers = Array.from(this.el.querySelectorAll(':scope .dropdown-close')) || null this.menu = this.el.querySelector(':scope > .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.autofocusOnKeyboardOnly = stringToBoolean( getClassProperty(this.el, '--autofocus-on-keyboard-only', '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) } if (typeof window !== 'undefined') { if (!window.HSAccessibilityObserver) { window.HSAccessibilityObserver = new HSAccessibilityObserver() } this.setupAccessibility() } } resizeHandler() { 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.autofocusOnKeyboardOnly = stringToBoolean( getClassProperty(this.el, '--autofocus-on-keyboard-only', 'true') || 'true' ) } private isOpen(): boolean { return this.el.classList.contains('open') && !this.menu.classList.contains('hidden') } 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' this.menu.tabIndex = -1 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))) this.menu.addEventListener('click', () => { this.menu.focus() }) } 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) { const currentTrigger = getClassProperty(this.el, '--trigger', 'click') const isMouseHoverTrigger = currentTrigger === 'hover' && window.matchMedia('(hover: hover)').matches && (evt as PointerEvent).pointerType === 'mouse' if (isMouseHoverTrigger) { const el = evt.currentTarget as HTMLElement const isAnchor = el.tagName === 'A' const isNavLink = isAnchor && el.hasAttribute('href') && el.getAttribute('href') !== '#' if (!isNavLink) { evt.preventDefault() evt.stopPropagation() evt.stopImmediatePropagation?.() } return false } if (this.el.classList.contains('open') && !this.menu.classList.contains('hidden')) { this.close() } else { this.open() } } private onMouseEnterHandler() { const currentTrigger = getClassProperty(this.el, '--trigger', 'click') if (currentTrigger !== '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() { const currentTrigger = getClassProperty(this.el, '--trigger', 'click') if (currentTrigger !== '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) { input.focus() return true } const menuItems = this.menu.querySelectorAll( 'a:not([hidden]), button:not([hidden]), [role="menuitem"]:not([hidden]), .dropdown-item' ) if (menuItems.length > 0) { const firstItem = menuItems[0] as HTMLElement firstItem.focus() return true } return false } 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') || '6').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 }: { x: number; y: number; placement: Placement }) => { 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 public calculateFloatingUIPosition(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, openedViaKeyboard: boolean = false) { if (this.el.classList.contains('open') || this.animationInProcess) { return false } this.openedViaKeyboard = openedViaKeyboard 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 (window.HSAccessibilityObserver && this.accessibilityComponent) { window.HSAccessibilityObserver.updateComponentState(this.accessibilityComponent, true) } if (scope === 'window') this.menu.classList.add('open') this.animationInProcess = false if (this.hasAutofocus && (!this.autofocusOnKeyboardOnly || this.openedViaKeyboard)) this.focusElement() this.fireEvent('open', this.el) dispatch('open.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.openedViaKeyboard = false this.fireEvent('close', this.el) dispatch('close.dropdown', this.el, this.el) } this.animationInProcess = true if (scope === 'window') this.menu.classList.remove('open') if (window.HSAccessibilityObserver && this.accessibilityComponent) { window.HSAccessibilityObserver.updateComponentState(this.accessibilityComponent, false) } if (isAnimated) { const el: HTMLElement = this.el.querySelector('[data-dropdown-transition]') || this.menu let hasCompleted = false const completeClose = () => { if (hasCompleted) return hasCompleted = true this.destroyFloatingUI() } afterTransition(el, completeClose) const computedStyle = window.getComputedStyle(el) const transitionDuration = computedStyle.getPropertyValue('transition-duration') const duration = parseFloat(transitionDuration) * 1000 || 150 setTimeout(completeClose, duration + 50) } else { this.destroyFloatingUI() } clearAfterClose() } public forceClearState() { this.destroyFloatingUI() this.menu.style.margin = null this.el.classList.remove('open') this.menu.classList.add('hidden') this.openedViaKeyboard = false } 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) // Unregister accessibility // if (typeof window !== "undefined" && window.HSAccessibilityObserver) { // window.HSAccessibilityObserver.unregisterPlugin(this); // } } // 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 = [] 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('.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, openedViaKeyboard: boolean = false) { const instance = HSDropdown.findInCollection(target) if (instance && instance.element.menu.classList.contains('hidden')) instance.element.open(undefined, openedViaKeyboard) } static close(target: HSDropdown | HTMLElement | string) { const instance = HSDropdown.findInCollection(target) if (instance && !instance.element.menu.classList.contains('hidden')) instance.element.close() } static closeCurrentlyOpened(evtTarget: HTMLElement | null = null, isAnimated = true) { const parent = evtTarget && evtTarget.closest('.dropdown') && evtTarget.closest('.dropdown').parentElement.closest('.dropdown') ? evtTarget.closest('.dropdown').parentElement.closest('.dropdown') : null let currentlyOpened = parent ? window.$hsDropdownCollection.filter( el => el.element.el.classList.contains('open') && el.element.menu.closest('.dropdown').parentElement.closest('.dropdown') === parent ) : window.$hsDropdownCollection.filter(el => el.element.el.classList.contains('open')) if (evtTarget) { const dropdownElement = evtTarget.closest('.dropdown') as HTMLElement if (dropdownElement) { if (getClassPropertyAlt(dropdownElement, '--auto-close') === 'inside') { currentlyOpened = currentlyOpened.filter(el => el.element.el !== dropdownElement) } } else { const dropdownMenu = evtTarget.closest('.dropdown-menu') if (dropdownMenu) { const originalDropdown = window.$hsDropdownCollection.find(item => item.element.menu === dropdownMenu) if (originalDropdown && getClassPropertyAlt(originalDropdown.element.el, '--auto-close') === 'inside') { currentlyOpened = currentlyOpened.filter(el => el.element.el !== originalDropdown.element.el) } } } } // This is added in FlyonUI if ( evtTarget && evtTarget.closest('.dropdown') && getClassPropertyAlt(evtTarget.closest('.dropdown'), '--auto-close') === 'outside' ) { currentlyOpened = currentlyOpened.filter(el => el.element.el == evtTarget.closest('.dropdown')) currentlyOpened.forEach(el => el.element.close(isAnimated)) } // This condition work when true. 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 = '' }) } } // Accessibility methods private setupAccessibility(): void { this.accessibilityComponent = window.HSAccessibilityObserver.registerComponent( this.el, { onEnter: () => { if (!this.isOpened()) this.open(undefined, true) }, onSpace: () => { if (!this.isOpened()) this.open(undefined, true) }, onEsc: () => { if (this.isOpened()) { this.close() if (this.toggle) this.toggle.focus() } }, onArrow: (evt: KeyboardEvent) => { if (evt.metaKey) return switch (evt.key) { case 'ArrowDown': if (!this.isOpened()) this.open(undefined, true) else this.focusMenuItem('next') break case 'ArrowUp': if (this.isOpened()) this.focusMenuItem('prev') break case 'ArrowRight': this.onArrowX(evt, 'right') break case 'ArrowLeft': this.onArrowX(evt, 'left') break } }, onHome: () => { if (this.isOpened()) this.onStartEnd(true) }, onEnd: () => { if (this.isOpened()) this.onStartEnd(false) }, onTab: () => { if (this.isOpened()) this.close() }, onFirstLetter: (key: string) => { if (this.isOpened()) this.onFirstLetter(key) } }, this.isOpened(), 'Dropdown', '.dropdown', this.menu ) } private onFirstLetter(key: string): void { if (!this.isOpened() || !this.menu) return const menuItems = this.menu.querySelectorAll( 'a:not([hidden]), button:not([hidden]), [role="menuitem"]:not([hidden]), .dropdown-item' ) if (menuItems.length === 0) return const currentIndex = Array.from(menuItems).indexOf(document.activeElement as HTMLElement) for (let i = 1; i <= menuItems.length; i++) { const index = (currentIndex + i) % menuItems.length const text = (menuItems[index] as HTMLElement).textContent?.trim().toLowerCase() || '' if (text.startsWith(key.toLowerCase())) { ;(menuItems[index] as HTMLElement).focus() return } } ;(menuItems[0] as HTMLElement).focus() } private onArrowX(evt: KeyboardEvent, direction: 'left' | 'right'): void { if (!this.isOpened()) return evt.preventDefault() evt.stopImmediatePropagation() const menuItems = this.menu.querySelectorAll( 'a:not([hidden]), button:not([hidden]), [role="menuitem"]:not([hidden]), .dropdown-item' ) if (!menuItems.length) return const currentIndex = Array.from(menuItems).indexOf(document.activeElement as HTMLElement) let nextIndex = -1 if (direction === 'right') { nextIndex = (currentIndex + 1) % menuItems.length } else { nextIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1 } ;(menuItems[nextIndex] as HTMLElement).focus() } private onStartEnd(toStart: boolean = true): void { if (!this.isOpened()) return const menuItems = this.menu.querySelectorAll( 'a:not([hidden]), button:not([hidden]), [role="menuitem"]:not([hidden]), .dropdown-item' ) if (!menuItems.length) return const index = toStart ? 0 : menuItems.length - 1 ;(menuItems[index] as HTMLElement).focus() } private focusMenuItem(direction: 'next' | 'prev'): void { const menuItems = this.menu.querySelectorAll( 'a:not([hidden]), button:not([hidden]), [role="menuitem"]:not([hidden]), .dropdown-item' ) if (!menuItems.length) return const currentIndex = Array.from(menuItems).indexOf(document.activeElement as HTMLElement) const nextIndex = direction === 'next' ? (currentIndex + 1) % menuItems.length : (currentIndex - 1 + menuItems.length) % menuItems.length ;(menuItems[nextIndex] as HTMLElement).focus() } // Backward compatibility static on(evt: string, target: HSDropdown | HTMLElement | string, cb: Function) { const instance = HSDropdown.findInCollection(target) if (instance) instance.element.events[evt] = cb } public isOpened(): boolean { return this.isOpen() } public containsElement(element: HTMLElement): boolean { return this.el.contains(element) } } 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