Spaces:
Sleeping
Sleeping
| export type StylesPanelMode = "compact" | "full" | "mobile"; | |
| export type EditorInterface = Readonly<{ | |
| formFactor: "phone" | "tablet" | "desktop"; | |
| desktopUIMode: "compact" | "full"; | |
| userAgent: Readonly<{ | |
| isMobileDevice: boolean; | |
| platform: "ios" | "android" | "other" | "unknown"; | |
| }>; | |
| isTouchScreen: boolean; | |
| canFitSidebar: boolean; | |
| isLandscape: boolean; | |
| }>; | |
| // storage key | |
| const DESKTOP_UI_MODE_STORAGE_KEY = "excalidraw.desktopUIMode"; | |
| // breakpoints | |
| 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 = 1180; // ipad air | |
| // desktop/laptop (NOTE: not used for form factor detection) | |
| 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); | |
| export const isFirefox = | |
| typeof window !== "undefined" && | |
| "netscape" in window && | |
| navigator.userAgent.indexOf("rv:") > 1 && | |
| navigator.userAgent.indexOf("Gecko") > 1; | |
| export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1; | |
| export const isSafari = | |
| !isChrome && navigator.userAgent.indexOf("Safari") !== -1; | |
| export const isIOS = | |
| /iPad|iPhone/i.test(navigator.platform) || | |
| // iPadOS 13+ | |
| (navigator.userAgent.includes("Mac") && "ontouchend" in document); | |
| // keeping function so it can be mocked in test | |
| export const isBrave = () => | |
| (navigator as any).brave?.isBrave?.name === "isBrave"; | |
| // export const isMobile = | |
| // isIOS || | |
| // /android|webos|ipod|blackberry|iemobile|opera mini/i.test( | |
| // navigator.userAgent, | |
| // ) || | |
| // /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; | |
| }; | |
| const isMobileOrTablet = (): boolean => { | |
| const ua = navigator.userAgent || ""; | |
| const platform = navigator.platform || ""; | |
| const uaData = (navigator as any).userAgentData as | |
| | { mobile?: boolean; platform?: string } | |
| | undefined; | |
| // --- 1) chromium: prefer ua client hints ------------------------------- | |
| if (uaData) { | |
| const plat = (uaData.platform || "").toLowerCase(); | |
| const isDesktopOS = | |
| plat === "windows" || | |
| plat === "macos" || | |
| plat === "linux" || | |
| plat === "chrome os"; | |
| if (uaData.mobile === true) { | |
| return true; | |
| } | |
| if (uaData.mobile === false && plat === "android") { | |
| const looksTouchTablet = | |
| matchMedia?.("(hover: none)").matches && | |
| matchMedia?.("(pointer: coarse)").matches; | |
| return looksTouchTablet; | |
| } | |
| if (isDesktopOS) { | |
| return false; | |
| } | |
| } | |
| // --- 2) ios (includes ipad) -------------------------------------------- | |
| if (isIOS) { | |
| return true; | |
| } | |
| // --- 3) android legacy ua fallback ------------------------------------- | |
| if (isAndroid) { | |
| const isAndroidPhone = /Mobile/i.test(ua); | |
| const isAndroidTablet = !isAndroidPhone; | |
| if (isAndroidPhone || isAndroidTablet) { | |
| const looksTouchTablet = | |
| matchMedia?.("(hover: none)").matches && | |
| matchMedia?.("(pointer: coarse)").matches; | |
| return looksTouchTablet; | |
| } | |
| } | |
| // --- 4) last resort desktop exclusion ---------------------------------- | |
| const looksDesktopPlatform = | |
| /Win|Linux|CrOS|Mac/.test(platform) || | |
| /Windows NT|X11|CrOS|Macintosh/.test(ua); | |
| if (looksDesktopPlatform) { | |
| return false; | |
| } | |
| return false; | |
| }; | |
| export const getFormFactor = ( | |
| editorWidth: number, | |
| editorHeight: number, | |
| ): EditorInterface["formFactor"] => { | |
| if (isMobileBreakpoint(editorWidth, editorHeight)) { | |
| return "phone"; | |
| } | |
| if (isTabletBreakpoint(editorWidth, editorHeight)) { | |
| return "tablet"; | |
| } | |
| return "desktop"; | |
| }; | |
| export const deriveStylesPanelMode = ( | |
| editorInterface: EditorInterface, | |
| ): StylesPanelMode => { | |
| if (editorInterface.formFactor === "phone") { | |
| return "mobile"; | |
| } | |
| if (editorInterface.formFactor === "tablet") { | |
| return "compact"; | |
| } | |
| return editorInterface.desktopUIMode; | |
| }; | |
| export const createUserAgentDescriptor = ( | |
| userAgentString: string, | |
| ): EditorInterface["userAgent"] => { | |
| const normalizedUA = userAgentString ?? ""; | |
| let platform: EditorInterface["userAgent"]["platform"] = "unknown"; | |
| if (isIOS) { | |
| platform = "ios"; | |
| } else if (isAndroid) { | |
| platform = "android"; | |
| } else if (normalizedUA) { | |
| platform = "other"; | |
| } | |
| return { | |
| isMobileDevice: isMobileOrTablet(), | |
| platform, | |
| } as const; | |
| }; | |
| export const loadDesktopUIModePreference = () => { | |
| if (typeof window === "undefined") { | |
| return null; | |
| } | |
| try { | |
| const stored = window.localStorage.getItem(DESKTOP_UI_MODE_STORAGE_KEY); | |
| if (stored === "compact" || stored === "full") { | |
| return stored as EditorInterface["desktopUIMode"]; | |
| } | |
| } catch (error) { | |
| // ignore storage access issues (e.g., Safari private mode) | |
| } | |
| return null; | |
| }; | |
| const persistDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => { | |
| if (typeof window === "undefined") { | |
| return; | |
| } | |
| try { | |
| window.localStorage.setItem(DESKTOP_UI_MODE_STORAGE_KEY, mode); | |
| } catch (error) { | |
| // ignore storage access issues (e.g., Safari private mode) | |
| } | |
| }; | |
| export const setDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => { | |
| if (mode !== "compact" && mode !== "full") { | |
| return; | |
| } | |
| persistDesktopUIMode(mode); | |
| return mode; | |
| }; | |