flyonui
Version:
The easiest, free and open-source Tailwind CSS component library with semantic classes.
285 lines (215 loc) • 8.8 kB
text/typescript
/*
* HSAccordion
* @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 { getClassProperty, stringToBoolean, dispatch, afterTransition } from '../../utils'
import { IAccordionOptions, IAccordion, IAccordionTreeView, IAccordionTreeViewStaticOptions } from './interfaces'
import HSBasePlugin from '../base-plugin'
import { ICollectionItem } from '../../interfaces'
class HSAccordion extends HSBasePlugin<IAccordionOptions> implements IAccordion {
private toggle: HTMLElement | null
public content: HTMLElement | null
private group: HTMLElement | null
private isAlwaysOpened: boolean
private keepOneOpen: boolean
private isToggleStopPropagated: boolean
private onToggleClickListener: (evt: Event) => void
static selectable: IAccordionTreeView[]
constructor(el: HTMLElement, options?: IAccordionOptions, events?: {}) {
super(el, options, events)
this.toggle = this.el.querySelector('.accordion-toggle') || null
this.content = this.el.querySelector('.accordion-content') || null
this.group = this.el.closest('.accordion') || null
this.update()
this.isToggleStopPropagated = stringToBoolean(
getClassProperty(this.toggle, '--stop-propagation', 'false') || 'false'
)
this.keepOneOpen = this.group
? stringToBoolean(getClassProperty(this.group, '--keep-one-open', 'false') || 'false')
: false
if (this.toggle && this.content) this.init()
}
private init() {
this.createCollection(window.$hsAccordionCollection, this)
this.onToggleClickListener = (evt: Event) => this.toggleClick(evt)
this.toggle.addEventListener('click', this.onToggleClickListener)
}
// Public methods
public toggleClick(evt: Event) {
if (this.el.classList.contains('active') && this.keepOneOpen) return false
if (this.isToggleStopPropagated) evt.stopPropagation()
if (this.el.classList.contains('active')) {
this.hide()
} else {
this.show()
}
}
public show() {
if (
this.group &&
!this.isAlwaysOpened &&
this.group.querySelector(':scope > .accordion-item.active') &&
this.group.querySelector(':scope > .accordion-item.active') !== this.el
) {
const currentlyOpened = window.$hsAccordionCollection.find(
el => el.element.el === this.group.querySelector(':scope > .accordion-item.active')
)
currentlyOpened.element.hide()
}
if (this.el.classList.contains('active')) return false
this.el.classList.add('active')
if (this?.toggle?.ariaExpanded) this.toggle.ariaExpanded = 'true'
this.fireEvent('beforeOpen', this.el)
dispatch('beforeOpen.accordion.item', this.el, this.el)
this.content.style.display = 'block'
this.content.style.height = '0'
setTimeout(() => {
this.content.style.height = `${this.content.scrollHeight}px`
afterTransition(this.content, () => {
this.content.style.display = 'block'
this.content.style.height = ''
this.fireEvent('open', this.el)
dispatch('open.accordion.item', this.el, this.el)
})
})
}
public hide() {
if (!this.el.classList.contains('active')) return false
this.el.classList.remove('active')
if (this?.toggle?.ariaExpanded) this.toggle.ariaExpanded = 'false'
this.fireEvent('beforeClose', this.el)
dispatch('beforeClose.accordion.item', this.el, this.el)
this.content.style.height = `${this.content.scrollHeight}px`
setTimeout(() => {
this.content.style.height = '0'
})
afterTransition(this.content, () => {
this.content.style.display = 'none'
this.content.style.height = ''
this.fireEvent('close', this.el)
dispatch('close.accordion.item', this.el, this.el)
})
}
public update() {
this.group = this.el.closest('.accordion') || null
if (!this.group) return false
this.isAlwaysOpened = this.group.hasAttribute('data-accordion-always-open') || false
window.$hsAccordionCollection.map(el => {
if (el.id === this.el.id) {
el.element.group = this.group
el.element.isAlwaysOpened = this.isAlwaysOpened
}
return el
})
}
public destroy() {
if (HSAccordion?.selectable?.length) {
HSAccordion.selectable.forEach(item => {
item.listeners.forEach(({ el, listener }) => {
el.removeEventListener('click', listener)
})
})
}
if (this.onToggleClickListener) {
this.toggle.removeEventListener('click', this.onToggleClickListener)
}
this.toggle = null
this.content = null
this.group = null
this.onToggleClickListener = null
window.$hsAccordionCollection = window.$hsAccordionCollection.filter(({ element }) => element.el !== this.el)
}
// Static methods
private static findInCollection(target: HSAccordion | HTMLElement | string): ICollectionItem<HSAccordion> | null {
return (
window.$hsAccordionCollection.find(el => {
if (target instanceof HSAccordion) 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 autoInit() {
if (!window.$hsAccordionCollection) window.$hsAccordionCollection = []
if (window.$hsAccordionCollection) {
window.$hsAccordionCollection = window.$hsAccordionCollection.filter(({ element }) =>
document.contains(element.el)
)
}
document.querySelectorAll('.accordion-item:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => {
if (!window.$hsAccordionCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) new HSAccordion(el)
})
}
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsAccordionCollection.find(
el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target)
)
return elInCollection ? (isInstance ? elInCollection : elInCollection.element.el) : null
}
static show(target: HSAccordion | HTMLElement | string) {
const instance = HSAccordion.findInCollection(target)
if (instance && instance.element.content.style.display !== 'block') instance.element.show()
}
static hide(target: HSAccordion | HTMLElement | string) {
const instance = HSAccordion.findInCollection(target)
const style = instance ? window.getComputedStyle(instance.element.content) : null
if (instance && style.display !== 'none') instance.element.hide()
}
static onSelectableClick = (evt: Event, item: IAccordionTreeView, el: HTMLElement) => {
evt.stopPropagation()
HSAccordion.toggleSelected(item, el)
}
static treeView() {
if (!document.querySelectorAll('.accordion-treeview-root').length) return false
this.selectable = []
document.querySelectorAll('.accordion-treeview-root').forEach((el: HTMLElement) => {
const data = el?.getAttribute('data-accordion-options')
const options: IAccordionTreeViewStaticOptions = data ? JSON.parse(data) : {}
this.selectable.push({
el,
options: { ...options },
listeners: []
})
})
if (this.selectable.length)
this.selectable.forEach(item => {
const { el } = item
el.querySelectorAll('.accordion-selectable').forEach((_el: HTMLElement) => {
const listener = (evt: Event) => this.onSelectableClick(evt, item, _el)
_el.addEventListener('click', listener)
item.listeners.push({ el: _el, listener })
})
})
}
static toggleSelected(root: IAccordionTreeView, item: HTMLElement) {
if (item.classList.contains('selected')) item.classList.remove('selected')
else {
root.el.querySelectorAll('.accordion-selectable').forEach((el: HTMLElement) => el.classList.remove('selected'))
item.classList.add('selected')
}
}
// Backward compatibility
static on(evt: string, target: HSAccordion | HTMLElement | string, cb: Function) {
const instance = HSAccordion.findInCollection(target)
if (instance) instance.element.events[evt] = cb
}
}
declare global {
interface Window {
HSAccordion: Function
$hsAccordionCollection: ICollectionItem<HSAccordion>[]
}
}
window.addEventListener('load', () => {
HSAccordion.autoInit()
if (document.querySelectorAll('.accordion-treeview-root').length) HSAccordion.treeView()
// Uncomment for debug
// console.log('Accordion collection:', window.$hsAccordionCollection);
})
if (typeof window !== 'undefined') {
window.HSAccordion = HSAccordion
}
export default HSAccordion