obsidian-visualiser/shared/tree.ts

193 lines
6.5 KiB
TypeScript

import { Content, getPath, type LocalContent } from "./content.util";
import { dom } 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(path: string)
{
const recursive = (data?: Recursive<T>[], parent?: T) => data?.filter(e => getPath(e) !== path)?.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 ? getPath(item.parent) : undefined;
const recursive = (data?: Recursive<T>[]) => data?.flatMap(e => {
if(getPath(e) === 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(path: string): T | undefined
{
const recursive = (data?: Recursive<T>[]): T | undefined => {
if(!data)
return;
for(const e of data)
{
if(getPath(e) === path)
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(path: string): Tree<T> | undefined
{
const subset = this.find(path);
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);
}
get data()
{
return this._data;
}
get flatten()
{
return this._flatten;
}
}
export class TreeDOM
{
container: HTMLElement;
tree: Tree<Omit<LocalContent & { element?: HTMLElement }, "content">>;
private filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined;
private folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement;
private leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement;
constructor(folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement, leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement, 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?: HTMLElement }, "content">>, 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<LocalContent & { element?: HTMLElement }, '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?: HTMLElement }, 'content'>): boolean | undefined
{
return item ? item.element!.getAttribute('data-state') === 'open' : undefined;
}
}