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
bulkSelectedElementsarray - 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:
SelectContextcentralizesbulkSelectedElementsarray accessible throughout the component tree - Drag-to-select:
bulkSelectRecttracks selection rectangle;collectSelectedElements()performs intersection tests - Modifier behavior: Ctrl/Meta enables additive selection; absence triggers replacement selection
- Individual toggling:
handlePointerDownOnElementsupports 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(), orupdateNote()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →