2024-05-23 01:42:10 -07:00
|
|
|
<script lang="ts" setup>
|
|
|
|
import type { CanvasConnection, CanvasElement } from '@/types';
|
2024-06-25 02:11:44 -07:00
|
|
|
import type { EdgeMouseEvent, NodeDragEvent, Connection, XYPosition } from '@vue-flow/core';
|
2024-06-04 05:36:27 -07:00
|
|
|
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
|
2024-05-23 01:42:10 -07:00
|
|
|
import { Background } from '@vue-flow/background';
|
|
|
|
import { Controls } from '@vue-flow/controls';
|
|
|
|
import { MiniMap } from '@vue-flow/minimap';
|
|
|
|
import CanvasNode from './elements/nodes/CanvasNode.vue';
|
|
|
|
import CanvasEdge from './elements/edges/CanvasEdge.vue';
|
2024-06-11 09:09:29 -07:00
|
|
|
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
2024-05-23 01:42:10 -07:00
|
|
|
|
|
|
|
const $style = useCssModule();
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
'update:modelValue': [elements: CanvasElement[]];
|
2024-06-25 02:11:44 -07:00
|
|
|
'update:node:position': [id: string, position: XYPosition];
|
2024-06-17 05:46:55 -07:00
|
|
|
'update:node:active': [id: string];
|
2024-06-25 02:11:44 -07:00
|
|
|
'update:node:selected': [id?: string];
|
2024-06-04 05:36:27 -07:00
|
|
|
'delete:node': [id: string];
|
|
|
|
'delete:connection': [connection: Connection];
|
2024-05-23 01:42:10 -07:00
|
|
|
'create:connection': [connection: Connection];
|
2024-06-25 02:11:44 -07:00
|
|
|
'click:pane': [position: XYPosition];
|
2024-05-23 01:42:10 -07:00
|
|
|
}>();
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
const props = withDefaults(
|
2024-05-23 01:42:10 -07:00
|
|
|
defineProps<{
|
|
|
|
id?: string;
|
|
|
|
elements: CanvasElement[];
|
|
|
|
connections: CanvasConnection[];
|
|
|
|
controlsPosition?: PanelPosition;
|
|
|
|
}>(),
|
|
|
|
{
|
|
|
|
id: 'canvas',
|
|
|
|
elements: () => [],
|
|
|
|
connections: () => [],
|
|
|
|
controlsPosition: PanelPosition.BottomLeft,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
const { getSelectedEdges, getSelectedNodes, viewportRef, project } = useVueFlow({
|
|
|
|
id: props.id,
|
|
|
|
});
|
2024-06-04 05:36:27 -07:00
|
|
|
|
2024-06-11 09:09:29 -07:00
|
|
|
const hoveredEdges = ref<Record<string, boolean>>({});
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
onMounted(() => {
|
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
|
|
});
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
document.removeEventListener('keydown', onKeyDown);
|
|
|
|
});
|
|
|
|
|
2024-05-23 01:42:10 -07:00
|
|
|
function onNodeDragStop(e: NodeDragEvent) {
|
|
|
|
e.nodes.forEach((node) => {
|
|
|
|
emit('update:node:position', node.id, node.position);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-17 05:46:55 -07:00
|
|
|
function onSetNodeActive(id: string) {
|
|
|
|
emit('update:node:active', id);
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function onSelectNode() {
|
|
|
|
const selectedNodeId = getSelectedNodes.value[getSelectedNodes.value.length - 1]?.id;
|
|
|
|
emit('update:node:selected', selectedNodeId);
|
|
|
|
}
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
function onDeleteNode(id: string) {
|
|
|
|
emit('delete:node', id);
|
|
|
|
}
|
|
|
|
|
|
|
|
function onDeleteConnection(connection: Connection) {
|
|
|
|
emit('delete:connection', connection);
|
|
|
|
}
|
|
|
|
|
2024-05-23 01:42:10 -07:00
|
|
|
function onConnect(...args: unknown[]) {
|
|
|
|
emit('create:connection', args[0] as Connection);
|
|
|
|
}
|
2024-06-04 05:36:27 -07:00
|
|
|
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
|
|
if (e.key === 'Delete') {
|
|
|
|
getSelectedEdges.value.forEach(onDeleteConnection);
|
|
|
|
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
|
|
|
|
}
|
|
|
|
}
|
2024-06-11 09:09:29 -07:00
|
|
|
|
|
|
|
function onMouseEnterEdge(event: EdgeMouseEvent) {
|
|
|
|
hoveredEdges.value[event.edge.id] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function onMouseLeaveEdge(event: EdgeMouseEvent) {
|
|
|
|
hoveredEdges.value[event.edge.id] = false;
|
|
|
|
}
|
2024-06-25 02:11:44 -07:00
|
|
|
|
|
|
|
function onClickPane(event: MouseEvent) {
|
|
|
|
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
|
|
|
const position = project({
|
|
|
|
x: event.offsetX - bounds.left,
|
|
|
|
y: event.offsetY - bounds.top,
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('click:pane', position);
|
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<VueFlow
|
|
|
|
:id="id"
|
|
|
|
:nodes="elements"
|
|
|
|
:edges="connections"
|
|
|
|
:apply-changes="false"
|
|
|
|
fit-view-on-init
|
|
|
|
pan-on-scroll
|
|
|
|
:min-zoom="0.2"
|
|
|
|
:max-zoom="2"
|
|
|
|
data-test-id="canvas"
|
|
|
|
@node-drag-stop="onNodeDragStop"
|
2024-06-11 09:09:29 -07:00
|
|
|
@edge-mouse-enter="onMouseEnterEdge"
|
|
|
|
@edge-mouse-leave="onMouseLeaveEdge"
|
2024-06-25 02:11:44 -07:00
|
|
|
@pane-click="onClickPane"
|
2024-05-23 01:42:10 -07:00
|
|
|
@connect="onConnect"
|
|
|
|
>
|
|
|
|
<template #node-canvas-node="canvasNodeProps">
|
2024-06-25 02:11:44 -07:00
|
|
|
<CanvasNode
|
|
|
|
v-bind="canvasNodeProps"
|
|
|
|
@delete="onDeleteNode"
|
|
|
|
@select="onSelectNode"
|
|
|
|
@activate="onSetNodeActive"
|
|
|
|
/>
|
2024-05-23 01:42:10 -07:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<template #edge-canvas-edge="canvasEdgeProps">
|
2024-06-11 09:09:29 -07:00
|
|
|
<CanvasEdge
|
|
|
|
v-bind="canvasEdgeProps"
|
|
|
|
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
|
|
|
@delete="onDeleteConnection"
|
|
|
|
/>
|
2024-05-23 01:42:10 -07:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="16" />
|
|
|
|
|
|
|
|
<MiniMap data-test-id="canvas-minimap" pannable />
|
|
|
|
|
|
|
|
<Controls
|
|
|
|
data-test-id="canvas-controls"
|
|
|
|
:class="$style.canvasControls"
|
|
|
|
:position="controlsPosition"
|
|
|
|
></Controls>
|
|
|
|
</VueFlow>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="scss" module></style>
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
.vue-flow__controls {
|
|
|
|
display: flex;
|
|
|
|
gap: var(--spacing-2xs);
|
|
|
|
box-shadow: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.vue-flow__controls-button {
|
|
|
|
width: 42px;
|
|
|
|
height: 42px;
|
|
|
|
border: var(--border-base);
|
|
|
|
border-radius: var(--border-radius-base);
|
|
|
|
padding: 0;
|
|
|
|
transition-property: transform, background, border, color;
|
|
|
|
transition-duration: 300ms;
|
|
|
|
transition-timing-function: ease;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
border-color: var(--color-button-secondary-hover-active-border);
|
|
|
|
background-color: var(--color-button-secondary-active-background);
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
|
|
svg {
|
|
|
|
fill: var(--color-primary);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
svg {
|
|
|
|
max-height: 16px;
|
|
|
|
max-width: 16px;
|
|
|
|
transition-property: fill;
|
|
|
|
transition-duration: 300ms;
|
|
|
|
transition-timing-function: ease;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|