UNPKG

preline

Version:

Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.

371 lines (296 loc) 9.63 kB
/* * HSTreeView * @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 { dispatch } from '../../utils'; import { ITreeViewOptions, ITreeView, ITreeViewItem, ITreeViewOptionsControlBy, } from './interfaces'; import HSBasePlugin from '../base-plugin'; import { ICollectionItem } from '../../interfaces'; 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-hs-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.hs.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-hs-tree-view-item]').forEach((el, ind) => { const data = JSON.parse(el.getAttribute('data-hs-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-hs-tree-view-item]'); while (!!parent) { const data = JSON.parse(parent.getAttribute('data-hs-tree-view-item')); path.push(data.value); parent = parent.parentElement?.closest('[data-hs-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-hs-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-hs-tree-view-item]'); while (!!parent) { const items = parent.querySelectorAll( '[data-hs-tree-view-item]:not(.disabled)', ); const data = JSON.parse(parent.getAttribute('data-hs-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-hs-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-hs-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;