Skip to content

Commit 59324cd

Browse files
authored
Merge pull request #1513 from lowcoder-org/feat/mobile-preview
Enable device based preview (mobile/tablet/desktop) with orientations (landscape/portrait)
2 parents 9491cb9 + c3aa49a commit 59324cd

File tree

6 files changed

+179
-8
lines changed

6 files changed

+179
-8
lines changed

client/packages/lowcoder/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"file-saver": "^2.0.5",
5353
"github-markdown-css": "^5.1.0",
5454
"hotkeys-js": "^3.8.7",
55+
"html5-device-mockups": "^3.2.1",
5556
"immer": "^9.0.7",
5657
"less": "^4.1.3",
5758
"lodash": "^4.17.21",
@@ -67,6 +68,7 @@
6768
"react": "^18.2.0",
6869
"react-best-gradient-color-picker": "^3.0.10",
6970
"react-colorful": "^5.5.1",
71+
"react-device-mockups": "^0.1.12",
7072
"react-documents": "^1.2.1",
7173
"react-dom": "^18.2.0",
7274
"react-draggable": "^4.4.4",

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

+13
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export type CompInfo = {
3535

3636
type SelectSourceType = "editor" | "leftPanel" | "addComp" | "rightPanel";
3737

38+
export type DeviceType = "desktop" | "tablet" | "mobile";
39+
export type DeviceOrientation = "landscape" | "portrait";
40+
3841
/**
3942
* All editor states are placed here and are still immutable.
4043
*
@@ -56,6 +59,8 @@ export class EditorState {
5659
readonly selectedBottomResType?: BottomResTypeEnum;
5760
readonly showResultCompName: string = "";
5861
readonly selectSource?: SelectSourceType; // the source of select type
62+
readonly deviceType: DeviceType = "desktop";
63+
readonly deviceOrientation: DeviceOrientation = "portrait";
5964

6065
private readonly setEditorState: (
6166
fn: (editorState: EditorState) => EditorState
@@ -357,6 +362,14 @@ export class EditorState {
357362
this.changeState({ editorModeStatus: newEditorModeStatus });
358363
}
359364

365+
setDeviceType(type: DeviceType) {
366+
this.changeState({ deviceType: type });
367+
}
368+
369+
setDeviceOrientation(orientation: DeviceOrientation) {
370+
this.changeState({ deviceOrientation: orientation });
371+
}
372+
360373
setDragging(dragging: boolean) {
361374
if (this.isDragging === dragging) {
362375
return;

client/packages/lowcoder/src/comps/hooks/screenInfoComp.tsx

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useCallback, useEffect, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useState } from "react";
22
import { hookToStateComp } from "../generators/hookToComp";
3+
import { CanvasContainerID } from "@lowcoder-ee/index.sdk";
34

45
enum ScreenTypes {
56
Mobile = 'mobile',
@@ -19,9 +20,13 @@ type ScreenInfo = {
1920
}
2021

2122
function useScreenInfo() {
22-
const getDeviceType = () => {
23-
if (window.innerWidth < 768) return ScreenTypes.Mobile;
24-
if (window.innerWidth < 889) return ScreenTypes.Tablet;
23+
const canvasContainer = document.getElementById(CanvasContainerID);
24+
const canvas = document.getElementsByClassName('lowcoder-app-canvas')?.[0];
25+
const canvasWidth = canvasContainer?.clientWidth || canvas?.clientWidth;
26+
27+
const getDeviceType = (width: number) => {
28+
if (width < 768) return ScreenTypes.Mobile;
29+
if (width < 889) return ScreenTypes.Tablet;
2530
return ScreenTypes.Desktop;
2631
}
2732
const getFlagsByDeviceType = (deviceType: ScreenType) => {
@@ -41,16 +46,17 @@ function useScreenInfo() {
4146

4247
const getScreenInfo = useCallback(() => {
4348
const { innerWidth, innerHeight } = window;
44-
const deviceType = getDeviceType();
49+
const deviceType = getDeviceType(canvasWidth || window.innerWidth);
4550
const flags = getFlagsByDeviceType(deviceType);
4651

4752
return {
4853
width: innerWidth,
4954
height: innerHeight,
55+
canvasWidth,
5056
deviceType,
5157
...flags
5258
};
53-
}, [])
59+
}, [canvasWidth])
5460

5561
const [screenInfo, setScreenInfo] = useState<ScreenInfo>({});
5662

@@ -64,6 +70,10 @@ function useScreenInfo() {
6470
return () => window.removeEventListener('resize', updateScreenInfo);
6571
}, [ updateScreenInfo ])
6672

73+
useEffect(() => {
74+
updateScreenInfo();
75+
}, [canvasWidth]);
76+
6777
return screenInfo;
6878
}
6979

client/packages/lowcoder/src/pages/common/previewHeader.tsx

+40-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ import ProfileDropdown from "./profileDropdown";
1515
import { trans } from "i18n";
1616
import { Logo } from "@lowcoder-ee/assets/images";
1717
import { AppPermissionDialog } from "../../components/PermissionDialog/AppPermissionDialog";
18-
import { useMemo, useState } from "react";
18+
import { useContext, useMemo, useState } from "react";
1919
import { getBrandingConfig } from "../../redux/selectors/configSelectors";
2020
import { HeaderStartDropdown } from "./headerStartDropdown";
2121
import { useParams } from "react-router";
2222
import { AppPathParams } from "constants/applicationConstants";
2323
import React from "react";
24+
import Segmented from "antd/es/segmented";
25+
import MobileOutlined from "@ant-design/icons/MobileOutlined";
26+
import TabletOutlined from "@ant-design/icons/TabletOutlined";
27+
import DesktopOutlined from "@ant-design/icons/DesktopOutlined";
28+
import { DeviceOrientation, DeviceType, EditorContext } from "@lowcoder-ee/comps/editorState";
2429

2530
const HeaderFont = styled.div<{ $bgColor: string }>`
2631
font-weight: 500;
@@ -130,6 +135,7 @@ export function HeaderProfile(props: { user: User }) {
130135

131136
const PreviewHeaderComp = () => {
132137
const params = useParams<AppPathParams>();
138+
const editorState = useContext(EditorContext);
133139
const user = useSelector(getUser);
134140
const application = useSelector(currentApplication);
135141
const isPublicApp = useSelector(isPublicApplication);
@@ -197,9 +203,42 @@ const PreviewHeaderComp = () => {
197203
<HeaderProfile user={user} />
198204
</Wrapper>
199205
);
206+
207+
const headerMiddle = (
208+
<>
209+
{/* Devices */}
210+
<Segmented<DeviceType>
211+
options={[
212+
{ value: 'mobile', icon: <MobileOutlined /> },
213+
{ value: 'tablet', icon: <TabletOutlined /> },
214+
{ value: 'desktop', icon: <DesktopOutlined /> },
215+
]}
216+
value={editorState.deviceType}
217+
onChange={(value) => {
218+
editorState.setDeviceType(value);
219+
}}
220+
/>
221+
222+
{/* Orientation */}
223+
{editorState.deviceType !== 'desktop' && (
224+
<Segmented<DeviceOrientation>
225+
options={[
226+
{ value: 'portrait', label: "Portrait" },
227+
{ value: 'landscape', label: "Landscape" },
228+
]}
229+
value={editorState.deviceOrientation}
230+
onChange={(value) => {
231+
editorState.setDeviceOrientation(value);
232+
}}
233+
/>
234+
)}
235+
</>
236+
);
237+
200238
return (
201239
<Header
202240
headerStart={headerStart}
241+
headerMiddle={headerMiddle}
203242
headerEnd={headerEnd}
204243
style={{ backgroundColor: brandingConfig?.headerColor }}
205244
/>

client/packages/lowcoder/src/pages/editor/editorView.tsx

+87-1
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import {
3030
UserGuideLocationState,
3131
} from "pages/tutorials/tutorialsConstant";
3232
import React, {
33+
ReactNode,
3334
Suspense,
3435
lazy,
3536
useCallback,
3637
useContext,
38+
useEffect,
3739
useLayoutEffect,
3840
useMemo,
3941
useState,
@@ -58,6 +60,7 @@ import EditorSkeletonView from "./editorSkeletonView";
5860
import { getCommonSettings } from "@lowcoder-ee/redux/selectors/commonSettingSelectors";
5961
import { isEqual, noop } from "lodash";
6062
import { AppSettingContext, AppSettingType } from "@lowcoder-ee/comps/utils/appSettingContext";
63+
import Flex from "antd/es/flex";
6164
// import { BottomSkeleton } from "./bottom/BottomContent";
6265

6366
const Header = lazy(
@@ -251,6 +254,13 @@ export const EditorWrapper = styled.div`
251254
flex: 1 1 0;
252255
`;
253256

257+
const DeviceWrapperInner = styled(Flex)`
258+
margin: 20px 0 0;
259+
.screen {
260+
overflow: auto;
261+
}
262+
`;
263+
254264
interface EditorViewProps {
255265
uiComp: InstanceType<typeof UIComp>;
256266
preloadComp: InstanceType<typeof PreloadComp>;
@@ -298,6 +308,64 @@ const aggregationSiderItems = [
298308
}
299309
];
300310

311+
const DeviceWrapper = ({
312+
deviceType,
313+
deviceOrientation,
314+
children,
315+
}: {
316+
deviceType: string,
317+
deviceOrientation: string,
318+
children: ReactNode,
319+
}) => {
320+
const [Wrapper, setWrapper] = useState<React.ElementType | null>(null);
321+
322+
useEffect(() => {
323+
const loadWrapper = async () => {
324+
if (deviceType === "tablet") {
325+
await import('html5-device-mockups/dist/device-mockups.min.css');
326+
const { IPad } = await import("react-device-mockups");
327+
setWrapper(() => IPad);
328+
} else if (deviceType === "mobile") {
329+
await import('html5-device-mockups/dist/device-mockups.min.css');
330+
const { IPhone7 } = await import("react-device-mockups");
331+
setWrapper(() => IPhone7);
332+
} else {
333+
setWrapper(() => null);
334+
}
335+
};
336+
337+
loadWrapper();
338+
}, [deviceType]);
339+
340+
const deviceWidth = useMemo(() => {
341+
if (deviceType === 'tablet' && deviceOrientation === 'portrait') {
342+
return 700;
343+
}
344+
if (deviceType === 'tablet' && deviceOrientation === 'landscape') {
345+
return 1000;
346+
}
347+
if (deviceType === 'mobile' && deviceOrientation === 'portrait') {
348+
return 400;
349+
}
350+
if (deviceType === 'mobile' && deviceOrientation === 'landscape') {
351+
return 800;
352+
}
353+
}, [deviceType, deviceOrientation]);
354+
355+
if (!Wrapper) return <>{children}</>;
356+
357+
return (
358+
<DeviceWrapperInner justify="center">
359+
<Wrapper
360+
orientation={deviceOrientation}
361+
width={deviceWidth}
362+
>
363+
{children}
364+
</Wrapper>
365+
</DeviceWrapperInner>
366+
);
367+
}
368+
301369
function EditorView(props: EditorViewProps) {
302370
const { uiComp } = props;
303371
const params = useParams<AppPathParams>();
@@ -416,6 +484,24 @@ function EditorView(props: EditorViewProps) {
416484
uiComp,
417485
]);
418486

487+
const uiCompViewWrapper = useMemo(() => {
488+
if (isViewMode) return uiComp.getView();
489+
490+
return (
491+
<DeviceWrapper
492+
deviceType={editorState.deviceType}
493+
deviceOrientation={editorState.deviceOrientation}
494+
>
495+
{uiComp.getView()}
496+
</DeviceWrapper>
497+
)
498+
}, [
499+
uiComp,
500+
isViewMode,
501+
editorState.deviceType,
502+
editorState.deviceOrientation,
503+
]);
504+
419505
// we check if we are on the public cloud
420506
const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud';
421507
const isLocalhost = window.location.hostname === 'localhost';
@@ -455,7 +541,7 @@ function EditorView(props: EditorViewProps) {
455541
{!hideBodyHeader && <PreviewHeader />}
456542
<EditorContainerWithViewMode>
457543
<ViewBody $hideBodyHeader={hideBodyHeader} $height={height}>
458-
{uiComp.getView()}
544+
{uiCompViewWrapper}
459545
</ViewBody>
460546
<div style={{ zIndex: Layers.hooksCompContainer }}>
461547
{hookCompViews}

client/yarn.lock

+21
Original file line numberDiff line numberDiff line change
@@ -11455,6 +11455,13 @@ coolshapes-react@lowcoder-org/coolshapes-react:
1145511455
languageName: node
1145611456
linkType: hard
1145711457

11458+
"html5-device-mockups@npm:^3.2.1":
11459+
version: 3.2.1
11460+
resolution: "html5-device-mockups@npm:3.2.1"
11461+
checksum: abba0bccc6398313102a9365203092a7c0844879d1b0492168279c516c9462d2a7e016045be565bc183e3405a1ae4929402eaceb1952abdbf16f1580afa68df3
11462+
languageName: node
11463+
linkType: hard
11464+
1145811465
"http-cache-semantics@npm:^4.1.1":
1145911466
version: 4.1.1
1146011467
resolution: "http-cache-semantics@npm:4.1.1"
@@ -14159,6 +14166,7 @@ coolshapes-react@lowcoder-org/coolshapes-react:
1415914166
file-saver: ^2.0.5
1416014167
github-markdown-css: ^5.1.0
1416114168
hotkeys-js: ^3.8.7
14169+
html5-device-mockups: ^3.2.1
1416214170
http-proxy-middleware: ^2.0.6
1416314171
immer: ^9.0.7
1416414172
less: ^4.1.3
@@ -14175,6 +14183,7 @@ coolshapes-react@lowcoder-org/coolshapes-react:
1417514183
react: ^18.2.0
1417614184
react-best-gradient-color-picker: ^3.0.10
1417714185
react-colorful: ^5.5.1
14186+
react-device-mockups: ^0.1.12
1417814187
react-documents: ^1.2.1
1417914188
react-dom: ^18.2.0
1418014189
react-draggable: ^4.4.4
@@ -17672,6 +17681,18 @@ coolshapes-react@lowcoder-org/coolshapes-react:
1767217681
languageName: node
1767317682
linkType: hard
1767417683

17684+
"react-device-mockups@npm:^0.1.12":
17685+
version: 0.1.12
17686+
resolution: "react-device-mockups@npm:0.1.12"
17687+
peerDependencies:
17688+
html5-device-mockups: ^3.2.1
17689+
prop-types: ^15.5.4
17690+
react: ^15.0.0 || ^16.0.0 || ^17.0.0
17691+
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0
17692+
checksum: 738e969802c32810c2ca3ca3bd6c9bacf9b3d7adda0569c4f5c7fb1d68bab860ac7bb5a50aa2677d852143cb30ab8520e556c4dc7f53be154fd16ca08a9ba32c
17693+
languageName: node
17694+
linkType: hard
17695+
1767517696
"react-documents@npm:^1.2.1":
1767617697
version: 1.2.1
1767717698
resolution: "react-documents@npm:1.2.1"

0 commit comments

Comments
 (0)