diff --git a/client/packages/lowcoder-design/src/icons/icon-responsive-layout-comp.svg b/client/packages/lowcoder-design/src/icons/icon-responsive-layout-comp.svg new file mode 100644 index 000000000..f26e79095 --- /dev/null +++ b/client/packages/lowcoder-design/src/icons/icon-responsive-layout-comp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/lowcoder-design/src/icons/icon-width.svg b/client/packages/lowcoder-design/src/icons/icon-width.svg new file mode 100644 index 000000000..1db8d6356 --- /dev/null +++ b/client/packages/lowcoder-design/src/icons/icon-width.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/packages/lowcoder-design/src/icons/index.ts b/client/packages/lowcoder-design/src/icons/index.ts index 4ac1b051e..6c9c2eb2b 100644 --- a/client/packages/lowcoder-design/src/icons/index.ts +++ b/client/packages/lowcoder-design/src/icons/index.ts @@ -291,3 +291,5 @@ export { ReactComponent as TimeLineIcon } from "icons/icon-timeline-comp.svg" export { ReactComponent as LottieIcon } from "icons/icon-lottie.svg"; export { ReactComponent as MentionIcon } from "icons/icon-mention-comp.svg"; export { ReactComponent as AutoCompleteCompIcon } from "icons/icon-autocomplete-comp.svg"; +export { ReactComponent as WidthIcon } from "icons/icon-width.svg"; +export { ReactComponent as ResponsiveLayoutCompIcon } from "icons/icon-responsive-layout-comp.svg"; diff --git a/client/packages/lowcoder/src/comps/comps/responsiveLayout/index.tsx b/client/packages/lowcoder/src/comps/comps/responsiveLayout/index.tsx new file mode 100644 index 000000000..668003a69 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/responsiveLayout/index.tsx @@ -0,0 +1 @@ +export { ResponsiveLayoutComp } from "./responsiveLayout"; diff --git a/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx new file mode 100644 index 000000000..53b06ede3 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/responsiveLayout/responsiveLayout.tsx @@ -0,0 +1,335 @@ +import { Row, Col } from "antd"; +import { JSONObject, JSONValue } from "util/jsonTypes"; +import { CompAction, CompActionTypes, deleteCompAction, wrapChildAction } from "lowcoder-core"; +import { DispatchType, RecordConstructorToView, wrapDispatch } from "lowcoder-core"; +import { AutoHeightControl } from "comps/controls/autoHeightControl"; +import { ColumnOptionControl } from "comps/controls/optionsControl"; +import { styleControl } from "comps/controls/styleControl"; +import { + ResponsiveLayoutRowStyle, + ResponsiveLayoutRowStyleType, + ResponsiveLayoutColStyleType, + ResponsiveLayoutColStyle +} from "comps/controls/styleControlConstants"; +import { sameTypeMap, UICompBuilder, withDefault } from "comps/generators"; +import { addMapChildAction } from "comps/generators/sameTypeMap"; +import { NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing"; +import { NameGenerator } from "comps/utils"; +import { Section, controlItem, sectionNames } from "lowcoder-design"; +import { HintPlaceHolder } from "lowcoder-design"; +import _ from "lodash"; +import React from "react"; +import styled from "styled-components"; +import { IContainer } from "../containerBase/iContainer"; +import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; +import { CompTree, mergeCompTrees } from "../containerBase/utils"; +import { + ContainerBaseProps, + gridItemCompToGridItems, + InnerGrid, +} from "../containerComp/containerView"; +import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; +import { trans } from "i18n"; +import { messageInstance } from "lowcoder-design"; +import { BoolControl } from "comps/controls/boolControl"; +import { NumberControl } from "comps/controls/codeControl"; + +const RowWrapper = styled(Row)<{$style: ResponsiveLayoutRowStyleType}>` + height: 100%; + border: 1px solid ${(props) => props.$style.border}; + border-radius: ${(props) => props.$style.radius}; + padding: ${(props) => props.$style.padding}; + background-color: ${(props) => props.$style.background}; + overflow-x: auto; +`; + +const ColWrapper = styled(Col)<{ + $style: ResponsiveLayoutColStyleType, + $minWidth?: string, + $matchColumnsHeight: boolean, +}>` + min-width: ${(props) => props.$minWidth}; + display: flex; + flex-direction: column; + + > div { + height: ${(props) => props.$matchColumnsHeight ? '100%' : 'auto'}; + } +`; + +const childrenMap = { + columns: ColumnOptionControl, + containers: withDefault(sameTypeMap(SimpleContainerComp), { + 0: { view: {}, layout: {} }, + 1: { view: {}, layout: {} }, + }), + autoHeight: AutoHeightControl, + rowBreak: withDefault(BoolControl, false), + matchColumnsHeight: withDefault(BoolControl, false), + rowStyle: withDefault(styleControl(ResponsiveLayoutRowStyle), {}), + columnStyle: withDefault(styleControl(ResponsiveLayoutColStyle), {}), + columnPerRowLG: withDefault(NumberControl, 4), + columnPerRowMD: withDefault(NumberControl, 2), + columnPerRowSM: withDefault(NumberControl, 1), + verticalSpacing: withDefault(NumberControl, 8), + horizontalSpacing: withDefault(NumberControl, 8), +}; + +type ViewProps = RecordConstructorToView; +type ResponsiveLayoutProps = ViewProps & { dispatch: DispatchType }; +type ColumnContainerProps = Omit & { + style: ResponsiveLayoutColStyleType, +} + +const ColumnContainer = (props: ColumnContainerProps) => { + return ( + + ); +}; + + +const ResponsiveLayout = (props: ResponsiveLayoutProps) => { + let { + columns, + containers, + dispatch, + rowBreak, + matchColumnsHeight, + rowStyle, + columnStyle, + columnPerRowLG, + columnPerRowMD, + columnPerRowSM, + verticalSpacing, + horizontalSpacing, + } = props; + + return ( + +
+ + {columns.map(column => { + const id = String(column.id); + const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); + if(!containers[id]) return null + const containerProps = containers[id].children; + + const columnCustomStyle = { + margin: !_.isEmpty(column.margin) ? column.margin : columnStyle.margin, + padding: !_.isEmpty(column.padding) ? column.padding : columnStyle.padding, + radius: !_.isEmpty(column.radius) ? column.radius : columnStyle.radius, + border: `1px solid ${!_.isEmpty(column.border) ? column.border : columnStyle.border}`, + background: !_.isEmpty(column.background) ? column.background : columnStyle.background, + } + const noOfColumns = columns.length; + let backgroundStyle = columnCustomStyle.background; + if(!_.isEmpty(column.backgroundImage)) { + backgroundStyle = `center / cover url('${column.backgroundImage}') no-repeat, ${backgroundStyle}`; + } + return ( + + + + ) + }) + } + +
+
+ ); +}; + +export const ResponsiveLayoutBaseComp = (function () { + return new UICompBuilder(childrenMap, (props, dispatch) => { + return ( + + ); + }) + .setPropertyViewFn((children) => { + return ( + <> +
+ {children.columns.propertyView({ + title: trans("responsiveLayout.column"), + newOptionLabel: "Column", + })} + {children.autoHeight.getPropertyView()} +
+
+ {children.rowBreak.propertyView({ + label: trans("responsiveLayout.rowBreak") + })} + {controlItem({}, ( +
+ {trans("responsiveLayout.columnsPerRow")} +
+ ))} + {children.columnPerRowLG.propertyView({ + label: trans("responsiveLayout.desktop") + })} + {children.columnPerRowMD.propertyView({ + label: trans("responsiveLayout.tablet") + })} + {children.columnPerRowSM.propertyView({ + label: trans("responsiveLayout.mobile") + })} +
+
+ {children.matchColumnsHeight.propertyView({ + label: trans("responsiveLayout.matchColumnsHeight") + })} + {controlItem({}, ( +
+ {trans("responsiveLayout.columnsSpacing")} +
+ ))} + {children.horizontalSpacing.propertyView({ + label: trans("responsiveLayout.horizontal") + })} + {children.verticalSpacing.propertyView({ + label: trans("responsiveLayout.vertical") + })} +
+
+ {children.rowStyle.getPropertyView()} +
+
+ {children.columnStyle.getPropertyView()} +
+ + ); + }) + .build(); +})(); + +class ResponsiveLayoutImplComp extends ResponsiveLayoutBaseComp implements IContainer { + private syncContainers(): this { + const columns = this.children.columns.getView(); + const ids: Set = new Set(columns.map((column) => String(column.id))); + let containers = this.children.containers.getView(); + // delete + const actions: CompAction[] = []; + Object.keys(containers).forEach((id) => { + if (!ids.has(id)) { + // log.debug("syncContainers delete. ids=", ids, " id=", id); + actions.push(wrapChildAction("containers", wrapChildAction(id, deleteCompAction()))); + } + }); + // new + ids.forEach((id) => { + if (!containers.hasOwnProperty(id)) { + // log.debug("syncContainers new containers: ", containers, " id: ", id); + actions.push( + wrapChildAction("containers", addMapChildAction(id, { layout: {}, items: {} })) + ); + } + }); + // log.debug("syncContainers. actions: ", actions); + let instance = this; + actions.forEach((action) => { + instance = instance.reduce(action); + }); + return instance; + } + + override reduce(action: CompAction): this { + const columns = this.children.columns.getView(); + if (action.type === CompActionTypes.CUSTOM) { + const value = action.value as JSONObject; + if (value.type === "push") { + const itemValue = value.value as JSONObject; + if (_.isEmpty(itemValue.key)) itemValue.key = itemValue.label; + action = { + ...action, + value: { + ...value, + value: { ...itemValue }, + }, + } as CompAction; + } + if (value.type === "delete" && columns.length <= 1) { + messageInstance.warning(trans("responsiveLayout.atLeastOneColumnError")); + // at least one column + return this; + } + } + // log.debug("before super reduce. action: ", action); + let newInstance = super.reduce(action); + if (action.type === CompActionTypes.UPDATE_NODES_V2) { + // Need eval to get the value in StringControl + newInstance = newInstance.syncContainers(); + } + // log.debug("reduce. instance: ", this, " newInstance: ", newInstance); + return newInstance; + } + + realSimpleContainer(key?: string): SimpleContainerComp | undefined { + return Object.values(this.children.containers.children).find((container) => + container.realSimpleContainer(key) + ); + } + + getCompTree(): CompTree { + const containerMap = this.children.containers.getView(); + const compTrees = Object.values(containerMap).map((container) => container.getCompTree()); + return mergeCompTrees(compTrees); + } + + findContainer(key: string): IContainer | undefined { + const containerMap = this.children.containers.getView(); + for (const container of Object.values(containerMap)) { + const foundContainer = container.findContainer(key); + if (foundContainer) { + return foundContainer === container ? this : foundContainer; + } + } + return undefined; + } + + getPasteValue(nameGenerator: NameGenerator): JSONValue { + const containerMap = this.children.containers.getView(); + const containerPasteValueMap = _.mapValues(containerMap, (container) => + container.getPasteValue(nameGenerator) + ); + + return { ...this.toJsonValue(), containers: containerPasteValueMap }; + } + + override autoHeight(): boolean { + return this.children.autoHeight.getView(); + } +} + +export const ResponsiveLayoutComp = withExposingConfigs( + ResponsiveLayoutImplComp, + [ NameConfigHidden] +); diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index cfed15f55..f4d04bff7 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -196,20 +196,6 @@ const TabbedContainer = (props: TabbedContainerProps) => { ) } - // return ( - // - // - // - // - // - // ); }) return ( diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index 05f1f6d63..24dd5f42f 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -1,5 +1,5 @@ import { ViewDocIcon } from "assets/icons"; -import { ArrayControl, BoolCodeControl, StringControl } from "comps/controls/codeControl"; +import { ArrayControl, BoolCodeControl, RadiusControl, StringControl } from "comps/controls/codeControl"; import { dropdownControl, LeftRightControl } from "comps/controls/dropdownControl"; import { IconControl } from "comps/controls/iconControl"; import { MultiCompBuilder, valueComp, withContext, withDefault } from "comps/generators"; @@ -20,13 +20,24 @@ import { MultiBaseComp, withFunction, } from "lowcoder-core"; -import { AutoArea, controlItem, Option } from "lowcoder-design"; +import { + AutoArea, + CompressIcon, + controlItem, + ExpandIcon, + IconRadius, + Option, + WidthIcon, + ImageCompIcon, +} from "lowcoder-design"; import styled from "styled-components"; import { lastValueIfEqual } from "util/objectUtils"; import { getNextEntityName } from "util/stringUtils"; import { JSONValue } from "util/jsonTypes"; import { ButtonEventHandlerControl } from "./eventHandlerControl"; import { ControlItemCompBuilder } from "comps/generators/controlCompBuilder"; +import { ColorControl } from "./colorControl"; +import { StringStateControl } from "./codeStateControl"; const OptionTypes = [ { @@ -521,3 +532,95 @@ export const TabsOptionControl = manualOptionsControl(TabsOption, { uniqField: "key", autoIncField: "id", }); + +const StyledIcon = styled.span` + margin: 0 4px 0 14px; +`; + +const StyledContent = styled.div` + > div { + margin: 4px 0; + flex-direction: row; + gap: 4px 0px; + flex-wrap: wrap; + + > div:nth-of-type(1) { + flex: 0 0 96px; + + div { + line-height: 16px; + } + } + + > svg { + height: 30px; + width: 25px; + } + + > div:nth-of-type(2) { + flex: 1 1 auto; + } + } +`; + +const ColumnOption = new MultiCompBuilder( + { + id: valueComp(-1), + label: StringControl, + key: StringControl, + minWidth: withDefault(RadiusControl, ""), + background: withDefault(ColorControl, ""), + backgroundImage: withDefault(StringControl, ""), + border: withDefault(ColorControl, ""), + radius: withDefault(RadiusControl, ""), + margin: withDefault(StringControl, ""), + padding: withDefault(StringControl, ""), + }, + (props) => props +) +.setPropertyViewFn((children) => ( + + {children.minWidth.propertyView({ + label: trans('responsiveLayout.minWidth'), + preInputNode: , + placeholder: '3px', + })} + {children.background.propertyView({ + label: trans('style.background'), + })} + {children.backgroundImage.propertyView({ + label: `Background Image`, + // preInputNode: , + placeholder: 'https://temp.im/350x400', + })} + {children.border.propertyView({ + label: trans('style.border') + })} + {children.radius.propertyView({ + label: trans('style.borderRadius'), + preInputNode: , + placeholder: '3px', + })} + {children.margin.propertyView({ + label: trans('style.margin'), + preInputNode: , + placeholder: '3px', + })} + {children.padding.propertyView({ + label: trans('style.padding'), + preInputNode: , + placeholder: '3px', + })} + +)) + .build(); + +export const ColumnOptionControl = manualOptionsControl(ColumnOption, { + initOptions: [ + { id: 0, key: "Column1", label: "Column1" }, + { id: 1, key: "Column2", label: "Column2" }, + ], + uniqField: "key", + autoIncField: "id", +}); + diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index a71c404ef..646de9c6e 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -905,6 +905,18 @@ export const LottieStyle = [ ] as const; ///////////////////// +export const ResponsiveLayoutRowStyle = [ + ...BG_STATIC_BORDER_RADIUS, + MARGIN, + PADDING, +] as const; + +export const ResponsiveLayoutColStyle = [ + ...BG_STATIC_BORDER_RADIUS, + MARGIN, + PADDING, +] as const; + export const CarouselStyle = [getBackground("canvas")] as const; export const RichTextEditorStyle = [getStaticBorder(), RADIUS] as const; @@ -943,6 +955,8 @@ export type CalendarStyleType = StyleConfigType; export type SignatureStyleType = StyleConfigType; export type CarouselStyleType = StyleConfigType; export type RichTextEditorStyleType = StyleConfigType; +export type ResponsiveLayoutRowStyleType = StyleConfigType; +export type ResponsiveLayoutColStyleType = StyleConfigType; export function widthCalculator(margin: string) { const marginArr = margin?.trim().replace(/\s+/g,' ').split(" ") || ""; diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 5dc0f2923..16668379d 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -96,6 +96,7 @@ import { LottieIcon, MentionIcon, AutoCompleteCompIcon, + ResponsiveLayoutCompIcon, } from "lowcoder-design"; import { defaultFormData, FormComp } from "./comps/formComp/formComp"; @@ -126,6 +127,7 @@ import { MentionComp } from "./comps/textInputComp/mentionComp"; import { AutoCompleteComp } from "./comps/autoCompleteComp/autoCompleteComp" //Added by Aqib Mirza import { JsonLottieComp } from "./comps/jsonComp/jsonLottieComp"; +import { ResponsiveLayoutComp } from "./comps/responsiveLayout"; type Registry = { [key in UICompType]?: UICompManifest; @@ -879,6 +881,21 @@ const uiCompMap: Registry = { layoutInfo: { w: 7, h: 5, + } + }, + responsiveLayout: { + name: trans("uiComp.responsiveLayoutCompName"), + enName: "Responsive Layout", + description: trans("uiComp.responsiveLayoutCompDesc"), + categories: ["container", "common"], + icon: ResponsiveLayoutCompIcon, + keywords: trans("uiComp.responsiveLayoutCompKeywords"), + comp: ResponsiveLayoutComp, + withoutLoading: true, + layoutInfo: { + w: 15, + h: 27, + delayCollision: true, }, }, }; diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index 6cd63e920..523409a01 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -114,6 +114,7 @@ export type UICompType = | "timeline" | "mention" | "autocomplete" + | "responsiveLayout" export const uiCompRegistry = {} as Record; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 89deb898d..58bc84f5d 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -342,6 +342,7 @@ export const en = { containerheaderpadding: "Header Padding", containerfooterpadding: "Footer Padding", containerbodypadding: "Body Padding", + minWidth: "Minimum Width", }, export: { hiddenDesc: "If true, the component is hidden", @@ -853,6 +854,9 @@ export const en = { autoCompleteCompName: "autoComplete", autoCompleteCompDesc: "autoComplete", autoCompleteCompKeywords: "", + responsiveLayoutCompName: "Responsive Layout", + responsiveLayoutCompDesc: "Responsive Layout", + responsiveLayoutCompKeywords: "", }, comp: { menuViewDocs: "View documentation", @@ -2509,4 +2513,22 @@ export const en = { helpLabel: "label", helpValue: "value", }, + responsiveLayout: { + column: "Columns", + atLeastOneColumnError: "Responsive layout keeps at least one Column", + columnsPerRow: "Columns per Row", + columnsSpacing: "Columns Spacing (px)", + horizontal: "Horizontal", + vertical: "Vertical", + mobile: "Mobile", + tablet: "Tablet", + desktop: "Desktop", + rowStyle: "Row Style", + columnStyle: "Column Style", + minWidth: "Min. Width", + rowBreak: "Row Break", + matchColumnsHeight: "Match Columns Height", + rowLayout: "Row Layout", + columnsLayout: "Columns Layout", + }, }; diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 27c4e21be..7cdfb89b2 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -338,6 +338,7 @@ style: { containerheaderpadding: "上内边距", containerfooterpadding: "下内边距", containerbodypadding: "内边距", + minWidth: "最小宽度", }, export: { hiddenDesc: "如果为true,则隐藏组件", @@ -836,6 +837,9 @@ uiComp: { autoCompleteCompName: "自动完成", autoCompleteCompDesc: "自动完成", autoCompleteCompKeywords: "zdwc", + responsiveLayoutCompName: "响应式布局", + responsiveLayoutCompDesc: "响应式布局", + responsiveLayoutCompKeywords: "", }, comp: { menuViewDocs: "查看文档", @@ -2499,5 +2503,23 @@ timeLine: { helpLabel: "标签", helpValue: "值", }, + responsiveLayout: { + column: "列", + atLeastOneColumnError: "响应式布局至少保留一列", + columnsPerRow: "每行列数", + columnsSpacing: "列间距 (px)", + horizontal: "水平的", + vertical: "垂直的", + mobile: "移动的", + tablet: "药片", + desktop: "桌面", + rowStyle: "行式", + columnStyle: "栏目样式", + minWidth: "分钟。宽度", + rowBreak: "断行", + matchColumnsHeight: "匹配列高度", + rowLayout: "行布局", + columnsLayout: "栏目布局", + } }; diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index a09a9a56e..345baf396 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -39,6 +39,7 @@ import { TimeLineIcon, MentionIcon, AutoCompleteCompIcon, + ResponsiveLayoutCompIcon, } from "lowcoder-design"; export const CompStateIcon: { @@ -107,4 +108,5 @@ export const CompStateIcon: { timeline: , mention: , autocomplete: , + responsiveLayout: , }; diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index d8b17b0ac..07f8d5aeb 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -302,7 +302,6 @@ function EditorView(props: EditorViewProps) { setMenuKey(params.key); }; const appSettingsComp = editorState.getAppSettingsComp(); - return ( {