@ -19,7 +19,6 @@ import {
normalizeDimensions ,
normalizeDimensions ,
} from "../element" ;
} from "../element" ;
import {
import {
clearSelection ,
deleteSelectedElements ,
deleteSelectedElements ,
getElementsWithinSelection ,
getElementsWithinSelection ,
isOverScrollBars ,
isOverScrollBars ,
@ -77,6 +76,7 @@ import {
} from "../constants" ;
} from "../constants" ;
import { LayerUI } from "./LayerUI" ;
import { LayerUI } from "./LayerUI" ;
import { ScrollBars } from "../scene/types" ;
import { ScrollBars } from "../scene/types" ;
import { invalidateShapeForElement } from "../renderer/renderElement" ;
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// TEST HOOKS
// TEST HOOKS
@ -179,8 +179,8 @@ export class App extends React.Component<any, AppState> {
if ( isWritableElement ( event . target ) ) {
if ( isWritableElement ( event . target ) ) {
return ;
return ;
}
}
copyToAppClipboard ( elements );
copyToAppClipboard ( elements , this . state );
elements = deleteSelectedElements ( elements );
elements = deleteSelectedElements ( elements , this . state );
history . resumeRecording ( ) ;
history . resumeRecording ( ) ;
this . setState ( { } ) ;
this . setState ( { } ) ;
event . preventDefault ( ) ;
event . preventDefault ( ) ;
@ -189,7 +189,7 @@ export class App extends React.Component<any, AppState> {
if ( isWritableElement ( event . target ) ) {
if ( isWritableElement ( event . target ) ) {
return ;
return ;
}
}
copyToAppClipboard ( elements );
copyToAppClipboard ( elements , this . state );
event . preventDefault ( ) ;
event . preventDefault ( ) ;
} ;
} ;
@ -296,7 +296,7 @@ export class App extends React.Component<any, AppState> {
public state : AppState = getDefaultAppState ( ) ;
public state : AppState = getDefaultAppState ( ) ;
private onResize = ( ) = > {
private onResize = ( ) = > {
elements = elements . map ( el = > ( { . . . el , shape : null } ) ) ;
elements . forEach ( element = > invalidateShapeForElement ( element ) ) ;
this . setState ( { } ) ;
this . setState ( { } ) ;
} ;
} ;
@ -325,7 +325,7 @@ export class App extends React.Component<any, AppState> {
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT ;
: ELEMENT_TRANSLATE_AMOUNT ;
elements = elements . map ( el = > {
elements = elements . map ( el = > {
if ( el . i sS elected) {
if ( this . stat e. selectedElementIds[ el . id ] ) {
const element = { . . . el } ;
const element = { . . . el } ;
if ( event . key === KEYS . ARROW_LEFT ) {
if ( event . key === KEYS . ARROW_LEFT ) {
element . x -= step ;
element . x -= step ;
@ -361,19 +361,18 @@ export class App extends React.Component<any, AppState> {
if ( this . state . elementType === "selection" ) {
if ( this . state . elementType === "selection" ) {
resetCursor ( ) ;
resetCursor ( ) ;
} else {
} else {
elements = clearSelection ( elements ) ;
document . documentElement . style . cursor =
document . documentElement . style . cursor =
this . state . elementType === "text"
this . state . elementType === "text"
? CURSOR_TYPE . TEXT
? CURSOR_TYPE . TEXT
: CURSOR_TYPE . CROSSHAIR ;
: CURSOR_TYPE . CROSSHAIR ;
this . setState ( { } ) ;
this . setState ( { selectedElementIds : { } } ) ;
}
}
isHoldingSpace = false ;
isHoldingSpace = false ;
}
}
} ;
} ;
private copyToAppClipboard = ( ) = > {
private copyToAppClipboard = ( ) = > {
copyToAppClipboard ( elements );
copyToAppClipboard ( elements , this . state );
} ;
} ;
private pasteFromClipboard = async ( event : ClipboardEvent | null ) = > {
private pasteFromClipboard = async ( event : ClipboardEvent | null ) = > {
@ -413,9 +412,8 @@ export class App extends React.Component<any, AppState> {
this . state . currentItemFont ,
this . state . currentItemFont ,
) ;
) ;
element . isSelected = true ;
elements = [ . . . elements , element ] ;
this . setState ( { selectedElementIds : { [ element . id ] : true } } ) ;
elements = [ . . . clearSelection ( elements ) , element ] ;
history . resumeRecording ( ) ;
history . resumeRecording ( ) ;
}
}
this . selectShapeTool ( "selection" ) ;
this . selectShapeTool ( "selection" ) ;
@ -431,9 +429,10 @@ export class App extends React.Component<any, AppState> {
document . activeElement . blur ( ) ;
document . activeElement . blur ( ) ;
}
}
if ( elementType !== "selection" ) {
if ( elementType !== "selection" ) {
elements = clearSelection ( elements ) ;
this . setState ( { elementType , selectedElementIds : { } } ) ;
} else {
this . setState ( { elementType } ) ;
}
}
this . setState ( { elementType } ) ;
}
}
private onGestureStart = ( event : GestureEvent ) = > {
private onGestureStart = ( event : GestureEvent ) = > {
@ -524,6 +523,7 @@ export class App extends React.Component<any, AppState> {
const element = getElementAtPosition (
const element = getElementAtPosition (
elements ,
elements ,
this . state ,
x ,
x ,
y ,
y ,
this . state . zoom ,
this . state . zoom ,
@ -545,10 +545,8 @@ export class App extends React.Component<any, AppState> {
return ;
return ;
}
}
if ( ! element . isSelected ) {
if ( ! this . state . selectedElementIds [ element . id ] ) {
elements = clearSelection ( elements ) ;
this . setState ( { selectedElementIds : { [ element . id ] : true } } ) ;
element . isSelected = true ;
this . setState ( { } ) ;
}
}
ContextMenu . push ( {
ContextMenu . push ( {
@ -760,12 +758,16 @@ export class App extends React.Component<any, AppState> {
if ( this . state . elementType === "selection" ) {
if ( this . state . elementType === "selection" ) {
const resizeElement = getElementWithResizeHandler (
const resizeElement = getElementWithResizeHandler (
elements ,
elements ,
this . state ,
{ x , y } ,
{ x , y } ,
this . state . zoom ,
this . state . zoom ,
event . pointerType ,
event . pointerType ,
) ;
) ;
const selectedElements = getSelectedElements ( elements ) ;
const selectedElements = getSelectedElements (
elements ,
this . state ,
) ;
if ( selectedElements . length === 1 && resizeElement ) {
if ( selectedElements . length === 1 && resizeElement ) {
this . setState ( {
this . setState ( {
resizingElement : resizeElement
resizingElement : resizeElement
@ -781,13 +783,19 @@ export class App extends React.Component<any, AppState> {
} else {
} else {
hitElement = getElementAtPosition (
hitElement = getElementAtPosition (
elements ,
elements ,
this . state ,
x ,
x ,
y ,
y ,
this . state . zoom ,
this . state . zoom ,
) ;
) ;
// clear selection if shift is not clicked
// clear selection if shift is not clicked
if ( ! hitElement ? . isSelected && ! event . shiftKey ) {
if (
elements = clearSelection ( elements ) ;
! (
hitElement && this . state . selectedElementIds [ hitElement . id ]
) &&
! event . shiftKey
) {
this . setState ( { selectedElementIds : { } } ) ;
}
}
// If we click on something
// If we click on something
@ -796,30 +804,37 @@ export class App extends React.Component<any, AppState> {
// if shift is not clicked, this will always return true
// if shift is not clicked, this will always return true
// otherwise, it will trigger selection based on current
// otherwise, it will trigger selection based on current
// state of the box
// state of the box
if ( ! hitElement . isSelected ) {
if ( ! this . state . selectedElementIds [ hitElement . id ] ) {
hitElement . isSelected = true ;
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ hitElement ! . id ] : true ,
} ,
} ) ) ;
elements = elements . slice ( ) ;
elements = elements . slice ( ) ;
elementIsAddedToSelection = true ;
elementIsAddedToSelection = true ;
}
}
// We duplicate the selected element if alt is pressed on pointer down
// We duplicate the selected element if alt is pressed on pointer down
if ( event . altKey ) {
if ( event . altKey ) {
elements = [
// Move the currently selected elements to the top of the z index stack, and
. . . elements . map ( element = > ( {
// put the duplicates where the selected elements used to be.
. . . element ,
const nextElements = [ ] ;
isSelected : false ,
const elementsToAppend = [ ] ;
} ) ) ,
for ( const element of elements ) {
. . . getSelectedElements ( elements ) . map ( element = > {
if ( this . state . selectedElementIds [ element . id ] ) {
const newElement = duplicateElement ( element ) ;
nextElements . push ( duplicateElement ( element ) ) ;
newElement . isSelected = true ;
elementsToAppend . push ( element ) ;
return newElement ;
} else {
} ) ,
nextElements . push ( element ) ;
] ;
}
}
elements = [ . . . nextElements , . . . elementsToAppend ] ;
}
}
}
}
}
}
} else {
} else {
elements = clearSelection ( elements ) ;
this . setState ( { selectedElementIds : { } } ) ;
}
}
if ( isTextElement ( element ) ) {
if ( isTextElement ( element ) ) {
@ -872,10 +887,15 @@ export class App extends React.Component<any, AppState> {
text ,
text ,
this . state . currentItemFont ,
this . state . currentItemFont ,
) ,
) ,
isSelected : true ,
} ,
} ,
] ;
] ;
}
}
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ element . id ] : true ,
} ,
} ) ) ;
if ( this . state . elementLocked ) {
if ( this . state . elementLocked ) {
setCursorForShape ( this . state . elementType ) ;
setCursorForShape ( this . state . elementType ) ;
}
}
@ -905,13 +925,23 @@ export class App extends React.Component<any, AppState> {
if ( this . state . multiElement ) {
if ( this . state . multiElement ) {
const { multiElement } = this . state ;
const { multiElement } = this . state ;
const { x : rx , y : ry } = multiElement ;
const { x : rx , y : ry } = multiElement ;
multiElement . isSelected = true ;
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ multiElement . id ] : true ,
} ,
} ) ) ;
multiElement . points . push ( [ x - rx , y - ry ] ) ;
multiElement . points . push ( [ x - rx , y - ry ] ) ;
multiElement . shape = null ;
invalidateShapeForElement( multiElement ) ;
} else {
} else {
element . isSelected = false ;
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ element . id ] : false ,
} ,
} ) ) ;
element . points . push ( [ 0 , 0 ] ) ;
element . points . push ( [ 0 , 0 ] ) ;
element . shape = null ;
invalidateShapeForElement( element ) ;
elements = [ . . . elements , element ] ;
elements = [ . . . elements , element ] ;
this . setState ( {
this . setState ( {
draggingElement : element ,
draggingElement : element ,
@ -1047,7 +1077,10 @@ export class App extends React.Component<any, AppState> {
if ( isResizingElements && this . state . resizingElement ) {
if ( isResizingElements && this . state . resizingElement ) {
this . setState ( { isResizing : true } ) ;
this . setState ( { isResizing : true } ) ;
const el = this . state . resizingElement ;
const el = this . state . resizingElement ;
const selectedElements = getSelectedElements ( elements ) ;
const selectedElements = getSelectedElements (
elements ,
this . state ,
) ;
if ( selectedElements . length === 1 ) {
if ( selectedElements . length === 1 ) {
const { x , y } = viewportCoordsToSceneCoords (
const { x , y } = viewportCoordsToSceneCoords (
event ,
event ,
@ -1261,7 +1294,7 @@ export class App extends React.Component<any, AppState> {
) ;
) ;
el . x = element . x ;
el . x = element . x ;
el . y = element . y ;
el . y = element . y ;
el. shape = null ;
invalidateShapeForElement( el ) ;
lastX = x ;
lastX = x ;
lastY = y ;
lastY = y ;
@ -1270,11 +1303,17 @@ export class App extends React.Component<any, AppState> {
}
}
}
}
if ( hitElement ? . isSelected ) {
if (
hitElement &&
this . state . selectedElementIds [ hitElement . id ]
) {
// Marking that click was used for dragging to check
// Marking that click was used for dragging to check
// if elements should be deselected on pointerup
// if elements should be deselected on pointerup
draggingOccurred = true ;
draggingOccurred = true ;
const selectedElements = getSelectedElements ( elements ) ;
const selectedElements = getSelectedElements (
elements ,
this . state ,
) ;
if ( selectedElements . length > 0 ) {
if ( selectedElements . length > 0 ) {
const { x , y } = viewportCoordsToSceneCoords (
const { x , y } = viewportCoordsToSceneCoords (
event ,
event ,
@ -1354,19 +1393,30 @@ export class App extends React.Component<any, AppState> {
draggingElement . height = height ;
draggingElement . height = height ;
}
}
draggingElement. shape = null ;
invalidateShapeForElement( draggingElement ) ;
if ( this . state . elementType === "selection" ) {
if ( this . state . elementType === "selection" ) {
if ( ! event . shiftKey && isSomeElementSelected ( elements ) ) {
if (
elements = clearSelection ( elements ) ;
! event . shiftKey &&
isSomeElementSelected ( elements , this . state )
) {
this . setState ( { selectedElementIds : { } } ) ;
}
}
const elementsWithinSelection = getElementsWithinSelection (
const elementsWithinSelection = getElementsWithinSelection (
elements ,
elements ,
draggingElement ,
draggingElement ,
) ;
) ;
elementsWithinSelection . forEach ( element = > {
this . setState ( prevState = > ( {
element . isSelected = true ;
selectedElementIds : {
} ) ;
. . . prevState . selectedElementIds ,
. . . Object . fromEntries (
elementsWithinSelection . map ( element = > [
element . id ,
true ,
] ) ,
) ,
} ,
} ) ) ;
}
}
this . setState ( { } ) ;
this . setState ( { } ) ;
} ;
} ;
@ -1406,20 +1456,27 @@ export class App extends React.Component<any, AppState> {
x - draggingElement . x ,
x - draggingElement . x ,
y - draggingElement . y ,
y - draggingElement . y ,
] ) ;
] ) ;
draggingElement. shape = null ;
invalidateShapeForElement( draggingElement ) ;
this . setState ( { multiElement : this.state.draggingElement } ) ;
this . setState ( { multiElement : this.state.draggingElement } ) ;
} else if ( draggingOccurred && ! multiElement ) {
} else if ( draggingOccurred && ! multiElement ) {
this . state . draggingElement ! . isSelected = true ;
if ( ! elementLocked ) {
if ( ! elementLocked ) {
resetCursor ( ) ;
resetCursor ( ) ;
this . setState ( {
this . setState ( prevState = > ( {
draggingElement : null ,
draggingElement : null ,
elementType : "selection" ,
elementType : "selection" ,
} ) ;
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ this . state . draggingElement ! . id ] : true ,
} ,
} ) ) ;
} else {
} else {
this . setState ( {
this . setState ( prevState = > ( {
draggingElement : null ,
draggingElement : null ,
} ) ;
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ this . state . draggingElement ! . id ] : true ,
} ,
} ) ) ;
}
}
}
}
return ;
return ;
@ -1470,27 +1527,37 @@ export class App extends React.Component<any, AppState> {
! elementIsAddedToSelection
! elementIsAddedToSelection
) {
) {
if ( event . shiftKey ) {
if ( event . shiftKey ) {
hitElement . isSelected = false ;
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ hitElement ! . id ] : false ,
} ,
} ) ) ;
} else {
} else {
elements = clearSelection ( elements ) ;
this . setState ( prevState = > ( {
hitElement . isSelected = true ;
selectedElementIds : { [ hitElement ! . id ] : true } ,
} ) ) ;
}
}
}
}
if ( draggingElement === null ) {
if ( draggingElement === null ) {
// if no element is clicked, clear the selection and redraw
// if no element is clicked, clear the selection and redraw
elements = clearSelection ( elements ) ;
this . setState ( { selectedElementIds : { } } ) ;
this . setState ( { } ) ;
return ;
return ;
}
}
if ( ! elementLocked ) {
if ( ! elementLocked ) {
draggingElement . isSelected = true ;
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ draggingElement . id ] : true ,
} ,
} ) ) ;
}
}
if (
if (
elementType !== "selection" ||
elementType !== "selection" ||
isSomeElementSelected ( elements )
isSomeElementSelected ( elements , this . state )
) {
) {
history . resumeRecording ( ) ;
history . resumeRecording ( ) ;
}
}
@ -1524,6 +1591,7 @@ export class App extends React.Component<any, AppState> {
const elementAtPosition = getElementAtPosition (
const elementAtPosition = getElementAtPosition (
elements ,
elements ,
this . state ,
x ,
x ,
y ,
y ,
this . state . zoom ,
this . state . zoom ,
@ -1616,10 +1684,15 @@ export class App extends React.Component<any, AppState> {
// we need to recreate the element to update dimensions &
// we need to recreate the element to update dimensions &
// position
// position
. . . newTextElement ( element , text , element . font ) ,
. . . newTextElement ( element , text , element . font ) ,
isSelected : true ,
} ,
} ,
] ;
] ;
}
}
this . setState ( prevState = > ( {
selectedElementIds : {
. . . prevState . selectedElementIds ,
[ element . id ] : true ,
} ,
} ) ) ;
history . resumeRecording ( ) ;
history . resumeRecording ( ) ;
resetSelection ( ) ;
resetSelection ( ) ;
} ,
} ,
@ -1695,7 +1768,7 @@ export class App extends React.Component<any, AppState> {
const pnt = points [ points . length - 1 ] ;
const pnt = points [ points . length - 1 ] ;
pnt [ 0 ] = x - originX ;
pnt [ 0 ] = x - originX ;
pnt [ 1 ] = y - originY ;
pnt [ 1 ] = y - originY ;
multiElement. shape = null ;
invalidateShapeForElement( multiElement ) ;
this . setState ( { } ) ;
this . setState ( { } ) ;
return ;
return ;
}
}
@ -1708,10 +1781,14 @@ export class App extends React.Component<any, AppState> {
return ;
return ;
}
}
const selectedElements = getSelectedElements ( elements ) ;
const selectedElements = getSelectedElements (
elements ,
this . state ,
) ;
if ( selectedElements . length === 1 && ! isOverScrollBar ) {
if ( selectedElements . length === 1 && ! isOverScrollBar ) {
const resizeElement = getElementWithResizeHandler (
const resizeElement = getElementWithResizeHandler (
elements ,
elements ,
this . state ,
{ x , y } ,
{ x , y } ,
this . state . zoom ,
this . state . zoom ,
event . pointerType ,
event . pointerType ,
@ -1725,6 +1802,7 @@ export class App extends React.Component<any, AppState> {
}
}
const hitElement = getElementAtPosition (
const hitElement = getElementAtPosition (
elements ,
elements ,
this . state ,
x ,
x ,
y ,
y ,
this . state . zoom ,
this . state . zoom ,
@ -1782,8 +1860,6 @@ export class App extends React.Component<any, AppState> {
private addElementsFromPaste = (
private addElementsFromPaste = (
clipboardElements : readonly ExcalidrawElement [ ] ,
clipboardElements : readonly ExcalidrawElement [ ] ,
) = > {
) = > {
elements = clearSelection ( elements ) ;
const [ minX , minY , maxX , maxY ] = getCommonBounds ( clipboardElements ) ;
const [ minX , minY , maxX , maxY ] = getCommonBounds ( clipboardElements ) ;
const elementsCenterX = distance ( minX , maxX ) / 2 ;
const elementsCenterX = distance ( minX , maxX ) / 2 ;
@ -1798,17 +1874,20 @@ export class App extends React.Component<any, AppState> {
const dx = x - elementsCenterX ;
const dx = x - elementsCenterX ;
const dy = y - elementsCenterY ;
const dy = y - elementsCenterY ;
elements = [
const newElements = clipboardElements . map ( clipboardElements = > {
. . . elements ,
const duplicate = duplicateElement ( clipboardElements ) ;
. . . clipboardElements . map ( clipboardElements = > {
duplicate . x += dx - minX ;
const duplicate = duplicateElement ( clipboardElements ) ;
duplicate . y += dy - minY ;
duplicate . x += dx - minX ;
return duplicate ;
duplicate . y += dy - minY ;
} ) ;
return duplicate ;
} ) ,
elements = [ . . . elements , . . . newElements ] ;
] ;
history . resumeRecording ( ) ;
history . resumeRecording ( ) ;
this . setState ( { } ) ;
this . setState ( {
selectedElementIds : Object.fromEntries (
newElements . map ( element = > [ element . id , true ] ) ,
) ,
} ) ;
} ;
} ;
private getTextWysiwygSnappedToCenterPosition ( x : number , y : number ) {
private getTextWysiwygSnappedToCenterPosition ( x : number , y : number ) {
@ -1845,6 +1924,7 @@ export class App extends React.Component<any, AppState> {
componentDidUpdate() {
componentDidUpdate() {
const { atLeastOneVisibleElement , scrollBars } = renderScene (
const { atLeastOneVisibleElement , scrollBars } = renderScene (
elements ,
elements ,
this . state ,
this . state . selectionElement ,
this . state . selectionElement ,
this . rc ! ,
this . rc ! ,
this . canvas ! ,
this . canvas ! ,