import { Content, type LocalContent } from "./content.util"; import { dom } from "./dom.util"; import { clamp } from "./general.util"; export type Recursive = T & { children?: T[]; parent?: T; }; export class Tree> { private _data: Recursive[]; private _flatten: T[]; constructor(data: T[]) { this._data = data; this._flatten = this.accumulate(e => e); } remove(id: string) { const recursive = (data?: Recursive[], parent?: T) => data?.filter(e => e.id !== id)?.map((e, i) => { e.order = i; e.children = recursive(e.children as T[], e); return e; }); this._data = recursive(this._data)!; this._flatten = this.accumulate(e => e); } insertAt(item: Recursive, pos: number) { const parent = item.parent ? item.parent.id : undefined; const recursive = (data?: Recursive[]) => data?.flatMap(e => { if(e.id === parent) { e.children = e.children ?? []; e.children.splice(clamp(pos, 0, e.children.length), 0, item); } else if(e.type === 'folder') e.children = recursive(e.children as T[]); return e; }).map((e, i) => { e.order = i; return e; }); if(!parent || parent === '') { this._data.splice(pos, 0, item); this._data.forEach((e, i) => e.order = i); } else this._data = recursive(this._data)!; this._flatten = this.accumulate(e => e); } find(id: string): T | undefined { const recursive = (data?: Recursive[]): T | undefined => { if(!data) return; for(const e of data) { if(e.id === id) return e; const result = recursive(e.children as T[]); if(result) return result; } }; return recursive(this._data); } search(prop: keyof T, value: string): T[] { const recursive = (data?: Recursive[]): T[] => { if(!data) return []; const arr = []; for(const e of data) { if(e[prop] === value) arr.push(e); else arr.push(...recursive(e.children as T[])); } return arr; }; return recursive(this._data); } subset(id: string): Tree | undefined { const subset = this.find(id); return subset ? new Tree([subset]) : undefined; } each(callback: (item: T, depth: number, parent?: T) => void) { const recursive = (depth: number, data?: Recursive[], parent?: T) => data?.forEach(e => { callback(e, depth, parent); recursive(depth + 1, e.children as T[], e) }); recursive(1, this._data); } accumulate(callback: (item: T, depth: number, parent?: T) => any): any[] { const recursive = (depth: number, data?: Recursive[], parent?: T): any[] => data?.flatMap(e => [callback(e, depth, parent), ...recursive(depth + 1, e.children as T[], e)]) ?? []; return recursive(1, this._data); } static each>(tree: Array, children: keyof T, callback: (item: T, depth: number, parent?: T) => void) { const recursive = (depth: number, data?: Array, parent?: T) => data?.forEach(e => { if(!e) return; callback(e, depth, parent); !Array.isArray(e[children]) ? undefined : recursive(depth + 1, e[children] as T[] | undefined, e) }); recursive(1, tree); } static accumulate>(tree: Array, children: keyof T, callback: (item: T, depth: number, parent?: T) => any): any[] { const recursive = (depth: number, data?: Array, parent?: T): any[] => data?.flatMap(e => e && [callback(e, depth, parent), ...!Array.isArray(e[children]) ? [] : recursive(depth + 1, e[children] as T[] | undefined, e)]) ?? []; return recursive(1, tree); } static flatten>(tree: T[], children: keyof T): T[] { return Tree.accumulate(tree, children, (item) => item); } get data() { return this._data; } get flatten() { return this._flatten; } } export class TreeDOM { container: HTMLElement; tree: Tree>; private filter?: (item: Recursive>, depth: number) => boolean | undefined; private folder: (item: Recursive>, depth: number) => HTMLElement; private leaf: (item: Recursive>, depth: number) => HTMLElement; constructor(folder: (item: Recursive>, depth: number) => HTMLElement, leaf: (item: Recursive>, depth: number) => HTMLElement, filter?: (item: Recursive>, depth: number) => boolean | undefined) { this.tree = new Tree(Content.tree); this.filter = filter; this.folder = folder; this.leaf = leaf; const elements = this.tree.accumulate(this.render.bind(this)); this.container = dom('div', { class: 'list-none select-none text-light-100 dark:text-dark-100 text-sm ps-2' }, elements); } render(item: Recursive>, depth: number): HTMLElement | undefined { if(this.filter && !(this.filter(item, depth) ?? true)) return; if(item.type === 'folder') { let folded = false; if(item.element) folded = item.element.getAttribute('data-state') === 'open'; item.element = this.folder(item, depth); if(!!item.parent) item.element.classList.toggle('hidden', item.parent.element!.getAttribute('data-state') === 'closed' || item.parent.element!.classList.contains('hidden')); item.element.setAttribute('data-state', folded ? 'open' : 'closed'); item.element.addEventListener('click', () => this.toggle(item)); return item.element; } else { item.element = this.leaf(item, depth); if(!!item.parent) item.element.classList.toggle('hidden', item.parent.element!.getAttribute('data-state') === 'closed' || item.parent.element!.classList.contains('hidden')); return item.element; } } update() { this.container.replaceChildren(...this.tree.flatten.map(e => e.element!)); } toggle(item?: Omit, state?: boolean) { if(item && item.type === 'folder') { const open = state ?? item.element!.getAttribute('data-state') !== 'open'; item.element!.setAttribute('data-state', open ? 'open' : 'closed'); new Tree([item]).each((e, _, parent) => { if(!parent) return; e.element!.classList.toggle('hidden', !this.opened(parent) || parent.element!.classList.contains('hidden')); }); } } opened(item?: Omit): boolean | undefined { return item ? item.element!.getAttribute('data-state') === 'open' : undefined; } }