Skip to content

Commit 9d2d043

Browse files
feat: added responsive layout component
1 parent 1c11818 commit 9d2d043

File tree

10 files changed

+401
-2
lines changed

10 files changed

+401
-2
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ResponsiveLayoutComp } from "./responsiveLayout";
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import { Row } from "antd";
2+
import { JSONObject, JSONValue } from "util/jsonTypes";
3+
import { CompAction, CompActionTypes, deleteCompAction, wrapChildAction } from "lowcoder-core";
4+
import { DispatchType, RecordConstructorToView, wrapDispatch } from "lowcoder-core";
5+
import { AutoHeightControl } from "comps/controls/autoHeightControl";
6+
import { stringExposingStateControl } from "comps/controls/codeStateControl";
7+
import { ColumnOptionControl } from "comps/controls/optionsControl";
8+
import { styleControl } from "comps/controls/styleControl";
9+
import { ResponsiveLayoutRowStyle, ResponsiveLayoutRowStyleType, ResponsiveLayoutColStyleType, TabContainerStyle, TabContainerStyleType, heightCalculator, widthCalculator, ResponsiveLayoutColStyle } from "comps/controls/styleControlConstants";
10+
import { sameTypeMap, UICompBuilder, withDefault } from "comps/generators";
11+
import { addMapChildAction } from "comps/generators/sameTypeMap";
12+
import { NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing";
13+
import { NameGenerator } from "comps/utils";
14+
import { Section, sectionNames } from "lowcoder-design";
15+
import { HintPlaceHolder } from "lowcoder-design";
16+
import _ from "lodash";
17+
import React, { useContext } from "react";
18+
import styled from "styled-components";
19+
import { IContainer } from "../containerBase/iContainer";
20+
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
21+
import { CompTree, mergeCompTrees } from "../containerBase/utils";
22+
import {
23+
ContainerBaseProps,
24+
gridItemCompToGridItems,
25+
InnerGrid,
26+
} from "../containerComp/containerView";
27+
import { BackgroundColorContext } from "comps/utils/backgroundColorContext";
28+
import { trans } from "i18n";
29+
import { EditorContext } from "comps/editorState";
30+
import { checkIsMobile } from "util/commonUtils";
31+
import { messageInstance } from "lowcoder-design";
32+
import { BoolControl } from "comps/controls/boolControl";
33+
34+
const RowWrapper = styled(Row)<{$style: ResponsiveLayoutRowStyleType}>`
35+
height: 100%;
36+
border: 1px solid ${(props) => props.$style.border};
37+
border-radius: ${(props) => props.$style.radius};
38+
padding: ${(props) => props.$style.padding};
39+
background-color: ${(props) => props.$style.background};
40+
`;
41+
42+
const ColWrapper = styled(InnerGrid)<{
43+
$style: ResponsiveLayoutColStyleType,
44+
$minWidth: string,
45+
}>`
46+
height: 100%;
47+
min-width: ${(props) => props.$minWidth};
48+
border: 1px solid ${(props) => props.$style.border};
49+
border-radius: ${(props) => props.$style.radius};
50+
padding: ${(props) => props.$style.padding};
51+
background-color: ${(props) => props.$style.background};
52+
flex: 1 1 auto;
53+
`;
54+
55+
const childrenMap = {
56+
columns: ColumnOptionControl,
57+
selectedTabKey: stringExposingStateControl("key", "Tab1"),
58+
containers: withDefault(sameTypeMap(SimpleContainerComp), {
59+
0: { view: {}, layout: {} },
60+
1: { view: {}, layout: {} },
61+
}),
62+
autoHeight: AutoHeightControl,
63+
rowBreak: withDefault(BoolControl, false),
64+
rowStyle: styleControl(ResponsiveLayoutRowStyle),
65+
columnStyle: styleControl(ResponsiveLayoutColStyle),
66+
};
67+
68+
type ViewProps = RecordConstructorToView<typeof childrenMap>;
69+
type ResponsiveLayoutProps = ViewProps & { dispatch: DispatchType };
70+
type ColumnContainerProps = Omit<ContainerBaseProps, 'style'> & {
71+
style: ResponsiveLayoutColStyleType
72+
minWidth: string,
73+
}
74+
75+
const ColumnContainer = (props: ColumnContainerProps) => {
76+
return (
77+
<ColWrapper
78+
{...props}
79+
emptyRows={15}
80+
bgColor={"white"}
81+
hintPlaceholder={HintPlaceHolder}
82+
$style={props.style}
83+
$minWidth={props.minWidth}
84+
/>
85+
);
86+
};
87+
88+
89+
const ResponsiveLayout = (props: ResponsiveLayoutProps) => {
90+
let {
91+
columns,
92+
containers,
93+
dispatch,
94+
rowBreak,
95+
rowStyle,
96+
columnStyle,
97+
} = props;
98+
console.log(props)
99+
100+
const editorState = useContext(EditorContext);
101+
const maxWidth = editorState.getAppSettings().maxWidth;
102+
const isMobile = checkIsMobile(maxWidth);
103+
const paddingWidth = isMobile ? 8 : 20;
104+
105+
return (
106+
<BackgroundColorContext.Provider value={props.rowStyle.background}>
107+
<RowWrapper
108+
$style={rowStyle}
109+
wrap={rowBreak}
110+
>
111+
{columns.map(column => {
112+
const id = String(column.id);
113+
const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id);
114+
if(!containers[id]) return null
115+
const containerProps = containers[id].children;
116+
const columnCustomStyle = {
117+
margin: !_.isEmpty(column.margin) ? column.margin : columnStyle.margin,
118+
padding: !_.isEmpty(column.padding) ? column.padding : columnStyle.padding,
119+
border: !_.isEmpty(column.border) ? column.border : columnStyle.border,
120+
radius: !_.isEmpty(column.radius) ? column.radius : columnStyle.radius,
121+
background: !_.isEmpty(column.background) ? column.background : columnStyle.background,
122+
}
123+
console.log(column);
124+
return (
125+
<ColumnContainer
126+
key={id}
127+
layout={containerProps.layout.getView()}
128+
items={gridItemCompToGridItems(containerProps.items.getView())}
129+
positionParams={containerProps.positionParams.getView()}
130+
dispatch={childDispatch}
131+
autoHeight={props.autoHeight}
132+
style={columnCustomStyle}
133+
minWidth={column.minWidth}
134+
/>
135+
)
136+
})
137+
}
138+
</RowWrapper>
139+
</BackgroundColorContext.Provider>
140+
);
141+
};
142+
143+
export const ResponsiveLayoutBaseComp = (function () {
144+
return new UICompBuilder(childrenMap, (props, dispatch) => {
145+
return (
146+
<ResponsiveLayout {...props} dispatch={dispatch} />
147+
);
148+
})
149+
.setPropertyViewFn((children) => {
150+
return (
151+
<>
152+
<Section name={sectionNames.basic}>
153+
{children.columns.propertyView({
154+
title: trans("responsiveLayout.column"),
155+
newOptionLabel: "Column",
156+
})}
157+
{children.autoHeight.getPropertyView()}
158+
</Section>
159+
<Section name={sectionNames.layout}>
160+
{children.rowBreak.propertyView({
161+
label: trans("responsiveLayout.rowBreak")
162+
})}
163+
</Section>
164+
<Section name={trans("responsiveLayout.rowStyle")}>
165+
{children.rowStyle.getPropertyView()}
166+
</Section>
167+
<Section name={trans("responsiveLayout.columnStyle")}>
168+
{children.columnStyle.getPropertyView()}
169+
</Section>
170+
</>
171+
);
172+
})
173+
.build();
174+
})();
175+
176+
class ResponsiveLayoutImplComp extends ResponsiveLayoutBaseComp implements IContainer {
177+
private syncContainers(): this {
178+
const columns = this.children.columns.getView();
179+
const ids: Set<string> = new Set(columns.map((column) => String(column.id)));
180+
let containers = this.children.containers.getView();
181+
// delete
182+
const actions: CompAction[] = [];
183+
Object.keys(containers).forEach((id) => {
184+
if (!ids.has(id)) {
185+
// log.debug("syncContainers delete. ids=", ids, " id=", id);
186+
actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction())));
187+
}
188+
});
189+
// new
190+
ids.forEach((id) => {
191+
if (!containers.hasOwnProperty(id)) {
192+
// log.debug("syncContainers new containers: ", containers, " id: ", id);
193+
actions.push(
194+
wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} }))
195+
);
196+
}
197+
});
198+
// log.debug("syncContainers. actions: ", actions);
199+
let instance = this;
200+
actions.forEach((action) => {
201+
instance = instance.reduce(action);
202+
});
203+
return instance;
204+
}
205+
206+
override reduce(action: CompAction): this {
207+
const columns = this.children.columns.getView();
208+
if (action.type === CompActionTypes.CUSTOM) {
209+
const value = action.value as JSONObject;
210+
if (value.type === "push") {
211+
const itemValue = value.value as JSONObject;
212+
if (_.isEmpty(itemValue.key)) itemValue.key = itemValue.label;
213+
action = {
214+
...action,
215+
value: {
216+
...value,
217+
value: { ...itemValue },
218+
},
219+
} as CompAction;
220+
}
221+
if (value.type === "delete" && columns.length <= 1) {
222+
messageInstance.warning(trans("responsiveLayout.atLeastOneColumnError"));
223+
// at least one column
224+
return this;
225+
}
226+
}
227+
// log.debug("before super reduce. action: ", action);
228+
let newInstance = super.reduce(action);
229+
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
230+
// Need eval to get the value in StringControl
231+
newInstance = newInstance.syncContainers();
232+
}
233+
// log.debug("reduce. instance: ", this, " newInstance: ", newInstance);
234+
return newInstance;
235+
}
236+
237+
realSimpleContainer(key?: string): SimpleContainerComp | undefined {
238+
let selectedTabKey = this.children.selectedTabKey.getView().value;
239+
const columns = this.children.columns.getView();
240+
const selectedTab = columns.find((column) => column.key === selectedTabKey) ?? columns[0];
241+
const id = String(selectedTab.id);
242+
if (_.isNil(key)) return this.children.containers.children[id];
243+
return Object.values(this.children.containers.children).find((container) =>
244+
container.realSimpleContainer(key)
245+
);
246+
}
247+
248+
getCompTree(): CompTree {
249+
const containerMap = this.children.containers.getView();
250+
const compTrees = Object.values(containerMap).map((container) => container.getCompTree());
251+
return mergeCompTrees(compTrees);
252+
}
253+
254+
findContainer(key: string): IContainer | undefined {
255+
const containerMap = this.children.containers.getView();
256+
for (const container of Object.values(containerMap)) {
257+
const foundContainer = container.findContainer(key);
258+
if (foundContainer) {
259+
return foundContainer === container ? this : foundContainer;
260+
}
261+
}
262+
return undefined;
263+
}
264+
265+
getPasteValue(nameGenerator: NameGenerator): JSONValue {
266+
const containerMap = this.children.containers.getView();
267+
const containerPasteValueMap = _.mapValues(containerMap, (container) =>
268+
container.getPasteValue(nameGenerator)
269+
);
270+
271+
return { ...this.toJsonValue(), containers: containerPasteValueMap };
272+
}
273+
274+
override autoHeight(): boolean {
275+
return this.children.autoHeight.getView();
276+
}
277+
}
278+
279+
export const ResponsiveLayoutComp = withExposingConfigs(
280+
ResponsiveLayoutImplComp,
281+
[ NameConfigHidden]
282+
);

client/packages/lowcoder/src/comps/controls/optionsControl.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ViewDocIcon } from "assets/icons";
2-
import { ArrayControl, BoolCodeControl, StringControl } from "comps/controls/codeControl";
2+
import { ArrayControl, BoolCodeControl, RadiusControl, StringControl } from "comps/controls/codeControl";
33
import { dropdownControl, LeftRightControl } from "comps/controls/dropdownControl";
44
import { IconControl } from "comps/controls/iconControl";
55
import { MultiCompBuilder, valueComp, withContext, withDefault } from "comps/generators";
@@ -27,6 +27,7 @@ import { getNextEntityName } from "util/stringUtils";
2727
import { JSONValue } from "util/jsonTypes";
2828
import { ButtonEventHandlerControl } from "./eventHandlerControl";
2929
import { ControlItemCompBuilder } from "comps/generators/controlCompBuilder";
30+
import { ColorControl } from "./colorControl";
3031

3132
const OptionTypes = [
3233
{
@@ -521,3 +522,51 @@ export const TabsOptionControl = manualOptionsControl(TabsOption, {
521522
uniqField: "key",
522523
autoIncField: "id",
523524
});
525+
526+
const ColumnOption = new MultiCompBuilder(
527+
{
528+
id: valueComp<number>(-1),
529+
label: StringControl,
530+
key: StringControl,
531+
minWidth: withDefault(RadiusControl, ""),
532+
background: withDefault(ColorControl, ""),
533+
border: withDefault(ColorControl, ""),
534+
radius: withDefault(RadiusControl, ""),
535+
margin: withDefault(StringControl, ""),
536+
padding: withDefault(StringControl, ""),
537+
},
538+
(props) => props
539+
)
540+
.setPropertyViewFn((children) => (
541+
<>
542+
{children.minWidth.propertyView({
543+
label: trans('responsiveLayout.minWidth')
544+
})}
545+
{children.background.propertyView({
546+
label: trans('style.background')
547+
})}
548+
{children.border.propertyView({
549+
label: trans('style.border')
550+
})}
551+
{children.radius.propertyView({
552+
label: trans('style.borderRadius')
553+
})}
554+
{children.margin.propertyView({
555+
label: trans('style.margin')
556+
})}
557+
{children.padding.propertyView({
558+
label: trans('style.padding')
559+
})}
560+
</>
561+
))
562+
.build();
563+
564+
export const ColumnOptionControl = manualOptionsControl(ColumnOption, {
565+
initOptions: [
566+
{ id: 0, key: "Column1", label: "Column1" },
567+
{ id: 1, key: "Column2", label: "Column2" },
568+
],
569+
uniqField: "key",
570+
autoIncField: "id",
571+
});
572+

client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,18 @@ export const LottieStyle = [
905905
] as const;
906906
/////////////////////
907907

908+
export const ResponsiveLayoutRowStyle = [
909+
...BG_STATIC_BORDER_RADIUS,
910+
MARGIN,
911+
PADDING,
912+
] as const;
913+
914+
export const ResponsiveLayoutColStyle = [
915+
...BG_STATIC_BORDER_RADIUS,
916+
MARGIN,
917+
PADDING,
918+
] as const;
919+
908920
export const CarouselStyle = [getBackground("canvas")] as const;
909921

910922
export const RichTextEditorStyle = [getStaticBorder(), RADIUS] as const;
@@ -943,6 +955,8 @@ export type CalendarStyleType = StyleConfigType<typeof CalendarStyle>;
943955
export type SignatureStyleType = StyleConfigType<typeof SignatureStyle>;
944956
export type CarouselStyleType = StyleConfigType<typeof CarouselStyle>;
945957
export type RichTextEditorStyleType = StyleConfigType<typeof RichTextEditorStyle>;
958+
export type ResponsiveLayoutRowStyleType = StyleConfigType<typeof ResponsiveLayoutRowStyle>;
959+
export type ResponsiveLayoutColStyleType = StyleConfigType<typeof ResponsiveLayoutColStyle>;
946960

947961
export function widthCalculator(margin: string) {
948962
const marginArr = margin?.trim().split(" ") || "";

0 commit comments

Comments
 (0)