Fix pull job links. Canvas now start centered and zoomed out based on nodes positions.
This commit is contained in:
parent
6100fd9411
commit
2c80cb2456
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -2,7 +2,7 @@ import useDatabase from "~/composables/useDatabase";
|
||||||
import { extname, basename } from 'node:path';
|
import { extname, basename } from 'node:path';
|
||||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||||
import type { FileType, ProjectContent } from "#shared/content.util";
|
import type { FileType, ProjectContent } from "#shared/content.util";
|
||||||
import { getID, ID_SIZE } from "#shared/general.util";
|
import { getID, ID_SIZE, parsePath } from "#shared/general.util";
|
||||||
import { projectContentTable, projectFilesTable } from "~/db/schema";
|
import { projectContentTable, projectFilesTable } from "~/db/schema";
|
||||||
|
|
||||||
const typeMapping: Record<string, FileType> = {
|
const typeMapping: Record<string, FileType> = {
|
||||||
|
|
@ -37,7 +37,7 @@ export default defineTask({
|
||||||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
||||||
return {
|
return {
|
||||||
id: getID(ID_SIZE),
|
id: getID(ID_SIZE),
|
||||||
path: path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
path: parsePath(path),
|
||||||
order: i,
|
order: i,
|
||||||
title: order && order[2] ? order[2] : title,
|
title: order && order[2] ? order[2] : title,
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
|
|
@ -57,7 +57,7 @@ export default defineTask({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: getID(ID_SIZE),
|
id: getID(ID_SIZE),
|
||||||
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
path: parsePath(extension === '.md' ? path.replace(extension, '') : path),
|
||||||
order: i,
|
order: i,
|
||||||
title: order && order[2] ? order[2] : title,
|
title: order && order[2] ? order[2] : title,
|
||||||
type: (typeMapping[extension] ?? 'file'),
|
type: (typeMapping[extension] ?? 'file'),
|
||||||
|
|
@ -69,6 +69,8 @@ export default defineTask({
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
files.forEach(e => e.content = reshapeLinks(e.content as string | null, files) ?? null);
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
db.transaction(tx => {
|
db.transaction(tx => {
|
||||||
db.delete(projectFilesTable).run();
|
db.delete(projectFilesTable).run();
|
||||||
|
|
@ -88,14 +90,19 @@ export default defineTask({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function reshapeLinks(content: string | null, all: ProjectContent[])
|
||||||
|
{
|
||||||
|
return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => {
|
||||||
|
return `[[${link ? parsePath(all.find(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reshapeContent(content: string, type: FileType): string | null
|
function reshapeContent(content: string, type: FileType): string | null
|
||||||
{
|
{
|
||||||
switch(type)
|
switch(type)
|
||||||
{
|
{
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
return content;
|
||||||
return `[[${a1?.split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/') ?? ''}${a2 ?? ''}${a3 ?? ''}]]`;
|
|
||||||
});
|
|
||||||
case "file":
|
case "file":
|
||||||
return content;
|
return content;
|
||||||
case "canvas":
|
case "canvas":
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ 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 type { History } from "./history.util";
|
||||||
import { EventEmitter } from "nodemailer/lib/xoauth2";
|
import { fakeA } from "./proses";
|
||||||
|
|
||||||
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 };
|
||||||
|
|
@ -129,7 +129,7 @@ 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 extends EventEmitter
|
export class Node extends EventTarget
|
||||||
{
|
{
|
||||||
properties: CanvasNode;
|
properties: CanvasNode;
|
||||||
|
|
||||||
|
|
@ -171,18 +171,60 @@ export class Node extends EventEmitter
|
||||||
{ 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 NodeEditable extends Node {
|
export class NodeEditable extends Node
|
||||||
|
{
|
||||||
|
|
||||||
|
private static input: HTMLInputElement = dom('input', { class: 'origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4', style: { 'max-width': '100%', 'font-size': 'calc(18px * var(--zoom-multiplier))' }, listeners: { click: e => e.stopImmediatePropagation() } });
|
||||||
constructor(properties: CanvasNode)
|
constructor(properties: CanvasNode)
|
||||||
{
|
{
|
||||||
super(properties);
|
super(properties);
|
||||||
}
|
}
|
||||||
protected override getDOM()
|
protected override getDOM()
|
||||||
{
|
{
|
||||||
super.getDOM();
|
const style = this.style;
|
||||||
|
|
||||||
|
/*
|
||||||
|
<div class="absolute" ref="dom" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
||||||
|
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||||
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
||||||
|
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
||||||
|
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex py-2" >
|
||||||
|
<FramedEditor v-model="node.text" autofocus :gutters="false"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(e)" @dblclick.left="(e) => editNode(e)" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" 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">{{ node.label }}</div>
|
||||||
|
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" v-autofocus :class="[style.border, style.outline]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4" />
|
||||||
|
</div>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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, style.outline] }, [
|
||||||
|
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'group' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), mouseleave: e => this.dispatchEvent(new CustomEvent('unfocus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: fakeA } })]) : undefined])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(this.properties.type === 'group')
|
||||||
|
{
|
||||||
|
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: this.properties.label, listeners: { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), mouseleave: e => this.dispatchEvent(new CustomEvent('unfocus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override get style()
|
||||||
|
{
|
||||||
|
return this.properties.color ? this.properties.color?.class ?
|
||||||
|
{ bg: `bg-light-${this.properties.color?.class} dark:bg-dark-${this.properties.color?.class}`, border: `border-light-${this.properties.color?.class} dark:border-dark-${this.properties.color?.class}`, outline: `outline-light-${this.properties.color?.class} dark:outline-dark-${this.properties.color?.class}` } :
|
||||||
|
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
|
||||||
|
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class Edge extends EventEmitter
|
|
||||||
|
export class Edge extends EventTarget
|
||||||
{
|
{
|
||||||
properties: CanvasEdge;
|
properties: CanvasEdge;
|
||||||
|
|
||||||
|
|
@ -201,6 +243,8 @@ export class Edge extends EventEmitter
|
||||||
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);
|
||||||
|
|
||||||
|
this.getDOM();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDOM()
|
protected getDOM()
|
||||||
|
|
@ -242,7 +286,7 @@ export class EdgeEditable extends Edge
|
||||||
protected override getDOM()
|
protected override getDOM()
|
||||||
{
|
{
|
||||||
const style = this.style;
|
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.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: { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } });
|
||||||
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.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.edgeDom = dom('div', { class: 'absolute overflow-visible group' }, [
|
||||||
this.inputDom,
|
this.inputDom,
|
||||||
|
|
@ -261,7 +305,6 @@ export class EdgeEditable extends Edge
|
||||||
|
|
||||||
export class Canvas
|
export class Canvas
|
||||||
{
|
{
|
||||||
static minZoom: number = 0.3;
|
|
||||||
static maxZoom: number = 3;
|
static maxZoom: number = 3;
|
||||||
|
|
||||||
protected content: Required<CanvasContent>;
|
protected content: Required<CanvasContent>;
|
||||||
|
|
@ -269,9 +312,9 @@ export class Canvas
|
||||||
protected x: number = 0;
|
protected x: number = 0;
|
||||||
protected y: number = 0;
|
protected y: number = 0;
|
||||||
|
|
||||||
private centerX: number = 0;
|
protected containZoom: number = this.zoom;
|
||||||
private centerY: number = 0;
|
protected centerX: number = this.x;
|
||||||
private containZoom: number = 0.5;
|
protected centerY: number = this.y;
|
||||||
|
|
||||||
protected visualZoom: number = this.zoom;
|
protected visualZoom: number = this.zoom;
|
||||||
protected visualX: number = this.x;
|
protected visualX: number = this.x;
|
||||||
|
|
@ -300,34 +343,29 @@ export class Canvas
|
||||||
this.content = content as Required<CanvasContent>;
|
this.content = content as Required<CanvasContent>;
|
||||||
|
|
||||||
this.createDOM();
|
this.createDOM();
|
||||||
|
|
||||||
this.computeLimits();
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
this.mount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createDOM()
|
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),
|
dom('div', {}, [...this.content.nodes.map(e => new Node(e).nodeDom)]), dom('div', {}, [...this.content.edges.map(e => new Edge(e, this.content.nodes).edgeDom)]),
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
//TODO: --zoom-multiplier dynamic
|
//TODO: --zoom-multiplier dynamic
|
||||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
|
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: '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' }, [
|
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')]), {
|
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, this.containZoom, 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'
|
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')]), {
|
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'
|
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')]), {
|
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, this.containZoom); } } }, [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'
|
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')]), {
|
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, this.containZoom, 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'
|
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('a') Edition link
|
]), //dom('a') Edition link
|
||||||
|
|
@ -343,21 +381,21 @@ export class Canvas
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeLimits()
|
protected computeLimits()
|
||||||
{
|
{
|
||||||
const box = this.container.getBoundingClientRect();
|
const box = this.container.getBoundingClientRect();
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
|
|
||||||
this.content.nodes.forEach(e => {
|
this.content.nodes.forEach(e => {
|
||||||
minX = Math.min(minX, e.x);
|
minX = Math.min(minX, e.x);
|
||||||
minY = Math.min(minY, e.y);
|
minY = Math.min(minY, e.y);
|
||||||
maxX = Math.max(maxX, e.x);
|
maxX = Math.max(maxX, e.x + e.width);
|
||||||
maxY = Math.max(maxY, e.y);
|
maxY = Math.max(maxY, e.y + e.height);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.centerX = minX + (maxX - minX) / 2;
|
this.containZoom = Math.pow(1 / Math.max((maxX - minX) / box.width, (maxY - minY) / box.height), 1.05);
|
||||||
this.centerY = minY + (maxY - minY) / 2;
|
this.centerX = -(minX + (maxX - minX) / 2) + box.width / 2;
|
||||||
this.containZoom = Math.min((maxX - minX) / box.width, (maxY - minY) / box.height);
|
this.centerY = -(minY + (maxY - minY) / 2) + box.height / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
mount()
|
mount()
|
||||||
|
|
@ -395,7 +433,7 @@ export class Canvas
|
||||||
window.addEventListener('mousemove', dragMove, { passive: true });
|
window.addEventListener('mousemove', dragMove, { passive: true });
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
this.container.addEventListener('wheel', (e) => {
|
this.container.addEventListener('wheel', (e) => {
|
||||||
if((this.zoom >= Canvas.maxZoom && e.deltaY < 0) || (this.zoom <= Canvas.minZoom && e.deltaY > 0))
|
if((this.zoom >= Canvas.maxZoom && e.deltaY < 0) || (this.zoom <= this.containZoom && e.deltaY > 0))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let box = this.container.getBoundingClientRect()!;
|
let box = this.container.getBoundingClientRect()!;
|
||||||
|
|
@ -404,7 +442,7 @@ export class Canvas
|
||||||
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
||||||
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
||||||
|
|
||||||
this.zoomTo(this.x - (mousex / (diff * this.zoom) - mousex / this.zoom), this.y - (mousey / (diff * this.zoom) - mousey / this.zoom), clamp(this.zoom * diff, Canvas.minZoom, Canvas.maxZoom));
|
this.zoomTo(this.x - (mousex / (diff * this.zoom) - mousex / this.zoom), this.y - (mousey / (diff * this.zoom) - mousey / this.zoom), clamp(this.zoom * diff, this.containZoom, Canvas.maxZoom));
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
this.container.addEventListener('touchstart', (e) => {
|
this.container.addEventListener('touchstart', (e) => {
|
||||||
({ x: lastX, y: lastY } = center(e.touches));
|
({ x: lastX, y: lastY } = center(e.touches));
|
||||||
|
|
@ -450,13 +488,14 @@ export class Canvas
|
||||||
const dist = distance(e.touches);
|
const dist = distance(e.touches);
|
||||||
const diff = dist / lastDistance;
|
const diff = dist / lastDistance;
|
||||||
|
|
||||||
this.zoom = clamp(this.zoom * diff, Canvas.minZoom, Canvas.maxZoom);
|
this.zoom = clamp(this.zoom * diff, this.containZoom, Canvas.maxZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateTransform();
|
this.updateTransform();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateTransform();
|
this.computeLimits();
|
||||||
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTransform()
|
private updateTransform()
|
||||||
|
|
@ -498,6 +537,10 @@ export class CanvasEditor extends Canvas
|
||||||
{
|
{
|
||||||
private history: History;
|
private history: History;
|
||||||
private selection: Array<NodeEditable | EdgeEditable> = [];
|
private selection: Array<NodeEditable | EdgeEditable> = [];
|
||||||
|
|
||||||
|
private nodes!: NodeEditable[];
|
||||||
|
private edges!: EdgeEditable[];
|
||||||
|
|
||||||
constructor(history: History, content?: CanvasContent)
|
constructor(history: History, content?: CanvasContent)
|
||||||
{
|
{
|
||||||
super(content);
|
super(content);
|
||||||
|
|
@ -505,25 +548,28 @@ export class CanvasEditor extends Canvas
|
||||||
}
|
}
|
||||||
protected override createDOM()
|
protected override createDOM()
|
||||||
{
|
{
|
||||||
|
this.nodes = this.content.nodes.map(e => new NodeEditable(e));
|
||||||
|
this.edges = this.content.edges.map(e => new EdgeEditable(e, this.content.nodes));
|
||||||
|
|
||||||
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 NodeEditable(e).nodeDom), ...this.content.edges.map(e => new EdgeEditable(e, this.content.nodes).edgeDom),
|
dom('div', {}, [...this.nodes.map(e => e.nodeDom)]), dom('div', {}, [...this.edges.map(e => e.edgeDom)]),
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
//TODO: --zoom-multiplier dynamic
|
//TODO: --zoom-multiplier dynamic
|
||||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
|
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: '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' }, [
|
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')]), {
|
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, this.containZoom, 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'
|
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')]), {
|
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'
|
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')]), {
|
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, this.containZoom); } } }, [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'
|
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')]), {
|
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, this.containZoom, 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'
|
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'
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,11 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
||||||
canvas: {
|
canvas: {
|
||||||
toString: (content) => JSON.stringify(content),
|
toString: (content) => JSON.stringify(content),
|
||||||
fromString: (str) => JSON.parse(str),
|
fromString: (str) => JSON.parse(str),
|
||||||
render: (content) => new Canvas(content.content).container,
|
render: (content) => {
|
||||||
|
const c = new Canvas(content.content);
|
||||||
|
queueMicrotask(() => c.mount());
|
||||||
|
return c.container;
|
||||||
|
},
|
||||||
renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
|
renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
|
||||||
},
|
},
|
||||||
markdown: {
|
markdown: {
|
||||||
|
|
@ -794,7 +798,6 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function getID(length: number)
|
||||||
}
|
}
|
||||||
export function parsePath(path: string): string
|
export function parsePath(path: string): string
|
||||||
{
|
{
|
||||||
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '');
|
return path.replace(/(\d+?)\. ?/, '').toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '').replace(/\-+/g, '-');
|
||||||
}
|
}
|
||||||
export function parseId(id: string | undefined): string |undefined
|
export function parseId(id: string | undefined): string |undefined
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ function renderContent(node: RootContent, proses: Record<string, Prose>): Node
|
||||||
export interface MDProperties
|
export interface MDProperties
|
||||||
{
|
{
|
||||||
class?: Class;
|
class?: Class;
|
||||||
style?: string | Record<string, string>
|
style?: string | Record<string, string>;
|
||||||
|
tags?: Record<string, Prose>;
|
||||||
}
|
}
|
||||||
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
|
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
|
||||||
{
|
{
|
||||||
|
|
@ -61,7 +62,7 @@ export default function(content: string, filter?: string, properties?: MDPropert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = renderMarkdown(data, { tag, a, blockquote, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, });
|
const el = renderMarkdown(data, { tag, a, blockquote, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
|
||||||
|
|
||||||
if(properties) styling(el, properties);
|
if(properties) styling(el, properties);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ export const a: Prose = {
|
||||||
onShow(content: HTMLDivElement) {
|
onShow(content: HTMLDivElement) {
|
||||||
if(!rendered)
|
if(!rendered)
|
||||||
{
|
{
|
||||||
console.log('')
|
|
||||||
Content.getContent(overview.id).then((_content) => {
|
Content.getContent(overview.id).then((_content) => {
|
||||||
if(_content?.type === 'markdown')
|
if(_content?.type === 'markdown')
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue