Skip to content

Enable device based preview (mobile/tablet/desktop) with orientations (landscape/portrait) #1513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/packages/lowcoder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"file-saver": "^2.0.5",
"github-markdown-css": "^5.1.0",
"hotkeys-js": "^3.8.7",
"html5-device-mockups": "^3.2.1",
"immer": "^9.0.7",
"less": "^4.1.3",
"lodash": "^4.17.21",
Expand All @@ -67,6 +68,7 @@
"react": "^18.2.0",
"react-best-gradient-color-picker": "^3.0.10",
"react-colorful": "^5.5.1",
"react-device-mockups": "^0.1.12",
"react-documents": "^1.2.1",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.4",
Expand Down
13 changes: 13 additions & 0 deletions client/packages/lowcoder/src/comps/editorState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export type CompInfo = {

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

export type DeviceType = "desktop" | "tablet" | "mobile";
export type DeviceOrientation = "landscape" | "portrait";

/**
* All editor states are placed here and are still immutable.
*
Expand All @@ -56,6 +59,8 @@ export class EditorState {
readonly selectedBottomResType?: BottomResTypeEnum;
readonly showResultCompName: string = "";
readonly selectSource?: SelectSourceType; // the source of select type
readonly deviceType: DeviceType = "desktop";
readonly deviceOrientation: DeviceOrientation = "portrait";

private readonly setEditorState: (
fn: (editorState: EditorState) => EditorState
Expand Down Expand Up @@ -357,6 +362,14 @@ export class EditorState {
this.changeState({ editorModeStatus: newEditorModeStatus });
}

setDeviceType(type: DeviceType) {
this.changeState({ deviceType: type });
}

setDeviceOrientation(orientation: DeviceOrientation) {
this.changeState({ deviceOrientation: orientation });
}

setDragging(dragging: boolean) {
if (this.isDragging === dragging) {
return;
Expand Down
22 changes: 16 additions & 6 deletions client/packages/lowcoder/src/comps/hooks/screenInfoComp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { hookToStateComp } from "../generators/hookToComp";
import { CanvasContainerID } from "@lowcoder-ee/index.sdk";

enum ScreenTypes {
Mobile = 'mobile',
Expand All @@ -19,9 +20,13 @@ type ScreenInfo = {
}

function useScreenInfo() {
const getDeviceType = () => {
if (window.innerWidth < 768) return ScreenTypes.Mobile;
if (window.innerWidth < 889) return ScreenTypes.Tablet;
const canvasContainer = document.getElementById(CanvasContainerID);
const canvas = document.getElementsByClassName('lowcoder-app-canvas')?.[0];
const canvasWidth = canvasContainer?.clientWidth || canvas?.clientWidth;

const getDeviceType = (width: number) => {
if (width < 768) return ScreenTypes.Mobile;
if (width < 889) return ScreenTypes.Tablet;
return ScreenTypes.Desktop;
}
const getFlagsByDeviceType = (deviceType: ScreenType) => {
Expand All @@ -41,16 +46,17 @@ function useScreenInfo() {

const getScreenInfo = useCallback(() => {
const { innerWidth, innerHeight } = window;
const deviceType = getDeviceType();
const deviceType = getDeviceType(canvasWidth || window.innerWidth);
const flags = getFlagsByDeviceType(deviceType);

return {
width: innerWidth,
height: innerHeight,
canvasWidth,
deviceType,
...flags
};
}, [])
}, [canvasWidth])

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

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

useEffect(() => {
updateScreenInfo();
}, [canvasWidth]);

return screenInfo;
}

Expand Down
41 changes: 40 additions & 1 deletion client/packages/lowcoder/src/pages/common/previewHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ import ProfileDropdown from "./profileDropdown";
import { trans } from "i18n";
import { Logo } from "@lowcoder-ee/assets/images";
import { AppPermissionDialog } from "../../components/PermissionDialog/AppPermissionDialog";
import { useMemo, useState } from "react";
import { useContext, useMemo, useState } from "react";
import { getBrandingConfig } from "../../redux/selectors/configSelectors";
import { HeaderStartDropdown } from "./headerStartDropdown";
import { useParams } from "react-router";
import { AppPathParams } from "constants/applicationConstants";
import React from "react";
import Segmented from "antd/es/segmented";
import MobileOutlined from "@ant-design/icons/MobileOutlined";
import TabletOutlined from "@ant-design/icons/TabletOutlined";
import DesktopOutlined from "@ant-design/icons/DesktopOutlined";
import { DeviceOrientation, DeviceType, EditorContext } from "@lowcoder-ee/comps/editorState";

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

const PreviewHeaderComp = () => {
const params = useParams<AppPathParams>();
const editorState = useContext(EditorContext);
const user = useSelector(getUser);
const application = useSelector(currentApplication);
const isPublicApp = useSelector(isPublicApplication);
Expand Down Expand Up @@ -197,9 +203,42 @@ const PreviewHeaderComp = () => {
<HeaderProfile user={user} />
</Wrapper>
);

const headerMiddle = (
<>
{/* Devices */}
<Segmented<DeviceType>
options={[
{ value: 'mobile', icon: <MobileOutlined /> },
{ value: 'tablet', icon: <TabletOutlined /> },
{ value: 'desktop', icon: <DesktopOutlined /> },
]}
value={editorState.deviceType}
onChange={(value) => {
editorState.setDeviceType(value);
}}
/>

{/* Orientation */}
{editorState.deviceType !== 'desktop' && (
<Segmented<DeviceOrientation>
options={[
{ value: 'portrait', label: "Portrait" },
{ value: 'landscape', label: "Landscape" },
]}
value={editorState.deviceOrientation}
onChange={(value) => {
editorState.setDeviceOrientation(value);
}}
/>
)}
</>
);

return (
<Header
headerStart={headerStart}
headerMiddle={headerMiddle}
headerEnd={headerEnd}
style={{ backgroundColor: brandingConfig?.headerColor }}
/>
Expand Down
88 changes: 87 additions & 1 deletion client/packages/lowcoder/src/pages/editor/editorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import {
UserGuideLocationState,
} from "pages/tutorials/tutorialsConstant";
import React, {
ReactNode,
Suspense,
lazy,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useState,
Expand All @@ -58,6 +60,7 @@ import EditorSkeletonView from "./editorSkeletonView";
import { getCommonSettings } from "@lowcoder-ee/redux/selectors/commonSettingSelectors";
import { isEqual, noop } from "lodash";
import { AppSettingContext, AppSettingType } from "@lowcoder-ee/comps/utils/appSettingContext";
import Flex from "antd/es/flex";
// import { BottomSkeleton } from "./bottom/BottomContent";

const Header = lazy(
Expand Down Expand Up @@ -251,6 +254,13 @@ export const EditorWrapper = styled.div`
flex: 1 1 0;
`;

const DeviceWrapperInner = styled(Flex)`
margin: 20px 0 0;
.screen {
overflow: auto;
}
`;

interface EditorViewProps {
uiComp: InstanceType<typeof UIComp>;
preloadComp: InstanceType<typeof PreloadComp>;
Expand Down Expand Up @@ -298,6 +308,64 @@ const aggregationSiderItems = [
}
];

const DeviceWrapper = ({
deviceType,
deviceOrientation,
children,
}: {
deviceType: string,
deviceOrientation: string,
children: ReactNode,
}) => {
const [Wrapper, setWrapper] = useState<React.ElementType | null>(null);

useEffect(() => {
const loadWrapper = async () => {
if (deviceType === "tablet") {
await import('html5-device-mockups/dist/device-mockups.min.css');
const { IPad } = await import("react-device-mockups");
setWrapper(() => IPad);
} else if (deviceType === "mobile") {
await import('html5-device-mockups/dist/device-mockups.min.css');
const { IPhone7 } = await import("react-device-mockups");
setWrapper(() => IPhone7);
} else {
setWrapper(() => null);
}
};

loadWrapper();
}, [deviceType]);

const deviceWidth = useMemo(() => {
if (deviceType === 'tablet' && deviceOrientation === 'portrait') {
return 700;
}
if (deviceType === 'tablet' && deviceOrientation === 'landscape') {
return 1000;
}
if (deviceType === 'mobile' && deviceOrientation === 'portrait') {
return 400;
}
if (deviceType === 'mobile' && deviceOrientation === 'landscape') {
return 800;
}
}, [deviceType, deviceOrientation]);

if (!Wrapper) return <>{children}</>;

return (
<DeviceWrapperInner justify="center">
<Wrapper
orientation={deviceOrientation}
width={deviceWidth}
>
{children}
</Wrapper>
</DeviceWrapperInner>
);
}

function EditorView(props: EditorViewProps) {
const { uiComp } = props;
const params = useParams<AppPathParams>();
Expand Down Expand Up @@ -416,6 +484,24 @@ function EditorView(props: EditorViewProps) {
uiComp,
]);

const uiCompViewWrapper = useMemo(() => {
if (isViewMode) return uiComp.getView();

return (
<DeviceWrapper
deviceType={editorState.deviceType}
deviceOrientation={editorState.deviceOrientation}
>
{uiComp.getView()}
</DeviceWrapper>
)
}, [
uiComp,
isViewMode,
editorState.deviceType,
editorState.deviceOrientation,
]);

// we check if we are on the public cloud
const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud';
const isLocalhost = window.location.hostname === 'localhost';
Expand Down Expand Up @@ -455,7 +541,7 @@ function EditorView(props: EditorViewProps) {
{!hideBodyHeader && <PreviewHeader />}
<EditorContainerWithViewMode>
<ViewBody $hideBodyHeader={hideBodyHeader} $height={height}>
{uiComp.getView()}
{uiCompViewWrapper}
</ViewBody>
<div style={{ zIndex: Layers.hooksCompContainer }}>
{hookCompViews}
Expand Down
21 changes: 21 additions & 0 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11455,6 +11455,13 @@ coolshapes-react@lowcoder-org/coolshapes-react:
languageName: node
linkType: hard

"html5-device-mockups@npm:^3.2.1":
version: 3.2.1
resolution: "html5-device-mockups@npm:3.2.1"
checksum: abba0bccc6398313102a9365203092a7c0844879d1b0492168279c516c9462d2a7e016045be565bc183e3405a1ae4929402eaceb1952abdbf16f1580afa68df3
languageName: node
linkType: hard

"http-cache-semantics@npm:^4.1.1":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
Expand Down Expand Up @@ -14159,6 +14166,7 @@ coolshapes-react@lowcoder-org/coolshapes-react:
file-saver: ^2.0.5
github-markdown-css: ^5.1.0
hotkeys-js: ^3.8.7
html5-device-mockups: ^3.2.1
http-proxy-middleware: ^2.0.6
immer: ^9.0.7
less: ^4.1.3
Expand All @@ -14175,6 +14183,7 @@ coolshapes-react@lowcoder-org/coolshapes-react:
react: ^18.2.0
react-best-gradient-color-picker: ^3.0.10
react-colorful: ^5.5.1
react-device-mockups: ^0.1.12
react-documents: ^1.2.1
react-dom: ^18.2.0
react-draggable: ^4.4.4
Expand Down Expand Up @@ -17672,6 +17681,18 @@ coolshapes-react@lowcoder-org/coolshapes-react:
languageName: node
linkType: hard

"react-device-mockups@npm:^0.1.12":
version: 0.1.12
resolution: "react-device-mockups@npm:0.1.12"
peerDependencies:
html5-device-mockups: ^3.2.1
prop-types: ^15.5.4
react: ^15.0.0 || ^16.0.0 || ^17.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0
checksum: 738e969802c32810c2ca3ca3bd6c9bacf9b3d7adda0569c4f5c7fb1d68bab860ac7bb5a50aa2677d852143cb30ab8520e556c4dc7f53be154fd16ca08a9ba32c
languageName: node
linkType: hard

"react-documents@npm:^1.2.1":
version: 1.2.1
resolution: "react-documents@npm:1.2.1"
Expand Down
Loading