Fix content read pages and proses getting content. Start working on CanvasEditor.
This commit is contained in:
parent
1d41514b26
commit
6100fd9411
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -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, () => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue