Skip to content

🐛 [BUG]: Cannot create rectangle selection tool #1984

@tradanghi1999

Description

@tradanghi1999

Is there an existing issue for this?

  • I have searched the existing issues and this is a new bug.

Current Behavior

Hi, followed the idea from React Flow Lasso Selection Node and Figma Select Tool.

I have tried to implement the Rect Selection Tool Feature.

I am currently stuck that when the mouse up the node that is detected selected in mouse move, is unselected.

Here is the code

<script setup>
import { ref, shallowRef, onMounted, onUnmounted } from "vue";
import {
  VueFlow,
  useVueFlow,
  getNodesInside,
  MarkerType,
  ConnectionLineType,
} from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { Controls } from "@vue-flow/controls";
import { MiniMap } from "@vue-flow/minimap";
import { initialEdges, initialNodes } from "./initial-elements.js";
import CustomNode from "./CustomNode.vue";
import SelectionRectangle from "./SelectionRectangle.vue"; // Thêm component mới

// --- State từ Code 1 (Logic chính) ---
const nodes = ref(initialNodes);
const edges = ref(initialEdges);
const nodeTypes = shallowRef({
  custom: CustomNode,
});
const dark = ref(true);
const deleteKeyCodes = ["Delete", "Backspace"];
const connectionLineOptions = {
  markerEnd: MarkerType.ArrowClosed,
};

// --- State từ Code 2 (Rectangle Select) ---
const isDrawing = ref(false);
const startPosition = ref({ x: 0, y: 0 });
const selectionRect = ref({ x: 0, y: 0, width: 0, height: 0 });

// --- Gộp `useVueFlow` từ cả 2 code ---
const {
  // Từ Code 1
  onInit,
  onNodeDragStop,
  onConnect,
  addEdges,
  setViewport,
  toObject,
  updateEdge,
  // Từ Code 2
  project,
  getNodes,
  viewport,
  //addSelectedNodes,
  //nodesSelectionActive,
  //removeSelectedElements,
  setNodes
} = useVueFlow();

// --- Logic từ Code 1 (Event Handlers) ---
const onEdgeUpdate = ({ edge, connection }) => {
  updateEdge(edge, connection);
};

onInit((vueFlowInstance) => {
  vueFlowInstance.fitView();
});

// onNodeDragStop(({ event, nodes, node }) => {
//   console.log("Node Drag Stop", { event, nodes, node });
// });

onConnect((connection) => {
  addEdges({
    ...connection,
    type: "step",
    markerEnd: MarkerType.ArrowClosed,
    updatable: true,
  });
});

// --- Logic từ Code 1 (Utility Functions) ---
function updatePos() {
  nodes.value = nodes.value.map((node) => {
    return {
      ...node,
      position: {
        x: Math.random() * 400,
        y: Math.random() * 400,
      },
    };
  });
}

// function logToObject() {
//   console.log(toObject());
// }

function resetTransform() {
  setViewport({ x: 0, y: 0, zoom: 1 });
}

function toggleDarkMode() {
  dark.value = !dark.value;
}

// --- Logic từ Code 2 (Rectangle Select Handlers) ---
// (Đã loại bỏ TypeScript types để phù hợp với code 1)
function handleMouseDown(event) {
  // Chỉ bắt đầu vẽ nếu nhấp chuột trái
  if (event.button !== 0) return;

  // Bỏ chọn tất cả khi bắt đầu kéo mới
  //removeSelectedElements();
  //nodesSelectionActive.value = false;

  isDrawing.value = true;
  startPosition.value = { x: event.clientX, y: event.clientY };
  selectionRect.value = {
    x: event.clientX,
    y: event.clientY,
    width: 0,
    height: 0,
  };

  // Ngăn chặn hành vi mặc định (như chọn văn bản)
  event.preventDefault();
}
function handleMouseMove(event) {
  if (!isDrawing.value) return;

  const currentX = event.clientX;
  const currentY = event.clientY;

  const rectX = Math.min(startPosition.value.x, currentX);
  const rectY = Math.min(startPosition.value.y, currentY);
  const rectWidth = Math.abs(startPosition.value.x - currentX);
  const rectHeight = Math.abs(startPosition.value.y - currentY);

  selectionRect.value = {
    x: rectX,
    y: rectY,
    width: rectWidth,
    height: rectHeight,
  };

  // Chuyển đổi sang tọa độ viewport
  const viewportRect = screenRectToViewportRect(selectionRect.value, project);

  // Lấy mảng nodes hiện tại từ instance (getNodes là hàm)
  const currentNodes = typeof getNodes === "function" ? getNodes() : (getNodes?.value ?? []);

  // Tìm nodes nằm trong rectangle (getNodesInside cần mảng nodes)
  const nodesInside = getNodesInside(
    currentNodes,
    viewportRect,
    viewport.value,
    true // partial selection?
  );

  // lấy id của nodes trong vùng
  const nodesToSelectIds = nodesInside.map((n) => n.id);

  // Vì bạn đang truyền `:nodes="nodes"` (controlled),
  // cần cập nhật nodes.value để phản ánh selection
  nodes.value = nodes.value.map((node) => ({
    ...node,
    selected: nodesToSelectIds.includes(node.id),
  }));

  // nếu bạn muốn debug
  // console.log('inside', nodesInside.map(n => n.id));
}

function handleMouseUp(event) {
  if (isDrawing.value) {
    event.stopPropagation();
    event.preventDefault();
  }

  if (!isDrawing.value) return;
  isDrawing.value = false;

  // Kích hoạt hộp bao lựa chọn trực quan
  // if (getNodes.value.some((n) => n.selected)) {
  //   nodesSelectionActive.value = true;
  // }
}

// --- Logic từ Code 2 (Utility & Lifecycle) ---
/**
 * Chuyển đổi một Rect từ tọa độ màn hình sang tọa độ viewport
 */
function screenRectToViewportRect(screenRect, project) {
  const topLeft = project({ x: screenRect.x, y: screenRect.y });
  const bottomRight = project({
    x: screenRect.x + screenRect.width,
    y: screenRect.y + screenRect.height,
  });

  return {
    x: topLeft.x,
    y: topLeft.y,
    width: bottomRight.x - topLeft.x,
    height: bottomRight.y - topLeft.y,
  };
}

//Lắng nghe mouseup trên window
// onMounted(() => {
//   window.addEventListener("mouseup", handleMouseUp);
// });

// onUnmounted(() => {
//   window.removeEventListener("mouseup", handleMouseUp);
// });
</script>

<template>
  <div @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
    style="width: 100vw; height: 100vh">
    <VueFlow :nodes="nodes" :node-types="nodeTypes" :edges="edges" :class="{ dark }" class="basic-flow"
      :default-viewport="{ zoom: 1.5 }" :min-zoom="0.2" :max-zoom="4" :pan-on-drag="false" :pan-on-scroll="true"
      :zoom-on-scrol="false" :connection-line-type="ConnectionLineType.Step" :edgesUpdatable="true"
      :delete-key-code="deleteKeyCodes" @edge-update="onEdgeUpdate" :style="{ background: '#ddd' }">
      <Background pattern-color="#aaa" :gap="16" />
      <MiniMap />
      <Controls position="top-left">
      </Controls>
    </VueFlow>

    <SelectionRectangle v-if="isDrawing" :rect="selectionRect" />
  </div>
</template>

<style>
/* Style từ Code 2 */
@import "@vue-flow/core/dist/style.css";
@import "@vue-flow/core/dist/theme-default.css";

html,
body {
  margin: 0;
  overflow: hidden;
}
</style>

Expected Behavior

When mouse up, the node is kept selected without being unselected.

Steps To Reproduce

No response

Minimal reproduction of the issue with CodeSandbox

No response

Relevant log output

Anything else?

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingtriageAwaiting triage

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions