mirror of
https://github.com/drawdb-io/drawdb.git
synced 2025-05-24 10:29:11 +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 { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||
import { Tab, Action, ObjectType, State } from "../../data/constants";
|
||||
@ -30,7 +30,8 @@ export default function Area({
|
||||
const { layout } = useLayout();
|
||||
const { settings } = useSettings();
|
||||
const { setSaveState } = useSaveState();
|
||||
const { selectedElement, setSelectedElement } = useSelect();
|
||||
const { selectedElement, setSelectedElement, bulkSelectedElements } =
|
||||
useSelect();
|
||||
|
||||
const handleResize = (e, dir) => {
|
||||
setResize({ id: data.id, dir: dir });
|
||||
@ -82,11 +83,21 @@ export default function Area({
|
||||
setSaveState(State.SAVING);
|
||||
};
|
||||
|
||||
const areaIsSelected = () =>
|
||||
const areaIsOpen = () =>
|
||||
selectedElement.element === ObjectType.AREA &&
|
||||
selectedElement.id === data.id &&
|
||||
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 (
|
||||
<g ref={ref}>
|
||||
<foreignObject
|
||||
@ -101,8 +112,7 @@ export default function Area({
|
||||
className={`w-full h-full p-2 rounded cursor-move border-2 ${
|
||||
isHovered
|
||||
? "border-dashed border-blue-500"
|
||||
: selectedElement.element === ObjectType.AREA &&
|
||||
selectedElement.id === data.id
|
||||
: isSelected
|
||||
? "border-blue-500 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">
|
||||
{data.name}
|
||||
</div>
|
||||
{(isHovered || (areaIsSelected() && !layout.sidebar)) && (
|
||||
{(isHovered || (areaIsOpen() && !layout.sidebar)) && (
|
||||
<Popover
|
||||
visible={areaIsSelected() && !layout.sidebar}
|
||||
visible={areaIsOpen() && !layout.sidebar}
|
||||
onClickOutSide={onClickOutSide}
|
||||
stopPropagation
|
||||
content={<EditPopoverContent data={data} />}
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
Constraint,
|
||||
darkBgTheme,
|
||||
ObjectType,
|
||||
tableFieldHeight,
|
||||
tableHeaderHeight,
|
||||
} from "../../data/constants";
|
||||
import { Toast } from "@douyinfe/semi-ui";
|
||||
import Table from "./Table";
|
||||
@ -25,6 +27,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import { areFieldsCompatible } from "../../utils/utils";
|
||||
import { getRectFromEndpoints, isInsideRect } from "../../utils/rect";
|
||||
|
||||
export default function Canvas() {
|
||||
const { t } = useTranslation();
|
||||
@ -44,12 +47,18 @@ export default function Canvas() {
|
||||
const { settings } = useSettings();
|
||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||
const { transform, setTransform } = useTransform();
|
||||
const { selectedElement, setSelectedElement } = useSelect();
|
||||
const {
|
||||
selectedElement,
|
||||
setSelectedElement,
|
||||
bulkSelectedElements,
|
||||
setBulkSelectedElements,
|
||||
} = useSelect();
|
||||
const [dragging, setDragging] = useState({
|
||||
element: ObjectType.NONE,
|
||||
id: -1,
|
||||
prevX: 0,
|
||||
prevY: 0,
|
||||
initialPositions: [],
|
||||
});
|
||||
const [linking, setLinking] = useState(false);
|
||||
const [linkingLine, setLinkingLine] = useState({
|
||||
@ -81,10 +90,96 @@ export default function Canvas() {
|
||||
pointerX: 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 {*} id
|
||||
* @param {number} id
|
||||
* @param {ObjectType[keyof ObjectType]} type
|
||||
*/
|
||||
const handlePointerDownOnElement = (e, id, type) => {
|
||||
@ -98,36 +193,52 @@ export default function Canvas() {
|
||||
x: table.x - pointer.spaces.diagram.x,
|
||||
y: table.y - pointer.spaces.diagram.y,
|
||||
});
|
||||
setDragging({
|
||||
setDragging((prev) => ({
|
||||
...prev,
|
||||
id,
|
||||
element: type,
|
||||
id: id,
|
||||
prevX: table.x,
|
||||
prevY: table.y,
|
||||
});
|
||||
}));
|
||||
} else if (type === ObjectType.AREA) {
|
||||
const area = areas.find((t) => t.id === id);
|
||||
setGrabOffset({
|
||||
x: area.x - pointer.spaces.diagram.x,
|
||||
y: area.y - pointer.spaces.diagram.y,
|
||||
});
|
||||
setDragging({
|
||||
setDragging((prev) => ({
|
||||
...prev,
|
||||
id,
|
||||
element: type,
|
||||
id: id,
|
||||
prevX: area.x,
|
||||
prevY: area.y,
|
||||
});
|
||||
}));
|
||||
} else if (type === ObjectType.NOTE) {
|
||||
const note = notes.find((t) => t.id === id);
|
||||
setGrabOffset({
|
||||
x: note.x - pointer.spaces.diagram.x,
|
||||
y: note.y - pointer.spaces.diagram.y,
|
||||
});
|
||||
setDragging({
|
||||
setDragging((prev) => ({
|
||||
...prev,
|
||||
id,
|
||||
element: type,
|
||||
id: id,
|
||||
prevX: note.x,
|
||||
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) => ({
|
||||
...prev,
|
||||
@ -151,6 +262,42 @@ export default function Canvas() {
|
||||
endX: pointer.spaces.diagram.x,
|
||||
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 (
|
||||
panning.isPanning &&
|
||||
dragging.element === ObjectType.NONE &&
|
||||
@ -224,6 +371,12 @@ export default function Canvas() {
|
||||
}
|
||||
|
||||
updateArea(areaResize.id, { ...newDims });
|
||||
} else if (bulkSelectRectPts.show) {
|
||||
setBulkSelectRectPts((prev) => ({
|
||||
...prev,
|
||||
x2: pointer.spaces.diagram.x,
|
||||
y2: pointer.spaces.diagram.y,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@ -243,6 +396,16 @@ export default function Canvas() {
|
||||
)
|
||||
return;
|
||||
|
||||
if (!settings.panning) {
|
||||
setBulkSelectRectPts({
|
||||
x1: pointer.spaces.diagram.x,
|
||||
y1: pointer.spaces.diagram.y,
|
||||
x2: pointer.spaces.diagram.x,
|
||||
y2: pointer.spaces.diagram.y,
|
||||
show: true,
|
||||
});
|
||||
pointer.setStyle("crosshair");
|
||||
} else {
|
||||
setPanning({
|
||||
isPanning: true,
|
||||
panStart: transform.pan,
|
||||
@ -251,28 +414,21 @@ export default function Canvas() {
|
||||
cursorStart: pointer.spaces.screen,
|
||||
});
|
||||
pointer.setStyle("grabbing");
|
||||
}
|
||||
};
|
||||
|
||||
const coordsDidUpdate = (element) => {
|
||||
switch (element) {
|
||||
case ObjectType.TABLE:
|
||||
return !(
|
||||
dragging.prevX === tables[dragging.id].x &&
|
||||
dragging.prevY === tables[dragging.id].y
|
||||
const elementData = getElement(element);
|
||||
const updated = !(
|
||||
dragging.prevX === elementData.x && dragging.prevY === elementData.y
|
||||
);
|
||||
case ObjectType.AREA:
|
||||
return !(
|
||||
dragging.prevX === areas[dragging.id].x &&
|
||||
dragging.prevY === areas[dragging.id].y
|
||||
|
||||
return (
|
||||
updated ||
|
||||
dragging.initialPositions.some(
|
||||
(el) => !(el.undo.x === elementData.x && el.undo.y === elementData.y),
|
||||
)
|
||||
);
|
||||
case ObjectType.NOTE:
|
||||
return !(
|
||||
dragging.prevX === notes[dragging.id].x &&
|
||||
dragging.prevY === notes[dragging.id].y
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const didResize = (id) => {
|
||||
@ -287,31 +443,6 @@ export default function Canvas() {
|
||||
const didPan = () =>
|
||||
!(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
|
||||
*/
|
||||
@ -320,8 +451,34 @@ export default function Canvas() {
|
||||
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
if (coordsDidUpdate(dragging.element)) {
|
||||
const info = getMovedElementDetails();
|
||||
if (coordsDidUpdate({ id: dragging.id, type: dragging.element })) {
|
||||
if (bulkSelectedElements.length) {
|
||||
setUndoStack((prev) => [
|
||||
...prev,
|
||||
{
|
||||
action: Action.MOVE,
|
||||
bulk: true,
|
||||
message: t("bulk_update"),
|
||||
elements: dragging.initialPositions.map((element) => ({
|
||||
...element,
|
||||
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,
|
||||
type: dragging.element,
|
||||
});
|
||||
setUndoStack((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@ -329,18 +486,39 @@ export default function Canvas() {
|
||||
element: dragging.element,
|
||||
x: dragging.prevX,
|
||||
y: dragging.prevY,
|
||||
toX: info.x,
|
||||
toY: info.y,
|
||||
toX: element.x,
|
||||
toY: element.y,
|
||||
id: dragging.id,
|
||||
message: t("move_element", {
|
||||
coords: `(${info.x}, ${info.y})`,
|
||||
name: info.name,
|
||||
coords: `(${element.x}, ${element.y})`,
|
||||
name: getElement({
|
||||
id: dragging.id,
|
||||
type: dragging.element,
|
||||
}).name,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
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()) {
|
||||
setUndoStack((prev) => [
|
||||
...prev,
|
||||
@ -361,11 +539,14 @@ export default function Canvas() {
|
||||
id: -1,
|
||||
open: false,
|
||||
}));
|
||||
setBulkSelectedElements([]);
|
||||
}
|
||||
setPanning((old) => ({ ...old, isPanning: false }));
|
||||
pointer.setStyle("default");
|
||||
|
||||
if (linking) handleLinking();
|
||||
setLinking(false);
|
||||
|
||||
if (areaResize.id !== -1 && didResize(areaResize.id)) {
|
||||
setUndoStack((prev) => [
|
||||
...prev,
|
||||
@ -402,7 +583,13 @@ export default function Canvas() {
|
||||
|
||||
const handleGripField = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
@ -444,7 +631,6 @@ export default function Canvas() {
|
||||
addRelationship(newRelationship);
|
||||
};
|
||||
|
||||
// Handle mouse wheel scrolling
|
||||
useEventListener(
|
||||
"wheel",
|
||||
(e) => {
|
||||
@ -520,7 +706,7 @@ export default function Canvas() {
|
||||
cy="4"
|
||||
r="0.85"
|
||||
fill="rgb(99, 152, 191)"
|
||||
></circle>
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
@ -529,7 +715,7 @@ export default function Canvas() {
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="url(#pattern-circles)"
|
||||
></rect>
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
{settings.showDebugCoordinates && (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Action, ObjectType, Tab, State } from "../../data/constants";
|
||||
import { Input, Button, Popover, ColorPicker } from "@douyinfe/semi-ui";
|
||||
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||
@ -22,7 +22,8 @@ export default function Note({ data, onPointerDown }) {
|
||||
const { setSaveState } = useSaveState();
|
||||
const { updateNote, deleteNote } = useNotes();
|
||||
const { setUndoStack, setRedoStack } = useUndoRedo();
|
||||
const { selectedElement, setSelectedElement } = useSelect();
|
||||
const { selectedElement, setSelectedElement, bulkSelectedElements } =
|
||||
useSelect();
|
||||
|
||||
const handleChange = (e) => {
|
||||
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 (
|
||||
<g
|
||||
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
|
||||
@ -95,14 +106,13 @@ export default function Note({ data, onPointerDown }) {
|
||||
stroke={
|
||||
hovered
|
||||
? "rgb(59 130 246)"
|
||||
: selectedElement.element === ObjectType.NOTE &&
|
||||
selectedElement.id === data.id
|
||||
: isSelected
|
||||
? "rgb(59 130 246)"
|
||||
: "rgb(168 162 158)"
|
||||
}
|
||||
strokeDasharray={hovered ? 4 : 0}
|
||||
strokeDasharray={hovered ? 5 : 0}
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.2"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d={`M${data.x} ${data.y + fold} L${data.x + fold - r} ${
|
||||
@ -114,14 +124,13 @@ export default function Note({ data, onPointerDown }) {
|
||||
stroke={
|
||||
hovered
|
||||
? "rgb(59 130 246)"
|
||||
: selectedElement.element === ObjectType.NOTE &&
|
||||
selectedElement.id === data.id
|
||||
: isSelected
|
||||
? "rgb(59 130 246)"
|
||||
: "rgb(168 162 158)"
|
||||
}
|
||||
strokeDasharray={hovered ? 4 : 0}
|
||||
strokeDasharray={hovered ? 5 : 0}
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.2"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<foreignObject
|
||||
x={data.x}
|
||||
|
@ -35,7 +35,8 @@ export default function Table(props) {
|
||||
const { deleteTable, deleteField } = useDiagram();
|
||||
const { settings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
const { selectedElement, setSelectedElement } = useSelect();
|
||||
const { selectedElement, setSelectedElement, bulkSelectedElements } =
|
||||
useSelect();
|
||||
|
||||
const borderColor = useMemo(
|
||||
() => (settings.mode === "light" ? "border-zinc-300" : "border-zinc-600"),
|
||||
@ -44,6 +45,15 @@ export default function Table(props) {
|
||||
|
||||
const height =
|
||||
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 = () => {
|
||||
if (!layout.sidebar) {
|
||||
@ -86,12 +96,7 @@ export default function Table(props) {
|
||||
settings.mode === "light"
|
||||
? "bg-zinc-100 text-zinc-800"
|
||||
: "bg-zinc-800 text-zinc-200"
|
||||
} ${
|
||||
selectedElement.id === tableData.id &&
|
||||
selectedElement.element === ObjectType.TABLE
|
||||
? "border-solid border-blue-500"
|
||||
: borderColor
|
||||
}`}
|
||||
} ${isSelected ? "border-solid border-blue-500" : borderColor}`}
|
||||
style={{ direction: "ltr" }}
|
||||
>
|
||||
<div
|
||||
|
@ -131,6 +131,22 @@ export default function ControlPanel({
|
||||
if (undoStack.length === 0) return;
|
||||
const a = undoStack[undoStack.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.element === ObjectType.TABLE) {
|
||||
deleteTable(tables[tables.length - 1].id, false);
|
||||
@ -341,6 +357,21 @@ export default function ControlPanel({
|
||||
if (redoStack.length === 0) return;
|
||||
const a = redoStack[redoStack.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.element === ObjectType.TABLE) {
|
||||
addTable(null, false);
|
||||
@ -1579,6 +1610,23 @@ export default function ControlPanel({
|
||||
<i className="fa-solid fa-magnifying-glass-minus" />
|
||||
</button>
|
||||
</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" />
|
||||
<Tooltip content={t("undo")} position="bottom">
|
||||
<button
|
||||
|
@ -13,9 +13,17 @@ export default function SelectContextProvider({ children }) {
|
||||
open: false, // open popover or sidesheet when sidebar is disabled
|
||||
openFromToolbar: false, // this is to handle triggering onClickOutside when sidebar is disabled
|
||||
});
|
||||
const [bulkSelectedElements, setBulkSelectedElements] = useState([]);
|
||||
|
||||
return (
|
||||
<SelectContext.Provider value={{ selectedElement, setSelectedElement }}>
|
||||
<SelectContext.Provider
|
||||
value={{
|
||||
selectedElement,
|
||||
setSelectedElement,
|
||||
bulkSelectedElements,
|
||||
setBulkSelectedElements,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
|
@ -247,6 +247,8 @@ const en = {
|
||||
show_relationship_labels: "Show relationship labels",
|
||||
docs: "Docs",
|
||||
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