209 lines
7.5 KiB
TypeScript
209 lines
7.5 KiB
TypeScript
import { Content, type LocalContent } from "./content.util";
|
|
import { dom, type RedrawableHTML } from "./dom.util";
|
|
import { clamp } from "./general.util";
|
|
|
|
export type Recursive<T> = T & {
|
|
children?: T[];
|
|
parent?: T;
|
|
};
|
|
export class Tree<T extends Omit<LocalContent, 'content'>>
|
|
{
|
|
private _data: Recursive<T>[];
|
|
private _flatten: T[];
|
|
|
|
constructor(data: T[])
|
|
{
|
|
this._data = data;
|
|
this._flatten = this.accumulate(e => e);
|
|
}
|
|
|
|
remove(id: string)
|
|
{
|
|
const recursive = (data?: Recursive<T>[], 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<T>, pos: number)
|
|
{
|
|
const parent = item.parent ? item.parent.id : undefined;
|
|
const recursive = (data?: Recursive<T>[]) => 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>[]): 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>[]): 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<T> | 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<T>[], 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<T>[], 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<T extends Record<string, any>>(tree: Array<T | undefined>, children: keyof T, callback: (item: T, depth: number, parent?: T) => void)
|
|
{
|
|
const recursive = (depth: number, data?: Array<T | undefined>, 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<T extends Record<string, any>>(tree: Array<T | undefined>, children: keyof T, callback: (item: T, depth: number, parent?: T) => any): any[]
|
|
{
|
|
const recursive = (depth: number, data?: Array<T | undefined>, 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<T extends Record<string, any>>(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: RedrawableHTML;
|
|
tree: Tree<Omit<LocalContent & { element?: RedrawableHTML }, "content">>;
|
|
|
|
private filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined;
|
|
private folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML;
|
|
private leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML;
|
|
|
|
constructor(folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML, leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML, filter?: (item: Recursive<Omit<LocalContent, 'content'>>, 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<Omit<LocalContent & { element?: RedrawableHTML }, "content">>, depth: number): RedrawableHTML | 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<LocalContent & { element?: RedrawableHTML }, 'content'>, 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<LocalContent & { element?: RedrawableHTML }, 'content'>): boolean | undefined
|
|
{
|
|
return item ? item.element!.getAttribute('data-state') === 'open' : undefined;
|
|
}
|
|
} |