Skip to content

Commit e68234f

Browse files
authored
feat: add jump between pods (#327)
1 parent 6cdd844 commit e68234f

File tree

2 files changed

+151
-1
lines changed

2 files changed

+151
-1
lines changed

ui/src/components/Canvas.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ReactFlow, {
2424
ReactFlowProvider,
2525
Edge,
2626
useViewport,
27+
XYPosition,
2728
} from "reactflow";
2829
import "reactflow/dist/style.css";
2930

@@ -47,6 +48,7 @@ import { YMap } from "yjs/dist/src/types/YMap";
4748
import FloatingEdge from "./nodes/FloatingEdge";
4849
import CustomConnectionLine from "./nodes/CustomConnectionLine";
4950
import HelperLines from "./HelperLines";
51+
import { getAbsPos } from "../lib/store/canvasSlice";
5052

5153
const nodeTypes = { SCOPE: ScopeNode, CODE: CodeNode, RICH: RichNode };
5254
const edgeTypes = {
@@ -273,6 +275,152 @@ function useInitNodes() {
273275
return { loading };
274276
}
275277

278+
function getBestNode(
279+
nodes: Node[],
280+
from,
281+
direction: "up" | "down" | "left" | "right"
282+
) {
283+
// find the best node to jump to from (x,y) in the given direction
284+
let bestNode: Node | null = null;
285+
let bestDistance = Infinity;
286+
nodes = nodes.filter((node) => {
287+
switch (direction) {
288+
case "up":
289+
return (
290+
node.position.y + node.height! / 2 <
291+
from.position.y + from.height! / 2
292+
);
293+
case "down":
294+
return (
295+
node.position.y + node.height! / 2 >
296+
from.position.y + from.height! / 2
297+
);
298+
case "left":
299+
return (
300+
node.position.x + node.width! / 2 < from.position.x + from.width! / 2
301+
);
302+
case "right":
303+
return (
304+
node.position.x + node.width! / 2 > from.position.x + from.width! / 2
305+
);
306+
}
307+
});
308+
for (let node of nodes) {
309+
// I should start from the edge, instead of the center
310+
const startPoint: XYPosition = (() => {
311+
// the center
312+
// return {
313+
// x: from.position.x + from.width! / 2,
314+
// y: from.position.y + from.height! / 2,
315+
// };
316+
// the edge depending on direction.
317+
switch (direction) {
318+
case "up":
319+
return {
320+
x: from.position.x + from.width! / 2,
321+
y: from.position.y,
322+
};
323+
case "down":
324+
return {
325+
x: from.position.x + from.width! / 2,
326+
y: from.position.y + from.height!,
327+
};
328+
case "left":
329+
return {
330+
x: from.position.x,
331+
y: from.position.y + from.height! / 2,
332+
};
333+
case "right":
334+
return {
335+
x: from.position.x + from.width!,
336+
y: from.position.y + from.height! / 2,
337+
};
338+
}
339+
})();
340+
let distance =
341+
Math.pow(node.position.x + node.width! / 2 - startPoint.x, 2) *
342+
(["left", "right"].includes(direction) ? 1 : 2) +
343+
Math.pow(node.position.y + node.height! / 2 - startPoint.y, 2) *
344+
(["up", "down"].includes(direction) ? 1 : 2);
345+
if (distance < bestDistance) {
346+
bestDistance = distance;
347+
bestNode = node;
348+
}
349+
}
350+
return bestNode;
351+
}
352+
353+
function useJump() {
354+
const store = useContext(RepoContext)!;
355+
356+
const selectPod = useStore(store, (state) => state.selectPod);
357+
const resetSelection = useStore(store, (state) => state.resetSelection);
358+
const nodesMap = useStore(store, (state) => state.ydoc.getMap<Node>("pods"));
359+
360+
const reactflow = useReactFlow();
361+
362+
const selectedPods = useStore(store, (state) => state.selectedPods);
363+
const handleKeyDown = (event) => {
364+
const id = selectedPods.values().next().value; // Assuming only one node can be selected at a time
365+
if (!id) {
366+
console.log("No node selected");
367+
return; // Ignore arrow key presses if there's no selected node or if the user is typing in an input field
368+
}
369+
const pod = nodesMap.get(id);
370+
if (!pod) {
371+
console.log("pod is undefined");
372+
return;
373+
}
374+
375+
// get the sibling nodes
376+
const nodes = Array.from(nodesMap.values()).filter(
377+
(node) => node.parentNode === pod.parentNode
378+
);
379+
380+
let to: null | Node = null;
381+
382+
switch (event.key) {
383+
case "ArrowUp":
384+
to = getBestNode(nodes, pod, "up");
385+
break;
386+
case "ArrowDown":
387+
to = getBestNode(nodes, pod, "down");
388+
break;
389+
case "ArrowLeft":
390+
to = getBestNode(nodes, pod, "left");
391+
break;
392+
case "ArrowRight":
393+
to = getBestNode(nodes, pod, "right");
394+
break;
395+
default:
396+
return;
397+
}
398+
399+
if (to) {
400+
// set the to node as selected
401+
resetSelection();
402+
selectPod(to.id, true);
403+
// move the viewport to the to node
404+
// get the absolute position of the to node
405+
const pos = getAbsPos(to, nodesMap);
406+
407+
reactflow.setCenter(pos.x + to.width! / 2, pos.y + to.height! / 2, {
408+
zoom: reactflow.getZoom(),
409+
duration: 800,
410+
});
411+
}
412+
413+
event.preventDefault(); // Prevent default browser behavior for arrow keys
414+
};
415+
416+
useEffect(() => {
417+
window.addEventListener("keydown", handleKeyDown);
418+
return () => {
419+
window.removeEventListener("keydown", handleKeyDown);
420+
};
421+
}, [selectedPods]);
422+
}
423+
276424
function usePaste(reactFlowWrapper) {
277425
const store = useContext(RepoContext);
278426
if (!store) throw new Error("Missing BearContext.Provider in the tree");
@@ -461,6 +609,7 @@ function CanvasImplWrap() {
461609
useEdgesYjsObserver();
462610
usePaste(reactFlowWrapper);
463611
useCut(reactFlowWrapper);
612+
useJump();
464613

465614
const { loading } = useInitNodes();
466615
if (loading) return <div>Loading...</div>;
@@ -678,6 +827,7 @@ function CanvasImpl() {
678827
// TODO restore previous viewport
679828
defaultViewport={{ zoom: 1, x: 0, y: 0 }}
680829
proOptions={{ hideAttribution: true }}
830+
disableKeyboardA11y={true}
681831
>
682832
<Box>
683833
<MiniMap

ui/src/lib/store/canvasSlice.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ function createNewNode(type: "SCOPE" | "CODE" | "RICH", position): Node {
165165
/**
166166
* Get the absoluate position of the node.
167167
*/
168-
function getAbsPos(node: Node, nodesMap: YMap<Node>): XYPosition {
168+
export function getAbsPos(node: Node, nodesMap: YMap<Node>): XYPosition {
169169
let x = node.position.x;
170170
let y = node.position.y;
171171
while (node.parentNode) {

0 commit comments

Comments
 (0)