@ -56,7 +56,7 @@ import { Panel } from "./components/Panel";
import "./styles.scss" ;
import { getElementWithResizeHandler } from "./element/resizeTest" ;
cons t { elements } = createScene ( ) ;
le t { elements } = createScene ( ) ;
const { history } = createHistory ( ) ;
const DEFAULT_PROJECT_NAME = ` excalidraw- ${ getDateTime ( ) } ` ;
@ -119,9 +119,16 @@ export class App extends React.Component<{}, AppState> {
document . addEventListener ( "mousemove" , this . getCurrentCursorPosition ) ;
window . addEventListener ( "resize" , this . onResize , false ) ;
const savedState = restoreFromLocalStorage ( elements ) ;
if ( savedState ) {
this . setState ( savedState ) ;
const { elements : newElements , appState } = restoreFromLocalStorage ( ) ;
if ( newElements ) {
elements = newElements ;
}
if ( appState ) {
this . setState ( appState ) ;
} else {
this . forceUpdate ( ) ;
}
}
@ -163,7 +170,7 @@ export class App extends React.Component<{}, AppState> {
if ( isInputLike ( event . target ) ) return ;
if ( event . key === KEYS . ESCAPE ) {
clearSelection( elements ) ;
elements = clearSelection( elements ) ;
this . forceUpdate ( ) ;
event . preventDefault ( ) ;
} else if ( event . key === KEYS . BACKSPACE || event . key === KEYS . DELETE ) {
@ -173,13 +180,16 @@ export class App extends React.Component<{}, AppState> {
const step = event . shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT ;
elements . forEach ( element = > {
if ( element . isSelected ) {
elements = elements . map ( el = > {
if ( el . isSelected ) {
const element = { . . . el } ;
if ( event . key === KEYS . ARROW_LEFT ) element . x -= step ;
else if ( event . key === KEYS . ARROW_RIGHT ) element . x += step ;
else if ( event . key === KEYS . ARROW_UP ) element . y -= step ;
else if ( event . key === KEYS . ARROW_DOWN ) element . y += step ;
return element ;
}
return el ;
} ) ;
this . forceUpdate ( ) ;
event . preventDefault ( ) ;
@ -215,9 +225,12 @@ export class App extends React.Component<{}, AppState> {
event . preventDefault ( ) ;
// Select all: Cmd-A
} else if ( event [ META_KEY ] && event . code === "KeyA" ) {
elements . forEach ( element = > {
let newElements = [ . . . elements ] ;
newElements . forEach ( element = > {
element . isSelected = true ;
} ) ;
elements = newElements ;
this . forceUpdate ( ) ;
event . preventDefault ( ) ;
} else if ( shapesShortcutKeys . includes ( event . key . toLowerCase ( ) ) ) {
@ -225,10 +238,16 @@ export class App extends React.Component<{}, AppState> {
} else if ( event [ META_KEY ] && event . code === "KeyZ" ) {
if ( event . shiftKey ) {
// Redo action
history . redoOnce ( elements ) ;
const data = history . redoOnce ( elements ) ;
if ( data !== null ) {
elements = data ;
}
} else {
// undo action
history . undoOnce ( elements ) ;
const data = history . undoOnce ( elements ) ;
if ( data !== null ) {
elements = data ;
}
}
this . forceUpdate ( ) ;
event . preventDefault ( ) ;
@ -243,13 +262,13 @@ export class App extends React.Component<{}, AppState> {
} ;
private deleteSelectedElements = ( ) = > {
deleteSelectedElements( elements ) ;
elements = deleteSelectedElements( elements ) ;
this . forceUpdate ( ) ;
} ;
private clearCanvas = ( ) = > {
if ( window . confirm ( "This will clear the whole canvas. Are you sure?" ) ) {
elements . splice ( 0 , elements . length ) ;
elements = [ ] ;
this . setState ( {
viewBackgroundColor : "#ffffff" ,
scrollX : 0 ,
@ -268,40 +287,45 @@ export class App extends React.Component<{}, AppState> {
private pasteStyles = ( ) = > {
const pastedElement = JSON . parse ( copiedStyles ) ;
elements . forEach ( element = > {
elements = elements . map ( element = > {
if ( element . isSelected ) {
element . backgroundColor = pastedElement ? . backgroundColor ;
element . strokeWidth = pastedElement ? . strokeWidth ;
element . strokeColor = pastedElement ? . strokeColor ;
element . fillStyle = pastedElement ? . fillStyle ;
element . opacity = pastedElement ? . opacity ;
element . roughness = pastedElement ? . roughness ;
if ( isTextElement ( element ) ) {
element . font = pastedElement ? . font ;
this . redrawTextBoundingBox ( element ) ;
const newElement = {
. . . element ,
backgroundColor : pastedElement?.backgroundColor ,
strokeWidth : pastedElement?.strokeWidth ,
strokeColor : pastedElement?.strokeColor ,
fillStyle : pastedElement?.fillStyle ,
opacity : pastedElement?.opacity ,
roughness : pastedElement?.roughness
} ;
if ( isTextElement ( newElement ) ) {
newElement . font = pastedElement ? . font ;
this . redrawTextBoundingBox ( newElement ) ;
}
return newElement ;
}
return element ;
} ) ;
this . forceUpdate ( ) ;
} ;
private moveAllLeft = ( ) = > {
moveAllLeft( elements , getSelectedIndices ( elements ) ) ;
elements = moveAllLeft( [ . . . elements ] , getSelectedIndices ( elements ) ) ;
this . forceUpdate ( ) ;
} ;
private moveOneLeft = ( ) = > {
moveOneLeft( elements , getSelectedIndices ( elements ) ) ;
elements = moveOneLeft( [ . . . elements ] , getSelectedIndices ( elements ) ) ;
this . forceUpdate ( ) ;
} ;
private moveAllRight = ( ) = > {
moveAllRight( elements , getSelectedIndices ( elements ) ) ;
elements = moveAllRight( [ . . . elements ] , getSelectedIndices ( elements ) ) ;
this . forceUpdate ( ) ;
} ;
private moveOneRight = ( ) = > {
moveOneRight( elements , getSelectedIndices ( elements ) ) ;
elements = moveOneRight( [ . . . elements ] , getSelectedIndices ( elements ) ) ;
this . forceUpdate ( ) ;
} ;
@ -311,27 +335,39 @@ export class App extends React.Component<{}, AppState> {
this . setState ( { name } ) ;
}
private changeProperty = ( callback : ( element : ExcalidrawElement ) = > void ) = > {
elements . forEach ( element = > {
private changeProperty = (
callback : ( element : ExcalidrawElement ) = > ExcalidrawElement
) = > {
elements = elements . map ( element = > {
if ( element . isSelected ) {
callback ( element ) ;
return callback ( element ) ;
}
return element ;
} ) ;
this . forceUpdate ( ) ;
} ;
private changeOpacity = ( event : React.ChangeEvent < HTMLInputElement > ) = > {
this . changeProperty ( element = > ( element . opacity = + event . target . value ) ) ;
this . changeProperty ( element = > ( {
. . . element ,
opacity : + event . target . value
} ) ) ;
} ;
private changeStrokeColor = ( color : string ) = > {
this . changeProperty ( element = > ( element . strokeColor = color ) ) ;
this . changeProperty ( element = > ( {
. . . element ,
strokeColor : color
} ) ) ;
this . setState ( { currentItemStrokeColor : color } ) ;
} ;
private changeBackgroundColor = ( color : string ) = > {
this . changeProperty ( element = > ( element . backgroundColor = color ) ) ;
this . changeProperty ( element = > ( {
. . . element ,
backgroundColor : color
} ) ) ;
this . setState ( { currentItemBackgroundColor : color } ) ;
} ;
@ -357,7 +393,6 @@ export class App extends React.Component<{}, AppState> {
element . width = metrics . width ;
element . height = metrics . height ;
element . baseline = metrics . baseline ;
this . forceUpdate ( ) ;
} ;
public render() {
@ -372,7 +407,7 @@ export class App extends React.Component<{}, AppState> {
"text/plain" ,
JSON . stringify ( elements . filter ( element = > element . isSelected ) )
) ;
deleteSelectedElements( elements ) ;
elements = deleteSelectedElements( elements ) ;
this . forceUpdate ( ) ;
e . preventDefault ( ) ;
} }
@ -394,7 +429,7 @@ export class App extends React.Component<{}, AppState> {
activeTool = { this . state . elementType }
onToolChange = { value = > {
this . setState ( { elementType : value } ) ;
clearSelection( elements ) ;
elements = clearSelection( elements ) ;
document . documentElement . style . cursor =
value === "text" ? "text" : "crosshair" ;
this . forceUpdate ( ) ;
@ -440,9 +475,10 @@ export class App extends React.Component<{}, AppState> {
element = > element . fillStyle
) }
onChange = { value = > {
this . changeProperty ( element = > {
element . fillStyle = value ;
} ) ;
this . changeProperty ( element = > ( {
. . . element ,
fillStyle : value
} ) ) ;
} }
/ >
< / >
@ -462,9 +498,10 @@ export class App extends React.Component<{}, AppState> {
element = > element . strokeWidth
) }
onChange = { value = > {
this . changeProperty ( element = > {
element . strokeWidth = value ;
} ) ;
this . changeProperty ( element = > ( {
. . . element ,
strokeWidth : value
} ) ) ;
} }
/ >
@ -480,9 +517,10 @@ export class App extends React.Component<{}, AppState> {
element = > element . roughness
) }
onChange = { value = >
this . changeProperty ( element = > {
element . roughness = value ;
} )
this . changeProperty ( element = > ( {
. . . element ,
roughness : value
} ) )
}
/ >
< / >
@ -511,6 +549,8 @@ export class App extends React.Component<{}, AppState> {
} ` ;
this . redrawTextBoundingBox ( element ) ;
}
return element ;
} )
}
/ >
@ -534,6 +574,8 @@ export class App extends React.Component<{}, AppState> {
} px $ { value } ` ;
this . redrawTextBoundingBox ( element ) ;
}
return element ;
} )
}
/ >
@ -575,7 +617,10 @@ export class App extends React.Component<{}, AppState> {
}
onSaveScene = { ( ) = > saveAsJSON ( elements , this . state . name ) }
onLoadScene = { ( ) = >
loadFromJSON ( elements ) . then ( ( ) = > this . forceUpdate ( ) )
loadFromJSON ( ) . then ( ( { elements : newElements } ) = > {
elements = newElements ;
this . forceUpdate ( ) ;
} )
}
/ >
< / div >
@ -638,7 +683,7 @@ export class App extends React.Component<{}, AppState> {
}
if ( ! element . isSelected ) {
clearSelection( elements ) ;
elements = clearSelection( elements ) ;
element . isSelected = true ;
this . forceUpdate ( ) ;
}
@ -730,36 +775,41 @@ export class App extends React.Component<{}, AppState> {
document . documentElement . style . cursor = ` ${ resizeHandle } -resize ` ;
isResizingElements = true ;
} else {
const selected = getElementAtPosition (
elements . filter ( el = > el . isSelected ) ,
x ,
y
) ;
// clear selection if shift is not clicked
if ( ! selected && ! e . shiftKey ) {
elements = clearSelection ( elements ) ;
}
const hitElement = getElementAtPosition ( elements , x , y ) ;
// If we click on something
if ( hitElement ) {
if ( hitElement . isSelected ) {
// If that element is already selected, do nothing,
// we're likely going to drag it
} else {
// We unselect every other elements unless shift is pressed
if ( ! e . shiftKey ) {
clearSelection ( elements ) ;
}
}
// No matter what, we select it
// deselect if item is selected
// if shift is not clicked, this will always return true
// otherwise, it will trigger selection based on current
// state of the box
hitElement . isSelected = true ;
// No matter what, we select it
// We duplicate the selected element if alt is pressed on Mouse down
if ( e . altKey ) {
elements . push (
elements = [
. . . elements ,
. . . elements . reduce ( ( duplicates , element ) = > {
if ( element . isSelected ) {
duplicates . push ( duplicateElement ( element ) ) ;
duplicates = duplicates . concat (
duplicateElement ( element )
) ;
element . isSelected = false ;
}
return duplicates ;
} , [ ] as typeof elements )
) ;
] ;
}
} else {
// If we don't click on anything, let's remove all the selected elements
clearSelection ( elements ) ;
}
isDraggingElements = someElementIsSelected ( elements ) ;
@ -794,8 +844,7 @@ export class App extends React.Component<{}, AppState> {
font : this.state.currentItemFont ,
onSubmit : text = > {
addTextElement ( element , text , this . state . currentItemFont ) ;
elements . push ( element ) ;
element . isSelected = true ;
elements = [ . . . elements , { . . . element , isSelected : true } ] ;
this . setState ( {
draggingElement : null ,
elementType : "selection"
@ -805,14 +854,14 @@ export class App extends React.Component<{}, AppState> {
return ;
}
elements . push ( element ) ;
if ( this . state . elementType === "text" ) {
elements = [ . . . elements , { . . . element , isSelected : true } ] ;
this . setState ( {
draggingElement : null ,
elementType : "selection"
} ) ;
element . isSelected = true ;
} else {
elements = [ . . . elements , element ] ;
this . setState ( { draggingElement : element } ) ;
}
@ -959,7 +1008,7 @@ export class App extends React.Component<{}, AppState> {
: height ;
if ( this . state . elementType === "selection" ) {
setSelection( elements , draggingElement ) ;
elements = setSelection( elements , draggingElement ) ;
}
// We don't want to save history when moving an element
history . skipRecording ( ) ;
@ -977,7 +1026,7 @@ export class App extends React.Component<{}, AppState> {
// if no element is clicked, clear the selection and redraw
if ( draggingElement === null ) {
clearSelection( elements ) ;
elements = clearSelection( elements ) ;
this . forceUpdate ( ) ;
return ;
}
@ -986,7 +1035,7 @@ export class App extends React.Component<{}, AppState> {
if ( isDraggingElements ) {
isDraggingElements = false ;
}
elements . pop ( ) ;
elements = elements . slice ( 0 , - 1 ) ;
} else {
draggingElement . isSelected = true ;
}
@ -1029,7 +1078,9 @@ export class App extends React.Component<{}, AppState> {
let textY = e . clientY ;
if ( elementAtPosition && isTextElement ( elementAtPosition ) ) {
elements . splice ( elements . indexOf ( elementAtPosition ) , 1 ) ;
elements = elements . filter (
element = > element . id !== elementAtPosition . id
) ;
this . forceUpdate ( ) ;
Object . assign ( element , elementAtPosition ) ;
@ -1073,8 +1124,7 @@ export class App extends React.Component<{}, AppState> {
text ,
element . font || this . state . currentItemFont
) ;
elements . push ( element ) ;
element . isSelected = true ;
elements = [ . . . elements , { . . . element , isSelected : true } ] ;
this . setState ( {
draggingElement : null ,
elementType : "selection"
@ -1134,15 +1184,15 @@ export class App extends React.Component<{}, AppState> {
parsedElements . length > 0 &&
parsedElements [ 0 ] . type // need to implement a better check here...
) {
clearSelection( elements ) ;
elements = clearSelection( elements ) ;
let subCanvasX1 = Infinity ;
let subCanvasX2 = 0 ;
let subCanvasY1 = Infinity ;
let subCanvasY2 = 0 ;
const minX = Math . min ( . . . parsedElements . map ( element = > element . x ) ) ;
const minY = Math . min ( . . . parsedElements . map ( element = > element . y ) ) ;
//const minX = Math.min(parsedElements.map(element => element.x));
//const minY = Math.min(parsedElements.map(element => element.y));
const distance = ( x : number , y : number ) = > {
return Math . abs ( x > y ? x - y : y - x ) ;
@ -1170,13 +1220,15 @@ export class App extends React.Component<{}, AppState> {
CANVAS_WINDOW_OFFSET_TOP -
elementsCenterY ;
parsedElements . forEach ( parsedElement = > {
const duplicate = duplicateElement ( parsedElement ) ;
duplicate . x += dx - minX ;
duplicate . y += dy - minY ;
elements . push ( duplicate ) ;
} ) ;
elements = [
. . . elements ,
. . . parsedElements . map ( parsedElement = > {
const duplicate = duplicateElement ( parsedElement ) ;
duplicate . x += dx ;
duplicate . y += dy ;
return duplicate ;
} )
] ;
this . forceUpdate ( ) ;
}
} ;