UNPKG

flyonui

Version:

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

337 lines (262 loc) 9.87 kB
/* * HSTreeView * @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 { dispatch } from '../../utils' import { ITreeView, ITreeViewItem, ITreeViewOptions, ITreeViewOptionsControlBy } from './interfaces' import { ICollectionItem } from '../../interfaces' import HSBasePlugin from '../base-plugin' class HSTreeView extends HSBasePlugin<ITreeViewOptions> implements ITreeView { private items: ITreeViewItem[] = [] private readonly controlBy: ITreeViewOptionsControlBy private readonly autoSelectChildren: boolean private readonly isIndeterminate: boolean static group: number = 0 private onElementClickListener: | { el: Element fn: (evt: PointerEvent) => void }[] | null private onControlChangeListener: | { el: Element fn: () => void }[] | null constructor(el: HTMLElement, options?: ITreeViewOptions, events?: {}) { super(el, options, events) const data = el.getAttribute('data-tree-view') const dataOptions: ITreeView = data ? JSON.parse(data) : {} const concatOptions = { ...dataOptions, ...options } this.controlBy = concatOptions?.controlBy || 'button' this.autoSelectChildren = concatOptions?.autoSelectChildren || false this.isIndeterminate = concatOptions?.isIndeterminate || true this.onElementClickListener = [] this.onControlChangeListener = [] this.init() } private elementClick(evt: PointerEvent, el: Element, data: ITreeViewItem) { evt.stopPropagation() if (el.classList.contains('disabled')) return false if (!evt.metaKey && !evt.shiftKey) this.unselectItem(data) this.selectItem(el, data) this.fireEvent('click', { el, data: data }) dispatch('click.treeView', this.el, { el, data: data }) } private controlChange(el: Element, data: ITreeViewItem) { if (this.autoSelectChildren) { this.selectItem(el, data) if (data.isDir) this.selectChildren(el, data) this.toggleParent(el) } else this.selectItem(el, data) } private init() { this.createCollection(window.$hsTreeViewCollection, this) HSTreeView.group += 1 this.initItems() } private initItems() { this.el.querySelectorAll('[data-tree-view-item]').forEach((el, ind) => { const data = JSON.parse(el.getAttribute('data-tree-view-item')) if (!el.id) el.id = `tree-view-item-${HSTreeView.group}-${ind}` const concatData = { ...data, id: data.id ?? el.id, path: this.getPath(el), isSelected: data.isSelected ?? false } this.items.push(concatData) if (this.controlBy === 'checkbox') this.controlByCheckbox(el, concatData) else this.controlByButton(el, concatData) }) } private controlByButton(el: Element, data: ITreeViewItem) { this.onElementClickListener.push({ el, fn: (evt: PointerEvent) => this.elementClick(evt, el, data) }) el.addEventListener('click', this.onElementClickListener.find(elI => elI.el === el).fn) } private controlByCheckbox(el: Element, data: ITreeViewItem) { const control = el.querySelector(`input[value="${data.value}"]`) if (!!control) { this.onControlChangeListener.push({ el: control, fn: () => this.controlChange(el, data) }) control.addEventListener('change', this.onControlChangeListener.find(elI => elI.el === control).fn) } } private getItem(id: string) { return this.items.find(el => el.id === id) } private getPath(el: Element) { const path: any = [] let parent = el.closest('[data-tree-view-item]') while (!!parent) { const data = JSON.parse(parent.getAttribute('data-tree-view-item')) path.push(data.value) parent = parent.parentElement?.closest('[data-tree-view-item]') } return path.reverse().join('/') } private unselectItem(exception: ITreeViewItem | null = null) { let selectedItems = this.getSelectedItems() if (exception) selectedItems = selectedItems.filter(el => el.id !== exception.id) if (selectedItems.length) { selectedItems.forEach(el => { const selectedElement = document.querySelector(`#${el.id}`) selectedElement.classList.remove('selected') this.changeItemProp(el.id, 'isSelected', false) }) } } private selectItem(el: Element, data: ITreeViewItem) { if (data.isSelected) { el.classList.remove('selected') this.changeItemProp(data.id, 'isSelected', false) } else { el.classList.add('selected') this.changeItemProp(data.id, 'isSelected', true) } } private selectChildren(el: Element, data: ITreeViewItem) { const items = el.querySelectorAll('[data-tree-view-item]') Array.from(items) .filter(elI => !elI.classList.contains('disabled')) .forEach(elI => { const initialItemData = elI.id ? this.getItem(elI.id) : null if (!initialItemData) return false if (data.isSelected) { elI.classList.add('selected') this.changeItemProp(initialItemData.id, 'isSelected', true) } else { elI.classList.remove('selected') this.changeItemProp(initialItemData.id, 'isSelected', false) } const currentItemData = this.getItem(elI.id) const control: HTMLFormElement = elI.querySelector(`input[value="${currentItemData.value}"]`) if (this.isIndeterminate) control.indeterminate = false if (currentItemData.isSelected) control.checked = true else control.checked = false }) } private toggleParent(el: Element) { let parent = el.parentElement?.closest('[data-tree-view-item]') while (!!parent) { const items = parent.querySelectorAll('[data-tree-view-item]:not(.disabled)') const data = JSON.parse(parent.getAttribute('data-tree-view-item')) const control: HTMLFormElement = parent.querySelector(`input[value="${data.value}"]`) let hasUnchecked = false let checkedItems = 0 items.forEach(elI => { const dataI = this.getItem(elI.id) if (dataI.isSelected) checkedItems += 1 if (!dataI.isSelected) hasUnchecked = true }) if (hasUnchecked) { parent.classList.remove('selected') this.changeItemProp(parent.id, 'isSelected', false) control.checked = false } else { parent.classList.add('selected') this.changeItemProp(parent.id, 'isSelected', true) control.checked = true } if (this.isIndeterminate) { if (checkedItems > 0 && checkedItems < items.length) control.indeterminate = true else control.indeterminate = false } parent = parent.parentElement?.closest('[data-tree-view-item]') } } // Public methods public update() { this.items.map((el: ITreeViewItem) => { const selector = document.querySelector(`#${el.id}`) if (el.path !== this.getPath(selector)) el.path = this.getPath(selector) return el }) } public getSelectedItems() { return this.items.filter(el => el.isSelected) } public changeItemProp(id: string, prop: string, val: any) { this.items.map(el => { // @ts-ignore if (el.id === id) el[prop] = val return el }) } public destroy() { // Remove listeners this.onElementClickListener.forEach(({ el, fn }) => { el.removeEventListener('click', fn) }) if (this.onControlChangeListener.length) this.onElementClickListener.forEach(({ el, fn }) => { el.removeEventListener('change', fn) }) this.unselectItem() this.items = [] window.$hsTreeViewCollection = window.$hsTreeViewCollection.filter(({ element }) => element.el !== this.el) HSTreeView.group -= 1 } // Static methods private static findInCollection(target: HSTreeView | HTMLElement | string): ICollectionItem<HSTreeView> | null { return ( window.$hsTreeViewCollection.find(el => { if (target instanceof HSTreeView) 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.$hsTreeViewCollection.find( el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target) ) return elInCollection ? (isInstance ? elInCollection : elInCollection.element.el) : null } static autoInit() { if (!window.$hsTreeViewCollection) window.$hsTreeViewCollection = [] if (window.$hsTreeViewCollection) window.$hsTreeViewCollection = window.$hsTreeViewCollection.filter(({ element }) => document.contains(element.el)) document.querySelectorAll('[data-tree-view]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => { if (!window.$hsTreeViewCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) new HSTreeView(el) }) } // Backward compatibility static on(evt: string, target: HSTreeView | HTMLElement | string, cb: Function) { const instance = HSTreeView.findInCollection(target) if (instance) instance.element.events[evt] = cb } } declare global { interface Window { HSTreeView: Function $hsTreeViewCollection: ICollectionItem<HSTreeView>[] } } window.addEventListener('load', () => { HSTreeView.autoInit() // Uncomment for debug // console.log('Tree view collection:', window.$hsTreeViewCollection); }) if (typeof window !== 'undefined') { window.HSTreeView = HSTreeView } export default HSTreeView