diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 028bfe9d83..b6a451d988 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -323,26 +323,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { }, }; -// breakpoints -// ----------------------------------------------------------------------------- - -// mobile: up to 699px -export const MQ_MAX_MOBILE = 599; - -export const MQ_MAX_WIDTH_LANDSCAPE = 1000; -export const MQ_MAX_HEIGHT_LANDSCAPE = 500; - -// tablets -export const MQ_MIN_TABLET = MQ_MAX_MOBILE + 1; // lower bound (excludes phones) -export const MQ_MAX_TABLET = 1400; // upper bound (excludes laptops/desktops) - -// desktop/laptop -export const MQ_MIN_WIDTH_DESKTOP = 1440; - -// sidebar -export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229; -// ----------------------------------------------------------------------------- - export const MAX_DECIMALS_FOR_SVG_EXPORT = 2; export const EXPORT_SCALES = [1, 2, 3]; diff --git a/packages/common/src/editorInterface.ts b/packages/common/src/editorInterface.ts index c0d3cd31b7..5473a085b4 100644 --- a/packages/common/src/editorInterface.ts +++ b/packages/common/src/editorInterface.ts @@ -13,8 +13,29 @@ export type EditorInterface = Readonly<{ isLandscape: boolean; }>; +// storage key export const DESKTOP_UI_MODE_STORAGE_KEY = "excalidraw.desktopUIMode"; +// breakpoints +// mobile: up to 699px +export const MQ_MAX_MOBILE = 599; + +export const MQ_MAX_WIDTH_LANDSCAPE = 1000; +export const MQ_MAX_HEIGHT_LANDSCAPE = 500; + +// tablets +export const MQ_MIN_TABLET = MQ_MAX_MOBILE + 1; // lower bound (excludes phones) +export const MQ_MAX_TABLET = 1400; // upper bound (excludes laptops/desktops) + +// desktop/laptop +export const MQ_MIN_WIDTH_DESKTOP = 1440; + +// sidebar +export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229; + +// ----------------------------------------------------------------------------- + +// user agent detections export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform); export const isAndroid = /\b(android)\b/i.test(navigator.userAgent); @@ -41,6 +62,24 @@ export const isMobile = ) || /android|ios|ipod|blackberry|windows phone/i.test(navigator.platform); +// utilities +export const isMobileBreakpoint = (width: number, height: number) => { + return ( + width <= MQ_MAX_MOBILE || + (height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE) + ); +}; + +export const isTabletBreakpoint = ( + editorWidth: number, + editorHeight: number, +) => { + const minSide = Math.min(editorWidth, editorHeight); + const maxSide = Math.max(editorWidth, editorHeight); + + return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET; +}; + export const isMobileOrTablet = (): boolean => { const ua = navigator.userAgent || ""; const platform = navigator.platform || ""; @@ -97,19 +136,15 @@ export const isMobileOrTablet = (): boolean => { return false; }; -export const deriveFormFactor = ( +export const getFormFactor = ( editorWidth: number, editorHeight: number, - breakpoints: { - isMobile: (width: number, height: number) => boolean; - isTablet: (width: number, height: number) => boolean; - }, ): EditorInterface["formFactor"] => { - if (breakpoints.isMobile(editorWidth, editorHeight)) { + if (isMobileBreakpoint(editorWidth, editorHeight)) { return "phone"; } - if (breakpoints.isTablet(editorWidth, editorHeight)) { + if (isTabletBreakpoint(editorWidth, editorHeight)) { return "tablet"; } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index ddcf872d99..64a111197f 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -96,14 +96,9 @@ import { Emitter, MINIMUM_ARROW_SIZE, DOUBLE_TAP_POSITION_THRESHOLD, - MQ_MAX_MOBILE, - MQ_MIN_TABLET, - MQ_MAX_TABLET, - MQ_MAX_HEIGHT_LANDSCAPE, - MQ_MAX_WIDTH_LANDSCAPE, DESKTOP_UI_MODE_STORAGE_KEY, createUserAgentDescriptor, - deriveFormFactor, + getFormFactor, deriveStylesPanelMode, isIOS, isBrave, @@ -755,7 +750,9 @@ class App extends React.Component { props.UIOptions.desktopUIMode ?? storedDesktopUIMode ?? this.editorInterface.desktopUIMode, - formFactor: props.UIOptions.formFactor ?? this.editorInterface.formFactor, + formFactor: + props.UIOptions.formFactor ?? + getFormFactor(this.state.width, this.state.height), userAgent: userAgentDescriptor, }); this.stylesPanelMode = deriveStylesPanelMode(this.editorInterface); @@ -806,6 +803,7 @@ class App extends React.Component { setActiveTool: this.setActiveTool, setCursor: this.setCursor, resetCursor: this.resetCursor, + getFormFactor: () => getFormFactor(this.state.width, this.state.height), updateFrameRendering: this.updateFrameRendering, toggleSidebar: this.toggleSidebar, onChange: (cb) => this.onChangeEmitter.on(cb), @@ -2498,20 +2496,6 @@ class App extends React.Component { } }; - private isMobileBreakpoint = (width: number, height: number) => { - return ( - width <= MQ_MAX_MOBILE || - (height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE) - ); - }; - - private isTabletBreakpoint = (editorWidth: number, editorHeight: number) => { - const minSide = Math.min(editorWidth, editorHeight); - const maxSide = Math.max(editorWidth, editorHeight); - - return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET; - }; - private refreshEditorInterface = () => { const container = this.excalidrawContainerRef.current; if (!container) { @@ -2526,27 +2510,10 @@ class App extends React.Component { ? this.props.UIOptions.dockedSidebarBreakpoint : MQ_RIGHT_SIDEBAR_MIN_WIDTH; - // if host doesn't control formFactor, we'll update it ourselves - if (!this.props.UIOptions.formFactor) { - const nextEditorInterface = updateObject(this.editorInterface, { - formFactor: deriveFormFactor(editorWidth, editorHeight, { - isMobile: (width, height) => this.isMobileBreakpoint(width, height), - isTablet: (width, height) => this.isTabletBreakpoint(width, height), - }), - canFitSidebar: editorWidth > sidebarBreakpoint, - isLandscape: editorWidth > editorHeight, - }); - const didChange = nextEditorInterface !== this.editorInterface; - if (didChange) { - this.editorInterface = nextEditorInterface; - this.reconcileStylesPanelMode(nextEditorInterface); - this.props.UIOptions.onEditorInterfaceChange?.(nextEditorInterface); - } - return didChange; - } - - // host controls formFactor, just update sidebar/landscape for context const nextEditorInterface = updateObject(this.editorInterface, { + formFactor: + this.props.UIOptions.formFactor ?? + getFormFactor(editorWidth, editorHeight), canFitSidebar: editorWidth > sidebarBreakpoint, isLandscape: editorWidth > editorHeight, }); diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 02dee807e2..127a600c32 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -857,6 +857,10 @@ export interface ExcalidrawImperativeAPI { setCursor: InstanceType["setCursor"]; resetCursor: InstanceType["resetCursor"]; toggleSidebar: InstanceType["toggleSidebar"]; + getFormFactor: ( + width: number, + height: number, + ) => EditorInterface["formFactor"]; /** * Disables rendering of frames (including element clipping), but currently * the frames are still interactive in edit mode. As such, this API should be