Skip to content

Commit a79ed65

Browse files
committed
Feat: added range selection
1 parent 589410e commit a79ed65

File tree

9 files changed

+144
-8
lines changed

9 files changed

+144
-8
lines changed

src/components/core/animated-switcher.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import { useEffect, useState } from "react";
22
import { twMerge } from "tailwind-merge";
33

44
interface AnimatedSwitcherProps {
5-
key?: string;
65
component: React.ReactNode;
76
className?: string;
87
style?: React.CSSProperties;
98
duration?: number;
109
}
1110

12-
const AnimatedSwitcher = ({ key, component, className, style, duration = 0.3 }: AnimatedSwitcherProps) => {
11+
const AnimatedSwitcher = ({ component, className, style, duration = 0.3 }: AnimatedSwitcherProps) => {
1312
const [opacity, setOpacity] = useState(0);
1413
const [currentComponent, setCurrentComponent] = useState(component);
1514

@@ -19,11 +18,10 @@ const AnimatedSwitcher = ({ key, component, className, style, duration = 0.3 }:
1918
setCurrentComponent(component);
2019
setOpacity(1);
2120
}, (duration / 2) * 1000);
22-
}, [key]);
21+
}, []);
2322

2423
return (
2524
<div
26-
key={key}
2725
className={twMerge("w-full h-full", className)}
2826
style={{ ...style, transitionDuration: `${duration / 2}s`, opacity }}
2927
>

src/components/workspace/elements/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export const Element: React.FC<IElementProps> = ({
131131
consumer={consumer}
132132
isSelected={isSelected}
133133
{...{ [dataAttributes.elementType]: type }}
134+
{...{ [dataAttributes.element]: "true" }}
134135
/>
135136
);
136137
};

src/components/workspace/zoom.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const Zoom = (props: Pick<ISTKProps, "mode" | "styles" | "options">) => {
8181
if (selectedTool == Tool.Pan) {
8282
selection.call(zoom);
8383
} else {
84-
selection.call(zoom).on("wheel.zoom", (e) => {
84+
const zoomSelection = selection.call(zoom).on("wheel.zoom", (e) => {
8585
e.preventDefault();
8686
const currentZoom = selection.property("__zoom").k || 1;
8787
if (e.ctrlKey) {
@@ -91,6 +91,7 @@ const Zoom = (props: Pick<ISTKProps, "mode" | "styles" | "options">) => {
9191
zoom.translateBy(selection, -(e.deltaX / currentZoom), -(e.deltaY / currentZoom));
9292
}
9393
});
94+
if (props.mode !== "user") zoomSelection.on("mousedown.zoom", null);
9495
}
9596
}, [selectedTool]);
9697

src/constants/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const ids = {
1212
toolbar: "stk-tool-bar",
1313
workspace: "stk-workspace",
1414
workspaceContainer: "stk-workspace-container",
15+
workspaceSelection: "stk-workspace-selection",
1516
zoomControls: "stk-zoom-controls",
1617
panControls: "stk-pan-controls",
1718
visibilityControls: "stk-visibility-controls",
@@ -24,6 +25,7 @@ export const selectors = {
2425
};
2526

2627
export const dataAttributes = {
28+
element: "data-stk-element",
2729
elementType: "data-element-type",
2830
shape: "data-shape",
2931
category: "data-category",

src/hooks/events/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { default as useDuplicate } from "./duplication";
55
import { default as useMove } from "./move";
66
import { default as usePolyline } from "./polyline";
77
import { default as useSeatSelectionChange } from "./seat-selection";
8+
import { default as useSelection } from "./selection";
89
import { default as useWorkspaceClick } from "./workspace-click";
910
import { default as useWorkspaceLoad } from "./workspace-load";
1011

@@ -14,6 +15,7 @@ export const useDesignerEvents = (props: ISTKProps) => {
1415
useDuplicate();
1516
useMove();
1617
usePolyline();
18+
useSelection();
1719
useWorkspaceClick();
1820
useWorkspaceLoad(props);
1921
};

src/hooks/events/selection.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { useLayoutEffect } from "react";
2+
import { ElementType } from "@/components/workspace/elements";
3+
import { dataAttributes, ids, selectors } from "@/constants";
4+
import { default as store } from "@/store";
5+
import { clearAndSelectElements } from "@/store/reducers/editor";
6+
import { coordsWithTransform, d3Extended } from "@/utils";
7+
8+
const useSelection = () => {
9+
useLayoutEffect(() => {
10+
const svg = d3Extended.selectById(ids.workspace);
11+
if (svg.node()) {
12+
const toolbarWidth = document.getElementById(ids.toolbar)?.clientWidth ?? 0;
13+
const operationBarHeight = document.getElementById(ids.operationBar)?.clientHeight ?? 0;
14+
const selectionRect = {
15+
element: null,
16+
currentY: 0,
17+
currentX: 0,
18+
originX: 0,
19+
originY: 0,
20+
setElement: function (ele) {
21+
this.element = ele;
22+
},
23+
getNewAttributes: function () {
24+
const x = this.currentX < this.originX ? this.currentX : this.originX;
25+
const y = this.currentY < this.originY ? this.currentY : this.originY;
26+
const width = Math.abs(this.currentX - this.originX);
27+
const height = Math.abs(this.currentY - this.originY);
28+
return {
29+
x: x,
30+
y: y,
31+
width: width,
32+
height: height
33+
};
34+
},
35+
getCurrentAttributes: function () {
36+
const transform = d3Extended.zoomTransform(document.querySelector(selectors.workspaceGroup));
37+
const { x, y } = coordsWithTransform({ x: +this.element.attr("x"), y: +this.element.attr("y") }, transform);
38+
return {
39+
x1: x,
40+
y1: y,
41+
x2: x + Number(this.element.attr("width")) / transform.k,
42+
y2: y + Number(this.element.attr("height")) / transform.k
43+
};
44+
},
45+
init: function (newX, newY) {
46+
const rectElement = svg
47+
.append("rect")
48+
.attr("id", ids.workspaceSelection)
49+
.attr("rx", 4)
50+
.attr("ry", 4)
51+
.attr("x", 0)
52+
.attr("y", 0)
53+
.attr("width", 0)
54+
.attr("height", 0)
55+
.classed("workspace-selection", true);
56+
this.setElement(rectElement);
57+
this.originX = newX;
58+
this.originY = newY;
59+
this.update(newX, newY);
60+
},
61+
update: function (newX: number, newY: number) {
62+
this.currentX = newX - (+this.element?.attr("width") > 2 ? toolbarWidth : 0);
63+
this.currentY = newY - (+this.element?.attr("height") > 2 ? operationBarHeight : 0);
64+
const attributes = this.getNewAttributes();
65+
Object.keys(attributes).forEach((key) => {
66+
this.element.attr(key, attributes[key]);
67+
});
68+
},
69+
remove: function () {
70+
this.element.remove();
71+
this.element = null;
72+
}
73+
};
74+
75+
const dragStart = (e) => {
76+
const p = d3Extended.pointer(e);
77+
selectionRect.init(p[0], p[1]);
78+
};
79+
80+
const dragMove = (e) => {
81+
const p = d3Extended.pointer(e);
82+
selectionRect.update(p[0], p[1]);
83+
};
84+
85+
const dragEnd = () => {
86+
const finalAttributes = selectionRect.getCurrentAttributes();
87+
selectionRect.remove();
88+
const elements = d3Extended.selectAll(`[${dataAttributes.element}]`);
89+
const idsToSelect = [];
90+
91+
elements.forEach((element) => {
92+
const isSeat = element.attr(dataAttributes.elementType) === ElementType.Seat;
93+
const x = isSeat ? +element.attr("cx") : +element.attr("x");
94+
const y = isSeat ? +element.attr("cy") : +element.attr("y");
95+
if (
96+
x >= finalAttributes.x1 &&
97+
x <= finalAttributes.x2 &&
98+
y >= finalAttributes.y1 &&
99+
y <= finalAttributes.y2
100+
) {
101+
const id = element.attr("id");
102+
if (!id?.includes("-label")) idsToSelect.push(id);
103+
}
104+
});
105+
store.dispatch(clearAndSelectElements(idsToSelect));
106+
};
107+
108+
svg.call(d3Extended.drag().on("drag", dragMove).on("start", dragStart).on("end", dragEnd));
109+
}
110+
}, []);
111+
};
112+
113+
export default useSelection;

src/styles/index.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,17 @@
55
@tailwind base;
66
@tailwind components;
77
@tailwind utilities;
8+
9+
rect.workspace-selection {
10+
-webkit-touch-callout: none !important;
11+
-webkit-user-select: none !important;
12+
-khtml-user-select: none !important;
13+
-moz-user-select: none !important;
14+
-ms-user-select: none !important;
15+
user-select: none !important;
16+
stroke: #545454;
17+
stroke-width: 2px;
18+
stroke-opacity: 1;
19+
fill: white;
20+
fill-opacity: 0.5;
21+
}

src/utils/d3.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { pointer, select, selectAll, selection, zoom, zoomIdentity, zoomTransform } from "d3";
1+
import { drag, pointer, select, selectAll, selection, zoom, zoomIdentity, zoomTransform } from "d3";
22

33
declare module "d3" {
44
interface Selection<GElement extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum> {
@@ -39,6 +39,7 @@ selection.prototype.forEach = function (callback) {
3939
};
4040

4141
export const d3Extended = {
42+
drag,
4243
pointer,
4344
select,
4445
selectAll,

src/utils/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { zoomTransform } from "d3";
1+
import { type ZoomTransform, zoomTransform } from "d3";
22
import { ids, selectors } from "@/constants";
33

44
export * from "./d3";
@@ -35,7 +35,11 @@ export const getRelativeWorkspaceClickCoords = (e: any) => {
3535

3636
export const getRelativeClickCoordsWithTransform = (e: any) => {
3737
const coords = getRelativeWorkspaceClickCoords(e);
38-
const transform = zoomTransform(document.querySelector(selectors.workspaceGroup));
38+
return coordsWithTransform(coords);
39+
};
40+
41+
export const coordsWithTransform = (coords: { x: number; y: number }, transform?: ZoomTransform) => {
42+
transform ??= zoomTransform(document.querySelector(selectors.workspaceGroup));
3943
return {
4044
x: (coords.x - transform.x) / transform.k,
4145
y: (coords.y - transform.y) / transform.k

0 commit comments

Comments
 (0)