Skip to content

Commit 496c100

Browse files
author
FalkWolsky
committed
SplitLayout Component
1 parent 15a98e8 commit 496c100

File tree

13 files changed

+328
-64
lines changed

13 files changed

+328
-64
lines changed

client/packages/lowcoder/src/comps/comps/splitLayout/splitLayout.tsx

+231-61
Original file line numberDiff line numberDiff line change
@@ -3,109 +3,155 @@ import { Splitter } from "antd";
33
import styled from "styled-components";
44
import { DispatchType, RecordConstructorToView, wrapDispatch } from "lowcoder-core";
55
import { CompAction, CompActionTypes, deleteCompAction, wrapChildAction } from "lowcoder-core";
6-
import { ColumnOptionControl } from "comps/controls/optionsControl";
7-
import { NumberControl } from "comps/controls/codeControl";
6+
import { SplitColumnOptionControl } from "comps/controls/optionsControl";
7+
import { NumberControl, StringControl } from "comps/controls/codeControl";
88
import { BoolControl } from "comps/controls/boolControl";
9+
import { dropdownControl } from "comps/controls/dropdownControl";
910
import { styleControl } from "comps/controls/styleControl";
10-
import { SplitLayoutColStyle, SplitLayoutColStyleType, AnimationStyle } from "comps/controls/styleControlConstants";
11+
import { SplitLayoutColStyle, SplitLayoutRowStyle, SplitLayoutRowStyleType, SplitLayoutColStyleType, AnimationStyle, heightCalculator } from "comps/controls/styleControlConstants";
1112
import { sameTypeMap, UICompBuilder, withDefault } from "comps/generators";
1213
import { addMapChildAction } from "comps/generators/sameTypeMap";
1314
import { BackgroundColorContext } from "comps/utils/backgroundColorContext";
14-
import { Section, sectionNames } from "lowcoder-design";
15+
import { Section, sectionNames} from "lowcoder-design";
1516
import { trans } from "i18n";
16-
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
1717
import { ContainerBaseProps, gridItemCompToGridItems, InnerGrid } from "../containerComp/containerView";
18+
import { useContext } from "react";
19+
import { EditorContext } from "comps/editorState";
20+
21+
import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils";
1822
import { DisabledContext } from "comps/generators/uiCompBuilder";
19-
import { useScreenInfo } from "../../hooks/screenInfoComp";
23+
import { JSONObject, JSONValue } from "util/jsonTypes";
24+
import { IContainer } from "../containerBase/iContainer";
25+
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
26+
import { CompTree, mergeCompTrees } from "../containerBase/utils";
27+
import { NameGenerator } from "comps/utils";
28+
import { AutoHeightControl } from "comps/controls/autoHeightControl";
29+
import { messageInstance } from "lowcoder-design/src/components/GlobalInstances";
30+
import { NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing";
31+
import SliderControl from "@lowcoder-ee/comps/controls/sliderControl";
32+
import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils";
33+
import _ from "lodash";
2034

21-
const SplitPanelWrapper = styled(Splitter.Panel)<{ $collapsible: boolean }>`
22-
flex-grow: 1;
23-
${(props) => props.$collapsible && `min-width: 50px;`}
35+
import {
36+
HorizontalIcon,
37+
VerticalIcon,
38+
} from "lowcoder-design/src/icons";
39+
import { BackgroundColor } from "@lowcoder-ee/constants/style";
40+
41+
const SplitPanelWrapper = styled(Splitter.Panel)<{ }>`
2442
`;
2543

26-
export interface SplitterLayoutTypes {
27-
orientation: "horizontal" | "vertical";
28-
}
44+
const SplitterWrapper = styled.div<{ $style: SplitLayoutRowStyleType }>`
45+
border-radius: ${(props) => props.$style?.radius || "0px"};
46+
border-width: ${(props) => props.$style?.borderWidth || "0px"};
47+
border-color: ${(props) => props.$style?.border || "transparent"};
48+
border-style: ${(props) => props.$style?.borderStyle || "solid"};
49+
margin: ${(props) => props.$style?.margin || "0px"};
50+
padding: ${(props) => props.$style?.padding || "0px"};
51+
${(props) => (props.$style ? getBackgroundStyle(props.$style) : "")}
52+
`;
2953

30-
/*
54+
const OrientationOptions = [
55+
{
56+
label: <HorizontalIcon />,
57+
value: "horizontal",
58+
},
59+
{
60+
label: <VerticalIcon />,
61+
value: "vertical",
62+
},
63+
] as const;
3164

3265
const childrenMap = {
3366
disabled: BoolControl,
34-
columns: ColumnOptionControl,
67+
columns: SplitColumnOptionControl,
3568
containers: withDefault(sameTypeMap(SimpleContainerComp), {
3669
0: { view: {}, layout: {} },
3770
1: { view: {}, layout: {} },
3871
}),
39-
collapsiblePanels: BoolControl,
40-
orientation: withDefault(ColumnOptionControl, "horizontal"),
41-
panelCount: withDefault(NumberControl, 2),
72+
autoHeight: AutoHeightControl,
73+
horizontalGridCells: SliderControl,
74+
verticalGridCells: SliderControl,
75+
orientation: dropdownControl(OrientationOptions, "horizontal"),
76+
matchColumnsHeight: withDefault(BoolControl, true),
4277
columnStyle: styleControl(SplitLayoutColStyle, "columnStyle"),
78+
bodyStyle: styleControl(SplitLayoutRowStyle, 'bodyStyle'),
4379
animationStyle: styleControl(AnimationStyle, "animationStyle"),
80+
mainScrollbar: withDefault(BoolControl, false),
4481
};
4582

4683
type ViewProps = RecordConstructorToView<typeof childrenMap>;
4784
type SplitLayoutProps = ViewProps & { dispatch: DispatchType };
85+
4886
type ColumnContainerProps = Omit<ContainerBaseProps, "style"> & {
4987
style: SplitLayoutColStyleType;
88+
matchColumnsHeight: boolean;
89+
backgroundColor: string;
90+
backgroundImage: string;
91+
padding: string;
92+
orientation: string;
93+
margin: string;
5094
};
5195

5296
const ColumnContainer = (props: ColumnContainerProps) => {
5397
return (
5498
<InnerGrid
5599
{...props}
56-
emptyRows={15}
57100
radius={props.style.radius}
58-
style={props.style}
101+
bgColor={props.backgroundColor}
102+
style={{
103+
...props.style,
104+
height: props.orientation === "horizontal"
105+
? (props.matchColumnsHeight ? heightCalculator(props.margin) : "auto")
106+
: (props.autoHeight ? "100%" : "auto"),
107+
}}
59108
/>
60109
);
61110
};
62111

63112
const SplitLayout = (props: SplitLayoutProps) => {
64-
const screenInfo = useScreenInfo();
65-
const containerRef = useRef<HTMLDivElement | null>(null);
66-
const [componentWidth, setComponentWidth] = useState<number | null>(null);
67-
68-
let { columns, containers, dispatch, collapsiblePanels, orientation, panelCount, columnStyle } = props;
69-
70-
useEffect(() => {
71-
if (!containerRef.current) return;
72-
const resizeObserver = new ResizeObserver((entries) => {
73-
for (let entry of entries) {
74-
setComponentWidth(entry.contentRect.width);
75-
}
76-
});
77-
78-
resizeObserver.observe(containerRef.current);
79-
return () => resizeObserver.disconnect();
80-
}, []);
81113

82114
return (
83-
<BackgroundColorContext.Provider value={props.style?.background}>
115+
<BackgroundColorContext.Provider value={props.columnStyle.background}>
84116
<DisabledContext.Provider value={props.disabled}>
85-
<div ref={containerRef} style={{ height: "100%" }}>
86-
<Splitter layout={orientation}>
87-
{Array.from({ length: panelCount }, (_, index) => {
88-
const id = String(index);
89-
const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id);
90-
if (!containers[id]) return null;
91-
const containerProps = containers[id].children;
92-
117+
<SplitterWrapper $style={props.bodyStyle}>
118+
<Splitter style={{ overflow: props.mainScrollbar ? "auto" : "hidden"}} layout={props.orientation}>
119+
{props.columns.map((col, index) => {
120+
const id = String(col.id);
121+
const childDispatch = wrapDispatch(wrapDispatch(props.dispatch, "containers"), id);
122+
const containerProps = props.containers[id]?.children;
93123
return (
94-
<SplitPanelWrapper key={id} $collapsible={collapsiblePanels}>
124+
<SplitPanelWrapper
125+
key={id}
126+
collapsible={col.collapsible}
127+
{...(col.minWidth !== undefined ? { min: col.minWidth } : {})}
128+
{...(col.maxWidth !== undefined ? { max: col.maxWidth } : {})}
129+
{...(col.width !== undefined ? { defaultSize: col.width } : {})}
130+
>
95131
<ColumnContainer
96132
layout={containerProps.layout.getView()}
97133
items={gridItemCompToGridItems(containerProps.items.getView())}
98134
positionParams={containerProps.positionParams.getView()}
99135
dispatch={childDispatch}
100-
style={columnStyle}
136+
style={props.columnStyle}
137+
backgroundColor={col.backgroundColor}
138+
backgroundImage={col.backgroundImage}
139+
padding={col.padding}
140+
autoHeight={props.autoHeight}
141+
horizontalGridCells={props.horizontalGridCells}
142+
emptyRows={props.verticalGridCells}
143+
matchColumnsHeight={props.matchColumnsHeight}
144+
orientation={props.orientation}
145+
margin={props.columnStyle.margin}
101146
/>
102147
</SplitPanelWrapper>
103148
);
104149
})}
105150
</Splitter>
106-
</div>
151+
</SplitterWrapper>
107152
</DisabledContext.Provider>
108153
</BackgroundColorContext.Provider>
154+
109155
);
110156
};
111157

@@ -116,27 +162,151 @@ export const SplitLayoutBaseComp = (function () {
116162
<Section name={sectionNames.basic}>
117163
{children.columns.propertyView({ title: trans("splitLayout.column") })}
118164
</Section>
119-
<Section name={sectionNames.layout}>
120-
{children.panelCount.propertyView({ label: trans("splitLayout.panelCount") })}
121-
{children.collapsiblePanels.propertyView({ label: trans("splitLayout.collapsiblePanels") })}
122-
{children.orientation.propertyView({ label: trans("splitLayout.orientation") })}
123-
</Section>
124-
<Section name={sectionNames.style}>
125-
{children.columnStyle.getPropertyView()}
126-
{children.animationStyle.getPropertyView()}
127-
</Section>
165+
166+
{(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && (
167+
<Section name={sectionNames.interaction}>
168+
{disabledPropertyView(children)}
169+
{hiddenPropertyView(children)}
170+
</Section>
171+
)}
172+
{["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (
173+
<>
174+
<Section name={sectionNames.layout}>
175+
{children.orientation.propertyView({
176+
label: trans("splitLayout.orientation"),
177+
radioButton: true,
178+
tooltip: trans("splitLayout.orientationTooltip"),
179+
})}
180+
{children.autoHeight.getPropertyView()}
181+
{(!children.autoHeight.getView()) && children.mainScrollbar.propertyView({
182+
label: trans("prop.mainScrollbar")
183+
})}
184+
{(children.orientation.getView() == "horizontal") &&
185+
children.matchColumnsHeight.propertyView({ label: trans("splitLayout.matchColumnsHeight") }
186+
)}
187+
{children.horizontalGridCells.propertyView({
188+
label: trans('prop.horizontalGridCells'),
189+
})}
190+
{children.verticalGridCells.propertyView({
191+
label: trans('prop.verticalGridCells'),
192+
})}
193+
</Section>
194+
<Section name={sectionNames.bodyStyle}>
195+
{children.bodyStyle.getPropertyView()}
196+
</Section>
197+
<Section name={sectionNames.detailStyle}>
198+
{children.columnStyle.getPropertyView()}
199+
</Section>
200+
<Section name={sectionNames.animationStyle} hasTooltip={true}>
201+
{children.animationStyle.getPropertyView()}
202+
</Section>
203+
</>
204+
)}
128205
</>
129206
))
130207
.build();
131208
})();
132209

133-
class SplitLayoutImplComp extends SplitLayoutBaseComp {
210+
class SplitLayoutImplComp extends SplitLayoutBaseComp implements IContainer {
211+
private syncContainers(): this {
212+
const columns = this.children.columns.getView();
213+
const ids: Set<string> = new Set(columns.map((column) => String(column.id)));
214+
let containers = this.children.containers.getView();
215+
// delete
216+
const actions: CompAction[] = [];
217+
Object.keys(containers).forEach((id) => {
218+
if (!ids.has(id)) {
219+
// log.debug("syncContainers delete. ids=", ids, " id=", id);
220+
actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction())));
221+
}
222+
});
223+
// new
224+
ids.forEach((id) => {
225+
if (!containers.hasOwnProperty(id)) {
226+
// log.debug("syncContainers new containers: ", containers, " id: ", id);
227+
actions.push(
228+
wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} }))
229+
);
230+
}
231+
});
232+
// log.debug("syncContainers. actions: ", actions);
233+
let instance = this;
234+
actions.forEach((action) => {
235+
instance = instance.reduce(action);
236+
});
237+
return instance;
238+
}
239+
134240
override reduce(action: CompAction): this {
241+
const columns = this.children.columns.getView();
242+
if (action.type === CompActionTypes.CUSTOM) {
243+
const value = action.value as JSONObject;
244+
if (value.type === "push") {
245+
const itemValue = value.value as JSONObject;
246+
if (_.isEmpty(itemValue.key)) itemValue.key = itemValue.label;
247+
action = {
248+
...action,
249+
value: {
250+
...value,
251+
value: { ...itemValue },
252+
},
253+
} as CompAction;
254+
}
255+
const { path } = action;
256+
if (value.type === "delete" && path[0] === 'columns' && columns.length <= 1) {
257+
messageInstance.warning(trans("responsiveLayout.atLeastOneColumnError"));
258+
// at least one column
259+
return this;
260+
}
261+
}
262+
// log.debug("before super reduce. action: ", action);
135263
let newInstance = super.reduce(action);
264+
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
265+
// Need eval to get the value in StringControl
266+
newInstance = newInstance.syncContainers();
267+
}
268+
// log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
136269
return newInstance;
137270
}
138-
}
139271

140-
*/
272+
realSimpleContainer(key?: string): SimpleContainerComp | undefined {
273+
return Object.values(this.children.containers.children).find((container) =>
274+
container.realSimpleContainer(key)
275+
);
276+
}
277+
278+
getCompTree(): CompTree {
279+
const containerMap = this.children.containers.getView();
280+
const compTrees = Object.values(containerMap).map((container) => container.getCompTree());
281+
return mergeCompTrees(compTrees);
282+
}
283+
284+
findContainer(key: string): IContainer | undefined {
285+
const containerMap = this.children.containers.getView();
286+
for (const container of Object.values(containerMap)) {
287+
const foundContainer = container.findContainer(key);
288+
if (foundContainer) {
289+
return foundContainer === container ? this : foundContainer;
290+
}
291+
}
292+
return undefined;
293+
}
294+
295+
getPasteValue(nameGenerator: NameGenerator): JSONValue {
296+
const containerMap = this.children.containers.getView();
297+
const containerPasteValueMap = _.mapValues(containerMap, (container) =>
298+
container.getPasteValue(nameGenerator)
299+
);
300+
301+
return { ...this.toJsonValue(), containers: containerPasteValueMap };
302+
}
303+
304+
override autoHeight(): boolean {
305+
return this.children.autoHeight.getView();
306+
}
307+
}
141308

142-
export const SplitLayoutComp = null;
309+
export const SplitLayoutComp = withExposingConfigs(
310+
SplitLayoutImplComp,
311+
[ NameConfigHidden]
312+
);

0 commit comments

Comments
 (0)