@ -1,5 +1,5 @@
import { Point , simplify } from "points-on-curve" ;
import React from "react" ;
import React , { useContext } from "react" ;
import { RoughCanvas } from "roughjs/bin/canvas" ;
import rough from "roughjs/bin/rough" ;
import clsx from "clsx" ;
@ -54,6 +54,9 @@ import {
GRID_SIZE ,
LINE_CONFIRM_THRESHOLD ,
MIME_TYPES ,
MQ_MAX_HEIGHT_LANDSCAPE ,
MQ_MAX_WIDTH_LANDSCAPE ,
MQ_MAX_WIDTH_PORTRAIT ,
POINTER_BUTTON ,
SCROLL_TIMEOUT ,
TAP_TWICE_TIMEOUT ,
@ -178,13 +181,15 @@ import {
viewportCoordsToSceneCoords ,
withBatchedUpdates ,
} from "../utils" ;
import { isMobile } from "../is-mobile" ;
import ContextMenu , { ContextMenuOption } from "./ContextMenu" ;
import LayerUI from "./LayerUI" ;
import { Stats } from "./Stats" ;
import { Toast } from "./Toast" ;
import { actionToggleViewMode } from "../actions/actionToggleViewMode" ;
export const IsMobileContext = React . createContext ( false ) ;
export const useIsMobile = ( ) = > useContext ( IsMobileContext ) ;
const { history } = createHistory ( ) ;
let didTapTwice : boolean = false ;
@ -286,6 +291,9 @@ class App extends React.Component<AppProps, AppState> {
rc : RoughCanvas | null = null ;
unmounted : boolean = false ;
actionManager : ActionManager ;
isMobile = false ;
detachIsMobileMqHandler ? : ( ) = > void ;
private excalidrawContainerRef = React . createRef < HTMLDivElement > ( ) ;
public static defaultProps : Partial < AppProps > = {
@ -437,60 +445,64 @@ class App extends React.Component<AppProps, AppState> {
< div
className = { clsx ( "excalidraw" , {
"excalidraw--view-mode" : viewModeEnabled ,
"excalidraw--mobile" : this . isMobile ,
} ) }
ref = { this . excalidrawContainerRef }
onDrop = { this . handleAppOnDrop }
>
< LayerUI
canvas = { this . canvas }
appState = { this . state }
setAppState = { this . setAppState }
actionManager = { this . actionManager }
elements = { this . scene . getElements ( ) }
onCollabButtonClick = { onCollabButtonClick }
onLockToggle = { this . toggleLock }
onInsertElements = { ( elements ) = >
this . addElementsFromPasteOrLibrary (
elements ,
DEFAULT_PASTE_X ,
DEFAULT_PASTE_Y ,
)
}
zenModeEnabled = { zenModeEnabled }
toggleZenMode = { this . toggleZenMode }
langCode = { getLanguage ( ) . code }
isCollaborating = { this . props . isCollaborating || false }
onExportToBackend = { onExportToBackend }
renderCustomFooter = { renderFooter }
viewModeEnabled = { viewModeEnabled }
showExitZenModeBtn = {
typeof this . props ? . zenModeEnabled === "undefined" && zenModeEnabled
}
showThemeBtn = {
typeof this . props ? . theme === "undefined" &&
this . props . UIOptions . canvasActions . theme
}
libraryReturnUrl = { this . props . libraryReturnUrl }
UIOptions = { this . props . UIOptions }
/ >
< div className = "excalidraw-textEditorContainer" / >
< div className = "excalidraw-contextMenuContainer" / >
{ this . state . showStats && (
< Stats
< IsMobileContext.Provider value = { this . isMobile } >
< LayerUI
canvas = { this . canvas }
appState = { this . state }
setAppState = { this . setAppState }
actionManager = { this . actionManager }
elements = { this . scene . getElements ( ) }
onClose = { this . toggleStats }
renderCustomStats = { renderCustomStats }
/ >
) }
{ this . state . toastMessage !== null && (
< Toast
message = { this . state . toastMessage }
clearToast = { this . clearToast }
onCollabButtonClick = { onCollabButtonClick }
onLockToggle = { this . toggleLock }
onInsertElements = { ( elements ) = >
this . addElementsFromPasteOrLibrary (
elements ,
DEFAULT_PASTE_X ,
DEFAULT_PASTE_Y ,
)
}
zenModeEnabled = { zenModeEnabled }
toggleZenMode = { this . toggleZenMode }
langCode = { getLanguage ( ) . code }
isCollaborating = { this . props . isCollaborating || false }
onExportToBackend = { onExportToBackend }
renderCustomFooter = { renderFooter }
viewModeEnabled = { viewModeEnabled }
showExitZenModeBtn = {
typeof this . props ? . zenModeEnabled === "undefined" &&
zenModeEnabled
}
showThemeBtn = {
typeof this . props ? . theme === "undefined" &&
this . props . UIOptions . canvasActions . theme
}
libraryReturnUrl = { this . props . libraryReturnUrl }
UIOptions = { this . props . UIOptions }
/ >
) }
< main > { this . renderCanvas ( ) } < / main >
< div className = "excalidraw-textEditorContainer" / >
< div className = "excalidraw-contextMenuContainer" / >
{ this . state . showStats && (
< Stats
appState = { this . state }
setAppState = { this . setAppState }
elements = { this . scene . getElements ( ) }
onClose = { this . toggleStats }
renderCustomStats = { renderCustomStats }
/ >
) }
{ this . state . toastMessage !== null && (
< Toast
message = { this . state . toastMessage }
clearToast = { this . clearToast }
/ >
) }
< main > { this . renderCanvas ( ) } < / main >
< / IsMobileContext.Provider >
< / div >
) ;
}
@ -776,10 +788,29 @@ class App extends React.Component<AppProps, AppState> {
if ( "ResizeObserver" in window && this . excalidrawContainerRef ? . current ) {
this . resizeObserver = new ResizeObserver ( ( ) = > {
// compute isMobile state
// ---------------------------------------------------------------------
const {
width ,
height ,
} = this . excalidrawContainerRef . current ! . getBoundingClientRect ( ) ;
this . isMobile =
width < MQ_MAX_WIDTH_PORTRAIT ||
( height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE ) ;
// refresh offsets
// ---------------------------------------------------------------------
this . updateDOMRect ( ) ;
} ) ;
this . resizeObserver ? . observe ( this . excalidrawContainerRef . current ) ;
} else if ( window . matchMedia ) {
const mediaQuery = 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 handler = ( ) = > ( this . isMobile = mediaQuery . matches ) ;
mediaQuery . addListener ( handler ) ;
this . detachIsMobileMqHandler = ( ) = > mediaQuery . removeListener ( handler ) ;
}
const searchParams = new URLSearchParams ( window . location . search . slice ( 1 ) ) ;
if ( searchParams . has ( "web-share-target" ) ) {
@ -839,6 +870,8 @@ class App extends React.Component<AppProps, AppState> {
this . onGestureEnd as any ,
false ,
) ;
this . detachIsMobileMqHandler ? . ( ) ;
}
private addEventListeners() {
@ -1016,7 +1049,7 @@ class App extends React.Component<AppProps, AppState> {
} ,
{
renderOptimizations : true ,
renderScrollbars : ! isMobile ( ) ,
renderScrollbars : ! this . isMobile ,
} ,
) ;
if ( scrollBars ) {
@ -3811,8 +3844,6 @@ class App extends React.Component<AppProps, AppState> {
const separator = "separator" ;
const _isMobile = isMobile ( ) ;
const elements = this . scene . getElements ( ) ;
const options : ContextMenuOption [ ] = [ ] ;
@ -3849,7 +3880,7 @@ class App extends React.Component<AppProps, AppState> {
ContextMenu . push ( {
options : [
_ isMobile &&
this . isMobile &&
navigator . clipboard && {
name : "paste" ,
perform : ( elements , appStates ) = > {
@ -3860,7 +3891,7 @@ class App extends React.Component<AppProps, AppState> {
} ,
contextItemLabel : "labels.paste" ,
} ,
_ isMobile && navigator . clipboard && separator ,
this . isMobile && navigator . clipboard && separator ,
probablySupportsClipboardBlob &&
elements . length > 0 &&
actionCopyAsPng ,
@ -3903,9 +3934,9 @@ class App extends React.Component<AppProps, AppState> {
ContextMenu . push ( {
options : [
_ isMobile && actionCut ,
_ isMobile && navigator . clipboard && actionCopy ,
_ isMobile &&
this . isMobile && actionCut ,
this . isMobile && navigator . clipboard && actionCopy ,
this . isMobile &&
navigator . clipboard && {
name : "paste" ,
perform : ( elements , appStates ) = > {
@ -3916,7 +3947,7 @@ class App extends React.Component<AppProps, AppState> {
} ,
contextItemLabel : "labels.paste" ,
} ,
_ isMobile && separator ,
this . isMobile && separator ,
. . . options ,
separator ,
actionCopyStyles ,