mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
Some checks failed
Test Master / install-and-build (push) Has been cancelled
Test Master / Unit tests (18.x) (push) Has been cancelled
Test Master / Unit tests (20.x) (push) Has been cancelled
Test Master / Unit tests (22.4) (push) Has been cancelled
Test Master / Lint (push) Has been cancelled
Test Master / Notify Slack on failure (push) Has been cancelled
230 lines
6.5 KiB
TypeScript
230 lines
6.5 KiB
TypeScript
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<string, string>) {
|
|
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,
|
|
};
|
|
}
|