feat: add selection navigation using the keyboard

This commit is contained in:
Alex Grozav 2024-11-11 15:40:06 +02:00
parent c0aa67b8f0
commit 4b8f20fefb

View file

@ -6,13 +6,7 @@ import type {
CanvasEventBusEvents, CanvasEventBusEvents,
ConnectStartEvent, ConnectStartEvent,
} from '@/types'; } from '@/types';
import type { import type { Connection, XYPosition, ViewportTransform, NodeDragEvent } from '@vue-flow/core';
Connection,
XYPosition,
ViewportTransform,
NodeChange,
NodePositionChange,
} from '@vue-flow/core';
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core'; import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
import { Background } from '@vue-flow/background'; import { Background } from '@vue-flow/background';
import { MiniMap } from '@vue-flow/minimap'; import { MiniMap } from '@vue-flow/minimap';
@ -29,7 +23,7 @@ import type { PinDataSource } from '@/composables/usePinnedData';
import { isPresent } from '@/utils/typesUtils'; import { isPresent } from '@/utils/typesUtils';
import { GRID_SIZE } from '@/utils/nodeViewUtils'; import { GRID_SIZE } from '@/utils/nodeViewUtils';
import { CanvasKey } from '@/constants'; import { CanvasKey } from '@/constants';
import { onKeyDown, onKeyUp, useDebounceFn } from '@vueuse/core'; import { onKeyDown, onKeyUp } from '@vueuse/core';
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'; import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue'; import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
@ -107,6 +101,8 @@ const {
zoomOut, zoomOut,
zoomTo, zoomTo,
setInteractive, setInteractive,
getIncomers,
getOutgoers,
elementsSelectable, elementsSelectable,
project, project,
nodes: graphNodes, nodes: graphNodes,
@ -131,7 +127,6 @@ const disableKeyBindings = computed(() => !props.keyBindings);
/** /**
* @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#whitespace_keys * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#whitespace_keys
*/ */
const panningKeyCode = ref<string[]>([' ', controlKeyCode]); const panningKeyCode = ref<string[]>([' ', controlKeyCode]);
const panningMouseButton = ref<number[]>([1]); const panningMouseButton = ref<number[]>([1]);
const selectionKeyCode = ref<true | null>(true); const selectionKeyCode = ref<true | null>(true);
@ -144,6 +139,63 @@ onKeyUp(panningKeyCode.value, () => {
selectionKeyCode.value = true; selectionKeyCode.value = true;
}); });
function sortNodesByVerticalPosition(nodes: CanvasNode[]) {
return nodes.sort((a, b) => a.position.y - b.position.y);
}
function getIncomingNodes(id: string) {
return sortNodesByVerticalPosition(getIncomers(id));
}
function getOutgoingNodes(id: string) {
return sortNodesByVerticalPosition(getOutgoers(id));
}
function getSiblingNodes(id: string) {
return sortNodesByVerticalPosition(
getIncomers(id)
.flatMap((incomingNode) => getOutgoers(incomingNode.id))
.filter((node, index, nodes) => nodes.findIndex((n) => n.id === node.id) === index),
);
}
function selectLeftNode(id: string) {
const incomingNodes = getIncomingNodes(id);
const previousNode = incomingNodes[0];
if (previousNode) {
onSelectNodes({ ids: [previousNode.id] });
}
}
function selectRightNode(id: string) {
const outgoingNodes = getOutgoingNodes(id);
const nextNode = outgoingNodes[0];
if (nextNode) {
onSelectNodes({ ids: [nextNode.id] });
}
}
function selectLowerSiblingNode(id: string) {
const siblingNodes = getSiblingNodes(id);
const index = siblingNodes.findIndex((n) => n.id === id);
const nextNode = siblingNodes[index + 1];
console.log(siblingNodes, nextNode);
if (nextNode) {
onSelectNodes({ ids: [nextNode.id] });
}
}
function selectUpperSiblingNode(id: string) {
const siblingNodes = getSiblingNodes(id);
const index = siblingNodes.findIndex((n) => n.id === id);
const previousNode = siblingNodes[index - 1];
if (previousNode) {
onSelectNodes({ ids: [previousNode.id] });
}
}
const keyMap = computed(() => ({ const keyMap = computed(() => ({
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)), ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)), enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
@ -152,6 +204,10 @@ const keyMap = computed(() => ({
'-|_': async () => await onZoomOut(), '-|_': async () => await onZoomOut(),
0: async () => await onResetZoom(), 0: async () => await onResetZoom(),
1: async () => await onFitView(), 1: async () => await onFitView(),
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
ArrowDown: emitWithLastSelectedNode(selectLowerSiblingNode),
ArrowLeft: emitWithLastSelectedNode(selectLeftNode),
ArrowRight: emitWithLastSelectedNode(selectRightNode),
// @TODO implement arrow key shortcuts to modify selection // @TODO implement arrow key shortcuts to modify selection
...(props.readOnly ...(props.readOnly
@ -185,23 +241,16 @@ function onClickNodeAdd(id: string, handle: string) {
emit('click:node:add', id, handle); emit('click:node:add', id, handle);
} }
// Debounced to prevent emitting too many events, necessary for undo/redo function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
const onUpdateNodesPosition = useDebounceFn((events: NodePositionChange[]) => {
emit('update:nodes:position', events); emit('update:nodes:position', events);
}, 200); }
function onUpdateNodePosition(id: string, position: XYPosition) { function onUpdateNodePosition(id: string, position: XYPosition) {
emit('update:node:position', id, position); emit('update:node:position', id, position);
} }
const isPositionChangeEvent = (event: NodeChange): event is NodePositionChange => function onNodeDragStop(event: NodeDragEvent) {
event.type === 'position' && 'position' in event; onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })));
function onNodesChange(events: NodeChange[]) {
const positionChangeEndEvents = events.filter(isPositionChangeEvent);
if (positionChangeEndEvents.length > 0) {
void onUpdateNodesPosition(positionChangeEndEvents);
}
} }
function onSetNodeActive(id: string) { function onSetNodeActive(id: string) {
@ -524,9 +573,9 @@ provide(CanvasKey, {
@pane-click="onClickPane" @pane-click="onClickPane"
@contextmenu="onOpenContextMenu" @contextmenu="onOpenContextMenu"
@viewport-change="onViewportChange" @viewport-change="onViewportChange"
@nodes-change="onNodesChange"
@move-start="onPaneMoveStart" @move-start="onPaneMoveStart"
@move-end="onPaneMoveEnd" @move-end="onPaneMoveEnd"
@node-drag-stop="onNodeDragStop"
> >
<template #node-canvas-node="canvasNodeProps"> <template #node-canvas-node="canvasNodeProps">
<Node <Node