preline
Version: 
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
329 lines (259 loc) • 8.71 kB
text/typescript
/*
 * HSAccordion
 * @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 { getClassProperty, stringToBoolean, dispatch, afterTransition } from '../../utils';
import {
	IAccordionOptions,
	IAccordion,
	IAccordionTreeView,
	IAccordionTreeViewStaticOptions,
} from '../accordion/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('.hs-accordion-toggle') || null;
		this.content = this.el.querySelector('.hs-accordion-content') || null;
		this.group = this.el.closest('.hs-accordion-group') || 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 > .hs-accordion.active') &&
			this.group.querySelector(':scope > .hs-accordion.active') !== this.el
		) {
			const currentlyOpened = window.$hsAccordionCollection.find(
				(el) =>
					el.element.el ===
					this.group.querySelector(':scope > .hs-accordion.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.hs.accordion', 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.hs.accordion', 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.hs.accordion', 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.hs.accordion', this.el, this.el);
		});
	}
	public update() {
		this.group = this.el.closest('.hs-accordion-group') || null;
		if (!this.group) return false;
		this.isAlwaysOpened =
			this.group.hasAttribute('data-hs-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('.hs-accordion: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('.hs-accordion-treeview-root').length)
			return false;
		this.selectable = [];
		document
			.querySelectorAll('.hs-accordion-treeview-root')
			.forEach((el: HTMLElement) => {
				const data = el?.getAttribute('data-hs-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('.hs-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('.hs-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('.hs-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;