I need to build a match/link pairs from 2 vertical lists for my project.
The pair list for example may be like this:
const [pairs, setPairs] = useState([
{ id: 1, left: "Apple", right: "Red" },
{ id: 2, left: "Banana", right: "Yellow" },
{ id: 3, left: "Grapes", right: "Purple" },
{ id: 4, left: "Orange", right: "Orange" },
]);
Check the image for better understanding.
Is there any way to do this?
You could use a canvas and give it a reference.
Now, you can animate a canvas and draws the points and edges.
Click (and drag) from a node to connect it to another. You can add in some logic to prevent edges between items in the same column.
const { useEffect, useMemo, useRef, useState } = React;
const nodeRadius = 8;
const defaultPairs = [
{ id: 1, left: "Apple", right: "Red" },
{ id: 2, left: "Banana", right: "Yellow" },
{ id: 3, left: "Grapes", right: "Purple" },
{ id: 4, left: "Orange", right: "Orange" },
];
const findClosestNode = (nodes, mouse, threshold = nodeRadius) =>
nodes.find(node =>
Math.abs(mouse.x - node.x) < threshold &&
Math.abs(mouse.y - node.y) < threshold);
const filterInPlace = (arr, rem) =>
arr
.reduce((r, e, i) => rem.includes(e) ? r.concat(i) : r, [])
.sort((a, b) => b - a)
.forEach(i => arr.splice(i, 1));
const sameHorizontal = (p1, p2) => p1 && p2 && p1.y === p2.y;
const sameVertical = (p1, p2) => p1 && p2 && p1.x === p2.x;
const equalsNode = (n1, n2) => n1 && n2 && n1.x === n2.x && n1.y === n2.y;
const equalsEdge = (e1, e2) => equalsNode(e1[0], e2[0]) && equalsNode(e1[1], e2[1]);
const hasEdge = (edges, edge) => edges.some(otherEdge => equalsEdge(edge, otherEdge));
const addEdge = (edges, edge) => {
if (!hasEdge(edges, edge)) {
filterInPlace(edges, edges.filter((o) => equalsNode(edge[0], o[0])));
edges.push(edge);
return true;
}
return false;
};
const renderCanvas = (canvas, leftArr, rightArr) => {
const ctx = canvas.getContext('2d');
ctx.canvas.width = 100;
const { height, width } = ctx.canvas;
const rowHeight = height / leftArr.length;
const margin = nodeRadius * 2.5;
const nodes = [
...leftArr.map((_, i) => ({
x: margin,
y: i * rowHeight + rowHeight / 2
})),
...rightArr.map((_, i) => ({
x: width - margin,
y: i * rowHeight + rowHeight / 2
})),
];
const edges = [];
let activeNode = null;
let mouse = null;
let isDragging = false;
const onMouseDown = (e) => {
const closestNode = findClosestNode(nodes, mouse);
// activeNode = closestNode !== activeNode ? closestNode : null;
activeNode = closestNode;
isDragging = true;
};
const onMouseMove = (e) => {
const rect = e.target.getBoundingClientRect();
mouse = { x: e.clientX - rect.left, y: e.clientY - rect.top };
};
const onMouseUp = (e) => {
isDragging = false;
const intentRadius = nodeRadius * 1.5;
const closestNode = findClosestNode(nodes, mouse, intentRadius);
if (
activeNode && closestNode &&
!equalsNode(activeNode, closestNode) &&
!sameVertical(activeNode, closestNode)
) {
const edge = [activeNode, closestNode].sort((a, b) => a.x - b.x);
addEdge(edges, edge);
activeNode = null;
}
};
const animate = (() => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
edges.forEach(([start, end]) => {
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = 'black';
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
});
nodes.forEach((node) => {
ctx.beginPath();
ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
ctx.fillStyle = equalsNode(node, activeNode) ? 'black' : 'darkgrey';
ctx.fill();
});
if (isDragging && activeNode) {
const intentRadius = nodeRadius * 1.5;
const closestNode = findClosestNode(nodes, mouse, intentRadius);
const intent = closestNode || mouse;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = 'red';
ctx.setLineDash([3, 3]);
ctx.moveTo(activeNode.x, activeNode.y);
ctx.lineTo(intent.x, intent.y);
ctx.stroke();
ctx.setLineDash([]);
}
requestAnimationFrame(animate);
});
animate();
return { onMouseDown, onMouseMove, onMouseUp };
};
const randomize = () => Math.random() - 0.5;
const App = () => {
const canvasRef = useRef();
const [pairs, setPairs] = useState(defaultPairs);
const leftArr = useMemo(
() => pairs.map((pair) => pair.left).sort(randomize),
[pairs]
);
const rightArr = useMemo(
() => pairs.map((pair) => pair.right).sort(randomize),
[pairs]
);
useEffect(() => {
const { onMouseDown, onMouseMove, onMouseUp } = renderCanvas(canvasRef.current, leftArr, rightArr);
// Add listeners
canvasRef.current.addEventListener('mousedown', onMouseDown);
canvasRef.current.addEventListener('mousemove', onMouseMove);
canvasRef.current.addEventListener('mouseup', onMouseUp);
// Cleanup listeners
return () => {
canvasRef.current.removeEventListener('mousedown', onMouseDown);
canvasRef.current.removeEventListener('mousemove', onMouseMove);
canvasRef.current.removeEventListener('mouseup', onMouseUp);
};
}, [canvasRef, leftArr, rightArr]);
return (
<div className="App">
<div className="Col">
{leftArr.map((e) => (
<div key={e}>
{e}
</div>
))}
</div>
<canvas ref={canvasRef} className="Lines"></canvas>
<div className="Col">
{rightArr.map((e) => (
<div key={e}>
{e}
</div>
))}
</div>
</div>
);
};
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.App {
display: grid;
grid-template-columns: auto auto auto;
flex: 1;
height: 80%;
}
.Col {
display: flex;
flex-direction: column;
justify-content: space-around;
flex: 1;
user-select: none;
}
.Col:first-child {
align-items: flex-end;
}
.Col:last-child {
align-items: flex-start;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Here is an example of items connected:
Now you could export the nodes and edges along with the listeners, and check the graph's relationships.