You've already forked obsidian-visualiser
Update DB schema to include an ID and split overview and content. Progressing on ContentEditor with the ID fixing many issues. Adding modal and sync features.
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { safeDestr as parse } from 'destr';
|
||||
import { Canvas } from "#shared/canvas.util";
|
||||
import render from "#shared/markdown.util";
|
||||
import { contextmenu, popper } from "#shared/floating.util";
|
||||
import { confirm, contextmenu, popper } from "#shared/floating.util";
|
||||
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
|
||||
import prose, { h1, h2, loading } from "#shared/proses";
|
||||
import { parsePath } from '#shared/general.util';
|
||||
import { Tree, TreeDOM, type Recursive } from '#shared/tree';
|
||||
import { getID, ID_SIZE, parsePath } from '#shared/general.util';
|
||||
import { TreeDOM, type Recursive } from '#shared/tree';
|
||||
import { History } from '#shared/history.util';
|
||||
import { MarkdownEditor } from '#shared/editor.util';
|
||||
import type { CanvasContent } from '~/types/canvas';
|
||||
@@ -13,6 +13,7 @@ import type { CanvasContent } from '~/types/canvas';
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
import type { CleanupFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types';
|
||||
|
||||
export type FileType = keyof ContentMap;
|
||||
@@ -26,6 +27,7 @@ export interface ContentMap
|
||||
}
|
||||
export interface Overview<T extends FileType>
|
||||
{
|
||||
id: string;
|
||||
path: string;
|
||||
owner: number;
|
||||
title: string;
|
||||
@@ -33,10 +35,9 @@ export interface Overview<T extends FileType>
|
||||
navigable: boolean;
|
||||
private: boolean;
|
||||
order: number;
|
||||
visit: number;
|
||||
type: T;
|
||||
}
|
||||
export type ExploreContent<T extends FileType = FileType> = Overview<T> & { content: ContentMap[T] };
|
||||
export type ProjectContent<T extends FileType = FileType> = Overview<T> & { content: ContentMap[T] };
|
||||
|
||||
class AsyncQueue
|
||||
{
|
||||
@@ -48,6 +49,7 @@ class AsyncQueue
|
||||
finished: boolean = true;
|
||||
|
||||
private res: (value: void | PromiseLike<void>) => void = () => {};
|
||||
private rej: (value: void | PromiseLike<void>) => void = () => {};
|
||||
|
||||
constructor(size: number = 8)
|
||||
{
|
||||
@@ -63,6 +65,7 @@ class AsyncQueue
|
||||
|
||||
this.promise = new Promise((res, rej) => {
|
||||
this.res = res;
|
||||
this.rej = rej;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +82,7 @@ class AsyncQueue
|
||||
this.count++;
|
||||
|
||||
const fn = this._queue.shift()!;
|
||||
fn().finally(() => {
|
||||
fn().catch(e => this.rej(e)).then(() => {
|
||||
this.count--;
|
||||
this.refresh();
|
||||
});
|
||||
@@ -100,7 +103,7 @@ export const DEFAULT_CONTENT: Record<FileType, ContentMap[FileType]> = {
|
||||
file: '',
|
||||
folder: null,
|
||||
};
|
||||
export type LocalContent<T extends FileType = FileType> = ExploreContent<T> & {
|
||||
export type LocalContent<T extends FileType = FileType> = ProjectContent<T> & {
|
||||
localEdit?: boolean;
|
||||
error?: boolean;
|
||||
};
|
||||
@@ -111,16 +114,15 @@ export class Content
|
||||
|
||||
private static root: FileSystemDirectoryHandle;
|
||||
|
||||
private static _overview: Omit<LocalContent, 'content'>[];
|
||||
private static dlQueue = new AsyncQueue();
|
||||
private static writeQueue = new AsyncQueue(1);
|
||||
private static _overview: Record<string, Omit<LocalContent, 'content'>>;
|
||||
private static queue = new AsyncQueue();
|
||||
|
||||
static init(): Promise<boolean>
|
||||
{
|
||||
if(Content._ready)
|
||||
return Promise.resolve(true);
|
||||
|
||||
Content.initPromise = new Promise(async (res, rej) => {
|
||||
Content.initPromise = new Promise(async (res) => {
|
||||
try
|
||||
{
|
||||
if(!('storage' in navigator))
|
||||
@@ -131,11 +133,11 @@ export class Content
|
||||
const overview = await Content.read('overview', { create: true });
|
||||
try
|
||||
{
|
||||
Content._overview = parse<Omit<LocalContent, 'content'>[]>(overview);
|
||||
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Content._overview = [];
|
||||
Content._overview = {};
|
||||
await Content.pull();
|
||||
}
|
||||
|
||||
@@ -151,51 +153,61 @@ export class Content
|
||||
|
||||
return Content.initPromise;
|
||||
}
|
||||
static overview(path: string): Omit<LocalContent, 'content'> | undefined
|
||||
static async get(id: string, content: boolean): Promise<LocalContent | undefined>
|
||||
{
|
||||
return Content._overview.find(e => getPath(e) === path);
|
||||
}
|
||||
static async content(path: string): Promise<LocalContent | undefined>
|
||||
{
|
||||
const overview = Content._overview.find(e => getPath(e) === path);
|
||||
const overview = Content._overview[id];
|
||||
|
||||
return overview ? { ...overview, content: Content.fromString(overview, await Content.read(encodeURIComponent(path)) ?? '') } as LocalContent : undefined;
|
||||
return overview ? { ...overview, content: content ? Content.fromString(overview, await Content.read(id) ?? '') : undefined } as LocalContent : undefined;
|
||||
}
|
||||
static update(item: Recursive<LocalContent>)
|
||||
static async set(id: string, overview?: Omit<LocalContent, 'content'> | Recursive<Omit<LocalContent, 'content'>>)
|
||||
{
|
||||
const index = Content._overview.findIndex(e => e.path === getPath(item));
|
||||
if(index !== -1)
|
||||
Content._overview[index] = item;
|
||||
if(overview === undefined)
|
||||
{
|
||||
delete Content._overview[id];
|
||||
}
|
||||
else
|
||||
{
|
||||
const {
|
||||
id: _id,
|
||||
path: _path,
|
||||
owner: _owner,
|
||||
title: _title,
|
||||
timestamp: _timestamp,
|
||||
navigable: _navigable,
|
||||
private: _private,
|
||||
order: _order,
|
||||
type: _type,
|
||||
...rest
|
||||
} = overview as Recursive<LocalContent>;
|
||||
Content._overview[id] = {
|
||||
id: _id,
|
||||
path: _path,
|
||||
owner: _owner,
|
||||
title: _title,
|
||||
timestamp: _timestamp,
|
||||
navigable: _navigable,
|
||||
private: _private,
|
||||
order: _order,
|
||||
type: _type,
|
||||
};
|
||||
}
|
||||
}
|
||||
static async save(content?: ProjectContent)
|
||||
{
|
||||
const overviewAsString = JSON.stringify(Content._overview)
|
||||
Content.queue.queue(() => Content.write("overview", overviewAsString));
|
||||
|
||||
const overview = JSON.stringify(Content._overview, (k, v) => ['parent', 'children', 'content'].includes(k) ? undefined : v);
|
||||
return Content.writeQueue.queue(() => Content.write('overview', overview));
|
||||
}
|
||||
static rename(from: string, to: string)
|
||||
{
|
||||
const index = Content._overview.findIndex(e => getPath(e) === from);
|
||||
if(index !== -1)
|
||||
Content._overview[index].path = to;
|
||||
if(content && content.content)
|
||||
{
|
||||
const contentAsString = Content.toString(content);
|
||||
Content.queue.queue(() => Content.write(content.id, contentAsString));
|
||||
}
|
||||
|
||||
return Content.writeQueue.queue(async () => {
|
||||
const content = await Content.read(encodeURIComponent(from));
|
||||
if(content !== undefined)
|
||||
{
|
||||
await Content.write(encodeURIComponent(to), content, { create: true });
|
||||
await Content.remove(encodeURIComponent(from));
|
||||
}
|
||||
});
|
||||
return Content.queue.promise;
|
||||
}
|
||||
static save(content?: Recursive<LocalContent>)
|
||||
static async pull()
|
||||
{
|
||||
if(!content)
|
||||
return;
|
||||
|
||||
const string = Content.toString(content), path = getPath(content);
|
||||
return Content.writeQueue.queue(() => Content.write(encodeURIComponent(path), string, { create: true }));
|
||||
}
|
||||
private static async pull()
|
||||
{
|
||||
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ExploreContent<FileType>[] | undefined;
|
||||
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined;
|
||||
|
||||
if(!overview)
|
||||
{
|
||||
@@ -206,44 +218,50 @@ export class Content
|
||||
|
||||
for(const file of overview)
|
||||
{
|
||||
let index = Content._overview.findIndex(e => getPath(e) === file.path), _overview = (index === -1 ? undefined : Content._overview[index]);
|
||||
if(!_overview || _overview.localEdit)
|
||||
const _overview = Content._overview[file.id];
|
||||
if(_overview && _overview.localEdit)
|
||||
{
|
||||
const encoded = encodeURIComponent(file.path);
|
||||
if(!_overview)
|
||||
{
|
||||
index = Content._overview.push(file) - 1;
|
||||
}
|
||||
else
|
||||
Content._overview[index] = file;
|
||||
|
||||
_overview = file;
|
||||
//TODO: Ask what to do about this file.
|
||||
}
|
||||
else
|
||||
{
|
||||
Content._overview[file.id] = file;
|
||||
|
||||
if(file.type === 'folder')
|
||||
continue;
|
||||
|
||||
Content.dlQueue.queue(() => {
|
||||
return useRequestFetch()(`/api/file/content/${encoded}`, { cache: 'no-cache' }).then(async (content: ContentMap[FileType] | undefined) => {
|
||||
Content.queue.queue(() => {
|
||||
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: ContentMap[FileType] | undefined) => {
|
||||
if(content)
|
||||
await Content.write(encoded, Content.toString({ ...file, content }), { create: true });
|
||||
Content.queue.queue(() => Content.write(file.id, Content.toString({ ...file, content }), { create: true }));
|
||||
else
|
||||
Content._overview[index].error = true;
|
||||
Content._overview[file.id].error = true;
|
||||
}).catch(e => {
|
||||
Content._overview[index].error = true;
|
||||
})
|
||||
Content._overview[file.id].error = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Content.dlQueue.queue(() => {
|
||||
return Content.queue.queue(() => {
|
||||
return Content.write('overview', JSON.stringify(Content._overview), { create: true });
|
||||
});
|
||||
|
||||
await Content.dlQueue.promise;
|
||||
}
|
||||
private static async push()
|
||||
static async push()
|
||||
{
|
||||
const blocked = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));
|
||||
|
||||
for(const [id, value] of Object.entries(Content._overview).filter(e => !blocked.includes(e[0])))
|
||||
{
|
||||
if(value.type === 'folder')
|
||||
continue;
|
||||
|
||||
Content.queue.queue(() => Content.read(id).then(e => {
|
||||
if(e) Content.queue.queue(() => useRequestFetch()(`/api/file/content/${id}`, { method: 'POST', body: e, cache: 'no-cache' }));
|
||||
}));
|
||||
}
|
||||
|
||||
return Content.queue.promise;
|
||||
}
|
||||
//Maybe store the file handles ? Is it safe to keep them ?
|
||||
private static async read(path: string, options?: FileSystemGetFileOptions): Promise<string | undefined>
|
||||
@@ -283,24 +301,18 @@ export class Content
|
||||
}
|
||||
console.timeEnd(`Writing ${size} bytes to '${path}'`);
|
||||
}
|
||||
private static async remove(path: string): Promise<void>
|
||||
{
|
||||
console.time(`Removing '${path}'`);
|
||||
await Content.root.removeEntry(path)
|
||||
console.timeEnd(`Removing '${path}'`);
|
||||
}
|
||||
|
||||
static get estimate(): Promise<StorageEstimate>
|
||||
{
|
||||
return Content._ready ? navigator.storage.estimate() : Promise.reject();
|
||||
}
|
||||
|
||||
static toString<T extends FileType>(content: ExploreContent<T>): string
|
||||
static toString<T extends FileType>(content: ProjectContent<T>): string
|
||||
{
|
||||
return handlers[content.type].toString(content.content);
|
||||
}
|
||||
|
||||
static fromString<T extends FileType>(overview: Omit<ExploreContent<T>, 'content'>, content: string): ContentMap[T]
|
||||
static fromString<T extends FileType>(overview: Omit<ProjectContent<T>, 'content'>, content: string): ContentMap[T]
|
||||
{
|
||||
return handlers[overview.type].fromString(content);
|
||||
}
|
||||
@@ -362,7 +374,7 @@ export class Content
|
||||
|
||||
for(const element of Object.values(Content._overview))
|
||||
{
|
||||
addChild(arr, element);
|
||||
addChild(arr, {...element});
|
||||
}
|
||||
|
||||
return arr;
|
||||
@@ -411,9 +423,9 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
||||
else
|
||||
{
|
||||
element = loading("large");
|
||||
Content.content(content.path).then(e => {
|
||||
Content.get(content.id, true).then(e => {
|
||||
if(!e)
|
||||
return element.parentElement?.replaceChild(dom('div', { class: '', text: '' }), element);
|
||||
return element.parentElement?.replaceChild(dom('div', { class: '', text: 'Une erreur est survenue.' }), element);
|
||||
|
||||
MarkdownEditor.singleton.content = content.content = (e as typeof content).content;
|
||||
element.parentElement?.replaceChild(MarkdownEditor.singleton.dom, element);
|
||||
@@ -452,7 +464,7 @@ export const iconByType: Record<FileType, string> = {
|
||||
'file': 'radix-icons:file',
|
||||
'markdown': 'radix-icons:file-text',
|
||||
'map': 'lucide:map',
|
||||
}
|
||||
};
|
||||
|
||||
export class Editor
|
||||
{
|
||||
@@ -472,7 +484,7 @@ export class Editor
|
||||
this.history.register('overview', {
|
||||
move: {
|
||||
undo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
action.element.parent = action.from.parent;
|
||||
action.element.order = action.from.order;
|
||||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
@@ -483,11 +495,10 @@ export class Editor
|
||||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
this.tree.update();
|
||||
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
redo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
action.element.parent = action.to.parent;
|
||||
action.element.order = action.to.order;
|
||||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
@@ -499,16 +510,16 @@ export class Editor
|
||||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
this.tree.update();
|
||||
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
},
|
||||
add: {
|
||||
undo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
if(this.selected === action.element) this.select();
|
||||
action.element.cleanup();
|
||||
action.element.remove();
|
||||
action.element.element?.remove();
|
||||
|
||||
},
|
||||
redo: (action) => {
|
||||
if(!action.element)
|
||||
@@ -527,10 +538,10 @@ export class Editor
|
||||
this.tree.update();
|
||||
},
|
||||
redo: (action) => {
|
||||
this.tree.tree.remove(getPath(action.element));
|
||||
this.tree.tree.remove(action.element.id);
|
||||
if(this.selected === action.element) this.select();
|
||||
action.element.cleanup();
|
||||
action.element.remove();
|
||||
action.element.element?.remove();
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
@@ -542,7 +553,6 @@ export class Editor
|
||||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
action.element?.cleanup();
|
||||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
redo: (action) => {
|
||||
@@ -553,7 +563,6 @@ export class Editor
|
||||
const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
|
||||
action.element?.cleanup();
|
||||
this.dragndrop(action.element, depth, action.element.parent);
|
||||
Content.rename(action.element.path, path);
|
||||
action.element.path = path;
|
||||
},
|
||||
},
|
||||
@@ -577,7 +586,7 @@ export class Editor
|
||||
action.element.element!.children[0].children[3].children[0].replaceWith(icon(action.element.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !action.element.private }] }));
|
||||
},
|
||||
},
|
||||
}, action => Content.update(action.element));
|
||||
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
|
||||
|
||||
this.tree = new TreeDOM((item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
|
||||
@@ -601,20 +610,20 @@ export class Editor
|
||||
|
||||
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
|
||||
}
|
||||
private contextmenu(e: MouseEvent, item: LocalContent)
|
||||
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
const close = contextmenu(e.clientX, e.clientY, { placement: 'right-start', offset: 8, content: [
|
||||
const close = contextmenu(e.clientX, e.clientY, [
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { this.remove(item); close() }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
] });
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
], { placement: 'right-start', offset: 8 });
|
||||
}
|
||||
private add(type: FileType, nextTo: Recursive<LocalContent>)
|
||||
{
|
||||
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
|
||||
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, visit: 0, parent: nextTo.parent };
|
||||
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(ID_SIZE), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
|
||||
this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
|
||||
}
|
||||
private remove(item: LocalContent & { element?: HTMLElement })
|
||||
@@ -670,6 +679,8 @@ export class Editor
|
||||
if (instruction !== null)
|
||||
this.updateTree(instruction, location.initial.dropTargets[0].data.id as string, target.data.id as string);
|
||||
},
|
||||
}), autoScrollForElements({
|
||||
element: this.tree.container,
|
||||
}));
|
||||
}
|
||||
private dragndrop(item: Omit<LocalContent & { element?: HTMLElement, cleanup?: () => void }, "content">, depth: number, parent?: Omit<LocalContent & { element?: HTMLElement }, "content">): CleanupFn
|
||||
@@ -697,7 +708,7 @@ export class Editor
|
||||
dropTargetForElements({
|
||||
element,
|
||||
getData: ({ input }) => {
|
||||
const data = { id: getPath(item) };
|
||||
const data = { id: item.id };
|
||||
|
||||
return attachInstruction(data, {
|
||||
input,
|
||||
|
||||
@@ -7,7 +7,7 @@ export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
||||
type Listener<K extends keyof HTMLElementEventMap> = | ((ev: HTMLElementEventMap[K]) => any) | {
|
||||
options?: boolean | AddEventListenerOptions;
|
||||
listener: (ev: HTMLElementEventMap[K]) => any;
|
||||
}
|
||||
} | undefined;
|
||||
|
||||
export interface NodeProperties
|
||||
{
|
||||
@@ -43,7 +43,7 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
|
||||
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
|
||||
if(typeof value === 'function')
|
||||
element.addEventListener(key, value);
|
||||
else
|
||||
else if(value)
|
||||
element.addEventListener(key, value.listener, value.options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { lintKeymap } from '@codemirror/lint';
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { IterMode, Tree } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { dom } from './dom.util';
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||
@@ -199,4 +200,52 @@ export class MarkdownEditor
|
||||
MarkdownEditor._singleton = new MarkdownEditor();
|
||||
return MarkdownEditor._singleton;
|
||||
}
|
||||
}
|
||||
|
||||
export class FramedEditor
|
||||
{
|
||||
editor: MarkdownEditor;
|
||||
dom: HTMLIFrameElement;
|
||||
|
||||
private static _singleton: FramedEditor;
|
||||
static get singleton()
|
||||
{
|
||||
if(!FramedEditor._singleton)
|
||||
FramedEditor._singleton = new FramedEditor();
|
||||
|
||||
return FramedEditor._singleton;
|
||||
}
|
||||
private constructor()
|
||||
{
|
||||
this.editor = MarkdownEditor.singleton;
|
||||
this.dom = dom('iframe');
|
||||
this.dom.addEventListener('load', () => {
|
||||
if(!this.dom.contentDocument)
|
||||
return;
|
||||
|
||||
this.dom.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
||||
this.dom.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
||||
|
||||
const base = this.dom.contentDocument.head.appendChild(this.dom.contentDocument.createElement('base'));
|
||||
base.setAttribute('href', window.location.href);
|
||||
|
||||
for(let element of document.getElementsByTagName('link'))
|
||||
{
|
||||
if(element.getAttribute('rel') === 'stylesheet')
|
||||
this.dom.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
for(let element of document.getElementsByTagName('style'))
|
||||
{
|
||||
this.dom.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
this.dom.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
||||
this.dom.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
||||
|
||||
this.dom.contentDocument.body.appendChild(this.editor.dom);
|
||||
|
||||
this.editor.focus();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,33 @@
|
||||
import * as FloatingUI from "@floating-ui/dom";
|
||||
import { cancelPropagation, dom, svg, type Class, type NodeChildren } from "./dom.util";
|
||||
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren } from "./dom.util";
|
||||
import { button } from "./proses";
|
||||
|
||||
export interface CommonProperties
|
||||
export interface ContextProperties
|
||||
{
|
||||
placement?: FloatingUI.Placement;
|
||||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
}
|
||||
export interface PopperProperties
|
||||
{
|
||||
placement?: FloatingUI.Placement;
|
||||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
content?: NodeChildren;
|
||||
}
|
||||
export interface PopperProperties extends CommonProperties
|
||||
{
|
||||
delay?: number;
|
||||
|
||||
onShow?: (element: HTMLDivElement) => boolean | void;
|
||||
onHide?: (element: HTMLDivElement) => boolean | void;
|
||||
}
|
||||
|
||||
export interface ModalProperties
|
||||
{
|
||||
priority?: boolean;
|
||||
closeWhenOutside?: boolean;
|
||||
}
|
||||
|
||||
let teleport: HTMLDivElement;
|
||||
export function init()
|
||||
{
|
||||
@@ -141,7 +152,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
||||
|
||||
return container;
|
||||
}
|
||||
export function contextmenu(x: number, y: number, properties?: CommonProperties): () => void
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties): () => void
|
||||
{
|
||||
const virtual = {
|
||||
getBoundingClientRect() {
|
||||
@@ -159,7 +170,7 @@ export function contextmenu(x: number, y: number, properties?: CommonProperties)
|
||||
};
|
||||
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, [...(properties?.content ?? [])]);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, content);
|
||||
|
||||
function update()
|
||||
{
|
||||
@@ -236,7 +247,26 @@ export function contextmenu(x: number, y: number, properties?: CommonProperties)
|
||||
|
||||
return close;
|
||||
}
|
||||
//TODO
|
||||
export function modal()
|
||||
|
||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
{
|
||||
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
|
||||
const _closer = properties?.priority ? undefined : dom('span', { class: 'absolute top-4 right-4', text: '×', listeners: { click: () => _modal.remove() } });
|
||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content)])
|
||||
|
||||
teleport.appendChild(_modal);
|
||||
|
||||
return {
|
||||
close: () => _modal.remove(),
|
||||
}
|
||||
}
|
||||
|
||||
export function confirm(title: string): Promise<boolean>
|
||||
{
|
||||
return new Promise(res => {
|
||||
const mod = modal([ dom('div', { class: 'flex flex-col justify-start gap-4' }, [ dom('div', { class: 'text-xl' }, [ text(title) ]), dom('div', { class: 'flex flex-row gap-2' }, [ button(text("Non"), () => (mod.close(), res(false)), 'h-[35px] px-[15px]'), button(text("Oui"), () => (mod.close(), res(true)), 'h-[35px] px-[15px] !border-light-red dark:!border-dark-red text-light:red dark:text-dark-red') ]) ]) ], {
|
||||
priority: true,
|
||||
closeWhenOutside: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
export const ID_SIZE = 32;
|
||||
|
||||
export function unifySlug(slug: string | string[]): string
|
||||
{
|
||||
return (Array.isArray(slug) ? slug.join('/') : slug);
|
||||
}
|
||||
export function getID(length: number)
|
||||
{
|
||||
for (var id = [], i = 0; i < length; i++)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
return id.join("");
|
||||
}
|
||||
export function parsePath(path: string): string
|
||||
{
|
||||
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '');
|
||||
|
||||
@@ -18,7 +18,7 @@ interface HistoryAction
|
||||
|
||||
export class History
|
||||
{
|
||||
private handlers: Record<string, { handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => void }>;
|
||||
private handlers: Record<string, { handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => any }>;
|
||||
private history: HistoryEvent[];
|
||||
private position: number;
|
||||
|
||||
@@ -83,7 +83,7 @@ export class History
|
||||
this.handlers[source] && this.handlers[source].any && this.handlers[source].any(e);
|
||||
});
|
||||
}
|
||||
register(source: string, handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => void)
|
||||
register(source: string, handlers: Record<string, HistoryHandler>, any?: (action: HistoryAction) => any)
|
||||
{
|
||||
this.handlers[source] = { handlers, any };
|
||||
}
|
||||
|
||||
@@ -252,4 +252,10 @@ export function link(properties?: NodeProperties & { active?: Class }, link?: Ro
|
||||
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement
|
||||
{
|
||||
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
|
||||
}
|
||||
export function button(content: Node, onClick?: () => void, cls?: Class)
|
||||
{
|
||||
return dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Content, getPath, type LocalContent } from "./content.util";
|
||||
import { Content, type LocalContent } from "./content.util";
|
||||
import { dom } from "./dom.util";
|
||||
import { clamp } from "./general.util";
|
||||
|
||||
@@ -17,9 +17,9 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
||||
this._flatten = this.accumulate(e => e);
|
||||
}
|
||||
|
||||
remove(path: string)
|
||||
remove(id: string)
|
||||
{
|
||||
const recursive = (data?: Recursive<T>[], parent?: T) => data?.filter(e => getPath(e) !== path)?.map((e, i) => {
|
||||
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;
|
||||
@@ -30,9 +30,9 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
||||
}
|
||||
insertAt(item: Recursive<T>, pos: number)
|
||||
{
|
||||
const parent = item.parent ? getPath(item.parent) : undefined;
|
||||
const parent = item.parent ? item.parent.id : undefined;
|
||||
const recursive = (data?: Recursive<T>[]) => data?.flatMap(e => {
|
||||
if(getPath(e) === parent)
|
||||
if(e.id === parent)
|
||||
{
|
||||
e.children = e.children ?? [];
|
||||
e.children.splice(clamp(pos, 0, e.children.length), 0, item);
|
||||
@@ -53,7 +53,7 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
||||
|
||||
this._flatten = this.accumulate(e => e);
|
||||
}
|
||||
find(path: string): T | undefined
|
||||
find(id: string): T | undefined
|
||||
{
|
||||
const recursive = (data?: Recursive<T>[]): T | undefined => {
|
||||
if(!data)
|
||||
@@ -61,7 +61,7 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
||||
|
||||
for(const e of data)
|
||||
{
|
||||
if(getPath(e) === path)
|
||||
if(e.id === id)
|
||||
return e;
|
||||
|
||||
const result = recursive(e.children as T[]);
|
||||
@@ -94,9 +94,9 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
||||
|
||||
return recursive(this._data);
|
||||
}
|
||||
subset(path: string): Tree<T> | undefined
|
||||
subset(id: string): Tree<T> | undefined
|
||||
{
|
||||
const subset = this.find(path);
|
||||
const subset = this.find(id);
|
||||
return subset ? new Tree([subset]) : undefined;
|
||||
}
|
||||
each(callback: (item: T, depth: number, parent?: T) => void)
|
||||
|
||||
Reference in New Issue
Block a user