mirror of
https://github.com/drawdb-io/drawdb.git
synced 2025-05-24 18:39:12 +00:00
Bulk select (#395)
* add rect for select * collect selected elements * increase note stroke width * move elements and undo redo * add icon to toolbar to toggle multiselect mode * set bulk selected elements to none when panning
This commit is contained in:
parent
73ed14982c
commit
0efb5470ab
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { Button, Popover, Input, ColorPicker } from "@douyinfe/semi-ui";
|
import { Button, Popover, Input, ColorPicker } from "@douyinfe/semi-ui";
|
||||||
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
import { Tab, Action, ObjectType, State } from "../../data/constants";
|
import { Tab, Action, ObjectType, State } from "../../data/constants";
|
||||||
@ -30,7 +30,8 @@ export default function Area({
|
|||||||
const { layout } = useLayout();
|
const { layout } = useLayout();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { setSaveState } = useSaveState();
|
const { setSaveState } = useSaveState();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement, bulkSelectedElements } =
|
||||||
|
useSelect();
|
||||||
|
|
||||||
const handleResize = (e, dir) => {
|
const handleResize = (e, dir) => {
|
||||||
setResize({ id: data.id, dir: dir });
|
setResize({ id: data.id, dir: dir });
|
||||||
@ -82,11 +83,21 @@ export default function Area({
|
|||||||
setSaveState(State.SAVING);
|
setSaveState(State.SAVING);
|
||||||
};
|
};
|
||||||
|
|
||||||
const areaIsSelected = () =>
|
const areaIsOpen = () =>
|
||||||
selectedElement.element === ObjectType.AREA &&
|
selectedElement.element === ObjectType.AREA &&
|
||||||
selectedElement.id === data.id &&
|
selectedElement.id === data.id &&
|
||||||
selectedElement.open;
|
selectedElement.open;
|
||||||
|
|
||||||
|
const isSelected = useMemo(() => {
|
||||||
|
return (
|
||||||
|
(selectedElement.id === data.id &&
|
||||||
|
selectedElement.element === ObjectType.AREA) ||
|
||||||
|
bulkSelectedElements.some(
|
||||||
|
(e) => e.type === ObjectType.AREA && e.id === data.id,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [selectedElement, data, bulkSelectedElements]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g ref={ref}>
|
<g ref={ref}>
|
||||||
<foreignObject
|
<foreignObject
|
||||||
@ -101,8 +112,7 @@ export default function Area({
|
|||||||
className={`w-full h-full p-2 rounded cursor-move border-2 ${
|
className={`w-full h-full p-2 rounded cursor-move border-2 ${
|
||||||
isHovered
|
isHovered
|
||||||
? "border-dashed border-blue-500"
|
? "border-dashed border-blue-500"
|
||||||
: selectedElement.element === ObjectType.AREA &&
|
: isSelected
|
||||||
selectedElement.id === data.id
|
|
||||||
? "border-blue-500 opacity-100"
|
? "border-blue-500 opacity-100"
|
||||||
: "border-slate-400 opacity-100"
|
: "border-slate-400 opacity-100"
|
||||||
}`}
|
}`}
|
||||||
@ -112,9 +122,9 @@ export default function Area({
|
|||||||
<div className="text-color select-none overflow-hidden text-ellipsis">
|
<div className="text-color select-none overflow-hidden text-ellipsis">
|
||||||
{data.name}
|
{data.name}
|
||||||
</div>
|
</div>
|
||||||
{(isHovered || (areaIsSelected() && !layout.sidebar)) && (
|
{(isHovered || (areaIsOpen() && !layout.sidebar)) && (
|
||||||
<Popover
|
<Popover
|
||||||
visible={areaIsSelected() && !layout.sidebar}
|
visible={areaIsOpen() && !layout.sidebar}
|
||||||
onClickOutSide={onClickOutSide}
|
onClickOutSide={onClickOutSide}
|
||||||
stopPropagation
|
stopPropagation
|
||||||
content={<EditPopoverContent data={data} />}
|
content={<EditPopoverContent data={data} />}
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
Constraint,
|
Constraint,
|
||||||
darkBgTheme,
|
darkBgTheme,
|
||||||
ObjectType,
|
ObjectType,
|
||||||
|
tableFieldHeight,
|
||||||
|
tableHeaderHeight,
|
||||||
} from "../../data/constants";
|
} from "../../data/constants";
|
||||||
import { Toast } from "@douyinfe/semi-ui";
|
import { Toast } from "@douyinfe/semi-ui";
|
||||||
import Table from "./Table";
|
import Table from "./Table";
|
||||||
@ -25,6 +27,7 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEventListener } from "usehooks-ts";
|
import { useEventListener } from "usehooks-ts";
|
||||||
import { areFieldsCompatible } from "../../utils/utils";
|
import { areFieldsCompatible } from "../../utils/utils";
|
||||||
|
import { getRectFromEndpoints, isInsideRect } from "../../utils/rect";
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -44,12 +47,18 @@ export default function Canvas() {
|
|||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const { transform, setTransform } = useTransform();
|
const { transform, setTransform } = useTransform();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const {
|
||||||
|
selectedElement,
|
||||||
|
setSelectedElement,
|
||||||
|
bulkSelectedElements,
|
||||||
|
setBulkSelectedElements,
|
||||||
|
} = useSelect();
|
||||||
const [dragging, setDragging] = useState({
|
const [dragging, setDragging] = useState({
|
||||||
element: ObjectType.NONE,
|
element: ObjectType.NONE,
|
||||||
id: -1,
|
id: -1,
|
||||||
prevX: 0,
|
prevX: 0,
|
||||||
prevY: 0,
|
prevY: 0,
|
||||||
|
initialPositions: [],
|
||||||
});
|
});
|
||||||
const [linking, setLinking] = useState(false);
|
const [linking, setLinking] = useState(false);
|
||||||
const [linkingLine, setLinkingLine] = useState({
|
const [linkingLine, setLinkingLine] = useState({
|
||||||
@ -81,10 +90,96 @@ export default function Canvas() {
|
|||||||
pointerX: 0,
|
pointerX: 0,
|
||||||
pointerY: 0,
|
pointerY: 0,
|
||||||
});
|
});
|
||||||
|
const [bulkSelectRectPts, setBulkSelectRectPts] = useState({
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 0,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const collectSelectedElements = () => {
|
||||||
|
const rect = getRectFromEndpoints(bulkSelectRectPts);
|
||||||
|
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
tables.forEach((table) => {
|
||||||
|
if (
|
||||||
|
isInsideRect(
|
||||||
|
{
|
||||||
|
x: table.x,
|
||||||
|
y: table.y,
|
||||||
|
width: settings.tableWidth,
|
||||||
|
height:
|
||||||
|
table.fields.length * tableFieldHeight + tableHeaderHeight + 7,
|
||||||
|
},
|
||||||
|
rect,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
elements.push({
|
||||||
|
id: table.id,
|
||||||
|
type: ObjectType.TABLE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
areas.forEach((area) => {
|
||||||
|
if (
|
||||||
|
isInsideRect(
|
||||||
|
{
|
||||||
|
x: area.x,
|
||||||
|
y: area.y,
|
||||||
|
width: area.width,
|
||||||
|
height: area.height,
|
||||||
|
},
|
||||||
|
rect,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
elements.push({
|
||||||
|
id: area.id,
|
||||||
|
type: ObjectType.AREA,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notes.forEach((note) => {
|
||||||
|
if (
|
||||||
|
isInsideRect(
|
||||||
|
{
|
||||||
|
x: note.x,
|
||||||
|
y: note.y,
|
||||||
|
width: 180,
|
||||||
|
height: note.height,
|
||||||
|
},
|
||||||
|
rect,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
elements.push({
|
||||||
|
id: note.id,
|
||||||
|
type: ObjectType.NOTE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setBulkSelectedElements(elements);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getElement = (element) => {
|
||||||
|
switch (element.type) {
|
||||||
|
case ObjectType.TABLE:
|
||||||
|
return tables[element.id];
|
||||||
|
case ObjectType.AREA:
|
||||||
|
return areas[element.id];
|
||||||
|
case ObjectType.NOTE:
|
||||||
|
return notes[element.id];
|
||||||
|
default:
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PointerEvent} e
|
* @param {PointerEvent} e
|
||||||
* @param {*} id
|
* @param {number} id
|
||||||
* @param {ObjectType[keyof ObjectType]} type
|
* @param {ObjectType[keyof ObjectType]} type
|
||||||
*/
|
*/
|
||||||
const handlePointerDownOnElement = (e, id, type) => {
|
const handlePointerDownOnElement = (e, id, type) => {
|
||||||
@ -98,36 +193,52 @@ export default function Canvas() {
|
|||||||
x: table.x - pointer.spaces.diagram.x,
|
x: table.x - pointer.spaces.diagram.x,
|
||||||
y: table.y - pointer.spaces.diagram.y,
|
y: table.y - pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging((prev) => ({
|
||||||
|
...prev,
|
||||||
|
id,
|
||||||
element: type,
|
element: type,
|
||||||
id: id,
|
|
||||||
prevX: table.x,
|
prevX: table.x,
|
||||||
prevY: table.y,
|
prevY: table.y,
|
||||||
});
|
}));
|
||||||
} else if (type === ObjectType.AREA) {
|
} else if (type === ObjectType.AREA) {
|
||||||
const area = areas.find((t) => t.id === id);
|
const area = areas.find((t) => t.id === id);
|
||||||
setGrabOffset({
|
setGrabOffset({
|
||||||
x: area.x - pointer.spaces.diagram.x,
|
x: area.x - pointer.spaces.diagram.x,
|
||||||
y: area.y - pointer.spaces.diagram.y,
|
y: area.y - pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging((prev) => ({
|
||||||
|
...prev,
|
||||||
|
id,
|
||||||
element: type,
|
element: type,
|
||||||
id: id,
|
|
||||||
prevX: area.x,
|
prevX: area.x,
|
||||||
prevY: area.y,
|
prevY: area.y,
|
||||||
});
|
}));
|
||||||
} else if (type === ObjectType.NOTE) {
|
} else if (type === ObjectType.NOTE) {
|
||||||
const note = notes.find((t) => t.id === id);
|
const note = notes.find((t) => t.id === id);
|
||||||
setGrabOffset({
|
setGrabOffset({
|
||||||
x: note.x - pointer.spaces.diagram.x,
|
x: note.x - pointer.spaces.diagram.x,
|
||||||
y: note.y - pointer.spaces.diagram.y,
|
y: note.y - pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging((prev) => ({
|
||||||
|
...prev,
|
||||||
|
id,
|
||||||
element: type,
|
element: type,
|
||||||
id: id,
|
|
||||||
prevX: note.x,
|
prevX: note.x,
|
||||||
prevY: note.y,
|
prevY: note.y,
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkSelectedElements.length) {
|
||||||
|
setDragging((prev) => ({
|
||||||
|
...prev,
|
||||||
|
initialPositions: bulkSelectedElements.map((element) => ({
|
||||||
|
...element,
|
||||||
|
undo: {
|
||||||
|
x: getElement(element).x,
|
||||||
|
y: getElement(element).y,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
setSelectedElement((prev) => ({
|
setSelectedElement((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -151,6 +262,42 @@ export default function Canvas() {
|
|||||||
endX: pointer.spaces.diagram.x,
|
endX: pointer.spaces.diagram.x,
|
||||||
endY: pointer.spaces.diagram.y,
|
endY: pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
dragging.element !== ObjectType.NONE &&
|
||||||
|
dragging.id >= 0 &&
|
||||||
|
bulkSelectedElements.length
|
||||||
|
) {
|
||||||
|
const currentX = pointer.spaces.diagram.x + grabOffset.x;
|
||||||
|
const currentY = pointer.spaces.diagram.y + grabOffset.y;
|
||||||
|
const deltaX = currentX - dragging.prevX;
|
||||||
|
const deltaY = currentY - dragging.prevY;
|
||||||
|
|
||||||
|
for (const element of bulkSelectedElements) {
|
||||||
|
if (element.type === ObjectType.TABLE) {
|
||||||
|
updateTable(element.id, {
|
||||||
|
x: tables[element.id].x + deltaX,
|
||||||
|
y: tables[element.id].y + deltaY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (element.type === ObjectType.AREA) {
|
||||||
|
updateArea(element.id, {
|
||||||
|
x: areas[element.id].x + deltaX,
|
||||||
|
y: areas[element.id].y + deltaY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (element.type === ObjectType.NOTE) {
|
||||||
|
updateNote(element.id, {
|
||||||
|
x: notes[element.id].x + deltaX,
|
||||||
|
y: notes[element.id].y + deltaY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragging((prev) => ({
|
||||||
|
...prev,
|
||||||
|
prevX: currentX,
|
||||||
|
prevY: currentY,
|
||||||
|
}));
|
||||||
} else if (
|
} else if (
|
||||||
panning.isPanning &&
|
panning.isPanning &&
|
||||||
dragging.element === ObjectType.NONE &&
|
dragging.element === ObjectType.NONE &&
|
||||||
@ -224,6 +371,12 @@ export default function Canvas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateArea(areaResize.id, { ...newDims });
|
updateArea(areaResize.id, { ...newDims });
|
||||||
|
} else if (bulkSelectRectPts.show) {
|
||||||
|
setBulkSelectRectPts((prev) => ({
|
||||||
|
...prev,
|
||||||
|
x2: pointer.spaces.diagram.x,
|
||||||
|
y2: pointer.spaces.diagram.y,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -243,36 +396,39 @@ export default function Canvas() {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
setPanning({
|
if (!settings.panning) {
|
||||||
isPanning: true,
|
setBulkSelectRectPts({
|
||||||
panStart: transform.pan,
|
x1: pointer.spaces.diagram.x,
|
||||||
// Diagram space depends on the current panning.
|
y1: pointer.spaces.diagram.y,
|
||||||
// Use screen space to avoid circular dependencies and undefined behavior.
|
x2: pointer.spaces.diagram.x,
|
||||||
cursorStart: pointer.spaces.screen,
|
y2: pointer.spaces.diagram.y,
|
||||||
});
|
show: true,
|
||||||
pointer.setStyle("grabbing");
|
});
|
||||||
|
pointer.setStyle("crosshair");
|
||||||
|
} else {
|
||||||
|
setPanning({
|
||||||
|
isPanning: true,
|
||||||
|
panStart: transform.pan,
|
||||||
|
// Diagram space depends on the current panning.
|
||||||
|
// Use screen space to avoid circular dependencies and undefined behavior.
|
||||||
|
cursorStart: pointer.spaces.screen,
|
||||||
|
});
|
||||||
|
pointer.setStyle("grabbing");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const coordsDidUpdate = (element) => {
|
const coordsDidUpdate = (element) => {
|
||||||
switch (element) {
|
const elementData = getElement(element);
|
||||||
case ObjectType.TABLE:
|
const updated = !(
|
||||||
return !(
|
dragging.prevX === elementData.x && dragging.prevY === elementData.y
|
||||||
dragging.prevX === tables[dragging.id].x &&
|
);
|
||||||
dragging.prevY === tables[dragging.id].y
|
|
||||||
);
|
return (
|
||||||
case ObjectType.AREA:
|
updated ||
|
||||||
return !(
|
dragging.initialPositions.some(
|
||||||
dragging.prevX === areas[dragging.id].x &&
|
(el) => !(el.undo.x === elementData.x && el.undo.y === elementData.y),
|
||||||
dragging.prevY === areas[dragging.id].y
|
)
|
||||||
);
|
);
|
||||||
case ObjectType.NOTE:
|
|
||||||
return !(
|
|
||||||
dragging.prevX === notes[dragging.id].x &&
|
|
||||||
dragging.prevY === notes[dragging.id].y
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const didResize = (id) => {
|
const didResize = (id) => {
|
||||||
@ -287,31 +443,6 @@ export default function Canvas() {
|
|||||||
const didPan = () =>
|
const didPan = () =>
|
||||||
!(transform.pan.x === panning.x && transform.pan.y === panning.y);
|
!(transform.pan.x === panning.x && transform.pan.y === panning.y);
|
||||||
|
|
||||||
const getMovedElementDetails = () => {
|
|
||||||
switch (dragging.element) {
|
|
||||||
case ObjectType.TABLE:
|
|
||||||
return {
|
|
||||||
name: tables[dragging.id].name,
|
|
||||||
x: Math.round(tables[dragging.id].x),
|
|
||||||
y: Math.round(tables[dragging.id].y),
|
|
||||||
};
|
|
||||||
case ObjectType.AREA:
|
|
||||||
return {
|
|
||||||
name: areas[dragging.id].name,
|
|
||||||
x: Math.round(areas[dragging.id].x),
|
|
||||||
y: Math.round(areas[dragging.id].y),
|
|
||||||
};
|
|
||||||
case ObjectType.NOTE:
|
|
||||||
return {
|
|
||||||
name: notes[dragging.id].title,
|
|
||||||
x: Math.round(notes[dragging.id].x),
|
|
||||||
y: Math.round(notes[dragging.id].y),
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PointerEvent} e
|
* @param {PointerEvent} e
|
||||||
*/
|
*/
|
||||||
@ -320,27 +451,74 @@ export default function Canvas() {
|
|||||||
|
|
||||||
if (!e.isPrimary) return;
|
if (!e.isPrimary) return;
|
||||||
|
|
||||||
if (coordsDidUpdate(dragging.element)) {
|
if (coordsDidUpdate({ id: dragging.id, type: dragging.element })) {
|
||||||
const info = getMovedElementDetails();
|
if (bulkSelectedElements.length) {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
action: Action.MOVE,
|
action: Action.MOVE,
|
||||||
element: dragging.element,
|
bulk: true,
|
||||||
x: dragging.prevX,
|
message: t("bulk_update"),
|
||||||
y: dragging.prevY,
|
elements: dragging.initialPositions.map((element) => ({
|
||||||
toX: info.x,
|
...element,
|
||||||
toY: info.y,
|
redo: {
|
||||||
|
x: getElement(element).x,
|
||||||
|
y: getElement(element).y,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setSelectedElement((prev) => ({
|
||||||
|
...prev,
|
||||||
|
element: ObjectType.NONE,
|
||||||
|
id: -1,
|
||||||
|
open: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const element = getElement({
|
||||||
id: dragging.id,
|
id: dragging.id,
|
||||||
message: t("move_element", {
|
type: dragging.element,
|
||||||
coords: `(${info.x}, ${info.y})`,
|
});
|
||||||
name: info.name,
|
setUndoStack((prev) => [
|
||||||
}),
|
...prev,
|
||||||
},
|
{
|
||||||
]);
|
action: Action.MOVE,
|
||||||
|
element: dragging.element,
|
||||||
|
x: dragging.prevX,
|
||||||
|
y: dragging.prevY,
|
||||||
|
toX: element.x,
|
||||||
|
toY: element.y,
|
||||||
|
id: dragging.id,
|
||||||
|
message: t("move_element", {
|
||||||
|
coords: `(${element.x}, ${element.y})`,
|
||||||
|
name: getElement({
|
||||||
|
id: dragging.id,
|
||||||
|
type: dragging.element,
|
||||||
|
}).name,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
setRedoStack([]);
|
setRedoStack([]);
|
||||||
}
|
}
|
||||||
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
|
setDragging({
|
||||||
|
element: ObjectType.NONE,
|
||||||
|
id: -1,
|
||||||
|
prevX: 0,
|
||||||
|
prevY: 0,
|
||||||
|
initialPositions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bulkSelectRectPts.show) {
|
||||||
|
setBulkSelectRectPts((prev) => ({
|
||||||
|
...prev,
|
||||||
|
x2: pointer.spaces.diagram.x,
|
||||||
|
y2: pointer.spaces.diagram.y,
|
||||||
|
show: false,
|
||||||
|
}));
|
||||||
|
collectSelectedElements();
|
||||||
|
}
|
||||||
|
|
||||||
if (panning.isPanning && didPan()) {
|
if (panning.isPanning && didPan()) {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@ -361,11 +539,14 @@ export default function Canvas() {
|
|||||||
id: -1,
|
id: -1,
|
||||||
open: false,
|
open: false,
|
||||||
}));
|
}));
|
||||||
|
setBulkSelectedElements([]);
|
||||||
}
|
}
|
||||||
setPanning((old) => ({ ...old, isPanning: false }));
|
setPanning((old) => ({ ...old, isPanning: false }));
|
||||||
pointer.setStyle("default");
|
pointer.setStyle("default");
|
||||||
|
|
||||||
if (linking) handleLinking();
|
if (linking) handleLinking();
|
||||||
setLinking(false);
|
setLinking(false);
|
||||||
|
|
||||||
if (areaResize.id !== -1 && didResize(areaResize.id)) {
|
if (areaResize.id !== -1 && didResize(areaResize.id)) {
|
||||||
setUndoStack((prev) => [
|
setUndoStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@ -402,7 +583,13 @@ export default function Canvas() {
|
|||||||
|
|
||||||
const handleGripField = () => {
|
const handleGripField = () => {
|
||||||
setPanning((old) => ({ ...old, isPanning: false }));
|
setPanning((old) => ({ ...old, isPanning: false }));
|
||||||
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
|
setDragging({
|
||||||
|
element: ObjectType.NONE,
|
||||||
|
id: -1,
|
||||||
|
prevX: 0,
|
||||||
|
prevY: 0,
|
||||||
|
initialPositions: [],
|
||||||
|
});
|
||||||
setLinking(true);
|
setLinking(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -444,7 +631,6 @@ export default function Canvas() {
|
|||||||
addRelationship(newRelationship);
|
addRelationship(newRelationship);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle mouse wheel scrolling
|
|
||||||
useEventListener(
|
useEventListener(
|
||||||
"wheel",
|
"wheel",
|
||||||
(e) => {
|
(e) => {
|
||||||
@ -520,7 +706,7 @@ export default function Canvas() {
|
|||||||
cy="4"
|
cy="4"
|
||||||
r="0.85"
|
r="0.85"
|
||||||
fill="rgb(99, 152, 191)"
|
fill="rgb(99, 152, 191)"
|
||||||
></circle>
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect
|
<rect
|
||||||
@ -529,7 +715,7 @@ export default function Canvas() {
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
fill="url(#pattern-circles)"
|
fill="url(#pattern-circles)"
|
||||||
></rect>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
<svg
|
<svg
|
||||||
@ -584,6 +770,15 @@ export default function Canvas() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{bulkSelectRectPts.show && (
|
||||||
|
<rect
|
||||||
|
{...getRectFromEndpoints(bulkSelectRectPts)}
|
||||||
|
stroke="grey"
|
||||||
|
fill="grey"
|
||||||
|
fillOpacity={0.15}
|
||||||
|
strokeDasharray={10}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{settings.showDebugCoordinates && (
|
{settings.showDebugCoordinates && (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Action, ObjectType, Tab, State } from "../../data/constants";
|
import { Action, ObjectType, Tab, State } from "../../data/constants";
|
||||||
import { Input, Button, Popover, ColorPicker } from "@douyinfe/semi-ui";
|
import { Input, Button, Popover, ColorPicker } from "@douyinfe/semi-ui";
|
||||||
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
@ -22,7 +22,8 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
const { setSaveState } = useSaveState();
|
const { setSaveState } = useSaveState();
|
||||||
const { updateNote, deleteNote } = useNotes();
|
const { updateNote, deleteNote } = useNotes();
|
||||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement, bulkSelectedElements } =
|
||||||
|
useSelect();
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const textarea = document.getElementById(`note_${data.id}`);
|
const textarea = document.getElementById(`note_${data.id}`);
|
||||||
@ -71,6 +72,16 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSelected = useMemo(() => {
|
||||||
|
return (
|
||||||
|
(selectedElement.id === data.id &&
|
||||||
|
selectedElement.element === ObjectType.NOTE) ||
|
||||||
|
bulkSelectedElements.some(
|
||||||
|
(e) => e.type === ObjectType.NOTE && e.id === data.id,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [selectedElement, data, bulkSelectedElements]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
|
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
|
||||||
@ -95,14 +106,13 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
stroke={
|
stroke={
|
||||||
hovered
|
hovered
|
||||||
? "rgb(59 130 246)"
|
? "rgb(59 130 246)"
|
||||||
: selectedElement.element === ObjectType.NOTE &&
|
: isSelected
|
||||||
selectedElement.id === data.id
|
|
||||||
? "rgb(59 130 246)"
|
? "rgb(59 130 246)"
|
||||||
: "rgb(168 162 158)"
|
: "rgb(168 162 158)"
|
||||||
}
|
}
|
||||||
strokeDasharray={hovered ? 4 : 0}
|
strokeDasharray={hovered ? 5 : 0}
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={`M${data.x} ${data.y + fold} L${data.x + fold - r} ${
|
d={`M${data.x} ${data.y + fold} L${data.x + fold - r} ${
|
||||||
@ -114,14 +124,13 @@ export default function Note({ data, onPointerDown }) {
|
|||||||
stroke={
|
stroke={
|
||||||
hovered
|
hovered
|
||||||
? "rgb(59 130 246)"
|
? "rgb(59 130 246)"
|
||||||
: selectedElement.element === ObjectType.NOTE &&
|
: isSelected
|
||||||
selectedElement.id === data.id
|
|
||||||
? "rgb(59 130 246)"
|
? "rgb(59 130 246)"
|
||||||
: "rgb(168 162 158)"
|
: "rgb(168 162 158)"
|
||||||
}
|
}
|
||||||
strokeDasharray={hovered ? 4 : 0}
|
strokeDasharray={hovered ? 5 : 0}
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="1.2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<foreignObject
|
<foreignObject
|
||||||
x={data.x}
|
x={data.x}
|
||||||
|
@ -35,7 +35,8 @@ export default function Table(props) {
|
|||||||
const { deleteTable, deleteField } = useDiagram();
|
const { deleteTable, deleteField } = useDiagram();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement, bulkSelectedElements } =
|
||||||
|
useSelect();
|
||||||
|
|
||||||
const borderColor = useMemo(
|
const borderColor = useMemo(
|
||||||
() => (settings.mode === "light" ? "border-zinc-300" : "border-zinc-600"),
|
() => (settings.mode === "light" ? "border-zinc-300" : "border-zinc-600"),
|
||||||
@ -44,6 +45,15 @@ export default function Table(props) {
|
|||||||
|
|
||||||
const height =
|
const height =
|
||||||
tableData.fields.length * tableFieldHeight + tableHeaderHeight + 7;
|
tableData.fields.length * tableFieldHeight + tableHeaderHeight + 7;
|
||||||
|
const isSelected = useMemo(() => {
|
||||||
|
return (
|
||||||
|
(selectedElement.id === tableData.id &&
|
||||||
|
selectedElement.element === ObjectType.TABLE) ||
|
||||||
|
bulkSelectedElements.some(
|
||||||
|
(e) => e.type === ObjectType.TABLE && e.id === tableData.id,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [selectedElement, tableData, bulkSelectedElements]);
|
||||||
|
|
||||||
const openEditor = () => {
|
const openEditor = () => {
|
||||||
if (!layout.sidebar) {
|
if (!layout.sidebar) {
|
||||||
@ -86,12 +96,7 @@ export default function Table(props) {
|
|||||||
settings.mode === "light"
|
settings.mode === "light"
|
||||||
? "bg-zinc-100 text-zinc-800"
|
? "bg-zinc-100 text-zinc-800"
|
||||||
: "bg-zinc-800 text-zinc-200"
|
: "bg-zinc-800 text-zinc-200"
|
||||||
} ${
|
} ${isSelected ? "border-solid border-blue-500" : borderColor}`}
|
||||||
selectedElement.id === tableData.id &&
|
|
||||||
selectedElement.element === ObjectType.TABLE
|
|
||||||
? "border-solid border-blue-500"
|
|
||||||
: borderColor
|
|
||||||
}`}
|
|
||||||
style={{ direction: "ltr" }}
|
style={{ direction: "ltr" }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -131,6 +131,22 @@ export default function ControlPanel({
|
|||||||
if (undoStack.length === 0) return;
|
if (undoStack.length === 0) return;
|
||||||
const a = undoStack[undoStack.length - 1];
|
const a = undoStack[undoStack.length - 1];
|
||||||
setUndoStack((prev) => prev.filter((_, i) => i !== prev.length - 1));
|
setUndoStack((prev) => prev.filter((_, i) => i !== prev.length - 1));
|
||||||
|
|
||||||
|
if (a.bulk) {
|
||||||
|
for (const element of a.elements) {
|
||||||
|
if (element.type === ObjectType.TABLE) {
|
||||||
|
updateTable(element.id, element.undo);
|
||||||
|
} else if (element.type === ObjectType.AREA) {
|
||||||
|
updateArea(element.id, element.undo);
|
||||||
|
} else if (element.type === ObjectType.NOTE) {
|
||||||
|
updateNote(element.id, element.undo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRedoStack((prev) => [...prev, a]);
|
||||||
|
console.log(a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (a.action === Action.ADD) {
|
if (a.action === Action.ADD) {
|
||||||
if (a.element === ObjectType.TABLE) {
|
if (a.element === ObjectType.TABLE) {
|
||||||
deleteTable(tables[tables.length - 1].id, false);
|
deleteTable(tables[tables.length - 1].id, false);
|
||||||
@ -341,6 +357,21 @@ export default function ControlPanel({
|
|||||||
if (redoStack.length === 0) return;
|
if (redoStack.length === 0) return;
|
||||||
const a = redoStack[redoStack.length - 1];
|
const a = redoStack[redoStack.length - 1];
|
||||||
setRedoStack((prev) => prev.filter((e, i) => i !== prev.length - 1));
|
setRedoStack((prev) => prev.filter((e, i) => i !== prev.length - 1));
|
||||||
|
|
||||||
|
if (a.bulk) {
|
||||||
|
for (const element of a.elements) {
|
||||||
|
if (element.type === ObjectType.TABLE) {
|
||||||
|
updateTable(element.id, element.redo);
|
||||||
|
} else if (element.type === ObjectType.AREA) {
|
||||||
|
updateArea(element.id, element.redo);
|
||||||
|
} else if (element.type === ObjectType.NOTE) {
|
||||||
|
updateNote(element.id, element.redo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUndoStack((prev) => [...prev, a]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (a.action === Action.ADD) {
|
if (a.action === Action.ADD) {
|
||||||
if (a.element === ObjectType.TABLE) {
|
if (a.element === ObjectType.TABLE) {
|
||||||
addTable(null, false);
|
addTable(null, false);
|
||||||
@ -1579,6 +1610,23 @@ export default function ControlPanel({
|
|||||||
<i className="fa-solid fa-magnifying-glass-minus" />
|
<i className="fa-solid fa-magnifying-glass-minus" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={settings.panning ? t("multiselect") : t("panning")}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="py-1 px-2 hover-2 rounded-sm text-lg w-10"
|
||||||
|
onClick={() =>
|
||||||
|
setSettings((prev) => ({ ...prev, panning: !prev.panning }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{settings.panning ? (
|
||||||
|
<i className="fa-solid fa-expand" />
|
||||||
|
) : (
|
||||||
|
<i className="fa-regular fa-hand"></i>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
<Divider layout="vertical" margin="8px" />
|
<Divider layout="vertical" margin="8px" />
|
||||||
<Tooltip content={t("undo")} position="bottom">
|
<Tooltip content={t("undo")} position="bottom">
|
||||||
<button
|
<button
|
||||||
|
@ -13,9 +13,17 @@ export default function SelectContextProvider({ children }) {
|
|||||||
open: false, // open popover or sidesheet when sidebar is disabled
|
open: false, // open popover or sidesheet when sidebar is disabled
|
||||||
openFromToolbar: false, // this is to handle triggering onClickOutside when sidebar is disabled
|
openFromToolbar: false, // this is to handle triggering onClickOutside when sidebar is disabled
|
||||||
});
|
});
|
||||||
|
const [bulkSelectedElements, setBulkSelectedElements] = useState([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectContext.Provider value={{ selectedElement, setSelectedElement }}>
|
<SelectContext.Provider
|
||||||
|
value={{
|
||||||
|
selectedElement,
|
||||||
|
setSelectedElement,
|
||||||
|
bulkSelectedElements,
|
||||||
|
setBulkSelectedElements,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</SelectContext.Provider>
|
</SelectContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -247,6 +247,8 @@ const en = {
|
|||||||
show_relationship_labels: "Show relationship labels",
|
show_relationship_labels: "Show relationship labels",
|
||||||
docs: "Docs",
|
docs: "Docs",
|
||||||
supported_types: "Supported file types:",
|
supported_types: "Supported file types:",
|
||||||
|
bulk_update: "Bulk update",
|
||||||
|
multiselect: "Multiselect",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
18
src/utils/rect.js
Normal file
18
src/utils/rect.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export function getRectFromEndpoints({ x1, x2, y1, y2 }) {
|
||||||
|
const width = Math.abs(x1 - x2);
|
||||||
|
const height = Math.abs(y1 - y2);
|
||||||
|
|
||||||
|
const x = Math.min(x1, x2);
|
||||||
|
const y = Math.min(y1, y2);
|
||||||
|
|
||||||
|
return { x, y, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInsideRect(rect1, rect2) {
|
||||||
|
return (
|
||||||
|
rect1.x > rect2.x &&
|
||||||
|
rect1.x + rect1.width < rect2.x + rect2.width &&
|
||||||
|
rect1.y > rect2.y &&
|
||||||
|
rect1.y + rect1.height < rect2.y + rect2.height
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user