How DrawDB Implements Bulk Selection of Tables on the Canvas

DrawDB uses a React context-based system with drag-to-select rectangles and Ctrl/Meta-click toggling to enable bulk selection of multiple tables, areas, and notes on the canvas.

The bulk selection feature in drawdb-io/drawdb allows database designers to efficiently manipulate multiple diagram elements simultaneously. This article examines the exact implementation, from the state management in SelectContext to the canvas interaction logic in Canvas.jsx.

The SelectContext: Centralized Bulk Selection State

All bulk selection state lives in src/context/SelectContext.jsx. This provider exposes two critical pieces of state: the single selectedElement and the array bulkSelectedElements.

// src/context/SelectContext.jsx
import { createContext, useState } from "react";

export const SelectContext = createContext(null);

export default function SelectContextProvider({ children }) {
  const [selectedElement, setSelectedElement] = useState({
    element: ObjectType.NONE,
    id: -1,
    openDialogue: false,
  });
  const [bulkSelectedElements, setBulkSelectedElements] = useState([]); // ⬅ bulk list

  return (
    <SelectContext.Provider
      value={{
        selectedElement,
        setSelectedElement,
        bulkSelectedElements,
        setBulkSelectedElements,
      }}
    >
      {children}
    </SelectContext.Provider>
  );
}

Child components access this state via the useSelect() hook. The Table.jsx, Area.jsx, and Note.jsx components all reference bulkSelectedElements to render highlighted borders when their id appears in the list.

Drag-to-Select: Creating the Selection Rectangle

The Canvas component in src/components/EditorCanvas/Canvas.jsx implements the drag-to-select workflow through the bulkSelectRect state.

// src/components/EditorCanvas/Canvas.jsx (lines 23-31)
const [bulkSelectRect, setBulkSelectRect] = useState({
  x1: 0, y1: 0, x2: 0, y2: 0,
  show: false,
  ctrlKey: false,
  metaKey: false,
});

When the user presses the mouse button on empty canvas space, the component initializes the rectangle with the starting coordinates and modifier key states. As the user drags, the rectangle updates in real-time to provide visual feedback.

Collecting Elements Inside the Rectangle

On mouse-up, the collectSelectedElements() function (lines 40–79) determines which tables, areas, and notes intersect the drag rectangle.

// src/components/EditorCanvas/Canvas.jsx
const collectSelectedElements = () => {
  const rect = getRectFromEndpoints(bulkSelectRect);
  const elements = [];

  // iterate tables, areas, notes …
  // add element if its bbox intersects `rect`

  if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) {
    setBulkSelectedElements([...bulkSelectedElements, ...elements]); // additive
  } else {
    setBulkSelectedElements(elements); // replace
  }
};

The hit-testing logic uses helper functions from src/utils/rect.js (getRectFromEndpoints, isInsideRect) to determine element intersection.

Modifier Key Behavior: Additive vs. Replacement Selection

The implementation distinguishes between two modes based on modifier keys held during selection:

  • Ctrl or Meta held: New elements are added to the existing bulkSelectedElements array
  • No modifier: The new selection replaces the previous bulk selection

This logic appears immediately after element collection (lines 19–23 in Canvas.jsx) and ensures familiar desktop-style selection behavior.

Ctrl/Meta-Click: Toggling Individual Elements

Users can also build bulk selections through individual clicks. The handlePointerDownOnElement function (lines 65–83) handles this interaction.

// src/components/EditorCanvas/Canvas.jsx
const handlePointerDownOnElement = (e, { element, type }) => {
  const elementInBulk = {
    id: element.id,
    type,
    currentCoords: { x: element.x, y: element.y },
    initialCoords: { x: element.x, y: element.y },
  };

  const isSelected = bulkSelectedElements.some(el => isSameElement(el, elementInBulk));

  if (e.ctrlKey || e.metaKey) {
    if (isSelected) {
      setBulkSelectedElements(bulkSelectedElements.filter(el => !isSameElement(el, elementInBulk)));
    } else {
      setBulkSelectedElements([...bulkSelectedElements, elementInBulk]);
    }
    return;
  }

  // plain click → replace bulk list with single element
  setBulkSelectedElements([elementInBulk]);
};

Each element in the bulk list maintains both currentCoords and initialCoords—the latter enables revert-to-original functionality and proper delta calculations during moves.

Moving Multiple Selected Elements Together

When dragging any element that belongs to bulkSelectedElements, the canvas applies identical deltas to all selected items simultaneously (lines 58–78).

// src/components/EditorCanvas/Canvas.jsx
if (isDragging()) {
  const { x: mainX, y: mainY } = coordinatesAfterSnappingToGrid({
    x: pointer.spaces.diagram.x - dragging.grabOffset.x,
    y: pointer.spaces.diagram.y - dragging.grabOffset.y,
  });

  const { currentCoords } = bulkSelectedElements.find(el => isSameElement(el, dragging));
  const deltaX = mainX - currentCoords.x;
  const deltaY = mainY - currentCoords.y;

  const newBulk = bulkSelectedElements.map(el => {
    const newPos = { x: el.currentCoords.x + deltaX, y: el.currentCoords.y + deltaY };
    // persist the new position in the diagram store
    if (el.type === ObjectType.TABLE)  updateTable(el.id, newPos);
    if (el.type === ObjectType.AREA)   updateArea(el.id, newPos);
    if (el.type === ObjectType.NOTE)   updateNote(el.id, newPos);
    return { ...el, currentCoords: newPos };
  });

  setBulkSelectedElements(newBulk);
}

The move operation calculates delta from the primary dragged element, then maps that delta across all bulk-selected items. Each item type delegates to its respective store update function: updateTable(), updateArea(), or updateNote().

Key Files in the Bulk Selection System

File Purpose
src/context/SelectContext.jsx React context providing bulkSelectedElements state
src/components/EditorCanvas/Canvas.jsx Core canvas: rectangle selection, collection, bulk move, click handling
src/components/EditorCanvas/Table.jsx Table component consuming bulk selection for visual feedback
src/components/EditorCanvas/Area.jsx Area component with same bulk selection integration
src/components/EditorCanvas/Note.jsx Note component with same bulk selection integration
src/utils/rect.js Geometry utilities for hit-testing during selection

Summary

  • State management: SelectContext centralizes bulkSelectedElements array accessible throughout the component tree
  • Drag-to-select: bulkSelectRect tracks selection rectangle; collectSelectedElements() performs intersection tests
  • Modifier behavior: Ctrl/Meta enables additive selection; absence triggers replacement selection
  • Individual toggling: handlePointerDownOnElement supports Ctrl/Meta-click to add or remove single items
  • Group movement: Dragging any selected element calculates deltas applied uniformly to all bulkSelectedElements
  • Type-specific updates: The move loop delegates to updateTable(), updateArea(), or updateNote() based on element type

Frequently Asked Questions

How does DrawDB detect which tables are inside the selection rectangle?

DrawDB uses helper functions from src/utils/rect.js—specifically getRectFromEndpoints and isInsideRect—to test whether each table's bounding box intersects the bulkSelectRect. The collectSelectedElements() function in Canvas.jsx iterates through all tables, areas, and notes, adding those that pass the intersection test to the selection array.

Can users add tables to an existing bulk selection without starting over?

Yes. Holding Ctrl (Linux/Windows) or Meta (macOS) during either drag-to-select or individual clicks triggers additive mode. The bulkSelectRect.ctrlKey/metaKey flags and the e.ctrlKey/e.metaKey properties in click handlers control this behavior, appending new elements rather than replacing the existing bulkSelectedElements array.

What happens when dragging a table that is part of a bulk selection?

The canvas identifies the delta between the dragged element's current and new position, then applies that same delta to every item in bulkSelectedElements. The implementation updates currentCoords for each element and persists changes through type-specific store functions like updateTable(), keeping all selected items moving as a rigid group.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →