expose getFormFactor

This commit is contained in:
Ryan Di
2025-10-20 10:48:14 +11:00
parent fffd105dc9
commit 75c5d1cefc
4 changed files with 54 additions and 68 deletions

View File

@@ -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];

View File

@@ -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";
}

View File

@@ -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<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
}
};
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<AppProps, AppState> {
? 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,
});

View File

@@ -857,6 +857,10 @@ export interface ExcalidrawImperativeAPI {
setCursor: InstanceType<typeof App>["setCursor"];
resetCursor: InstanceType<typeof App>["resetCursor"];
toggleSidebar: InstanceType<typeof App>["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