You've already forked obsidian-visualiser
Big rework to include OPFS API for local edits. Big components rework in vanilla JS to optimize. Unfinished, DO NOT SHIP YET !
This commit is contained in:
193
shared/tree.ts
Normal file
193
shared/tree.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user