@ -1900,28 +1900,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
) = > {
event . persist ( ) ;
// deal with opening context menu on touch devices
if ( event . pointerType === "touch" ) {
touchMoving = false ;
// open the context menu with the first touch's clientX and clientY
// if the touch is not moving
touchTimeout = window . setTimeout ( ( ) = > {
if ( ! touchMoving ) {
this . openContextMenu ( {
clientX : event.clientX ,
clientY : event.clientY ,
} ) ;
}
} , TOUCH_CTX_MENU_TIMEOUT ) ;
}
if ( lastPointerUp !== null ) {
// Unfortunately, sometimes we don't get a pointerup after a pointerdown,
// this can happen when a contextual menu or alert is triggered. In order to avoid
// being in a weird state, we clean up on the next pointerdown
lastPointerUp ( event ) ;
}
this . maybeOpenContextMenuAfterPointerDownOnTouchDevices ( event ) ;
this . maybeCleanupAfterMissingPointerUp ( event ) ;
if ( isPanning ) {
return ;
@ -1933,89 +1913,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} ) ;
this . savePointer ( event . clientX , event . clientY , "down" ) ;
// pan canvas on wheel button drag or space+drag
if (
gesture . pointers . size === 0 &&
( event . button === POINTER_BUTTON . WHEEL ||
( event . button === POINTER_BUTTON . MAIN && isHoldingSpace ) )
) {
isPanning = true ;
let nextPastePrevented = false ;
const isLinux = /Linux/ . test ( window . navigator . platform ) ;
document . documentElement . style . cursor = CURSOR_TYPE . GRABBING ;
let { clientX : lastX , clientY : lastY } = event ;
const onPointerMove = withBatchedUpdates ( ( event : PointerEvent ) = > {
const deltaX = lastX - event . clientX ;
const deltaY = lastY - event . clientY ;
lastX = event . clientX ;
lastY = event . clientY ;
/ *
* Prevent paste event if we move while middle clicking on Linux .
* See issue # 1383 .
* /
if (
isLinux &&
! nextPastePrevented &&
( Math . abs ( deltaX ) > 1 || Math . abs ( deltaY ) > 1 )
) {
nextPastePrevented = true ;
/* Prevent the next paste event */
const preventNextPaste = ( event : ClipboardEvent ) = > {
document . body . removeEventListener ( EVENT . PASTE , preventNextPaste ) ;
event . stopPropagation ( ) ;
} ;
/ *
* Reenable next paste in case of disabled middle click paste for
* any reason :
* - rigth click paste
* - empty clipboard
* /
const enableNextPaste = ( ) = > {
setTimeout ( ( ) = > {
document . body . removeEventListener ( EVENT . PASTE , preventNextPaste ) ;
window . removeEventListener ( EVENT . POINTER_UP , enableNextPaste ) ;
} , 100 ) ;
} ;
document . body . addEventListener ( EVENT . PASTE , preventNextPaste ) ;
window . addEventListener ( EVENT . POINTER_UP , enableNextPaste ) ;
}
this . setState ( {
scrollX : normalizeScroll (
this . state . scrollX - deltaX / this . state . zoom ,
) ,
scrollY : normalizeScroll (
this . state . scrollY - deltaY / this . state . zoom ,
) ,
} ) ;
} ) ;
const teardown = withBatchedUpdates (
( lastPointerUp = ( ) = > {
lastPointerUp = null ;
isPanning = false ;
if ( ! isHoldingSpace ) {
setCursorForShape ( this . state . elementType ) ;
}
this . setState ( {
cursorButton : "up" ,
} ) ;
this . savePointer ( event . clientX , event . clientY , "up" ) ;
window . removeEventListener ( EVENT . POINTER_MOVE , onPointerMove ) ;
window . removeEventListener ( EVENT . POINTER_UP , teardown ) ;
window . removeEventListener ( EVENT . BLUR , teardown ) ;
} ) ,
) ;
window . addEventListener ( EVENT . BLUR , teardown ) ;
window . addEventListener ( EVENT . POINTER_MOVE , onPointerMove , {
passive : true ,
} ) ;
window . addEventListener ( EVENT . POINTER_UP , teardown ) ;
if ( this . handleCanvasPanUsingWheelOrSpaceDrag ( event ) ) {
return ;
}
@ -2027,18 +1925,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return ;
}
gesture . pointers . set ( event . pointerId , {
x : event.clientX ,
y : event.clientY ,
} ) ;
if ( gesture . pointers . size === 2 ) {
gesture . lastCenter = getCenter ( gesture . pointers ) ;
gesture . initialScale = this . state . zoom ;
gesture . initialDistance = getDistance (
Array . from ( gesture . pointers . values ( ) ) ,
) ;
}
this . updateGestureOnPointerDown ( event ) ;
// fixes pointermove causing selection of UI texts #32
event . preventDefault ( ) ;
@ -2055,10 +1942,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
// Handle scrollbars dragging
const isOverScrollBarsNow = isOverScrollBars (
currentScrollBars ,
event . clientX ,
event . clientY ,
) ;
const {
isOverHorizontalScrollBar ,
isOverVerticalScrollBar ,
} = isOverScrollBars ( currentScrollBars , event . clientX , event . clientY ) ;
} = isOverScrollBars Now ;
const { x , y } = viewportCoordsToSceneCoords (
event ,
@ -2066,58 +1958,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this . canvas ,
window . devicePixelRatio ,
) ;
let lastX = x ;
let lastY = y ;
const lastCoords = { x , y } ;
if (
( isOverHorizontalScrollBar || isOverVerticalScrollBar ) &&
! this . state . multiElement
) {
isDraggingScrollBar = true ;
lastX = event . clientX ;
lastY = event . clientY ;
const onPointerMove = withBatchedUpdates ( ( event : PointerEvent ) = > {
const target = event . target ;
if ( ! ( target instanceof HTMLElement ) ) {
return ;
}
if ( isOverHorizontalScrollBar ) {
const x = event . clientX ;
const dx = x - lastX ;
this . setState ( {
scrollX : normalizeScroll ( this . state . scrollX - dx / this . state . zoom ) ,
} ) ;
lastX = x ;
return ;
}
if ( isOverVerticalScrollBar ) {
const y = event . clientY ;
const dy = y - lastY ;
this . setState ( {
scrollY : normalizeScroll ( this . state . scrollY - dy / this . state . zoom ) ,
} ) ;
lastY = y ;
}
} ) ;
const onPointerUp = withBatchedUpdates ( ( ) = > {
isDraggingScrollBar = false ;
setCursorForShape ( this . state . elementType ) ;
lastPointerUp = null ;
this . setState ( {
cursorButton : "up" ,
} ) ;
this . savePointer ( event . clientX , event . clientY , "up" ) ;
window . removeEventListener ( EVENT . POINTER_MOVE , onPointerMove ) ;
window . removeEventListener ( EVENT . POINTER_UP , onPointerUp ) ;
} ) ;
lastPointerUp = onPointerUp ;
window . addEventListener ( EVENT . POINTER_MOVE , onPointerMove ) ;
window . addEventListener ( EVENT . POINTER_UP , onPointerUp ) ;
if ( this . handleDraggingScrollBar ( event , lastCoords , isOverScrollBarsNow ) ) {
return ;
}
@ -2142,6 +1985,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
let hitElement : ExcalidrawElement | null = null ;
let hitElementWasAddedToSelection = false ;
if ( this . state . elementType !== "selection" ) {
this . setState ( {
selectedElementIds : { } ,
selectedGroupIds : { } ,
editingGroupId : null ,
} ) ;
}
if ( this . state . elementType === "selection" ) {
const elements = globalSceneState . getElements ( ) ;
const selectedElements = getSelectedElements ( elements , this . state ) ;
@ -2154,17 +2005,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this . state . zoom ,
event . pointerType ,
) ;
if ( elementWithResizeHandler ) {
this . setState ( {
resizingElement : elementWithResizeHandler
? elementWithResizeHandler . element
: null ,
} ) ;
if ( elementWithResizeHandler != null ) {
this . setState ( { resizingElement : elementWithResizeHandler.element } ) ;
resizeHandle = elementWithResizeHandler . resizeHandle ;
document . documentElement . style . cursor = getCursorForResizingElement (
elementWithResizeHandler ,
) ;
isResizingElements = true ;
}
} else if ( selectedElements . length > 1 ) {
resizeHandle = getResizeHandlerFromCoords (
@ -2174,14 +2017,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this . state . zoom ,
event . pointerType ,
) ;
}
if ( resizeHandle ) {
document . documentElement . style . cursor = getCursorForResizingElement ( {
resizeHandle ,
} ) ;
isResizingElements = true ;
}
}
if ( isResizingElements ) {
resizeOffsetXY = getResizeOffsetXY (
resizeHandle ,
selectedElements ,
@ -2198,8 +2039,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
selectedElements [ 0 ] ,
) ;
}
}
if ( ! isResizingElements ) {
} else {
if ( this . state . editingLinearElement ) {
const ret = LinearElementEditor . handlePointerDown (
event ,
@ -2222,27 +2062,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
hitElement ||
getElementAtPosition ( elements , this . state , x , y , this . state . zoom ) ;
// clear selection if shift is not clicked
if (
! ( hitElement && this . state . selectedElementIds [ hitElement . id ] ) &&
! event . shiftKey
) {
this . setState ( ( prevState ) = > ( {
selectedElementIds : { } ,
selectedGroupIds : { } ,
editingGroupId :
prevState . editingGroupId &&
hitElement &&
isElementInGroup ( hitElement , prevState . editingGroupId )
? prevState . editingGroupId
: null ,
} ) ) ;
const { selectedElementIds } = this . state ;
this . setState ( {
selectedElementIds : { } ,
previousSelectedElementIds : selectedElementIds ,
} ) ;
}
this . maybeClearSelectionWhenHittingElement ( event , hitElement ) ;
// If we click on something
if ( hitElement ) {
@ -2289,12 +2109,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
previousSelectedElementIds : selectedElementIds ,
} ) ;
}
} else {
this . setState ( {
selectedElementIds : { } ,
selectedGroupIds : { } ,
editingGroupId : null ,
} ) ;
}
if ( this . state . elementType === "text" ) {
@ -2457,21 +2271,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if ( isOverHorizontalScrollBar ) {
const x = event . clientX ;
const dx = x - last X ;
const dx = x - last Coords. x ;
this . setState ( {
scrollX : normalizeScroll ( this . state . scrollX - dx / this . state . zoom ) ,
} ) ;
last X = x ;
last Coords. x = x ;
return ;
}
if ( isOverVerticalScrollBar ) {
const y = event . clientY ;
const dy = y - last Y ;
const dy = y - last Coords. y ;
this . setState ( {
scrollY : normalizeScroll ( this . state . scrollY - dy / this . state . zoom ) ,
} ) ;
last Y = y ;
last Coords. y = y ;
return ;
}
@ -2534,13 +2348,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
( appState ) = > this . setState ( appState ) ,
x ,
y ,
last X ,
last Y ,
last Coords. x ,
last Coords. y ,
) ;
if ( didDrag ) {
last X = x ;
last Y = y ;
last Coords. x = x ;
last Coords. y = y ;
return ;
}
}
@ -2905,6 +2719,241 @@ class App extends React.Component<ExcalidrawProps, AppState> {
window . addEventListener ( EVENT . POINTER_UP , onPointerUp ) ;
} ;
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
event : React.PointerEvent < HTMLCanvasElement > ,
) : void = > {
// deal with opening context menu on touch devices
if ( event . pointerType === "touch" ) {
touchMoving = false ;
// open the context menu with the first touch's clientX and clientY
// if the touch is not moving
touchTimeout = window . setTimeout ( ( ) = > {
if ( ! touchMoving ) {
this . openContextMenu ( {
clientX : event.clientX ,
clientY : event.clientY ,
} ) ;
}
} , TOUCH_CTX_MENU_TIMEOUT ) ;
}
} ;
private maybeCleanupAfterMissingPointerUp (
event : React.PointerEvent < HTMLCanvasElement > ,
) : void {
if ( lastPointerUp !== null ) {
// Unfortunately, sometimes we don't get a pointerup after a pointerdown,
// this can happen when a contextual menu or alert is triggered. In order to avoid
// being in a weird state, we clean up on the next pointerdown
lastPointerUp ( event ) ;
}
}
// Returns whether the event is a panning
private handleCanvasPanUsingWheelOrSpaceDrag = (
event : React.PointerEvent < HTMLCanvasElement > ,
) : boolean = > {
if (
! (
gesture . pointers . size === 0 &&
( event . button === POINTER_BUTTON . WHEEL ||
( event . button === POINTER_BUTTON . MAIN && isHoldingSpace ) )
)
) {
return false ;
}
isPanning = true ;
let nextPastePrevented = false ;
const isLinux = /Linux/ . test ( window . navigator . platform ) ;
document . documentElement . style . cursor = CURSOR_TYPE . GRABBING ;
let { clientX : lastX , clientY : lastY } = event ;
const onPointerMove = withBatchedUpdates ( ( event : PointerEvent ) = > {
const deltaX = lastX - event . clientX ;
const deltaY = lastY - event . clientY ;
lastX = event . clientX ;
lastY = event . clientY ;
/ *
* Prevent paste event if we move while middle clicking on Linux .
* See issue # 1383 .
* /
if (
isLinux &&
! nextPastePrevented &&
( Math . abs ( deltaX ) > 1 || Math . abs ( deltaY ) > 1 )
) {
nextPastePrevented = true ;
/* Prevent the next paste event */
const preventNextPaste = ( event : ClipboardEvent ) = > {
document . body . removeEventListener ( EVENT . PASTE , preventNextPaste ) ;
event . stopPropagation ( ) ;
} ;
/ *
* Reenable next paste in case of disabled middle click paste for
* any reason :
* - rigth click paste
* - empty clipboard
* /
const enableNextPaste = ( ) = > {
setTimeout ( ( ) = > {
document . body . removeEventListener ( EVENT . PASTE , preventNextPaste ) ;
window . removeEventListener ( EVENT . POINTER_UP , enableNextPaste ) ;
} , 100 ) ;
} ;
document . body . addEventListener ( EVENT . PASTE , preventNextPaste ) ;
window . addEventListener ( EVENT . POINTER_UP , enableNextPaste ) ;
}
this . setState ( {
scrollX : normalizeScroll ( this . state . scrollX - deltaX / this . state . zoom ) ,
scrollY : normalizeScroll ( this . state . scrollY - deltaY / this . state . zoom ) ,
} ) ;
} ) ;
const teardown = withBatchedUpdates (
( lastPointerUp = ( ) = > {
lastPointerUp = null ;
isPanning = false ;
if ( ! isHoldingSpace ) {
setCursorForShape ( this . state . elementType ) ;
}
this . setState ( {
cursorButton : "up" ,
} ) ;
this . savePointer ( event . clientX , event . clientY , "up" ) ;
window . removeEventListener ( EVENT . POINTER_MOVE , onPointerMove ) ;
window . removeEventListener ( EVENT . POINTER_UP , teardown ) ;
window . removeEventListener ( EVENT . BLUR , teardown ) ;
} ) ,
) ;
window . addEventListener ( EVENT . BLUR , teardown ) ;
window . addEventListener ( EVENT . POINTER_MOVE , onPointerMove , {
passive : true ,
} ) ;
window . addEventListener ( EVENT . POINTER_UP , teardown ) ;
return true ;
} ;
private updateGestureOnPointerDown (
event : React.PointerEvent < HTMLCanvasElement > ,
) : void {
gesture . pointers . set ( event . pointerId , {
x : event.clientX ,
y : event.clientY ,
} ) ;
if ( gesture . pointers . size === 2 ) {
gesture . lastCenter = getCenter ( gesture . pointers ) ;
gesture . initialScale = this . state . zoom ;
gesture . initialDistance = getDistance (
Array . from ( gesture . pointers . values ( ) ) ,
) ;
}
}
// Returns whether the event is a dragging a scrollbar
private handleDraggingScrollBar (
event : React.PointerEvent < HTMLCanvasElement > ,
lastCoords : { x : number ; y : number } ,
{
isOverHorizontalScrollBar ,
isOverVerticalScrollBar ,
} : {
isOverHorizontalScrollBar : boolean ;
isOverVerticalScrollBar : boolean ;
} ,
) : boolean {
if (
! (
( isOverHorizontalScrollBar || isOverVerticalScrollBar ) &&
! this . state . multiElement
)
) {
return false ;
}
isDraggingScrollBar = true ;
lastCoords . x = event . clientX ;
lastCoords . y = event . clientY ;
const onPointerMove = withBatchedUpdates ( ( event : PointerEvent ) = > {
const target = event . target ;
if ( ! ( target instanceof HTMLElement ) ) {
return ;
}
if ( isOverHorizontalScrollBar ) {
const x = event . clientX ;
const dx = x - lastCoords . x ;
this . setState ( {
scrollX : normalizeScroll ( this . state . scrollX - dx / this . state . zoom ) ,
} ) ;
lastCoords . x = x ;
return ;
}
if ( isOverVerticalScrollBar ) {
const y = event . clientY ;
const dy = y - lastCoords . y ;
this . setState ( {
scrollY : normalizeScroll ( this . state . scrollY - dy / this . state . zoom ) ,
} ) ;
lastCoords . y = y ;
}
} ) ;
const onPointerUp = withBatchedUpdates ( ( ) = > {
isDraggingScrollBar = false ;
setCursorForShape ( this . state . elementType ) ;
lastPointerUp = null ;
this . setState ( {
cursorButton : "up" ,
} ) ;
this . savePointer ( event . clientX , event . clientY , "up" ) ;
window . removeEventListener ( EVENT . POINTER_MOVE , onPointerMove ) ;
window . removeEventListener ( EVENT . POINTER_UP , onPointerUp ) ;
} ) ;
lastPointerUp = onPointerUp ;
window . addEventListener ( EVENT . POINTER_MOVE , onPointerMove ) ;
window . addEventListener ( EVENT . POINTER_UP , onPointerUp ) ;
return true ;
}
private maybeClearSelectionWhenHittingElement (
event : React.PointerEvent < HTMLCanvasElement > ,
hitElement : ExcalidrawElement | null ,
) : void {
const isHittingASelectedElement =
hitElement != null && this . state . selectedElementIds [ hitElement . id ] ;
// clear selection if shift is not clicked
if ( isHittingASelectedElement || event . shiftKey ) {
return ;
}
this . setState ( ( prevState ) = > ( {
selectedElementIds : { } ,
selectedGroupIds : { } ,
// Continue editing the same group if the user selected a different
// element from it
editingGroupId :
prevState . editingGroupId &&
hitElement != null &&
isElementInGroup ( hitElement , prevState . editingGroupId )
? prevState . editingGroupId
: null ,
} ) ) ;
const { selectedElementIds } = this . state ;
this . setState ( {
selectedElementIds : { } ,
previousSelectedElementIds : selectedElementIds ,
} ) ;
}
private handleCanvasRef = ( canvas : HTMLCanvasElement ) = > {
// canvas is null when unmounting
if ( canvas !== null ) {