Minimal history handler, handle node move. Auto parse JSON content for accurate typing.
This commit is contained in:
parent
b1a9eb859e
commit
62950be032
|
|
@ -8,7 +8,22 @@ const rotation: Record<Direction, string> = {
|
||||||
right: "270"
|
right: "270"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ActionMap {
|
||||||
|
move: Position;
|
||||||
|
edit: string;
|
||||||
|
resize: string;
|
||||||
|
}
|
||||||
|
type Action = keyof ActionMap;
|
||||||
|
interface HistoryAction<T extends Action = Action>
|
||||||
|
{
|
||||||
|
event: T;
|
||||||
|
element: number | number[];
|
||||||
|
from: ActionMap[T];
|
||||||
|
to: ActionMap[T];
|
||||||
|
}
|
||||||
|
|
||||||
const cancelEvent = (e: Event) => e.preventDefault();
|
const cancelEvent = (e: Event) => e.preventDefault();
|
||||||
|
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||||
function center(touches: TouchList): Position
|
function center(touches: TouchList): Position
|
||||||
{
|
{
|
||||||
const pos = { x: 0, y: 0 };
|
const pos = { x: 0, y: 0 };
|
||||||
|
|
@ -28,38 +43,37 @@ function distance(touches: TouchList): number
|
||||||
const [A, B] = touches;
|
const [A, B] = touches;
|
||||||
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
||||||
}
|
}
|
||||||
|
function contains(group: CanvasNode, node: CanvasNode): boolean
|
||||||
|
{
|
||||||
|
return group.x < node.x && group.y < node.y && group.x + group.width > node.x + node.width && group.y + group.height > node.y + node.height;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDrag, useHover, usePinch, useWheel } from '@vueuse/gesture';
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import { clamp } from '#shared/general.utils';
|
import { clamp } from '#shared/general.utils';
|
||||||
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
|
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||||
import { labelCenter, getPath } from '#shared/canvas.util';
|
import { labelCenter, getPath } from '#shared/canvas.util';
|
||||||
import FakeA from './prose/FakeA.vue';
|
|
||||||
|
|
||||||
const { canvas } = defineProps<{
|
const canvas = defineModel<CanvasContent>({ required: true, });
|
||||||
canvas: CanvasContent
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||||
const focusing = ref<CanvasNode | CanvasEdge>(), editing = ref<CanvasNode | CanvasEdge>();
|
const focusing = ref<number>(), editing = ref<number>();
|
||||||
const canvasRef = useTemplateRef('canvasRef');
|
const canvasRef = useTemplateRef('canvasRef');
|
||||||
|
const nodes = useTemplateRef('nodes');
|
||||||
|
|
||||||
const nodes = computed(() => {
|
|
||||||
return canvas.nodes.map(e => ({ ...e, class: e.color ? e.color?.class ?
|
|
||||||
{ bg: `bg-light-${e.color?.class} dark:bg-dark-${e.color?.class}`, border: `border-light-${e.color?.class} dark:border-dark-${e.color?.class}`, outline: `outline-light-${e.color?.class} dark:outline-dark-${e.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` } }))
|
|
||||||
});
|
|
||||||
const edges = computed(() => {
|
const edges = computed(() => {
|
||||||
return canvas.edges.map(e => {
|
return canvas.value.edges.map(e => {
|
||||||
const from = canvas.nodes.find(f => f.id === e.fromNode), to = canvas.nodes.find(f => f.id === e.toNode);
|
const from = canvas.value.nodes.find(f => f.id === e.fromNode), to = canvas.value.nodes.find(f => f.id === e.toNode);
|
||||||
const path = getPath(from!, e.fromSide, to!, e.toSide)!;
|
const path = getPath(from!, e.fromSide, to!, e.toSide)!;
|
||||||
return { ...e, from, to, path };
|
return { ...e, from, to, path };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const history = ref<HistoryAction[]>([]);
|
||||||
|
const historyPos = ref(-1);
|
||||||
|
const lastActiveAction = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
||||||
|
|
||||||
const reset = (_: MouseEvent) => {
|
const reset = (_: MouseEvent) => {
|
||||||
zoom.value = minZoom.value;
|
zoom.value = minZoom.value;
|
||||||
|
|
||||||
|
|
@ -67,6 +81,12 @@ const reset = (_: MouseEvent) => {
|
||||||
dispY.value = 0;
|
dispY.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addAction<T extends Action = Action>(event: Action, element: number | number[], from: ActionMap[T], to: ActionMap[T])
|
||||||
|
{
|
||||||
|
historyPos.value++;
|
||||||
|
history.value.splice(historyPos.value, history.value.length - historyPos.value);
|
||||||
|
history.value[historyPos.value] = { event, element, from, to };
|
||||||
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||||
|
|
@ -103,8 +123,6 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
canvasRef.value?.addEventListener('wheel', (e) => {
|
canvasRef.value?.addEventListener('wheel', (e) => {
|
||||||
if(!editing.value)
|
|
||||||
{
|
|
||||||
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -116,7 +134,6 @@ onMounted(() => {
|
||||||
dispY.value -= mousey / (diff * zoom.value) - mousey / zoom.value;
|
dispY.value -= mousey / (diff * zoom.value) - mousey / zoom.value;
|
||||||
|
|
||||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
||||||
}
|
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
canvasRef.value?.addEventListener('touchstart', (e) => {
|
canvasRef.value?.addEventListener('touchstart', (e) => {
|
||||||
({ x: lastX, y: lastY } = center(e.touches));
|
({ x: lastX, y: lastY } = center(e.touches));
|
||||||
|
|
@ -165,59 +182,155 @@ onMounted(() => {
|
||||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3); //@TODO
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3); //@TODO
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
function selectNode(node: CanvasNode, e: MouseEvent)
|
function move(index: number, x: number, y: number)
|
||||||
{
|
{
|
||||||
const target = e.currentTarget as HTMLElement;
|
const node = canvas.value.nodes[index];
|
||||||
let lastX = 0, lastY = 0;
|
const oldx = node.x, oldy = node.y;
|
||||||
|
|
||||||
const unselect = (_e: MouseEvent) => {
|
forElements(index, (e) => {
|
||||||
if(_e.button === 0 && _e.target !== target)
|
e.x -= x / zoom.value;
|
||||||
|
e.y -= y / zoom.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(lastActiveAction.value && lastActiveAction.value.event === 'move' && lastActiveAction.value.element === index)
|
||||||
{
|
{
|
||||||
focusing.value = undefined;
|
const action = lastActiveAction.value as HistoryAction<'move'>;
|
||||||
|
|
||||||
|
action.to.x -= x / zoom.value;
|
||||||
|
action.to.y -= y / zoom.value;
|
||||||
}
|
}
|
||||||
};
|
else
|
||||||
const dragStart = (_e: MouseEvent) => {
|
{
|
||||||
if(e.button !== 0)
|
addAction('move', index, { x: oldx, y: oldy }, { x: canvas.value.nodes[index].x, y: canvas.value.nodes[index].y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function select(node: CanvasNode, index: number, event: Event)
|
||||||
|
{
|
||||||
|
if(focusing.value !== index)
|
||||||
|
{
|
||||||
|
unselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.value![index]?.dom?.addEventListener('click', stopPropagation);
|
||||||
|
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
||||||
|
|
||||||
|
focusing.value = index;
|
||||||
|
}
|
||||||
|
function edit(node: CanvasNode, index: number, event: Event)
|
||||||
|
{
|
||||||
|
nodes.value![index]?.dom?.addEventListener('wheel', stopPropagation);
|
||||||
|
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
||||||
|
|
||||||
|
editing.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unselect = () => {
|
||||||
|
if(focusing.value !== undefined)
|
||||||
|
{
|
||||||
|
nodes.value![focusing.value]?.dom?.removeEventListener('click', stopPropagation);
|
||||||
|
nodes.value![focusing.value]?.unselect();
|
||||||
|
}
|
||||||
|
focusing.value = undefined;
|
||||||
|
|
||||||
|
if(editing.value !== undefined)
|
||||||
|
{
|
||||||
|
debugger;
|
||||||
|
nodes.value![editing.value]?.dom?.removeEventListener('wheel', stopPropagation);
|
||||||
|
nodes.value![editing.value]?.dom?.removeEventListener('click', stopPropagation);
|
||||||
|
nodes.value![editing.value]?.unselect();
|
||||||
|
}
|
||||||
|
editing.value = undefined;
|
||||||
|
};
|
||||||
|
const undo = () => {
|
||||||
|
if(!lastActiveAction.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lastX = _e.clientX;
|
switch(lastActiveAction.value.event)
|
||||||
lastY= _e.clientY;
|
{
|
||||||
|
case 'move':
|
||||||
|
{
|
||||||
|
const action = lastActiveAction.value as HistoryAction<'move'>;
|
||||||
|
|
||||||
window.addEventListener('mousemove', dragMove, { passive: true });
|
const x = action.to.x - action.from.x, y = action.to.y - action.from.y;
|
||||||
window.addEventListener('mouseup', dragEnd, { passive: true });
|
|
||||||
};
|
|
||||||
const dragMove = (_e: MouseEvent) => {
|
|
||||||
node.x -= (lastX - e.clientX)/ zoom.value;
|
|
||||||
node.y -= (lastY - e.clientY)/ zoom.value;
|
|
||||||
|
|
||||||
focusing.value = node;
|
forElements(action.element, (e) => {
|
||||||
|
e.x -= x;
|
||||||
|
e.y -= y;
|
||||||
|
});
|
||||||
|
|
||||||
lastX = _e.clientX;
|
break;
|
||||||
lastY= _e.clientY;
|
}
|
||||||
|
case 'edit':
|
||||||
|
{
|
||||||
|
const action = lastActiveAction.value as HistoryAction<'edit'>;
|
||||||
|
|
||||||
|
forElements(action.element, (e) => {
|
||||||
|
e.text = action.from;
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
const dragEnd = (_e: MouseEvent) => {
|
|
||||||
window.removeEventListener('mousemove', dragMove);
|
|
||||||
window.removeEventListener('mouseup', dragEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('mousemove', dragMove);
|
historyPos.value--;
|
||||||
window.removeEventListener('mouseup', dragEnd);
|
};
|
||||||
target.removeEventListener('mousedown', dragStart);
|
const redo = () => {
|
||||||
|
if(!history.value || history.value.length - 1 <= historyPos.value)
|
||||||
|
return;
|
||||||
|
|
||||||
focusing.value = node;
|
historyPos.value++;
|
||||||
|
|
||||||
//canvasRef.value?.addEventListener('mousedown', unselect, { once: true, passive: true });
|
if(!lastActiveAction.value)
|
||||||
target.addEventListener('mousedown', dragStart, { passive: true });
|
{
|
||||||
}
|
historyPos.value--;
|
||||||
function editNode(node: CanvasNode, e: MouseEvent)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(lastActiveAction.value.event)
|
||||||
|
{
|
||||||
|
case 'move':
|
||||||
|
{
|
||||||
|
const action = lastActiveAction.value as HistoryAction<'move'>;
|
||||||
|
|
||||||
|
const x = action.from.x - action.to.x, y = action.from.y - action.to.y;
|
||||||
|
|
||||||
|
forElements(action.element, (e) => {
|
||||||
|
e.x -= x;
|
||||||
|
e.y -= y;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'edit':
|
||||||
|
{
|
||||||
|
const action = lastActiveAction.value as HistoryAction<'edit'>;
|
||||||
|
|
||||||
|
forElements(action.element, (e) => {
|
||||||
|
e.text = action.to;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useShortcuts({
|
||||||
|
meta_z: undo,
|
||||||
|
meta_y: redo,
|
||||||
|
})
|
||||||
|
|
||||||
|
function forElements(element: number | number[], fn: (e: CanvasNode) => void)
|
||||||
{
|
{
|
||||||
editing.value = node;
|
if(Array.isArray(element))
|
||||||
}
|
{
|
||||||
function resizeNode(e: MouseEvent, x: number, y: number)
|
for(const e of element)
|
||||||
{
|
{
|
||||||
const target = e.currentTarget;
|
fn(canvas.value.nodes[e]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fn(canvas.value.nodes[element]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -287,29 +400,7 @@ function resizeNode(e: MouseEvent, x: number, y: number)
|
||||||
}" class="h-full">
|
}" class="h-full">
|
||||||
<div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
<div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="node of nodes" :key="node.id" class="absolute" :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'}">
|
<CanvasNodeEditor v-for="(node, index) of canvas.nodes" :key="node.id" ref="nodes" :node="node" :index="index" @select="select" @edit="edit" @move="move"/>
|
||||||
<div v-if="editing?.id !== node.id" style="outline-style: solid;" :class="[node.class.border, node.class.outline, { '!outline-4 cursor-move': focusing?.id === node.id }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full flex">
|
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="node.class.bg" @click.left="(e) => selectNode(node, e)" @dblclick.left="(e) => editNode(node, e)">
|
|
||||||
<div v-if="node.text?.length > 0" class="flex items-center">
|
|
||||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="focusing?.id === node.id">
|
|
||||||
<span @mousedown="(e) => resizeNode(e, 0, -1)" id=" n" class="cursor-n-resize absolute -top-2 left-0 right-0 h-4"></span> <!-- North -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, 0, 1)" id=" s" class="cursor-s-resize absolute -bottom-2 left-0 right-0 h-4"></span> <!-- South -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, -1, 0)" id=" e" class="cursor-e-resize absolute top-0 bottom-0 -left-2 w-4"></span> <!-- East -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, 1, 0)" id=" w" class="cursor-w-resize absolute top-0 bottom-0 -right-2 w-4"></span> <!-- West -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, 1, -1)" id="nw" class="cursor-nw-resize absolute -top-2 -left-2 w-4 h-4"></span> <!-- North West -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, -1, -1)" id="ne" class="cursor-ne-resize absolute -top-2 -right-2 w-4 h-4"></span> <!-- North East -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, -1, 1)" id="se" class="cursor-se-resize absolute -bottom-2 -right-2 w-4 h-4"></span> <!-- South East -->
|
|
||||||
<span @mousedown="(e) => resizeNode(e, 1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-2 -left-2 w-4 h-4"></span> <!-- South West -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else style="outline-style: solid;" :class="[node.class.border, node.class.outline, { '!outline-4': focusing?.id === node.id }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex" >
|
|
||||||
<Editor v-model="node.text" />
|
|
||||||
</div>
|
|
||||||
<div v-if="node.type === 'group' && node.label !== undefined" :class="node.class.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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-for="edge of edges">
|
<template v-for="edge of edges">
|
||||||
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
||||||
|
|
@ -330,7 +421,7 @@ function resizeNode(e: MouseEvent, x: number, y: number)
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuPortal>
|
<ContextMenuPortal>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem @select="(e) => canvas.nodes.push({ id: useId(), })" >Nouveau</ContextMenuItem>
|
<ContextMenuItem @select="(e) => canvas.value.nodes.push({ id: useId(), })" >Nouveau</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenuPortal>
|
</ContextMenuPortal>
|
||||||
</ContextMenuRoot> -->
|
</ContextMenuRoot> -->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<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 flex">
|
||||||
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="style.bg" @click.left="(e) => selectNode(node, e)" @dblclick.left="(e) => editNode(node, e)">
|
||||||
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
|
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="focusing">
|
||||||
|
<span @mousedown="(e) => resizeNode(e, 1, -1)" id="nw" class="cursor-nw-resize absolute -top-2 -left-2 w-4 h-4"></span> <!-- North West -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, -1, -1)" id="ne" class="cursor-ne-resize absolute -top-2 -right-2 w-4 h-4"></span> <!-- North East -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, -1, 1)" id="se" class="cursor-se-resize absolute -bottom-2 -right-2 w-4 h-4"></span> <!-- South East -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, 1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-2 -left-2 w-4 h-4"></span> <!-- South West -->
|
||||||
|
</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" >
|
||||||
|
<Editor v-model="node.text" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(node, e)" @dblclick.left="(e) => editNode(node, 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" 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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FakeA from '../prose/FakeA.vue';
|
||||||
|
import type { CanvasNode } from '~/types/canvas';
|
||||||
|
|
||||||
|
const { node, index } = defineProps<{
|
||||||
|
node: CanvasNode
|
||||||
|
index: number
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', node: CanvasNode, index: number, event: Event): void,
|
||||||
|
(e: 'edit', node: CanvasNode, index: number, event: Event): void,
|
||||||
|
(e: 'resize', node: CanvasNode, index: number, event: Event): void,
|
||||||
|
(e: 'move', index: number, x: number, y: number): void,
|
||||||
|
(e: 'resize', index: number, x: number, y: number): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dom = useTemplateRef('dom');
|
||||||
|
const focusing = ref(false), editing = ref(false);
|
||||||
|
|
||||||
|
function selectNode(node: CanvasNode, _e: Event) {
|
||||||
|
if(editing.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
focusing.value = true;
|
||||||
|
emit('select', node, index, _e);
|
||||||
|
|
||||||
|
dom.value?.addEventListener('mousedown', dragstart, { passive: true });
|
||||||
|
}
|
||||||
|
function editNode(node: CanvasNode, e: Event) {
|
||||||
|
focusing.value = true;
|
||||||
|
editing.value = true;
|
||||||
|
|
||||||
|
dom.value?.removeEventListener('mousedown', dragstart);
|
||||||
|
emit('edit', node, index, e);
|
||||||
|
}
|
||||||
|
function resizeNode(e: Event, x: number, y: number) {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
emit('resize', node, index, e);
|
||||||
|
}
|
||||||
|
function unselect() {
|
||||||
|
focusing.value = false;
|
||||||
|
editing.value = false;
|
||||||
|
|
||||||
|
dom.value?.removeEventListener('mousedown', dragstart);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastx = 0, lasty = 0;
|
||||||
|
const dragmove = (e: MouseEvent) => {
|
||||||
|
if(e.button !== 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
emit('move', index, lastx - e.clientX, lasty - e.clientY);
|
||||||
|
|
||||||
|
lastx = e.clientX, lasty = e.clientY;
|
||||||
|
};
|
||||||
|
const dragend = (e: MouseEvent) => {
|
||||||
|
if(e.button !== 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
window.removeEventListener('mousemove', dragmove);
|
||||||
|
window.removeEventListener('mouseup', dragend);
|
||||||
|
};
|
||||||
|
const dragstart = (e: MouseEvent) => {
|
||||||
|
if(e.button !== 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastx = e.clientX, lasty = e.clientY;
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', dragmove, { passive: true });
|
||||||
|
window.addEventListener('mouseup', dragend, { passive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ unselect, dom });
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
return node.color ? node.color?.class ?
|
||||||
|
{ bg: `bg-light-${node.color?.class} dark:bg-dark-${node.color?.class}`, border: `border-light-${node.color?.class} dark:border-dark-${node.color?.class}`, outline: `outline-light-${node.color?.class} dark:outline-dark-${node.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` }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
</SplitterGroup>
|
</SplitterGroup>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="selected.type === 'canvas'">
|
<template v-else-if="selected.type === 'canvas'">
|
||||||
<CanvasEditor :canvas="JSON.parse(selected.content ?? '{}')" />
|
<CanvasEditor v-if="selected.content" :modelValue="selected.content" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="selected.type === 'map'">
|
<template v-else-if="selected.type === 'map'">
|
||||||
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
|
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
|
||||||
|
|
@ -206,8 +206,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||||
import { parsePath } from '#shared/general.utils';
|
import { convertContent, parsePath } from '#shared/general.utils';
|
||||||
import type { ExploreContent, FileType, MarkdownContent, TreeItem } from '~/types/content';
|
import type { CanvasContent, ExploreContent, FileType, TreeItem } from '~/types/content';
|
||||||
import { iconByType } from '#shared/general.utils';
|
import { iconByType } from '#shared/general.utils';
|
||||||
import FakeA from '~/components/prose/FakeA.vue';
|
import FakeA from '~/components/prose/FakeA.vue';
|
||||||
|
|
||||||
|
|
@ -247,7 +247,7 @@ watch(selected, async (value, old) => {
|
||||||
|
|
||||||
if(storedEdit)
|
if(storedEdit)
|
||||||
{
|
{
|
||||||
selected.value.content = storedEdit;
|
selected.value.content = convertContent(selected.value.type, storedEdit);
|
||||||
contentStatus.value = 'success';
|
contentStatus.value = 'success';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq, sql } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { explorerContentTable } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { convertContent } from '~/shared/general.utils';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||||
|
|
@ -16,6 +17,7 @@ export default defineEventHandler(async (e) => {
|
||||||
const content = db.select({
|
const content = db.select({
|
||||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||||
'private': explorerContentTable.private,
|
'private': explorerContentTable.private,
|
||||||
|
'type': explorerContentTable.type,
|
||||||
'owner': explorerContentTable.owner,
|
'owner': explorerContentTable.owner,
|
||||||
'visit': explorerContentTable.visit,
|
'visit': explorerContentTable.visit,
|
||||||
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||||
|
|
@ -45,7 +47,7 @@ export default defineEventHandler(async (e) => {
|
||||||
content.content = convertFromStorableLinks(content.content);
|
content.content = convertFromStorableLinks(content.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.content;
|
return convertContent(content.type, content.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
setResponseStatus(e, 404);
|
setResponseStatus(e, 404);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { CanvasContent } from '~/types/canvas';
|
||||||
import type { FileType } from '~/types/content';
|
import type { FileType } from '~/types/content';
|
||||||
|
|
||||||
export function unifySlug(slug: string | string[]): string
|
export function unifySlug(slug: string | string[]): string
|
||||||
|
|
@ -46,6 +47,20 @@ export function clamp(x: number, min: number, max: number): number {
|
||||||
return min;
|
return min;
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
export function convertContent(type: FileType, content: string): CanvasContent | string {
|
||||||
|
switch(type)
|
||||||
|
{
|
||||||
|
case 'canvas':
|
||||||
|
return JSON.parse(content) as CanvasContent;
|
||||||
|
case 'map':
|
||||||
|
case 'file':
|
||||||
|
case 'folder':
|
||||||
|
case 'markdown':
|
||||||
|
return content;
|
||||||
|
default:
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const iconByType: Record<FileType, string> = {
|
export const iconByType: Record<FileType, string> = {
|
||||||
'folder': 'lucide:folder',
|
'folder': 'lucide:folder',
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface Overview {
|
||||||
}
|
}
|
||||||
export interface CanvasContent extends Overview {
|
export interface CanvasContent extends Overview {
|
||||||
type: 'canvas';
|
type: 'canvas';
|
||||||
content?: string;
|
content?: Canvas;
|
||||||
}
|
}
|
||||||
export interface MapContent extends Overview {
|
export interface MapContent extends Overview {
|
||||||
type: 'map';
|
type: 'map';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue