flyonui
Version:
The easiest, free and open-source Tailwind CSS component library with semantic classes.
899 lines (724 loc) • 28.5 kB
text/typescript
/*
* HSDropdown
* @version: 3.0.1
* @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 > .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.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') || '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`,
margin: '0',
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) {
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.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.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-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.el) : 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('.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('.select.active')) {
evt.preventDefault()
this.onEscape(evt)
}
break
case 'Enter':
if (
!target.element.menu.querySelector('.select button:focus') &&
!target.element.menu.querySelector('.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('.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('.dropdown')) ?? null
if (element && target.classList.contains('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], .dropdown-item, form)'
)
)
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, .dropdown-item:focus, button: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('.dropdown.open')
const isRootDropdown = !!closestDropdown && !closestDropdown?.parentElement.closest('.dropdown')
const menuToOpen =
(HSDropdown.getInstance(toggle.closest('.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('dropdown-toggle')) {
return false
}
const menuToClose =
(HSDropdown.getInstance(toggle.closest('.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
}
const futurePosition = menuToOpen.element.calculateFLoatingUIPosition()
if (isRootDropdown && !futurePosition.includes(direction)) return false
if (futurePosition.includes(direction) && toggle.classList.contains('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('.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 &&
evtTarget.closest('.dropdown') &&
getClassPropertyAlt(evtTarget.closest('.dropdown'), '--auto-close') === 'inside'
) {
currentlyOpened = currentlyOpened.filter(el => el.element.el !== evtTarget.closest('.dropdown'))
}
// 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 = ''
})
}
}
// 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