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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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

Lines changed: 16 additions & 6 deletions
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

Lines changed: 40 additions & 1 deletion
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

Lines changed: 87 additions & 1 deletion
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

Lines changed: 21 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy