mirror of
https://github.com/drawdb-io/drawdb.git
synced 2025-08-29 02:25:26 +00:00
Add ctrl select support (#527)
* Add ctrl select support * fix the issue with clicking on overlapping objects * simplify the code, add meta to ctrl for macos * fix lock unlock with command on macos
This commit is contained in:
@@ -48,11 +48,47 @@ export default function Area({
|
||||
});
|
||||
};
|
||||
|
||||
const lockUnlockArea = () => {
|
||||
setBulkSelectedElements((prev) =>
|
||||
prev.filter((el) => el.id !== data.id || el.type !== ObjectType.AREA),
|
||||
);
|
||||
updateArea(data.id, { locked: !data.locked });
|
||||
const lockUnlockArea = (e) => {
|
||||
const locking = !data.locked;
|
||||
updateArea(data.id, { locked: locking });
|
||||
|
||||
const lockArea = () => {
|
||||
setSelectedElement({
|
||||
...selectedElement,
|
||||
element: ObjectType.NONE,
|
||||
id: -1,
|
||||
open: false,
|
||||
});
|
||||
setBulkSelectedElements((prev) =>
|
||||
prev.filter((el) => el.id !== data.id || el.type !== ObjectType.AREA),
|
||||
);
|
||||
};
|
||||
|
||||
const unlockArea = () => {
|
||||
const elementInBulk = {
|
||||
id: data.id,
|
||||
type: ObjectType.AREA,
|
||||
initialCoords: { x: data.x, y: data.y },
|
||||
currentCoords: { x: data.x, y: data.y },
|
||||
};
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
setBulkSelectedElements((prev) => [...prev, elementInBulk]);
|
||||
} else {
|
||||
setBulkSelectedElements([elementInBulk]);
|
||||
}
|
||||
setSelectedElement((prev) => ({
|
||||
...prev,
|
||||
element: ObjectType.AREA,
|
||||
id: data.id,
|
||||
open: false,
|
||||
}));
|
||||
};
|
||||
|
||||
if (locking) {
|
||||
lockArea();
|
||||
} else {
|
||||
unlockArea();
|
||||
}
|
||||
};
|
||||
|
||||
const edit = () => {
|
||||
|
@@ -29,7 +29,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import { areFieldsCompatible, getTableHeight } from "../../utils/utils";
|
||||
import { getRectFromEndpoints, isInsideRect } from "../../utils/rect";
|
||||
import { noteWidth, State } from "../../data/constants";
|
||||
import { State, noteWidth } from "../../data/constants";
|
||||
|
||||
export default function Canvas() {
|
||||
const { t } = useTranslation();
|
||||
@@ -89,145 +89,170 @@ export default function Canvas() {
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
const [bulkSelectRectPts, setBulkSelectRectPts] = useState({
|
||||
const [bulkSelectRect, setBulkSelectRect] = useState({
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 0,
|
||||
y2: 0,
|
||||
show: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
});
|
||||
// this is used to store the element that is clicked on
|
||||
// at the moment, and shouldn't be a part of the state
|
||||
let elementPointerDown = null;
|
||||
|
||||
const isSameElement = (el1, el2) => {
|
||||
return el1.id === el2.id && el1.type === el2.type;
|
||||
};
|
||||
|
||||
const collectSelectedElements = () => {
|
||||
const rect = getRectFromEndpoints(bulkSelectRectPts);
|
||||
|
||||
const rect = getRectFromEndpoints(bulkSelectRect);
|
||||
const elements = [];
|
||||
const shouldAddElement = (elementRect, element) => {
|
||||
// if ctrl key is pressed, only add the elements that are not already selected
|
||||
// can theoretically be optimized later if the selected elements is
|
||||
// a map from id to element (after the ids are made unique)
|
||||
return (
|
||||
isInsideRect(elementRect, rect) &&
|
||||
((!bulkSelectRect.ctrlKey && !bulkSelectRect.metaKey) ||
|
||||
!bulkSelectedElements.some((el) => isSameElement(el, element)))
|
||||
);
|
||||
};
|
||||
|
||||
tables.forEach((table) => {
|
||||
if (table.locked) return;
|
||||
|
||||
if (
|
||||
isInsideRect(
|
||||
{
|
||||
x: table.x,
|
||||
y: table.y,
|
||||
width: settings.tableWidth,
|
||||
height: getTableHeight(table),
|
||||
},
|
||||
rect,
|
||||
)
|
||||
) {
|
||||
elements.push({
|
||||
id: table.id,
|
||||
type: ObjectType.TABLE,
|
||||
currentCoords: { x: table.x, y: table.y },
|
||||
initialCoords: { x: table.x, y: table.y },
|
||||
});
|
||||
const element = {
|
||||
id: table.id,
|
||||
type: ObjectType.TABLE,
|
||||
currentCoords: { x: table.x, y: table.y },
|
||||
initialCoords: { x: table.x, y: table.y },
|
||||
};
|
||||
const tableRect = {
|
||||
x: table.x,
|
||||
y: table.y,
|
||||
width: settings.tableWidth,
|
||||
height: getTableHeight(table),
|
||||
};
|
||||
if (shouldAddElement(tableRect, element)) {
|
||||
elements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
areas.forEach((area) => {
|
||||
if (area.locked) return;
|
||||
|
||||
if (
|
||||
isInsideRect(
|
||||
{
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
},
|
||||
rect,
|
||||
)
|
||||
) {
|
||||
elements.push({
|
||||
id: area.id,
|
||||
type: ObjectType.AREA,
|
||||
currentCoords: { x: area.x, y: area.y },
|
||||
initialCoords: { x: area.x, y: area.y },
|
||||
});
|
||||
const element = {
|
||||
id: area.id,
|
||||
type: ObjectType.AREA,
|
||||
currentCoords: { x: area.x, y: area.y },
|
||||
initialCoords: { x: area.x, y: area.y },
|
||||
};
|
||||
const areaRect = {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
};
|
||||
if (shouldAddElement(areaRect, element)) {
|
||||
elements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
notes.forEach((note) => {
|
||||
if (note.locked) return;
|
||||
|
||||
if (
|
||||
isInsideRect(
|
||||
{
|
||||
x: note.x,
|
||||
y: note.y,
|
||||
width: noteWidth,
|
||||
height: note.height,
|
||||
},
|
||||
rect,
|
||||
)
|
||||
) {
|
||||
elements.push({
|
||||
id: note.id,
|
||||
type: ObjectType.NOTE,
|
||||
currentCoords: { x: note.x, y: note.y },
|
||||
initialCoords: { x: note.x, y: note.y },
|
||||
});
|
||||
const element = {
|
||||
id: note.id,
|
||||
type: ObjectType.NOTE,
|
||||
currentCoords: { x: note.x, y: note.y },
|
||||
initialCoords: { x: note.x, y: note.y },
|
||||
};
|
||||
const noteRect = {
|
||||
x: note.x,
|
||||
y: note.y,
|
||||
width: noteWidth,
|
||||
height: note.height,
|
||||
};
|
||||
if (shouldAddElement(noteRect, element)) {
|
||||
elements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
setBulkSelectedElements(elements);
|
||||
};
|
||||
|
||||
const getElement = (element) => {
|
||||
switch (element.type) {
|
||||
case ObjectType.TABLE:
|
||||
return tables.find((t) => t.id === element.id);
|
||||
case ObjectType.AREA:
|
||||
return areas[element.id];
|
||||
case ObjectType.NOTE:
|
||||
return notes[element.id];
|
||||
default:
|
||||
return { x: 0, y: 0, locked: false };
|
||||
if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) {
|
||||
setBulkSelectedElements([...bulkSelectedElements, ...elements]);
|
||||
} else {
|
||||
setBulkSelectedElements(elements);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} e
|
||||
* @param {number} id
|
||||
* @param {ObjectType[keyof ObjectType]} type
|
||||
*/
|
||||
const handlePointerDownOnElement = (e, id, type) => {
|
||||
const handlePointerDownOnElement = (e, { element, type }) => {
|
||||
if (selectedElement.open && !layout.sidebar) return;
|
||||
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
const element = getElement({ id, type });
|
||||
|
||||
setSelectedElement((prev) => ({
|
||||
...prev,
|
||||
element: type,
|
||||
id: id,
|
||||
open: false,
|
||||
}));
|
||||
if (!element.locked || !(e.ctrlKey || e.metaKey)) {
|
||||
setSelectedElement((prev) => ({
|
||||
...prev,
|
||||
element: type,
|
||||
id: element.id,
|
||||
open: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (element.locked) {
|
||||
setBulkSelectedElements([]);
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
setBulkSelectedElements([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let newBulkSelectedElements;
|
||||
if (bulkSelectedElements.some((el) => el.id === id && el.type === type)) {
|
||||
newBulkSelectedElements = bulkSelectedElements;
|
||||
} else {
|
||||
newBulkSelectedElements = [
|
||||
{
|
||||
id,
|
||||
type,
|
||||
currentCoords: { x: element.x, y: element.y },
|
||||
initialCoords: { x: element.x, y: element.y },
|
||||
},
|
||||
];
|
||||
setBulkSelectedElements(newBulkSelectedElements);
|
||||
setBulkSelectRect((prev) => ({
|
||||
...prev,
|
||||
show: false,
|
||||
}));
|
||||
|
||||
// this is the object that will be added to the bulk selected elements
|
||||
// if necessary
|
||||
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) {
|
||||
if (bulkSelectedElements.length > 1) {
|
||||
setBulkSelectedElements(
|
||||
bulkSelectedElements.filter(
|
||||
(el) => !isSameElement(el, elementInBulk),
|
||||
),
|
||||
);
|
||||
setSelectedElement({
|
||||
...selectedElement,
|
||||
element: ObjectType.NONE,
|
||||
id: -1,
|
||||
open: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setBulkSelectedElements([...bulkSelectedElements, elementInBulk]);
|
||||
}
|
||||
setDragging(notDragging);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSelected) {
|
||||
setBulkSelectedElements([elementInBulk]);
|
||||
}
|
||||
setDragging({
|
||||
id,
|
||||
id: element.id,
|
||||
type,
|
||||
grabOffset: {
|
||||
x: pointer.spaces.diagram.x - element.x,
|
||||
@@ -285,8 +310,8 @@ export default function Canvas() {
|
||||
y: pointer.spaces.diagram.y - dragging.grabOffset.y,
|
||||
});
|
||||
|
||||
const { currentCoords } = bulkSelectedElements.find(
|
||||
(el) => el.id === dragging.id && el.type === dragging.type,
|
||||
const { currentCoords } = bulkSelectedElements.find((el) =>
|
||||
isSameElement(el, dragging),
|
||||
);
|
||||
|
||||
const deltaX = mainElementFinalX - currentCoords.x;
|
||||
@@ -352,8 +377,8 @@ export default function Canvas() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulkSelectRectPts.show) {
|
||||
setBulkSelectRectPts((prev) => ({
|
||||
if (bulkSelectRect.show) {
|
||||
setBulkSelectRect((prev) => ({
|
||||
...prev,
|
||||
x2: pointer.spaces.diagram.x,
|
||||
y2: pointer.spaces.diagram.y,
|
||||
@@ -379,13 +404,18 @@ export default function Canvas() {
|
||||
const isMouseMiddleButton = e.button === 1;
|
||||
|
||||
if (isMouseLeftButton) {
|
||||
setBulkSelectRectPts({
|
||||
setBulkSelectRect({
|
||||
x1: pointer.spaces.diagram.x,
|
||||
y1: pointer.spaces.diagram.y,
|
||||
x2: pointer.spaces.diagram.x,
|
||||
y2: pointer.spaces.diagram.y,
|
||||
show: true,
|
||||
show: elementPointerDown === null || !elementPointerDown.element.locked,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
});
|
||||
if (elementPointerDown !== null) {
|
||||
handlePointerDownOnElement(e, elementPointerDown);
|
||||
}
|
||||
pointer.setStyle("crosshair");
|
||||
} else if (isMouseMiddleButton) {
|
||||
setPanning({
|
||||
@@ -459,8 +489,8 @@ export default function Canvas() {
|
||||
);
|
||||
}
|
||||
|
||||
if (bulkSelectRectPts.show) {
|
||||
setBulkSelectRectPts((prev) => ({
|
||||
if (bulkSelectRect.show) {
|
||||
setBulkSelectRect((prev) => ({
|
||||
...prev,
|
||||
x2: pointer.spaces.diagram.x,
|
||||
y2: pointer.spaces.diagram.y,
|
||||
@@ -661,11 +691,14 @@ export default function Canvas() {
|
||||
<Area
|
||||
key={a.id}
|
||||
data={a}
|
||||
onPointerDown={(e) =>
|
||||
handlePointerDownOnElement(e, a.id, ObjectType.AREA)
|
||||
}
|
||||
setResize={setAreaResize}
|
||||
setInitDimensions={setAreaInitDimensions}
|
||||
onPointerDown={() => {
|
||||
elementPointerDown = {
|
||||
element: a,
|
||||
type: ObjectType.AREA,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{relationships.map((e, i) => (
|
||||
@@ -678,9 +711,12 @@ export default function Canvas() {
|
||||
setHoveredTable={setHoveredTable}
|
||||
handleGripField={handleGripField}
|
||||
setLinkingLine={setLinkingLine}
|
||||
onPointerDown={(e) =>
|
||||
handlePointerDownOnElement(e, table.id, ObjectType.TABLE)
|
||||
}
|
||||
onPointerDown={() => {
|
||||
elementPointerDown = {
|
||||
element: table,
|
||||
type: ObjectType.TABLE,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{linking && (
|
||||
@@ -695,14 +731,17 @@ export default function Canvas() {
|
||||
<Note
|
||||
key={n.id}
|
||||
data={n}
|
||||
onPointerDown={(e) =>
|
||||
handlePointerDownOnElement(e, n.id, ObjectType.NOTE)
|
||||
}
|
||||
onPointerDown={() => {
|
||||
elementPointerDown = {
|
||||
element: n,
|
||||
type: ObjectType.NOTE,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{bulkSelectRectPts.show && (
|
||||
{bulkSelectRect.show && (
|
||||
<rect
|
||||
{...getRectFromEndpoints(bulkSelectRectPts)}
|
||||
{...getRectFromEndpoints(bulkSelectRect)}
|
||||
stroke="grey"
|
||||
fill="grey"
|
||||
fillOpacity={0.15}
|
||||
|
@@ -100,11 +100,47 @@ export default function Note({ data, onPointerDown }) {
|
||||
setRedoStack([]);
|
||||
};
|
||||
|
||||
const lockUnlockNote = () => {
|
||||
setBulkSelectedElements((prev) =>
|
||||
prev.filter((el) => el.id !== data.id || el.type !== ObjectType.NOTE),
|
||||
);
|
||||
updateNote(data.id, { locked: !data.locked });
|
||||
const lockUnlockNote = (e) => {
|
||||
const locking = !data.locked;
|
||||
updateNote(data.id, { locked: locking });
|
||||
|
||||
const lockNote = () => {
|
||||
setSelectedElement({
|
||||
...selectedElement,
|
||||
element: ObjectType.NONE,
|
||||
id: -1,
|
||||
open: false,
|
||||
});
|
||||
setBulkSelectedElements((prev) =>
|
||||
prev.filter((el) => el.id !== data.id || el.type !== ObjectType.NOTE),
|
||||
);
|
||||
};
|
||||
|
||||
const unlockNote = () => {
|
||||
const elementInBulk = {
|
||||
id: data.id,
|
||||
type: ObjectType.NOTE,
|
||||
initialCoords: { x: data.x, y: data.y },
|
||||
currentCoords: { x: data.x, y: data.y },
|
||||
};
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
setBulkSelectedElements((prev) => [...prev, elementInBulk]);
|
||||
} else {
|
||||
setBulkSelectedElements([elementInBulk]);
|
||||
}
|
||||
setSelectedElement((prev) => ({
|
||||
...prev,
|
||||
element: ObjectType.NOTE,
|
||||
id: data.id,
|
||||
open: false,
|
||||
}));
|
||||
};
|
||||
|
||||
if (locking) {
|
||||
lockNote();
|
||||
} else {
|
||||
unlockNote();
|
||||
}
|
||||
};
|
||||
|
||||
const edit = () => {
|
||||
|
@@ -24,16 +24,15 @@ import { isRtl } from "../../i18n/utils/rtl";
|
||||
import i18n from "../../i18n/i18n";
|
||||
import { getTableHeight } from "../../utils/utils";
|
||||
|
||||
export default function Table(props) {
|
||||
export default function Table({
|
||||
tableData,
|
||||
onPointerDown,
|
||||
setHoveredTable,
|
||||
handleGripField,
|
||||
setLinkingLine,
|
||||
}) {
|
||||
const [hoveredField, setHoveredField] = useState(null);
|
||||
const { database } = useDiagram();
|
||||
const {
|
||||
tableData,
|
||||
onPointerDown,
|
||||
setHoveredTable,
|
||||
handleGripField,
|
||||
setLinkingLine,
|
||||
} = props;
|
||||
const { layout } = useLayout();
|
||||
const { deleteTable, deleteField, updateTable } = useDiagram();
|
||||
const { settings } = useSettings();
|
||||
@@ -62,11 +61,49 @@ export default function Table(props) {
|
||||
);
|
||||
}, [selectedElement, tableData, bulkSelectedElements]);
|
||||
|
||||
const lockUnlockTable = () => {
|
||||
setBulkSelectedElements((prev) =>
|
||||
prev.filter((el) => el.id !== tableData.id || el.type !== ObjectType.TABLE),
|
||||
);
|
||||
updateTable(tableData.id, { locked: !tableData.locked });
|
||||
const lockUnlockTable = (e) => {
|
||||
const locking = !tableData.locked;
|
||||
updateTable(tableData.id, { locked: locking });
|
||||
|
||||
const lockTable = () => {
|
||||
setSelectedElement({
|
||||
...selectedElement,
|
||||
element: ObjectType.NONE,
|
||||
id: -1,
|
||||
open: false,
|
||||
});
|
||||
setBulkSelectedElements((prev) =>
|
||||
prev.filter(
|
||||
(el) => el.id !== tableData.id || el.type !== ObjectType.TABLE,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const unlockTable = () => {
|
||||
const elementInBulk = {
|
||||
id: tableData.id,
|
||||
type: ObjectType.TABLE,
|
||||
initialCoords: { x: tableData.x, y: tableData.y },
|
||||
currentCoords: { x: tableData.x, y: tableData.y },
|
||||
};
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
setBulkSelectedElements((prev) => [...prev, elementInBulk]);
|
||||
} else {
|
||||
setBulkSelectedElements([elementInBulk]);
|
||||
}
|
||||
setSelectedElement((prev) => ({
|
||||
...prev,
|
||||
element: ObjectType.TABLE,
|
||||
id: tableData.id,
|
||||
open: false,
|
||||
}));
|
||||
};
|
||||
|
||||
if (locking) {
|
||||
lockTable();
|
||||
} else {
|
||||
unlockTable();
|
||||
}
|
||||
};
|
||||
|
||||
const openEditor = () => {
|
||||
|
Reference in New Issue
Block a user