@ -74,7 +74,6 @@ import {
MQ_MAX_WIDTH_LANDSCAPE ,
MQ_MAX_WIDTH_PORTRAIT ,
MQ_RIGHT_SIDEBAR_MIN_WIDTH ,
MQ_SM_MAX_WIDTH ,
POINTER_BUTTON ,
ROUNDNESS ,
SCROLL_TIMEOUT ,
@ -381,11 +380,15 @@ const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React . createContext < AppProps > ( null ! ) ;
const deviceContextInitialValue = {
isSmScreen : false ,
isMobile : false ,
viewport : {
isMobile : false ,
isLandscape : false ,
} ,
editor : {
isMobile : false ,
canFitSidebar : false ,
} ,
isTouchScreen : false ,
canDeviceFitSidebar : false ,
isLandscape : false ,
} ;
const DeviceContext = React . createContext < Device > ( deviceContextInitialValue ) ;
DeviceContext . displayName = "DeviceContext" ;
@ -436,6 +439,9 @@ export const useExcalidrawSetAppState = () =>
export const useExcalidrawActionManager = ( ) = >
useContext ( ExcalidrawActionManagerContext ) ;
const supportsResizeObserver =
typeof window !== "undefined" && "ResizeObserver" in window ;
let didTapTwice : boolean = false ;
let tappedTwiceTimer = 0 ;
let isHoldingSpace : boolean = false ;
@ -472,7 +478,6 @@ class App extends React.Component<AppProps, AppState> {
unmounted : boolean = false ;
actionManager : ActionManager ;
device : Device = deviceContextInitialValue ;
detachIsMobileMqHandler ? : ( ) = > void ;
private excalidrawContainerRef = React . createRef < HTMLDivElement > ( ) ;
@ -1180,7 +1185,7 @@ class App extends React.Component<AppProps, AppState> {
< div
className = { clsx ( "excalidraw excalidraw-container" , {
"excalidraw--view-mode" : this . state . viewModeEnabled ,
"excalidraw--mobile" : this . device . isMobile,
"excalidraw--mobile" : this . device . editor. isMobile,
} ) }
style = { {
[ "--ui-pointerEvents" as any ] :
@ -1657,20 +1662,62 @@ class App extends React.Component<AppProps, AppState> {
} ) ;
} ;
private refreshDeviceState = ( container : HTMLDivElement ) = > {
const { width , height } = container . getBoundingClientRect ( ) ;
private isMobileBreakpoint = ( width : number , height : number ) = > {
return (
width < MQ_MAX_WIDTH_PORTRAIT ||
( height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE )
) ;
} ;
private refreshViewportBreakpoints = ( ) = > {
const container = this . excalidrawContainerRef . current ;
if ( ! container ) {
return ;
}
const { clientWidth : viewportWidth , clientHeight : viewportHeight } =
document . body ;
const prevViewportState = this . device . viewport ;
const nextViewportState = updateObject ( prevViewportState , {
isLandscape : viewportWidth > viewportHeight ,
isMobile : this.isMobileBreakpoint ( viewportWidth , viewportHeight ) ,
} ) ;
if ( prevViewportState !== nextViewportState ) {
this . device = { . . . this . device , viewport : nextViewportState } ;
return true ;
}
return false ;
} ;
private refreshEditorBreakpoints = ( ) = > {
const container = this . excalidrawContainerRef . current ;
if ( ! container ) {
return ;
}
const { width : editorWidth , height : editorHeight } =
container . getBoundingClientRect ( ) ;
const sidebarBreakpoint =
this . props . UIOptions . dockedSidebarBreakpoint != null
? this . props . UIOptions . dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH ;
this . device = updateObject ( this . device , {
isLandscape : width > height ,
isSmScreen : width < MQ_SM_MAX_WIDTH ,
isMobile :
width < MQ_MAX_WIDTH_PORTRAIT ||
( height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE ) ,
canDeviceFitSidebar : width > sidebarBreakpoint ,
const prevEditorState = this . device . editor ;
const nextEditorState = updateObject ( prevEditorState , {
isMobile : this.isMobileBreakpoint ( editorWidth , editorHeight ) ,
canFitSidebar : editorWidth > sidebarBreakpoint ,
} ) ;
if ( prevEditorState !== nextEditorState ) {
this . device = { . . . this . device , editor : nextEditorState } ;
return true ;
}
return false ;
} ;
public async componentDidMount() {
@ -1712,52 +1759,21 @@ class App extends React.Component<AppProps, AppState> {
}
if (
this . excalidrawContainerRef . current &&
// bounding rects don't work in tests so updating
// the state on init would result in making the test enviro run
// in mobile breakpoint (0 width/height), making everything fail
! isTestEnv ( )
) {
this . refreshDeviceState ( this . excalidrawContainerRef . current ) ;
this . refreshViewportBreakpoints ( ) ;
this . refreshEditorBreakpoints ( ) ;
}
if ( "ResizeObserver" in window && this . excalidrawContainerRef ? . current ) {
if ( supportsResizeObserver && this . excalidrawContainerRef . current ) {
this . resizeObserver = new ResizeObserver ( ( ) = > {
// recompute device dimensions state
// ---------------------------------------------------------------------
this . refreshDeviceState ( this . excalidrawContainerRef . current ! ) ;
// refresh offsets
// ---------------------------------------------------------------------
this . refreshEditorBreakpoints ( ) ;
this . updateDOMRect ( ) ;
} ) ;
this . resizeObserver ? . observe ( this . excalidrawContainerRef . current ) ;
} else if ( window . matchMedia ) {
const mdScreenQuery = window . matchMedia (
` (max-width: ${ MQ_MAX_WIDTH_PORTRAIT } px), (max-height: ${ MQ_MAX_HEIGHT_LANDSCAPE } px) and (max-width: ${ MQ_MAX_WIDTH_LANDSCAPE } px) ` ,
) ;
const smScreenQuery = window . matchMedia (
` (max-width: ${ MQ_SM_MAX_WIDTH } px) ` ,
) ;
const canDeviceFitSidebarMediaQuery = window . matchMedia (
` (min-width: ${
// NOTE this won't update if a different breakpoint is supplied
// after mount
this . props . UIOptions . dockedSidebarBreakpoint != null
? this . props . UIOptions . dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH
} px ) ` ,
) ;
const handler = ( ) = > {
this . excalidrawContainerRef . current ! . getBoundingClientRect ( ) ;
this . device = updateObject ( this . device , {
isSmScreen : smScreenQuery.matches ,
isMobile : mdScreenQuery.matches ,
canDeviceFitSidebar : canDeviceFitSidebarMediaQuery.matches ,
} ) ;
} ;
mdScreenQuery . addListener ( handler ) ;
this . detachIsMobileMqHandler = ( ) = >
mdScreenQuery . removeListener ( handler ) ;
}
const searchParams = new URLSearchParams ( window . location . search . slice ( 1 ) ) ;
@ -1802,6 +1818,11 @@ class App extends React.Component<AppProps, AppState> {
this . scene
. getElementsIncludingDeleted ( )
. forEach ( ( element ) = > ShapeCache . delete ( element ) ) ;
this . refreshViewportBreakpoints ( ) ;
this . updateDOMRect ( ) ;
if ( ! supportsResizeObserver ) {
this . refreshEditorBreakpoints ( ) ;
}
this . setState ( { } ) ;
} ) ;
@ -1855,7 +1876,6 @@ class App extends React.Component<AppProps, AppState> {
false ,
) ;
this . detachIsMobileMqHandler ? . ( ) ;
window . removeEventListener ( EVENT . MESSAGE , this . onWindowMessage , false ) ;
}
@ -1940,11 +1960,10 @@ class App extends React.Component<AppProps, AppState> {
}
if (
this . excalidrawContainerRef . current &&
prevProps . UIOptions . dockedSidebarBreakpoint !==
this . props . UIOptions . dockedSidebarBreakpoint
this . props . UIOptions . dockedSidebarBreakpoint
) {
this . refresh DeviceState( this . excalidrawContainerRef . current ) ;
this . refresh EditorBreakpoints( ) ;
}
if (
@ -2410,7 +2429,7 @@ class App extends React.Component<AppProps, AppState> {
// from library, not when pasting from clipboard. Alas.
openSidebar :
this . state . openSidebar &&
this . device . canDevice FitSidebar &&
this . device . editor. canFitSidebar &&
jotaiStore . get ( isSidebarDockedAtom )
? this . state . openSidebar
: null ,
@ -2624,7 +2643,7 @@ class App extends React.Component<AppProps, AppState> {
! isPlainPaste &&
textElements . length > 1 &&
PLAIN_PASTE_TOAST_SHOWN === false &&
! this . device . isMobile
! this . device . editor. isMobile
) {
this . setToast ( {
message : t ( "toast.pasteAsSingleElement" , {
@ -2658,7 +2677,7 @@ class App extends React.Component<AppProps, AppState> {
trackEvent (
"toolbar" ,
"toggleLock" ,
` ${ source } ( ${ this . device . isMobile ? "mobile" : "desktop" } ) ` ,
` ${ source } ( ${ this . device . editor. isMobile ? "mobile" : "desktop" } ) ` ,
) ;
}
this . setState ( ( prevState ) = > {
@ -3153,7 +3172,9 @@ class App extends React.Component<AppProps, AppState> {
trackEvent (
"toolbar" ,
shape ,
` keyboard ( ${ this . device . isMobile ? "mobile" : "desktop" } ) ` ,
` keyboard ( ${
this . device . editor . isMobile ? "mobile" : "desktop"
} ) ` ,
) ;
}
this . setActiveTool ( { type : shape } ) ;
@ -3887,7 +3908,7 @@ class App extends React.Component<AppProps, AppState> {
element ,
this . state ,
[ scenePointer . x , scenePointer . y ] ,
this . device . isMobile,
this . device . editor. isMobile,
)
) ;
} ) ;
@ -3919,7 +3940,7 @@ class App extends React.Component<AppProps, AppState> {
this . hitLinkElement ,
this . state ,
[ lastPointerDownCoords . x , lastPointerDownCoords . y ] ,
this . device . isMobile,
this . device . editor. isMobile,
) ;
const lastPointerUpCoords = viewportCoordsToSceneCoords (
this . lastPointerUpEvent ! ,
@ -3929,7 +3950,7 @@ class App extends React.Component<AppProps, AppState> {
this . hitLinkElement ,
this . state ,
[ lastPointerUpCoords . x , lastPointerUpCoords . y ] ,
this . device . isMobile,
this . device . editor. isMobile,
) ;
if ( lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon ) {
let url = this . hitLinkElement . link ;
@ -4791,7 +4812,7 @@ class App extends React.Component<AppProps, AppState> {
) ;
const clicklength =
event . timeStamp - ( this . lastPointerDownEvent ? . timeStamp ? ? 0 ) ;
if ( this . device . isMobile && clicklength < 300 ) {
if ( this . device . editor. isMobile && clicklength < 300 ) {
const hitElement = this . getElementAtPosition (
scenePointer . x ,
scenePointer . y ,