mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 19:11:13 +00:00
feat(editor): Add selection navigation using the keyboard on new canvas (#11679)
This commit is contained in:
@@ -10,8 +10,8 @@ import type {
|
||||
Connection,
|
||||
XYPosition,
|
||||
ViewportTransform,
|
||||
NodeChange,
|
||||
NodePositionChange,
|
||||
NodeDragEvent,
|
||||
GraphNode,
|
||||
} from '@vue-flow/core';
|
||||
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
|
||||
import { Background } from '@vue-flow/background';
|
||||
@@ -29,9 +29,10 @@ import type { PinDataSource } from '@/composables/usePinnedData';
|
||||
import { isPresent } from '@/utils/typesUtils';
|
||||
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
||||
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 CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
|
||||
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
@@ -97,6 +98,7 @@ const props = withDefaults(
|
||||
|
||||
const { controlKeyCode } = useDeviceSupport();
|
||||
|
||||
const vueFlow = useVueFlow({ id: props.id, deleteKeyCode: null });
|
||||
const {
|
||||
getSelectedNodes: selectedNodes,
|
||||
addSelectedNodes,
|
||||
@@ -113,7 +115,14 @@ const {
|
||||
onPaneReady,
|
||||
findNode,
|
||||
viewport,
|
||||
} = useVueFlow({ id: props.id, deleteKeyCode: null });
|
||||
} = vueFlow;
|
||||
const {
|
||||
getIncomingNodes,
|
||||
getOutgoingNodes,
|
||||
getSiblingNodes,
|
||||
getDownstreamNodes,
|
||||
getUpstreamNodes,
|
||||
} = useCanvasTraversal(vueFlow);
|
||||
|
||||
const isPaneReady = ref(false);
|
||||
|
||||
@@ -131,7 +140,6 @@ const disableKeyBindings = computed(() => !props.keyBindings);
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#whitespace_keys
|
||||
*/
|
||||
|
||||
const panningKeyCode = ref<string[]>([' ', controlKeyCode]);
|
||||
const panningMouseButton = ref<number[]>([1]);
|
||||
const selectionKeyCode = ref<true | null>(true);
|
||||
@@ -144,6 +152,54 @@ onKeyUp(panningKeyCode.value, () => {
|
||||
selectionKeyCode.value = true;
|
||||
});
|
||||
|
||||
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] ?? siblingNodes[0];
|
||||
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] ?? siblingNodes[siblingNodes.length - 1];
|
||||
if (previousNode) {
|
||||
onSelectNodes({
|
||||
ids: [previousNode.id],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function selectDownstreamNodes(id: string) {
|
||||
const downstreamNodes = getDownstreamNodes(id);
|
||||
onSelectNodes({ ids: [...downstreamNodes.map((node) => node.id), id] });
|
||||
}
|
||||
|
||||
function selectUpstreamNodes(id: string) {
|
||||
const upstreamNodes = getUpstreamNodes(id);
|
||||
onSelectNodes({ ids: [...upstreamNodes.map((node) => node.id), id] });
|
||||
}
|
||||
|
||||
const keyMap = computed(() => ({
|
||||
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
|
||||
@@ -152,6 +208,12 @@ const keyMap = computed(() => ({
|
||||
'-|_': async () => await onZoomOut(),
|
||||
0: async () => await onResetZoom(),
|
||||
1: async () => await onFitView(),
|
||||
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
|
||||
ArrowDown: emitWithLastSelectedNode(selectLowerSiblingNode),
|
||||
ArrowLeft: emitWithLastSelectedNode(selectLeftNode),
|
||||
ArrowRight: emitWithLastSelectedNode(selectRightNode),
|
||||
shift_ArrowLeft: emitWithLastSelectedNode(selectUpstreamNodes),
|
||||
shift_ArrowRight: emitWithLastSelectedNode(selectDownstreamNodes),
|
||||
// @TODO implement arrow key shortcuts to modify selection
|
||||
|
||||
...(props.readOnly
|
||||
@@ -177,31 +239,30 @@ useKeybindings(keyMap, { disabled: disableKeyBindings });
|
||||
* Nodes
|
||||
*/
|
||||
|
||||
const lastSelectedNode = computed(() => selectedNodes.value[selectedNodes.value.length - 1]);
|
||||
const hasSelection = computed(() => selectedNodes.value.length > 0);
|
||||
const selectedNodeIds = computed(() => selectedNodes.value.map((node) => node.id));
|
||||
|
||||
const lastSelectedNode = ref<GraphNode>();
|
||||
watch(selectedNodes, (nodes) => {
|
||||
if (!lastSelectedNode.value || !nodes.find((node) => node.id === lastSelectedNode.value?.id)) {
|
||||
lastSelectedNode.value = nodes[nodes.length - 1];
|
||||
}
|
||||
});
|
||||
|
||||
function onClickNodeAdd(id: string, handle: string) {
|
||||
emit('click:node:add', id, handle);
|
||||
}
|
||||
|
||||
// Debounced to prevent emitting too many events, necessary for undo/redo
|
||||
const onUpdateNodesPosition = useDebounceFn((events: NodePositionChange[]) => {
|
||||
function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
|
||||
emit('update:nodes:position', events);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function onUpdateNodePosition(id: string, position: XYPosition) {
|
||||
emit('update:node:position', id, position);
|
||||
}
|
||||
|
||||
const isPositionChangeEvent = (event: NodeChange): event is NodePositionChange =>
|
||||
event.type === 'position' && 'position' in event;
|
||||
|
||||
function onNodesChange(events: NodeChange[]) {
|
||||
const positionChangeEndEvents = events.filter(isPositionChangeEvent);
|
||||
if (positionChangeEndEvents.length > 0) {
|
||||
void onUpdateNodesPosition(positionChangeEndEvents);
|
||||
}
|
||||
function onNodeDragStop(event: NodeDragEvent) {
|
||||
onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })));
|
||||
}
|
||||
|
||||
function onSetNodeActive(id: string) {
|
||||
@@ -517,6 +578,7 @@ provide(CanvasKey, {
|
||||
:class="classes"
|
||||
:selection-key-code="selectionKeyCode"
|
||||
:pan-activation-key-code="panningKeyCode"
|
||||
:disable-keyboard-a11y="true"
|
||||
data-test-id="canvas"
|
||||
@connect-start="onConnectStart"
|
||||
@connect="onConnect"
|
||||
@@ -524,9 +586,9 @@ provide(CanvasKey, {
|
||||
@pane-click="onClickPane"
|
||||
@contextmenu="onOpenContextMenu"
|
||||
@viewport-change="onViewportChange"
|
||||
@nodes-change="onNodesChange"
|
||||
@move-start="onPaneMoveStart"
|
||||
@move-end="onPaneMoveEnd"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
>
|
||||
<template #node-canvas-node="canvasNodeProps">
|
||||
<Node
|
||||
|
||||
Reference in New Issue
Block a user