Improve history handling, add color picking and node creation.
This commit is contained in:
parent
62950be032
commit
823f3d7730
|
|
@ -12,18 +12,31 @@ interface ActionMap {
|
||||||
move: Position;
|
move: Position;
|
||||||
edit: string;
|
edit: string;
|
||||||
resize: string;
|
resize: string;
|
||||||
|
remove: CanvasNode | undefined;
|
||||||
|
create: CanvasNode | undefined;
|
||||||
|
property: CanvasNode;
|
||||||
}
|
}
|
||||||
type Action = keyof ActionMap;
|
type Action = keyof ActionMap;
|
||||||
interface HistoryAction<T extends Action = Action>
|
interface HistoryEvent<T extends Action = Action>
|
||||||
{
|
{
|
||||||
event: T;
|
event: T;
|
||||||
element: number | number[];
|
actions: HistoryAction<T>[];
|
||||||
|
}
|
||||||
|
interface HistoryAction<T extends Action>
|
||||||
|
{
|
||||||
|
element: number;
|
||||||
from: ActionMap[T];
|
from: ActionMap[T];
|
||||||
to: ActionMap[T];
|
to: ActionMap[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEvent = (e: Event) => e.preventDefault();
|
const cancelEvent = (e: Event) => e.preventDefault();
|
||||||
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||||
|
function getID(length: number)
|
||||||
|
{
|
||||||
|
for (var id = [], i = 0; i < length; i++)
|
||||||
|
id.push((16 * Math.random() | 0).toString(16));
|
||||||
|
return id.join("");
|
||||||
|
}
|
||||||
function center(touches: TouchList): Position
|
function center(touches: TouchList): Position
|
||||||
{
|
{
|
||||||
const pos = { x: 0, y: 0 };
|
const pos = { x: 0, y: 0 };
|
||||||
|
|
@ -70,9 +83,11 @@ const edges = computed(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const history = ref<HistoryAction[]>([]);
|
|
||||||
|
|
||||||
|
const history = ref<HistoryEvent[]>([]);
|
||||||
const historyPos = ref(-1);
|
const historyPos = ref(-1);
|
||||||
const lastActiveAction = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
const historyCursor = 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;
|
||||||
|
|
@ -81,11 +96,11 @@ 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])
|
function addAction<T extends Action = Action>(event: T, actions: HistoryAction<T>[])
|
||||||
{
|
{
|
||||||
historyPos.value++;
|
historyPos.value++;
|
||||||
history.value.splice(historyPos.value, history.value.length - historyPos.value);
|
history.value.splice(historyPos.value, history.value.length - historyPos.value);
|
||||||
history.value[historyPos.value] = { event, element, from, to };
|
history.value[historyPos.value] = { event, actions };
|
||||||
}
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||||
|
|
@ -184,49 +199,73 @@ onMounted(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function move(index: number, x: number, y: number)
|
function moveNode(index: number[], deltax: number, deltay: number)
|
||||||
{
|
{
|
||||||
const node = canvas.value.nodes[index];
|
const actions: HistoryAction<'move'>[] = [];
|
||||||
const oldx = node.x, oldy = node.y;
|
for(const i of index)
|
||||||
|
|
||||||
forElements(index, (e) => {
|
|
||||||
e.x -= x / zoom.value;
|
|
||||||
e.y -= y / zoom.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(lastActiveAction.value && lastActiveAction.value.event === 'move' && lastActiveAction.value.element === index)
|
|
||||||
{
|
{
|
||||||
const action = lastActiveAction.value as HistoryAction<'move'>;
|
const node = canvas.value.nodes[i];
|
||||||
|
|
||||||
action.to.x -= x / zoom.value;
|
actions.push({ element: i, from: { x: node.x - deltax, y: node.y - deltay }, to: { x: node.x, y: node.y } });
|
||||||
action.to.y -= y / zoom.value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
addAction('move', index, { x: oldx, y: oldy }, { x: canvas.value.nodes[index].x, y: canvas.value.nodes[index].y });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addAction('move', actions);
|
||||||
}
|
}
|
||||||
function select(node: CanvasNode, index: number, event: Event)
|
function selectNode(index: number)
|
||||||
{
|
{
|
||||||
if(focusing.value !== index)
|
if(focusing.value !== index)
|
||||||
{
|
{
|
||||||
unselect();
|
unselectNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.value![index]?.dom?.addEventListener('click', stopPropagation);
|
nodes.value![index]?.dom?.addEventListener('click', stopPropagation, { passive: true });
|
||||||
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
canvasRef.value?.addEventListener('click', unselectNode, { once: true });
|
||||||
|
|
||||||
focusing.value = index;
|
focusing.value = index;
|
||||||
}
|
}
|
||||||
function edit(node: CanvasNode, index: number, event: Event)
|
function editNode(index: number)
|
||||||
{
|
{
|
||||||
nodes.value![index]?.dom?.addEventListener('wheel', stopPropagation);
|
nodes.value![index]?.dom?.addEventListener('wheel', stopPropagation, { passive: true });
|
||||||
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
nodes.value![index]?.dom?.addEventListener('dblclick', stopPropagation, { passive: true });
|
||||||
|
canvasRef.value?.addEventListener('click', unselectNode, { once: true });
|
||||||
|
|
||||||
editing.value = index;
|
editing.value = index;
|
||||||
}
|
}
|
||||||
|
function addNode(e: MouseEvent)
|
||||||
|
{
|
||||||
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||||
|
canvas.value.nodes.push({ id: getID(16), x: (e.layerX / zoom.value) - box.width / 2 - 50, y: (e.layerY / zoom.value) - box.height / 2 - 25, width: 100, height: 50, type: 'text' });
|
||||||
|
}
|
||||||
|
function removeNode(index: number[])
|
||||||
|
{
|
||||||
|
/*const actions: HistoryAction<'remove'>[] = [];
|
||||||
|
unselectNode();
|
||||||
|
|
||||||
const unselect = () => {
|
for(const i of index)
|
||||||
|
{
|
||||||
|
const [node] = canvas.value.nodes.splice(i, 1);
|
||||||
|
actions.push({ element: i, from: node, to: undefined });
|
||||||
|
|
||||||
|
console.log("Removing %s", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction('remove', actions);*/
|
||||||
|
}
|
||||||
|
function editNodeProperty<T extends keyof CanvasNode>(index: number[], property: T, value: CanvasNode[T])
|
||||||
|
{
|
||||||
|
const actions: HistoryAction<'remove'>[] = [];
|
||||||
|
|
||||||
|
for(const i of index)
|
||||||
|
{
|
||||||
|
const copy = JSON.parse(JSON.stringify(canvas.value.nodes[i])) as CanvasNode;
|
||||||
|
canvas.value.nodes[i][property] = value;
|
||||||
|
actions.push({ element: i, from: copy, to: canvas.value.nodes[i] });
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction('property', actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unselectNode = () => {
|
||||||
if(focusing.value !== undefined)
|
if(focusing.value !== undefined)
|
||||||
{
|
{
|
||||||
nodes.value![focusing.value]?.dom?.removeEventListener('click', stopPropagation);
|
nodes.value![focusing.value]?.dom?.removeEventListener('click', stopPropagation);
|
||||||
|
|
@ -236,44 +275,52 @@ const unselect = () => {
|
||||||
|
|
||||||
if(editing.value !== undefined)
|
if(editing.value !== undefined)
|
||||||
{
|
{
|
||||||
debugger;
|
|
||||||
nodes.value![editing.value]?.dom?.removeEventListener('wheel', stopPropagation);
|
nodes.value![editing.value]?.dom?.removeEventListener('wheel', stopPropagation);
|
||||||
|
nodes.value![editing.value]?.dom?.removeEventListener('dblclick', stopPropagation);
|
||||||
nodes.value![editing.value]?.dom?.removeEventListener('click', stopPropagation);
|
nodes.value![editing.value]?.dom?.removeEventListener('click', stopPropagation);
|
||||||
nodes.value![editing.value]?.unselect();
|
nodes.value![editing.value]?.unselect();
|
||||||
}
|
}
|
||||||
editing.value = undefined;
|
editing.value = undefined;
|
||||||
};
|
};
|
||||||
const undo = () => {
|
const undo = () => {
|
||||||
if(!lastActiveAction.value)
|
if(!historyCursor.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch(lastActiveAction.value.event)
|
for(const action of historyCursor.value.actions)
|
||||||
{
|
{
|
||||||
case 'move':
|
switch(historyCursor.value.event)
|
||||||
{
|
{
|
||||||
const action = lastActiveAction.value as HistoryAction<'move'>;
|
case 'move':
|
||||||
|
{
|
||||||
const x = action.to.x - action.from.x, y = action.to.y - action.from.y;
|
const a = action as HistoryAction<'move'>;
|
||||||
|
canvas.value.nodes[action.element].x = a.from.x;
|
||||||
forElements(action.element, (e) => {
|
canvas.value.nodes[action.element].y = a.from.y;
|
||||||
e.x -= x;
|
break;
|
||||||
e.y -= y;
|
}
|
||||||
});
|
case 'edit':
|
||||||
|
{
|
||||||
break;
|
const a = action as HistoryAction<'edit'>;
|
||||||
}
|
canvas.value.nodes[action.element].label = a.from;
|
||||||
case 'edit':
|
break;
|
||||||
{
|
}
|
||||||
const action = lastActiveAction.value as HistoryAction<'edit'>;
|
case 'remove':
|
||||||
|
{
|
||||||
forElements(action.element, (e) => {
|
const a = action as HistoryAction<'remove'>;
|
||||||
e.text = action.from;
|
canvas.value.nodes.splice(action.element, 0, a.from!);
|
||||||
});
|
break;
|
||||||
break;
|
}
|
||||||
|
case 'property':
|
||||||
|
{
|
||||||
|
const a = action as HistoryAction<'property'>;
|
||||||
|
canvas.value.nodes[action.element] = a.from;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
historyPos.value--;
|
historyPos.value--;
|
||||||
|
|
||||||
|
console.log(historyPos.value, history.value.length);
|
||||||
};
|
};
|
||||||
const redo = () => {
|
const redo = () => {
|
||||||
if(!history.value || history.value.length - 1 <= historyPos.value)
|
if(!history.value || history.value.length - 1 <= historyPos.value)
|
||||||
|
|
@ -281,61 +328,56 @@ const redo = () => {
|
||||||
|
|
||||||
historyPos.value++;
|
historyPos.value++;
|
||||||
|
|
||||||
if(!lastActiveAction.value)
|
if(!historyCursor.value)
|
||||||
{
|
{
|
||||||
historyPos.value--;
|
historyPos.value--;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(lastActiveAction.value.event)
|
for(const action of historyCursor.value.actions)
|
||||||
{
|
{
|
||||||
case 'move':
|
switch(historyCursor.value.event)
|
||||||
{
|
{
|
||||||
const action = lastActiveAction.value as HistoryAction<'move'>;
|
case 'move':
|
||||||
|
{
|
||||||
const x = action.from.x - action.to.x, y = action.from.y - action.to.y;
|
const a = action as HistoryAction<'move'>;
|
||||||
|
canvas.value.nodes[action.element].x = a.to.x;
|
||||||
forElements(action.element, (e) => {
|
canvas.value.nodes[action.element].y = a.to.y;
|
||||||
e.x -= x;
|
break;
|
||||||
e.y -= y;
|
}
|
||||||
});
|
case 'edit':
|
||||||
break;
|
{
|
||||||
}
|
const a = action as HistoryAction<'edit'>;
|
||||||
case 'edit':
|
canvas.value.nodes[action.element].label = a.to;
|
||||||
{
|
break;
|
||||||
const action = lastActiveAction.value as HistoryAction<'edit'>;
|
}
|
||||||
|
case 'remove':
|
||||||
forElements(action.element, (e) => {
|
{
|
||||||
e.text = action.to;
|
const a = action as HistoryAction<'remove'>;
|
||||||
});
|
canvas.value.nodes.splice(action.element, 1);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case 'property':
|
||||||
|
{
|
||||||
|
const a = action as HistoryAction<'property'>;
|
||||||
|
canvas.value.nodes[action.element] = a.to;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(historyPos.value, history.value.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
useShortcuts({
|
useShortcuts({
|
||||||
meta_z: undo,
|
meta_z: undo,
|
||||||
meta_y: redo,
|
meta_y: redo,
|
||||||
})
|
Delete: () => { if(focusing.value !== undefined) { removeNode([focusing.value]) } }
|
||||||
|
});
|
||||||
function forElements(element: number | number[], fn: (e: CanvasNode) => void)
|
|
||||||
{
|
|
||||||
if(Array.isArray(element))
|
|
||||||
{
|
|
||||||
for(const e of element)
|
|
||||||
{
|
|
||||||
fn(canvas.value.nodes[e]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fn(canvas.value.nodes[element]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }" @dblclick.left="addNode">
|
||||||
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
<Tooltip message="Zoom avant" side="right">
|
<Tooltip message="Zoom avant" side="right">
|
||||||
|
|
@ -359,6 +401,18 @@ function forElements(element: number | number[], fn: (e: CanvasNode) => void)
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
|
<Tooltip message="Annuler (Ctrl+Z)" side="right">
|
||||||
|
<div @click="undo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === -1 }">
|
||||||
|
<Icon icon="ph:arrow-bend-up-left" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Retablir (Ctrl+Y)" side="right">
|
||||||
|
<div @click="redo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === history.length - 1 }">
|
||||||
|
<Icon icon="ph:arrow-bend-up-right" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
<Tooltip message="Aide" side="right">
|
<Tooltip message="Aide" side="right">
|
||||||
<Dialog title="Aide" iconClose>
|
<Dialog title="Aide" iconClose>
|
||||||
|
|
@ -371,10 +425,10 @@ function forElements(element: number | number[], fn: (e: CanvasNode) => void)
|
||||||
<div class="flex flex-row justify-between px-4">
|
<div class="flex flex-row justify-between px-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<ProseH4>Ordinateur</ProseH4>
|
<ProseH4>Ordinateur</ProseH4>
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
|
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
|
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
|
<div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
|
<div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<ProseH4>Mobile</ProseH4>
|
<ProseH4>Mobile</ProseH4>
|
||||||
|
|
@ -399,8 +453,58 @@ function forElements(element: number | number[], fn: (e: CanvasNode) => void)
|
||||||
'transform-origin': 'center center',
|
'transform-origin': 'center center',
|
||||||
}" 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 v-if="focusing !== undefined" class="absolute z-20 origin-bottom" :style="{transform: `translate(${canvas.nodes[focusing].x}px, ${canvas.nodes[focusing].y}px) translateY(-100%) translateY(-12px) translateX(-50%) translateX(${canvas.nodes[focusing].width / 2}px) scale(calc(1 / var(--tw-scale)))`}">
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row">
|
||||||
|
<PopoverRoot>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div @click="stopPropagation">
|
||||||
|
<Tooltip message="Couleur" side="top">
|
||||||
|
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="ph:palette" class="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverPortal disabled>
|
||||||
|
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div @click="editNodeProperty([focusing], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||||
|
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
|
</PopoverRoot>
|
||||||
|
<Tooltip message="Supprimer" side="top">
|
||||||
|
<div @click="removeNode([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CanvasNodeEditor v-for="(node, index) of canvas.nodes" :key="node.id" ref="nodes" :node="node" :index="index" @select="select" @edit="edit" @move="move"/>
|
<CanvasNodeEditor v-for="(node, index) of canvas.nodes" :key="node.id" ref="nodes" :node="node" :index="index" :zoom="zoom" @select="selectNode" @edit="editNode" @move="(i, x, y) => moveNode([i], x, y)"/>
|
||||||
</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"
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,53 @@
|
||||||
<template>
|
<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 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 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">
|
||||||
<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 class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :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?.length > 0" class="flex items-center">
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="focusing">
|
<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, 0, -1)" id="n " class="cursor-n-resize absolute -top-3 -right-3 -left-3 h-6 group">
|
||||||
<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) => dragEdge(e, 'top')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -top-1.5 left-1/2 -translate-x-3"></span>
|
||||||
<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> <!-- North -->
|
||||||
<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 -->
|
<span @mousedown="(e) => resizeNode(e, 0, 1)" id="s " class="cursor-s-resize absolute -bottom-3 -right-3 -left-3 h-6 group">
|
||||||
|
<span @mousedown="(e) => dragEdge(e, 'bottom')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -bottom-1.5 left-1/2 -translate-x-3"></span>
|
||||||
|
</span> <!-- South -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, 1, 0)" id="e " class="cursor-e-resize absolute -top-3 -bottom-3 -right-3 w-6 group">
|
||||||
|
<span @mousedown="(e) => dragEdge(e, 'right')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -right-1.5 top-1/2 -translate-y-3"></span>
|
||||||
|
</span> <!-- East -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, -1, 0)" id="w " class="cursor-w-resize absolute -top-3 -bottom-3 -left-3 w-6 group">
|
||||||
|
<span @mousedown="(e) => dragEdge(e, 'left')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -left-1.5 top-1/2 -translate-y-3"></span>
|
||||||
|
</span> <!-- West -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, 1, -1)" id="nw" class="cursor-nw-resize absolute -top-4 -left-4 w-8 h-8"></span> <!-- North West -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, -1, -1)" id="ne" class="cursor-ne-resize absolute -top-4 -right-4 w-8 h-8"></span> <!-- North East -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, -1, 1)" id="se" class="cursor-se-resize absolute -bottom-4 -right-4 w-8 h-8"></span> <!-- South East -->
|
||||||
|
<span @mousedown="(e) => resizeNode(e, 1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-4 -left-4 w-8 h-8"></span> <!-- South West -->
|
||||||
</div>
|
</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" >
|
<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" />
|
<Editor v-model="node.text" />
|
||||||
</div>
|
</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>
|
<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" 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" />
|
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Direction } from '~/shared/canvas.util';
|
||||||
import FakeA from '../prose/FakeA.vue';
|
import FakeA from '../prose/FakeA.vue';
|
||||||
import type { CanvasNode } from '~/types/canvas';
|
import type { CanvasNode } from '~/types/canvas';
|
||||||
|
|
||||||
const { node, index } = defineProps<{
|
const { node, index, zoom } = defineProps<{
|
||||||
node: CanvasNode
|
node: CanvasNode
|
||||||
index: number
|
index: number
|
||||||
|
zoom: number
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'select', node: CanvasNode, index: number, event: Event): void,
|
(e: 'select', index: number): void,
|
||||||
(e: 'edit', node: CanvasNode, index: number, event: Event): void,
|
(e: 'edit', index: number): void,
|
||||||
(e: 'resize', node: CanvasNode, index: number, event: Event): void,
|
(e: 'resize', index: number): void,
|
||||||
(e: 'move', index: number, x: number, y: number): void,
|
(e: 'move', index: number, x: number, y: number): void,
|
||||||
(e: 'resize', index: number, x: number, y: number): void,
|
(e: 'resize', index: number, x: number, y: number): void,
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -41,25 +55,30 @@ const emit = defineEmits<{
|
||||||
const dom = useTemplateRef('dom');
|
const dom = useTemplateRef('dom');
|
||||||
const focusing = ref(false), editing = ref(false);
|
const focusing = ref(false), editing = ref(false);
|
||||||
|
|
||||||
function selectNode(node: CanvasNode, _e: Event) {
|
function selectNode(e: Event) {
|
||||||
if(editing.value)
|
if(editing.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
focusing.value = true;
|
focusing.value = true;
|
||||||
emit('select', node, index, _e);
|
emit('select', index);
|
||||||
|
|
||||||
dom.value?.addEventListener('mousedown', dragstart, { passive: true });
|
dom.value?.addEventListener('mousedown', dragstart, { passive: true });
|
||||||
}
|
}
|
||||||
function editNode(node: CanvasNode, e: Event) {
|
function editNode(e: Event) {
|
||||||
focusing.value = true;
|
focusing.value = true;
|
||||||
editing.value = true;
|
editing.value = true;
|
||||||
|
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
dom.value?.removeEventListener('mousedown', dragstart);
|
dom.value?.removeEventListener('mousedown', dragstart);
|
||||||
emit('edit', node, index, e);
|
emit('edit', index);
|
||||||
}
|
}
|
||||||
function resizeNode(e: Event, x: number, y: number) {
|
function resizeNode(e: Event, x: number, y: number) {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
emit('resize', node, index, e);
|
emit('resize', index);
|
||||||
|
}
|
||||||
|
function dragEdge(e: Event, direction: Direction) {
|
||||||
|
e.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
function unselect() {
|
function unselect() {
|
||||||
focusing.value = false;
|
focusing.value = false;
|
||||||
|
|
@ -73,9 +92,8 @@ const dragmove = (e: MouseEvent) => {
|
||||||
if(e.button !== 0)
|
if(e.button !== 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
emit('move', index, lastx - e.clientX, lasty - e.clientY);
|
node.x += e.movementX / zoom;
|
||||||
|
node.y += e.movementY / zoom;
|
||||||
lastx = e.clientX, lasty = e.clientY;
|
|
||||||
};
|
};
|
||||||
const dragend = (e: MouseEvent) => {
|
const dragend = (e: MouseEvent) => {
|
||||||
if(e.button !== 0)
|
if(e.button !== 0)
|
||||||
|
|
@ -83,12 +101,15 @@ const dragend = (e: MouseEvent) => {
|
||||||
|
|
||||||
window.removeEventListener('mousemove', dragmove);
|
window.removeEventListener('mousemove', dragmove);
|
||||||
window.removeEventListener('mouseup', dragend);
|
window.removeEventListener('mouseup', dragend);
|
||||||
|
|
||||||
|
if(node.x - lastx !== 0 && node.y - lasty !== 0)
|
||||||
|
emit('move', index, node.x - lastx, node.y - lasty);
|
||||||
};
|
};
|
||||||
const dragstart = (e: MouseEvent) => {
|
const dragstart = (e: MouseEvent) => {
|
||||||
if(e.button !== 0)
|
if(e.button !== 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lastx = e.clientX, lasty = e.clientY;
|
lastx = node.x, lasty = node.y;
|
||||||
|
|
||||||
window.addEventListener('mousemove', dragmove, { passive: true });
|
window.addEventListener('mousemove', dragmove, { passive: true });
|
||||||
window.addEventListener('mouseup', dragend, { passive: true });
|
window.addEventListener('mouseup', dragend, { passive: true });
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,6 @@ if(overview.value && !overview.value.content)
|
||||||
await get(path);
|
await get(path);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
const canvas = computed(() => overview.value && overview.value.content ? JSON.parse(overview.value.content) as Canvas : undefined);
|
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "bun i",
|
"predev": "bun i",
|
||||||
"dev": "bunx --bun nuxi dev"
|
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue