Fix content read pages and proses getting content. Start working on CanvasEditor.

This commit is contained in:
Clément Pons 2025-04-01 17:23:26 +02:00
parent 1d41514b26
commit 6100fd9411
7 changed files with 222 additions and 88 deletions

Binary file not shown.

Binary file not shown.

View File

@ -57,6 +57,7 @@ import { hasPermissions } from '#shared/auth.util';
import { TreeDOM } from '#shared/tree'; import { TreeDOM } from '#shared/tree';
import { Content, iconByType } from '#shared/content.util'; import { Content, iconByType } from '#shared/content.util';
import { dom, icon, text } from '#shared/dom.util'; import { dom, icon, text } from '#shared/dom.util';
import { unifySlug } from '#shared/general.util';
import { popper } from '#shared/floating.util'; import { popper } from '#shared/floating.util';
import { link } from '#shared/proses'; import { link } from '#shared/proses';
@ -77,7 +78,7 @@ const { fetch } = useContent();
await fetch(false); await fetch(false);
const route = useRouter().currentRoute; const route = useRouter().currentRoute;
const path = computed(() => route.value.params.path ? decodeURIComponent(Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path) : undefined); const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
await Content.init(); await Content.init();
const tree = new TreeDOM((item, depth) => { const tree = new TreeDOM((item, depth) => {
@ -100,7 +101,7 @@ const unmount = useRouter().afterEach((to, from, failure) => {
if(failure) if(failure)
return; return;
to.name === 'explore-path' && ((to.params.path as string).split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true)); to.name === 'explore-path' && (unifySlug(to.params.path).split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true));
}); });
watch(route, () => { watch(route, () => {

View File

@ -7,16 +7,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Content } from '~/shared/content.util'; import { Content } from '#shared/content.util';
import { unifySlug } from '#shared/general.util';
const element = useTemplateRef('element'), overview = ref(); const element = useTemplateRef('element'), overview = ref();
const route = useRouter().currentRoute; const route = useRouter().currentRoute;
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path); const path = computed(() => unifySlug(route.value.params.path));
onMounted(async () => { onMounted(async () => {
if(element.value && path.value) if(element.value && path.value && await Content.ready)
{ {
await Content.init()
overview.value = Content.render(element.value, path.value); overview.value = Content.render(element.value, path.value);
} }
}); });

View File

@ -4,6 +4,8 @@ import { dom, icon, svg, text } from "./dom.util";
import render from "./markdown.util"; import render from "./markdown.util";
import { popper } from "#shared/floating.util"; import { popper } from "#shared/floating.util";
import { Content } from "./content.util"; import { Content } from "./content.util";
import type { History } from "./history.util";
import { EventEmitter } from "nodemailer/lib/xoauth2";
export type Direction = 'bottom' | 'top' | 'left' | 'right'; export type Direction = 'bottom' | 'top' | 'left' | 'right';
export type Position = { x: number, y: number }; export type Position = { x: number, y: number };
@ -127,29 +129,36 @@ function distance(touches: TouchList): number
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY); return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
} }
export class Node export class Node extends EventEmitter
{ {
properties: CanvasNode; properties: CanvasNode;
nodeDom: HTMLDivElement; nodeDom!: HTMLDivElement;
constructor(properties: CanvasNode) constructor(properties: CanvasNode)
{ {
super();
this.properties = properties; this.properties = properties;
this.getDOM()
}
protected getDOM()
{
const style = this.style; const style = this.style;
this.nodeDom = dom('div', { class: ['absolute', {'-z-10': properties.type === 'group', 'z-10': properties.type !== 'group'}], style: { transform: `translate(${properties.x}px, ${properties.y}px)`, width: `${properties.width}px`, height: `${properties.height}px`, '--canvas-color': properties.color?.hex } }, [ this.nodeDom = dom('div', { class: ['absolute', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4', style.border] }, [ dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4', style.border] }, [
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg] }, [properties.text ? dom('div', { class: 'flex items-center' }, [render(properties.text)]) : undefined]) dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg] }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text)]) : undefined])
]) ])
]); ]);
if(properties.type === 'group') if(this.properties.type === 'group')
{ {
if(properties.label !== undefined) if(this.properties.label !== undefined)
{ {
this.nodeDom.appendChild(dom('div', { class: ['origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin', style.border], style: 'max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))', text: properties.label })); this.nodeDom.appendChild(dom('div', { class: ['origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin', style.border], style: 'max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))', text: this.properties.label }));
} }
} }
} }
@ -162,46 +171,49 @@ export class Node
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` } { border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
} }
} }
export class Edge export class NodeEditable extends Node {
constructor(properties: CanvasNode)
{
super(properties);
}
protected override getDOM()
{
super.getDOM();
}
}
export class Edge extends EventEmitter
{ {
properties: CanvasEdge; properties: CanvasEdge;
edgeDom: HTMLDivElement; edgeDom!: HTMLDivElement;
#from: CanvasNode; protected from: CanvasNode;
#to: CanvasNode; protected to: CanvasNode;
#path: Path; protected path: Path;
#labelPos: string; protected labelPos: string;
constructor(properties: CanvasEdge, nodes: CanvasNode[]) constructor(properties: CanvasEdge, nodes: CanvasNode[])
{ {
super();
this.properties = properties; this.properties = properties;
this.#from = nodes.find(f => f.id === properties.fromNode)!; this.from = nodes.find(f => f.id === properties.fromNode)!;
this.#to = nodes.find(f => f.id === properties.toNode)!; this.to = nodes.find(f => f.id === properties.toNode)!;
this.#path = getPath(this.#from, properties.fromSide, this.#to, properties.toSide)!; this.path = getPath(this.from, properties.fromSide, this.to, properties.toSide)!;
this.#labelPos = labelCenter(this.#from, properties.fromSide, this.#to, properties.toSide); this.labelPos = labelCenter(this.from, properties.fromSide, this.to, properties.toSide);
}
protected getDOM()
{
const style = this.style; const style = this.style;
/* <div class="absolute overflow-visible">
<div v-if="edge.label" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20">{{ edge.label }}</div>
<svg class="absolute top-0 overflow-visible h-px w-px">
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
<g :style="`transform: translate(${this.#path!.to.x}px, ${this.#path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[this.#path!.side]}deg);`">
<polygon :class="style.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
</g>
<path :style="`stroke-width: calc(3px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="style.stroke" class="fill-none stroke-[4px]" :d="this.#path!.path"></path>
</g>
</svg>
</div> */
this.edgeDom = dom('div', { class: 'absolute overflow-visible' }, [ this.edgeDom = dom('div', { class: 'absolute overflow-visible' }, [
properties.label ? dom('div', { style: { transform: `${this.#labelPos} translate(-50%, -50%)` }, class: 'relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20', text: properties.label }) : undefined, this.properties.label ? dom('div', { style: { transform: `${this.labelPos} translate(-50%, -50%)` }, class: 'relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20', text: this.properties.label }) : undefined,
svg('svg', { class: 'absolute top-0 overflow-visible h-px w-px' }, [ svg('svg', { class: 'absolute top-0 overflow-visible h-px w-px' }, [
svg('g', { style: {'--canvas-color': properties.color?.hex}, class: 'z-0' }, [ svg('g', { style: {'--canvas-color': this.properties.color?.hex}, class: 'z-0' }, [
svg('g', { style: `transform: translate(${this.#path!.to.x}px, ${this.#path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[this.#path!.side]}deg);` }, [ svg('g', { style: `transform: translate(${this.path!.to.x}px, ${this.path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[this.path!.side]}deg);` }, [
svg('polygon', { class: style.fill, attributes: { points: '0,0 6.5,10.4 -6.5,10.4' } }), svg('polygon', { class: style.fill, attributes: { points: '0,0 6.5,10.4 -6.5,10.4' } }),
]), ]),
svg('path', { style: `stroke-width: calc(3px * var(--zoom-multiplier)); stroke-linecap: butt;`, class: [style.stroke, 'fill-none stroke-[4px]'], attributes: { d: this.#path!.path } }), svg('path', { style: `stroke-width: calc(3px * var(--zoom-multiplier)); stroke-linecap: butt;`, class: [style.stroke, 'fill-none stroke-[4px]'], attributes: { d: this.path!.path } }),
]), ]),
]), ]),
]); ]);
@ -215,26 +227,61 @@ export class Edge
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` } { stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` }
} }
} }
export class EdgeEditable extends Edge
{
private static input: HTMLInputElement = dom('input', { class: 'relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20 -translate-x-1/2 -translate-y-1/2', listeners: { click: e => e.stopImmediatePropagation() } });
private focusing: boolean = false;
private editing: boolean = false;
private pathDom!: SVGPathElement;
private inputDom!: HTMLDivElement;
constructor(properties: CanvasEdge, nodes: CanvasNode[])
{
super(properties, nodes);
}
protected override getDOM()
{
const style = this.style;
this.pathDom = svg('path', { style: { 'stroke-width': `calc(3px * var(--zoom-multiplier))`, 'stroke-linecap': 'butt' }, class: ['transition-[stroke-width] fill-none stroke-[4px]', style.stroke], listeners: { click: e => this.emit('select', e), dblclick: e => this.emit('edit', e) } });
this.inputDom = dom('div', { class: ['relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20 -translate-x-1/2 -translate-y-1/2', { 'hidden': this.properties.label === undefined }], style: { transform: this.labelPos }, text: this.properties.label });
this.edgeDom = dom('div', { class: 'absolute overflow-visible group' }, [
this.inputDom,
svg('svg', { class: 'absolute top-0 overflow-visible h-px w-px' }, [
svg('g', { style: { '--canvas-color': this.properties.color?.hex } }, [
svg('g', { style: { transform: `translate(${this.path.to.x}px, ${this.path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[this.path.side]}deg)` } }, [
svg('polygon', { class: style.fill, attributes: { 'points': '0,0 6.5,10.4 -6.5,10.4' } }),
]),
this.pathDom,
svg('path', { style: { 'stroke-width': `calc(22px * var(--zoom-multiplier))` }, class: ['fill-none transition-opacity z-30 opacity-0 hover:opacity-25', style.stroke], attributes: { d: this.path.path } }),
]),
]),
]);
}
}
export class Canvas export class Canvas
{ {
static minZoom: number = 0.3; static minZoom: number = 0.3;
static maxZoom: number = 3; static maxZoom: number = 3;
private content: Required<CanvasContent>; protected content: Required<CanvasContent>;
private zoom: number = 0.5; protected zoom: number = 0.5;
private x: number = 0; protected x: number = 0;
private y: number = 0; protected y: number = 0;
private visualZoom: number = this.zoom; private centerX: number = 0;
private visualX: number = this.x; private centerY: number = 0;
private visualY: number = this.y; private containZoom: number = 0.5;
private tweener: Tweener = new Tweener(); protected visualZoom: number = this.zoom;
protected visualX: number = this.x;
protected visualY: number = this.y;
protected tweener: Tweener = new Tweener();
private debouncedTimeout: Timer = setTimeout(() => {}, 0); private debouncedTimeout: Timer = setTimeout(() => {}, 0);
private transform: HTMLDivElement; protected transform!: HTMLDivElement;
container: HTMLDivElement; container!: HTMLDivElement;
constructor(content?: CanvasContent) constructor(content?: CanvasContent)
{ {
@ -252,6 +299,16 @@ export class Canvas
this.content = content as Required<CanvasContent>; this.content = content as Required<CanvasContent>;
this.createDOM();
this.computeLimits();
this.reset();
this.mount();
}
protected createDOM()
{
this.transform = dom('div', { class: 'origin-center h-full' }, [ this.transform = dom('div', { class: 'origin-center h-full' }, [
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [ dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
...this.content.nodes.map(e => new Node(e).nodeDom), ...this.content.edges.map(e => new Edge(e, this.content.nodes).edgeDom), ...this.content.nodes.map(e => new Node(e).nodeDom), ...this.content.edges.map(e => new Edge(e, this.content.nodes).edgeDom),
@ -275,7 +332,8 @@ export class Canvas
}), }),
]), //dom('a') Edition link ]), //dom('a') Edition link
]), this.transform, ]), this.transform,
]) ]);
/* /*
<NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10"> <NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Modifier" side="right"> <Tooltip message="Modifier" side="right">
@ -283,8 +341,23 @@ export class Canvas
</Tooltip> </Tooltip>
</NuxtLink> </NuxtLink>
*/ */
}
this.mount(); private computeLimits()
{
const box = this.container.getBoundingClientRect();
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
this.content.nodes.forEach(e => {
minX = Math.min(minX, e.x);
minY = Math.min(minY, e.y);
maxX = Math.max(maxX, e.x);
maxY = Math.max(maxY, e.y);
});
this.centerX = minX + (maxX - minX) / 2;
this.centerY = minY + (maxY - minY) / 2;
this.containZoom = Math.min((maxX - minX) / box.width, (maxY - minY) / box.height);
} }
mount() mount()
@ -400,7 +473,7 @@ export class Canvas
this.container.style.setProperty('--zoom-multiplier', (1 / Math.pow(this.visualZoom, 0.7)).toFixed(3)); this.container.style.setProperty('--zoom-multiplier', (1 / Math.pow(this.visualZoom, 0.7)).toFixed(3));
} }
private zoomTo(x: number, y: number, zoom: number) protected zoomTo(x: number, y: number, zoom: number)
{ {
const oldX = this.x, oldY = this.y, oldZoom = this.zoom; const oldX = this.x, oldY = this.y, oldZoom = this.zoom;
this.x = x; this.x = x;
@ -416,16 +489,62 @@ export class Canvas
}, 50); }, 50);
} }
private reset() protected reset()
{ {
this.tweener.stop(); this.zoomTo(this.centerX, this.centerY, this.containZoom);
}
this.zoom = this.visualZoom = 0.5; }
this.x = this.visualX = 0; export class CanvasEditor extends Canvas
this.y = this.visualY = 0; {
private history: History;
this.updateTransform(); private selection: Array<NodeEditable | EdgeEditable> = [];
this.updateScale(); constructor(history: History, content?: CanvasContent)
{
super(content);
this.history = history;
}
protected override createDOM()
{
this.transform = dom('div', { class: 'origin-center h-full' }, [
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
...this.content.nodes.map(e => new NodeEditable(e).nodeDom), ...this.content.edges.map(e => new EdgeEditable(e, this.content.nodes).edgeDom),
])
]);
//TODO: --zoom-multiplier dynamic
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom * 1.1, Canvas.minZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:reload')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Reset')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.zoomTo(this.x, this.y, Canvas.minZoom); } } }, [icon('radix-icons:corners')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom / 1.1, Canvas.minZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
]),
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('ph:arrow-bend-up-left')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Annuler (Ctrl+Z)')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('ph:arrow-bend-up-right')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Rétablir (Ctrl+Y)')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
]),
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('radix-icons:gear')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Préférences')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('radix-icons:question-mark-circled')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Aide')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
}),
]),
]), this.transform,
]);
} }
} }

View File

@ -153,13 +153,24 @@ export class Content
return Content.initPromise; return Content.initPromise;
} }
static async get(id: string, content: boolean): Promise<LocalContent | undefined> static getFromPath(path: string, content: boolean = false)
{
return Object.values(Content._overview).find(e => e.path === path);
}
static get(id: string, content: boolean = false)
{
return Content._overview[id];
}
static async getContent(id: string): Promise<LocalContent | undefined>
{ {
const overview = Content._overview[id]; const overview = Content._overview[id];
return overview ? { ...overview, content: content ? Content.fromString(overview, await Content.read(id) ?? '') : undefined } as LocalContent : undefined; if(!overview)
return;
return { ...overview, content: Content.fromString(overview, (await Content.read(id, { create: true }))!) };
} }
static async set(id: string, overview?: Omit<LocalContent, 'content'> | Recursive<Omit<LocalContent, 'content'>>) static set(id: string, overview?: Omit<LocalContent, 'content'> | Recursive<Omit<LocalContent, 'content'>>)
{ {
if(overview === undefined) if(overview === undefined)
{ {
@ -231,9 +242,9 @@ export class Content
continue; continue;
Content.queue.queue(() => { Content.queue.queue(() => {
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: ContentMap[FileType] | undefined) => { return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined) => {
if(content) if(content)
Content.queue.queue(() => Content.write(file.id, Content.toString({ ...file, content }), { create: true })); Content.queue.queue(() => Content.write(file.id, content, { create: true }));
else else
Content._overview[file.id].error = true; Content._overview[file.id].error = true;
}).catch(e => { }).catch(e => {
@ -319,7 +330,7 @@ export class Content
static render(parent: HTMLElement, path: string): Omit<LocalContent, 'content'> | undefined static render(parent: HTMLElement, path: string): Omit<LocalContent, 'content'> | undefined
{ {
const overview = Content.overview(path); const overview = Content.getFromPath(path);
if(!!overview) if(!!overview)
{ {
@ -332,7 +343,7 @@ export class Content
el && parent.replaceChild(el, load); el && parent.replaceChild(el, load);
} }
Content.content(path).then(content => _render(content!)); Content.getContent(overview.id).then(content => _render(content!));
} }
else else
{ {
@ -423,7 +434,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
else else
{ {
element = loading("large"); element = loading("large");
Content.get(content.id, true).then(e => { Content.getContent(content.id).then(e => {
if(!e) if(!e)
return element.parentElement?.replaceChild(dom('div', { class: '', text: 'Une erreur est survenue.' }), element); return element.parentElement?.replaceChild(dom('div', { class: '', text: 'Une erreur est survenue.' }), element);
@ -783,6 +794,7 @@ export class Editor
{ {
if(this.selected && item) if(this.selected && item)
{ {
console.log(this.selected.content);
Content.save(this.selected); Content.save(this.selected);
} }
if(this.selected === item) if(this.selected === item)

View File

@ -1,10 +1,11 @@
import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses } from "./dom.util"; import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses } from "#shared/dom.util";
import { parseURL } from 'ufo'; import { parseURL } from 'ufo';
import render from "./markdown.util"; import render from "#shared/markdown.util";
import { popper } from "#shared/floating.util"; import { popper } from "#shared/floating.util";
import { Canvas } from "./canvas.util"; import { Canvas } from "#shared/canvas.util";
import { Content, iconByType, type LocalContent } from "./content.util"; import { Content, iconByType, type LocalContent } from "#shared/content.util";
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router"; import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
import { unifySlug } from "#shared/general.util";
export type CustomProse = (properties: any, children: NodeChildren) => Node; export type CustomProse = (properties: any, children: NodeChildren) => Node;
export type Prose = { class: string } | { custom: CustomProse }; export type Prose = { class: string } | { custom: CustomProse };
@ -12,7 +13,7 @@ export const tag: Prose = {
custom(properties, children) { custom(properties, children) {
const tag = properties.tag as string; const tag = properties.tag as string;
const el = dom('span', { class: "before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30" }, children); const el = dom('span', { class: "before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30" }, children);
const overview = Content.overview('tags'); const overview = Content.getFromPath('tags');
let rendered = false; let rendered = false;
@ -28,8 +29,8 @@ export const tag: Prose = {
onShow(content: HTMLDivElement) { onShow(content: HTMLDivElement) {
if(!rendered) if(!rendered)
{ {
Content.content('tags').then((overview) => { Content.getContent(overview.id).then((_content) => {
content.replaceChild(render((overview as LocalContent<'markdown'>).content ?? '', tag, { class: 'max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]); content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', tag, { class: 'max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]);
}); });
rendered = true; rendered = true;
} }
@ -46,7 +47,7 @@ export const a: Prose = {
const { hash, pathname } = parseURL(href); const { hash, pathname } = parseURL(href);
const router = useRouter(); const router = useRouter();
const overview = Content.overview(pathname === '' && hash.length > 0 ? router.currentRoute.value.params.path[0] : pathname); const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path) : pathname);
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link); const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
@ -76,14 +77,15 @@ export const a: Prose = {
onShow(content: HTMLDivElement) { onShow(content: HTMLDivElement) {
if(!rendered) if(!rendered)
{ {
Content.content(overview.path).then((overview) => { console.log('')
if(overview?.type === 'markdown') Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{ {
content.replaceChild(render((overview as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]); content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]);
} }
if(overview?.type === 'canvas') if(_content?.type === 'canvas')
{ {
const canvas = new Canvas((overview as LocalContent<'canvas'>).content); const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]); content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]);
canvas.mount(); canvas.mount();
} }
@ -103,7 +105,7 @@ export const fakeA: Prose = {
const { hash, pathname } = parseURL(href); const { hash, pathname } = parseURL(href);
const router = useRouter(); const router = useRouter();
const overview = Content.overview(pathname === '' && hash.length > 0 ? router.currentRoute.value.params.path[0] : pathname); const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path) : pathname);
const el = dom('span', { class: 'cursor-pointer text-accent-blue inline-flex items-center' }, [ const el = dom('span', { class: 'cursor-pointer text-accent-blue inline-flex items-center' }, [
dom('span', {}, [ dom('span', {}, [
@ -127,14 +129,14 @@ export const fakeA: Prose = {
return false; return false;
content.replaceChild(loading("large"), content.children[0]); content.replaceChild(loading("large"), content.children[0]);
Content.content(overview.path).then((overview) => { Content.getContent(overview.id).then((_content) => {
if(overview?.type === 'markdown') if(_content?.type === 'markdown')
{ {
content.replaceChild(render((overview as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]); content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]);
} }
if(overview?.type === 'canvas') if(_content?.type === 'canvas')
{ {
const canvas = new Canvas((overview as LocalContent<'canvas'>).content); const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]); content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]);
canvas.mount(); canvas.mount();
} }