Skip to content

Commit b604d43

Browse files
optimise canvas view/inner grid, root comp, gridLayout, gridItem and uiCompBuilder
1 parent dfc5955 commit b604d43

File tree

9 files changed

+388
-215
lines changed

9 files changed

+388
-215
lines changed

client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EditorContext } from "comps/editorState";
22
import { EditorContainer } from "pages/common/styledComponent";
3-
import React, { Profiler, useContext, useMemo, useRef, useState } from "react";
3+
import React, { Profiler, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
44
import styled from "styled-components";
55
import { profilerCallback } from "util/cacheUtils";
66
import {
@@ -97,11 +97,44 @@ export const CanvasView = React.memo((props: ContainerBaseProps) => {
9797
const isDefaultTheme = useContext(ThemeContext)?.themeId === 'default-theme-id';
9898
const isPreviewTheme = useContext(ThemeContext)?.themeId === 'preview-theme';
9999
const editorState = useContext(EditorContext);
100-
const [dragSelectedComps, setDragSelectedComp] = useState(EmptySet);
101-
const scrollContainerRef = useRef(null);
100+
const [dragSelectedComps, setDragSelectedComp] = useState<Set<string>>(new Set());
101+
const scrollContainerRef = useRef<HTMLDivElement>(null);
102+
const mountedRef = useRef(true);
102103
const appSettings = editorState.getAppSettings();
103104
const maxWidthFromHook = useMaxWidth();
104105

106+
// Cleanup on unmount
107+
useEffect(() => {
108+
return () => {
109+
mountedRef.current = false;
110+
setDragSelectedComp(new Set());
111+
};
112+
}, []);
113+
114+
// Memoized drag selection handler
115+
const handleDragSelection = useCallback((checkSelectFunc?: CheckSelectFn) => {
116+
if (!mountedRef.current) return new Set<string>();
117+
118+
const selectedComps = new Set<string>();
119+
if (checkSelectFunc) {
120+
Object.values(props.layout).forEach((layoutItem) => {
121+
const key = layoutItem.i;
122+
if (props.items.hasOwnProperty(key)) {
123+
const item = props.items[key];
124+
const name = item.name;
125+
const element = document.getElementById(key);
126+
if (element) {
127+
checkSelectFunc(
128+
element as HTMLDivElement,
129+
(result) => (result ? selectedComps.add(name) : selectedComps.delete(name))
130+
);
131+
}
132+
}
133+
});
134+
}
135+
return selectedComps;
136+
}, [props.items, props.layout]);
137+
105138
const maxWidth = useMemo(
106139
() => appSettings.maxWidth ?? maxWidthFromHook,
107140
[appSettings, maxWidthFromHook]
@@ -244,6 +277,25 @@ export const CanvasView = React.memo((props: ContainerBaseProps) => {
244277
rowHeight: parseInt(defaultRowHeight),
245278
}), [props.positionParams, defaultGrid, defaultRowHeight]);
246279

280+
// Memoized mouse event handlers
281+
const handleMouseDown = useCallback(() => {
282+
setDragSelectedComp(new Set());
283+
}, []);
284+
285+
const handleMouseUp = useCallback(() => {
286+
if (mountedRef.current) {
287+
editorState.setSelectedCompNames(dragSelectedComps);
288+
setDragSelectedComp(new Set());
289+
}
290+
}, [editorState, dragSelectedComps]);
291+
292+
const handleMouseMove = useCallback((checkSelectFunc: CheckSelectFn) => {
293+
if (mountedRef.current) {
294+
const selectedName = handleDragSelection(checkSelectFunc);
295+
setDragSelectedComp(selectedName);
296+
}
297+
}, [handleDragSelection]);
298+
247299
if (readOnly) {
248300
return (
249301
<UICompContainer
@@ -291,17 +343,9 @@ export const CanvasView = React.memo((props: ContainerBaseProps) => {
291343
$bgImagePosition={bgImagePosition}
292344
>
293345
<DragSelector
294-
onMouseDown={() => {
295-
setDragSelectedComp(EmptySet);
296-
}}
297-
onMouseUp={() => {
298-
editorState.setSelectedCompNames(dragSelectedComps);
299-
setDragSelectedComp(EmptySet);
300-
}}
301-
onMouseMove={(checkSelectFunc) => {
302-
const selectedName = getDragSelectedNames(props.items, props.layout, checkSelectFunc);
303-
setDragSelectedComp(selectedName);
304-
}}
346+
onMouseDown={handleMouseDown}
347+
onMouseUp={handleMouseUp}
348+
onMouseMove={handleMouseMove}
305349
>
306350
<Profiler id="Panel" onRender={profilerCallback}>
307351
<InnerGrid
@@ -318,7 +362,7 @@ export const CanvasView = React.memo((props: ContainerBaseProps) => {
318362
positionParams={positionParams}
319363
emptyRows={defaultRowCount}
320364
minHeight={defaultMinHeight}
321-
extraHeight={defaultRowCount === DEFAULT_ROW_COUNT ? rootContainerExtraHeight : undefined }
365+
extraHeight={defaultRowCount === DEFAULT_ROW_COUNT ? rootContainerExtraHeight : undefined}
322366
/>
323367
</Profiler>
324368
</DragSelector>
Lines changed: 112 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Layers } from "constants/Layers";
2-
import React, { ReactNode } from "react";
2+
import React, { ReactNode, useCallback, useRef, useState, useEffect } from "react";
33

44
export type CheckSelectFn = (
55
item?: HTMLDivElement | null,
@@ -29,159 +29,154 @@ interface SectionState {
2929
mouseDown: boolean;
3030
selectionBox?: Rect;
3131
startPoint?: Point;
32+
appendMode: boolean;
3233
}
3334

34-
const InitialState = {
35+
const createInitialState = (): SectionState => ({
3536
mouseDown: false,
3637
appendMode: false,
3738
selectionBox: undefined,
3839
startPoint: undefined,
39-
};
40-
41-
class DragSelectorComp extends React.Component<SectionProps, SectionState> {
42-
private readonly selectAreaRef: React.RefObject<HTMLDivElement>;
43-
44-
constructor(props: SectionProps) {
45-
super(props);
46-
this.selectAreaRef = React.createRef<HTMLDivElement>();
47-
this.state = InitialState;
48-
this._onMouseMove = this._onMouseMove.bind(this);
49-
this._onMouseUp = this._onMouseUp.bind(this);
50-
}
51-
52-
componentWillUnmount(): void {
53-
window.document.removeEventListener("mousemove", this._onMouseMove);
54-
window.document.removeEventListener("mouseup", this._onMouseUp);
55-
}
56-
57-
_onMouseDown(e: React.MouseEvent<HTMLDivElement>) {
58-
if (e.button === 2 || e.nativeEvent.which === 2) {
59-
return;
60-
}
61-
let nextState: SectionState = { mouseDown: false };
62-
nextState.mouseDown = true;
63-
nextState.startPoint = {
64-
x: e.pageX - (this.selectAreaRef.current?.getBoundingClientRect().left ?? 0),
65-
y: e.pageY - (this.selectAreaRef.current?.getBoundingClientRect().top ?? 0),
40+
});
41+
42+
export const DragSelector = React.memo((props: SectionProps) => {
43+
const selectAreaRef = useRef<HTMLDivElement>(null);
44+
const [state, setState] = useState<SectionState>(createInitialState());
45+
const mountedRef = useRef(true);
46+
47+
// Cleanup on unmount
48+
useEffect(() => {
49+
return () => {
50+
mountedRef.current = false;
51+
// Clean up any remaining event listeners
52+
window.document.removeEventListener("mousemove", handleMouseMove);
53+
window.document.removeEventListener("mouseup", handleMouseUp);
54+
};
55+
}, []);
56+
57+
const handleMouseMove = useCallback((e: MouseEvent) => {
58+
if (!mountedRef.current || !state.mouseDown) return;
59+
60+
const endPoint = {
61+
x: e.pageX - (selectAreaRef.current?.getBoundingClientRect().left ?? 0),
62+
y: e.pageY - (selectAreaRef.current?.getBoundingClientRect().top ?? 0),
6663
};
67-
this.setState(nextState);
68-
window.document.addEventListener("mousemove", this._onMouseMove);
69-
window.document.addEventListener("mouseup", this._onMouseUp);
70-
this.props.onMouseDown();
71-
}
72-
73-
_onMouseUp() {
74-
window.document.removeEventListener("mousemove", this._onMouseMove);
75-
window.document.removeEventListener("mouseup", this._onMouseUp);
76-
this.props.onMouseUp();
77-
this.setState(InitialState);
78-
}
79-
80-
_onMouseMove(e: MouseEvent) {
81-
if (this.state.mouseDown) {
82-
let endPoint = {
83-
x: e.pageX - (this.selectAreaRef.current?.getBoundingClientRect().left ?? 0),
84-
y: e.pageY - (this.selectAreaRef.current?.getBoundingClientRect().top ?? 0),
85-
};
86-
this.setState({
87-
selectionBox: this._calculateSelectionBox(this.state.startPoint, endPoint),
88-
});
64+
65+
setState(prevState => ({
66+
...prevState,
67+
selectionBox: calculateSelectionBox(prevState.startPoint, endPoint),
68+
}));
69+
70+
// Clean up selection properly
71+
const selection = window.getSelection();
72+
if (selection) {
73+
selection.removeAllRanges();
8974
}
90-
// Disable selection of text during mouse movement
91-
var selection = window.getSelection();
92-
selection!.removeAllRanges();
93-
selection = null;
94-
this.props.onMouseMove(this.childrenViewCheckFunc);
95-
}
96-
97-
rectIntersect = (
75+
76+
props.onMouseMove(childrenViewCheckFunc);
77+
}, [state.mouseDown, state.startPoint, props.onMouseMove]);
78+
79+
const handleMouseUp = useCallback(() => {
80+
if (!mountedRef.current) return;
81+
82+
window.document.removeEventListener("mousemove", handleMouseMove);
83+
window.document.removeEventListener("mouseup", handleMouseUp);
84+
props.onMouseUp();
85+
setState(createInitialState());
86+
}, [handleMouseMove, props.onMouseUp]);
87+
88+
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
89+
if (!mountedRef.current || e.button === 2 || e.nativeEvent.which === 2) return;
90+
91+
const startPoint = {
92+
x: e.pageX - (selectAreaRef.current?.getBoundingClientRect().left ?? 0),
93+
y: e.pageY - (selectAreaRef.current?.getBoundingClientRect().top ?? 0),
94+
};
95+
96+
setState({
97+
mouseDown: true,
98+
startPoint,
99+
selectionBox: undefined,
100+
appendMode: false,
101+
});
102+
103+
window.document.addEventListener("mousemove", handleMouseMove);
104+
window.document.addEventListener("mouseup", handleMouseUp);
105+
props.onMouseDown();
106+
}, [handleMouseMove, handleMouseUp, props.onMouseDown]);
107+
108+
const rectIntersect = useCallback((
98109
selectionBox: Rect | undefined,
99110
item: HTMLElement | null | undefined
100111
): boolean => {
101-
if (!selectionBox || !item) {
102-
return false;
103-
}
112+
if (!selectionBox || !item || !selectAreaRef.current) return false;
113+
114+
const containerRect = selectAreaRef.current.getBoundingClientRect();
104115
const itemBox = {
105-
top:
106-
item.getBoundingClientRect().top -
107-
(this.selectAreaRef.current?.getBoundingClientRect().top ?? 0),
108-
left:
109-
item.getBoundingClientRect().left -
110-
(this.selectAreaRef.current?.getBoundingClientRect().left ?? 0),
116+
top: item.getBoundingClientRect().top - containerRect.top,
117+
left: item.getBoundingClientRect().left - containerRect.left,
111118
width: item.getBoundingClientRect().width,
112119
height: item.getBoundingClientRect().height,
113120
};
121+
114122
return (
115123
selectionBox.left <= itemBox.left + itemBox.width &&
116124
selectionBox.left + selectionBox.width >= itemBox.left &&
117125
selectionBox.top <= itemBox.top + itemBox.height &&
118126
selectionBox.top + selectionBox.height >= itemBox.top
119127
);
120-
};
128+
}, []);
121129

122-
childrenViewCheckFunc = (
130+
const childrenViewCheckFunc = useCallback((
123131
item?: HTMLDivElement | null,
124132
afterCheck?: (checkResult: boolean) => void
125133
) => {
126-
const result = this.rectIntersect(this.state.selectionBox, item);
127-
if (!!afterCheck) {
134+
const result = rectIntersect(state.selectionBox, item);
135+
if (afterCheck) {
128136
afterCheck(result);
129137
}
130138
return result;
131-
};
139+
}, [state.selectionBox, rectIntersect]);
132140

133-
render() {
134-
return (
135-
<div
136-
ref={this.selectAreaRef}
137-
onMouseDown={this._onMouseDown.bind(this)}
138-
style={{ position: "relative" }}
139-
>
140-
{this.props.children}
141-
{this.renderSelectionBox()}
142-
</div>
143-
);
144-
}
145-
146-
renderSelectionBox() {
147-
if (
148-
!this.state.mouseDown ||
149-
!this.state.startPoint ||
150-
!this.state.selectionBox ||
151-
!this.selectAreaRef.current
152-
) {
141+
const calculateSelectionBox = useCallback((startPoint: Point | undefined, endPoint: Point) => {
142+
if (!state.mouseDown || !startPoint || !endPoint) return undefined;
143+
144+
return {
145+
left: Math.min(startPoint.x, endPoint.x),
146+
top: Math.min(startPoint.y, endPoint.y),
147+
width: Math.abs(startPoint.x - endPoint.x),
148+
height: Math.abs(startPoint.y - endPoint.y),
149+
};
150+
}, [state.mouseDown]);
151+
152+
const renderSelectionBox = useCallback(() => {
153+
if (!state.mouseDown || !state.startPoint || !state.selectionBox || !selectAreaRef.current) {
153154
return null;
154155
}
156+
155157
return (
156158
<div
157159
style={{
158160
background: "rgba(51, 119, 255, 0.1)",
159161
position: "absolute",
160162
zIndex: Layers.dragSelectBox,
161-
left: this.state.selectionBox.left,
162-
top: this.state.selectionBox.top,
163-
height: this.state.selectionBox.height,
164-
width: this.state.selectionBox.width,
163+
left: state.selectionBox.left,
164+
top: state.selectionBox.top,
165+
height: state.selectionBox.height,
166+
width: state.selectionBox.width,
165167
}}
166168
/>
167169
);
168-
}
169-
170-
_calculateSelectionBox(startPoint: Point | undefined, endPoint: Point) {
171-
if (!this.state.mouseDown || !startPoint || !endPoint) {
172-
return undefined;
173-
}
174-
let left = Math.min(startPoint.x, endPoint.x);
175-
let top = Math.min(startPoint.y, endPoint.y);
176-
let width = Math.abs(startPoint.x - endPoint.x);
177-
let height = Math.abs(startPoint.y - endPoint.y);
178-
return {
179-
left: left,
180-
top: top,
181-
width: width,
182-
height: height,
183-
};
184-
}
185-
}
186-
187-
export const DragSelector = React.memo(DragSelectorComp);
170+
}, [state.mouseDown, state.startPoint, state.selectionBox]);
171+
172+
return (
173+
<div
174+
ref={selectAreaRef}
175+
onMouseDown={handleMouseDown}
176+
style={{ position: "relative" }}
177+
>
178+
{props.children}
179+
{renderSelectionBox()}
180+
</div>
181+
);
182+
});

0 commit comments

Comments
 (0)