n8n/packages/editor-ui/src/composables/useCanvasMouseSelect.ts
Mutasem Aldmour 351134f786
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
fix(editor): Ensure toasts show above modal overlays (#11410)
2024-10-25 16:56:47 +02:00

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,
};
}