import type { INodeUi, XYPosition } from '@/Interface'; import { useDeviceSupport } from 'n8n-design-system'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { getMousePosition, getRelativePosition } from '@/utils/nodeViewUtils'; import { ref, computed } from 'vue'; import { useCanvasStore } from '@/stores/canvas.store'; import { useContextMenu } from './useContextMenu'; import { useStyles } from './useStyles'; interface ExtendedHTMLSpanElement extends HTMLSpanElement { x: number; y: number; } export default function useCanvasMouseSelect() { const selectActive = ref(false); const selectBox = ref(document.createElement('span') as ExtendedHTMLSpanElement); const { isTouchDevice, isCtrlKeyPressed } = useDeviceSupport(); const uiStore = useUIStore(); const canvasStore = useCanvasStore(); const workflowsStore = useWorkflowsStore(); const { isOpen: isContextMenuOpen } = useContextMenu(); const { APP_Z_INDEXES } = useStyles(); function _setSelectBoxStyle(styles: Record) { Object.assign(selectBox.value.style, styles); } function _showSelectBox(event: MouseEvent) { const [x, y] = getMousePositionWithinNodeView(event); selectBox.value = Object.assign(selectBox.value, { x, y }); _setSelectBoxStyle({ left: selectBox.value.x + 'px', top: selectBox.value.y + 'px', visibility: 'visible', }); selectActive.value = true; } function _updateSelectBox(event: MouseEvent) { const selectionBox = _getSelectionBox(event); _setSelectBoxStyle({ left: selectionBox.x + 'px', top: selectionBox.y + 'px', width: selectionBox.width + 'px', height: selectionBox.height + 'px', }); } function _hideSelectBox() { selectBox.value.x = 0; selectBox.value.y = 0; _setSelectBoxStyle({ visibility: 'hidden', left: '0px', top: '0px', width: '0px', height: '0px', }); selectActive.value = false; } function _getSelectionBox(event: MouseEvent | TouchEvent) { const [x, y] = getMousePositionWithinNodeView(event); return { x: Math.min(x, selectBox.value.x), y: Math.min(y, selectBox.value.y), width: Math.abs(x - selectBox.value.x), height: Math.abs(y - selectBox.value.y), }; } function _getNodesInSelection(event: MouseEvent | TouchEvent): INodeUi[] { const returnNodes: INodeUi[] = []; const selectionBox = _getSelectionBox(event); // Go through all nodes and check if they are selected workflowsStore.allNodes.forEach((node: INodeUi) => { // TODO: Currently always uses the top left corner for checking. Should probably use the center instead if ( node.position[0] < selectionBox.x || node.position[0] > selectionBox.x + selectionBox.width ) { return; } if ( node.position[1] < selectionBox.y || node.position[1] > selectionBox.y + selectionBox.height ) { return; } returnNodes.push(node); }); return returnNodes; } function _createSelectBox() { selectBox.value.id = 'select-box'; _setSelectBoxStyle({ margin: '0px auto', border: '2px dotted #FF0000', // Positioned absolutely within #node-view. This is consistent with how nodes are positioned. position: 'absolute', zIndex: `${APP_Z_INDEXES.SELECT_BOX}`, visibility: 'hidden', }); selectBox.value.addEventListener('mouseup', mouseUpMouseSelect); const nodeViewEl = document.querySelector('#node-view') as HTMLDivElement; nodeViewEl.appendChild(selectBox.value); } function _mouseMoveSelect(e: MouseEvent) { if (e.buttons === 0) { // Mouse button is not pressed anymore so stop selection mode // Happens normally when mouse leave the view pressed and then // comes back unpressed. mouseUpMouseSelect(e); return; } _updateSelectBox(e); } function mouseUpMouseSelect(e: MouseEvent | TouchEvent) { // Ignore right-click if (('button' in e && e.button === 2) || isContextMenuOpen.value) return; if (!selectActive.value) { if (isTouchDevice && e.target instanceof HTMLElement) { if (e.target && e.target.id.includes('node-view')) { // Deselect all nodes deselectAllNodes(); } } // If it is not active return directly. // Else normal node dragging will not work. return; } document.removeEventListener('mousemove', _mouseMoveSelect); // Deselect all nodes deselectAllNodes(); // Select the nodes which are in the selection box const selectedNodes = _getNodesInSelection(e); selectedNodes.forEach((node) => { nodeSelected(node); }); if (selectedNodes.length === 1) { uiStore.lastSelectedNode = selectedNodes[0].name; } _hideSelectBox(); } function mouseDownMouseSelect(e: MouseEvent, moveButtonPressed: boolean) { if (isCtrlKeyPressed(e) || moveButtonPressed || e.button === 2) { // We only care about it when the ctrl key is not pressed at the same time. // So we exit when it is pressed. return; } if (uiStore.isActionActive['dragActive']) { // If a node does currently get dragged we do not activate the selection return; } _showSelectBox(e); // Leave like this. // Do not add an anonymous function because then remove would not work anymore document.addEventListener('mousemove', _mouseMoveSelect); } function getMousePositionWithinNodeView(event: MouseEvent | TouchEvent): XYPosition { const mousePosition = getMousePosition(event); const [relativeX, relativeY] = canvasStore.canvasPositionFromPagePosition(mousePosition); const nodeViewScale = canvasStore.nodeViewScale; const nodeViewOffsetPosition = uiStore.nodeViewOffsetPosition; return getRelativePosition(relativeX, relativeY, nodeViewScale, nodeViewOffsetPosition); } function nodeDeselected(node: INodeUi) { uiStore.removeNodeFromSelection(node); instance.value.removeFromDragSelection(instance.value.getManagedElement(node?.id)); } function nodeSelected(node: INodeUi) { uiStore.addSelectedNode(node); instance.value.addToDragSelection(instance.value.getManagedElement(node?.id)); } function deselectAllNodes() { instance.value.clearDragSelection(); uiStore.resetSelectedNodes(); uiStore.lastSelectedNode = null; uiStore.lastSelectedNodeOutputIndex = null; canvasStore.newNodeInsertPosition = null; canvasStore.setLastSelectedConnection(undefined); } const instance = computed(() => canvasStore.jsPlumbInstance); function initializeCanvasMouseSelect() { _createSelectBox(); } return { selectActive, getMousePositionWithinNodeView, mouseUpMouseSelect, mouseDownMouseSelect, nodeDeselected, nodeSelected, deselectAllNodes, initializeCanvasMouseSelect, }; }