Profectus-Niffix/src/features/boards/Board.vue

280 lines
8.8 KiB
Vue
Raw Normal View History

2022-01-13 22:25:47 -06:00
<template>
<panZoom
v-if="isVisible(visibility)"
2022-01-13 22:25:47 -06:00
:style="[
{
width,
height
},
style
]"
:class="classes"
selector=".g1"
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
ref="stage"
@init="onInit"
@mousemove="drag"
@touchmove="drag"
@mousedown="(e: MouseEvent) => mouseDown(e)"
@touchstart="(e: TouchEvent) => mouseDown(e)"
@mouseup="() => endDragging(unref(draggingNode))"
@touchend.passive="() => endDragging(unref(draggingNode))"
2023-04-25 23:51:18 -05:00
@mouseleave="() => endDragging(unref(draggingNode), true)"
2022-01-13 22:25:47 -06:00
>
<svg class="stage" width="100%" height="100%">
<g class="g1">
<transition-group name="link" appear>
2023-04-21 19:49:21 -05:00
<g
v-for="link in unref(links) || []"
:key="`${link.startNode.id}-${link.endNode.id}`"
>
2022-01-13 22:25:47 -06:00
<BoardLinkVue :link="link" />
</g>
</transition-group>
<transition-group name="grow" :duration="500" appear>
<g v-for="node in sortedNodes" :key="node.id" style="transition-duration: 0s">
<BoardNodeVue
:node="node"
:nodeType="types[node.type]"
:dragging="unref(draggingNode)"
:dragged="unref(draggingNode) === node ? dragged : undefined"
2022-01-13 22:25:47 -06:00
:hasDragged="hasDragged"
:receivingNode="unref(receivingNode)?.id === node.id"
:selectedNode="unref(selectedNode)"
:selectedAction="unref(selectedAction)"
2022-01-13 22:25:47 -06:00
@mouseDown="mouseDown"
@endDragging="endDragging"
2023-04-22 23:20:23 -05:00
@clickAction="(actionId: string) => clickAction(node, actionId)"
2022-01-13 22:25:47 -06:00
/>
</g>
</transition-group>
</g>
</svg>
</panZoom>
</template>
<script setup lang="ts">
2022-06-26 19:17:22 -05:00
import type {
BoardData,
BoardNode,
BoardNodeLink,
GenericBoardNodeAction,
2022-06-26 19:17:22 -05:00
GenericNodeType
} from "features/boards/board";
2022-06-26 19:17:22 -05:00
import { getNodeProperty } from "features/boards/board";
import type { StyleValue } from "features/feature";
2023-04-21 19:49:21 -05:00
import { Visibility, isVisible } from "features/feature";
2022-06-26 19:17:22 -05:00
import type { ProcessedComputable } from "util/computed";
import { Ref, computed, ref, toRefs, unref, watchEffect } from "vue";
2022-01-13 22:25:47 -06:00
import BoardLinkVue from "./BoardLink.vue";
import BoardNodeVue from "./BoardNode.vue";
const _props = defineProps<{
nodes: Ref<BoardNode[]>;
types: Record<string, GenericNodeType>;
2022-09-07 20:01:05 -05:00
state: Ref<BoardData>;
visibility: ProcessedComputable<Visibility | boolean>;
width?: ProcessedComputable<string>;
height?: ProcessedComputable<string>;
style?: ProcessedComputable<StyleValue>;
classes?: ProcessedComputable<Record<string, boolean>>;
links: Ref<BoardNodeLink[] | null>;
selectedAction: Ref<GenericBoardNodeAction | null>;
selectedNode: Ref<BoardNode | null>;
draggingNode: Ref<BoardNode | null>;
receivingNode: Ref<BoardNode | null>;
2022-09-07 20:01:05 -05:00
mousePosition: Ref<{ x: number; y: number } | null>;
setReceivingNode: (node: BoardNode | null) => void;
setDraggingNode: (node: BoardNode | null) => void;
}>();
2022-01-24 22:25:34 -06:00
const props = toRefs(_props);
2022-01-13 22:25:47 -06:00
const lastMousePosition = ref({ x: 0, y: 0 });
const dragged = ref({ x: 0, y: 0 });
const hasDragged = ref(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stage = ref<any>(null);
const sortedNodes = computed(() => {
const nodes = props.nodes.value.slice();
if (props.draggingNode.value) {
const node = nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0];
2022-01-13 22:25:47 -06:00
nodes.push(node);
}
return nodes;
});
watchEffect(() => {
const node = props.draggingNode.value;
2022-01-13 22:25:47 -06:00
if (node == null) {
return null;
}
const position = {
x: node.position.x + dragged.value.x,
y: node.position.y + dragged.value.y
};
let smallestDistance = Number.MAX_VALUE;
props.setReceivingNode.value(
props.nodes.value.reduce((smallest: BoardNode | null, curr: BoardNode) => {
if (curr.id === node.id) {
return smallest;
}
const nodeType = props.types.value[curr.type];
2023-04-23 00:37:46 -05:00
const canAccept = getNodeProperty(nodeType.canAccept, curr, node);
if (!canAccept) {
return smallest;
}
const distanceSquared =
Math.pow(position.x - curr.position.x, 2) +
Math.pow(position.y - curr.position.y, 2);
let size = getNodeProperty(nodeType.size, curr);
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
return smallest;
}
2022-01-13 22:25:47 -06:00
smallestDistance = distanceSquared;
return curr;
}, null)
);
2022-01-13 22:25:47 -06:00
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function onInit(panzoomInstance: any) {
panzoomInstance.setTransformOrigin(null);
2023-04-21 19:49:21 -05:00
panzoomInstance.moveTo(stage.value.$el.clientWidth / 2, stage.value.$el.clientHeight / 2);
2022-01-13 22:25:47 -06:00
}
function mouseDown(e: MouseEvent | TouchEvent, node: BoardNode | null = null, draggable = false) {
if (props.draggingNode.value == null) {
2022-01-13 22:25:47 -06:00
e.preventDefault();
e.stopPropagation();
let clientX, clientY;
if ("touches" in e) {
if (e.touches.length === 1) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
return;
}
} else {
clientX = e.clientX;
clientY = e.clientY;
}
lastMousePosition.value = {
x: clientX,
y: clientY
};
dragged.value = { x: 0, y: 0 };
hasDragged.value = false;
if (draggable) {
props.setDraggingNode.value(node);
2022-01-13 22:25:47 -06:00
}
}
if (node != null) {
2022-09-07 20:01:05 -05:00
props.state.value.selectedNode = null;
props.state.value.selectedAction = null;
2022-01-13 22:25:47 -06:00
}
}
function drag(e: MouseEvent | TouchEvent) {
2022-09-07 20:01:05 -05:00
const { x, y, scale } = stage.value.panZoomInstance.getTransform();
2022-01-13 22:25:47 -06:00
let clientX, clientY;
if ("touches" in e) {
if (e.touches.length === 1) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
endDragging(props.draggingNode.value);
2022-09-07 20:01:05 -05:00
props.mousePosition.value = null;
2022-01-13 22:25:47 -06:00
return;
}
} else {
clientX = e.clientX;
clientY = e.clientY;
}
2022-09-07 20:01:05 -05:00
props.mousePosition.value = {
x: (clientX - x) / scale,
y: (clientY - y) / scale
};
2022-01-13 22:25:47 -06:00
dragged.value = {
2022-09-07 20:01:05 -05:00
x: dragged.value.x + (clientX - lastMousePosition.value.x) / scale,
y: dragged.value.y + (clientY - lastMousePosition.value.y) / scale
2022-01-13 22:25:47 -06:00
};
lastMousePosition.value = {
x: clientX,
y: clientY
};
if (Math.abs(dragged.value.x) > 10 || Math.abs(dragged.value.y) > 10) {
hasDragged.value = true;
}
if (props.draggingNode.value != null) {
2022-01-13 22:25:47 -06:00
e.preventDefault();
e.stopPropagation();
}
}
2023-04-25 23:51:18 -05:00
function endDragging(node: BoardNode | null, mouseLeave = false) {
if (props.draggingNode.value != null && props.draggingNode.value === node) {
2023-04-23 00:37:46 -05:00
if (props.receivingNode.value == null) {
props.draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
props.draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25;
}
2022-01-13 22:25:47 -06:00
const nodes = props.nodes.value;
nodes.push(nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0]);
2022-01-13 22:25:47 -06:00
if (props.receivingNode.value) {
props.types.value[props.receivingNode.value.type].onDrop?.(
props.receivingNode.value,
props.draggingNode.value
2022-01-13 22:25:47 -06:00
);
}
props.setDraggingNode.value(null);
2023-04-25 23:51:18 -05:00
} else if (!hasDragged.value && !mouseLeave) {
2022-09-07 20:01:05 -05:00
props.state.value.selectedNode = null;
props.state.value.selectedAction = null;
2022-01-13 22:25:47 -06:00
}
}
function clickAction(node: BoardNode, actionId: string) {
if (props.state.value.selectedAction === actionId) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(props.selectedAction)!.onClick(unref(props.selectedNode)!);
} else {
props.state.value = { ...props.state.value, selectedAction: actionId };
}
}
2022-01-13 22:25:47 -06:00
</script>
<style>
.vue-pan-zoom-scene {
width: 100%;
height: 100%;
2022-09-07 20:01:05 -05:00
cursor: grab;
}
.vue-pan-zoom-scene:active {
cursor: grabbing;
2022-01-13 22:25:47 -06:00
}
.g1 {
transition-duration: 0s;
}
.link-enter-from,
.link-leave-to {
opacity: 0;
}
</style>