UNPKG

flyonui

Version:

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

285 lines (215 loc) 8.8 kB
/* * 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