diff --git a/src/index.tsx b/src/index.tsx index b78984551..f595e5433 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -53,7 +53,7 @@ import { Panel } from "./components/Panel"; import "./styles.scss"; -const { elements } = createScene(); +let { elements } = createScene(); const { history } = createHistory(); const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`; @@ -106,9 +106,10 @@ export class App extends React.Component<{}, AppState> { document.addEventListener("keydown", this.onKeyDown, false); window.addEventListener("resize", this.onResize, false); - const savedState = restoreFromLocalStorage(elements); - if (savedState) { - this.setState(savedState); + const appState = restoreFromLocalStorage(elements); + + if (appState) { + this.setState(appState); } } @@ -139,7 +140,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) { @@ -149,13 +150,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(); @@ -191,9 +195,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())) { @@ -219,13 +226,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, @@ -244,36 +251,40 @@ 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; + return { + ...element, + backgroundColor: pastedElement?.backgroundColor, + strokeWidth: pastedElement?.strokeWidth, + strokeColor: pastedElement?.strokeColor, + fillStyle: pastedElement?.fillStyle, + opacity: pastedElement?.opacity, + roughness: pastedElement?.roughness + }; } + 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(); }; @@ -283,27 +294,38 @@ export class App extends React.Component<{}, AppState> { this.setState({ name }); } - private changeProperty = (callback: (element: ExcalidrawElement) => void) => { - elements.forEach(element => { - if (element.isSelected) { - callback(element); - } - }); + private changeProperty = ( + callback: (element: ExcalidrawElement) => ExcalidrawElement + ) => { + elements = elements + .filter(el => el.isSelected) + .map(element => { + return callback(element); + }); this.forceUpdate(); }; private changeOpacity = (event: React.ChangeEvent) => { - 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 }); }; @@ -336,7 +358,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(); }} @@ -358,7 +380,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(); @@ -404,9 +426,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 + })); }} /> @@ -426,9 +449,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 + })); }} /> @@ -444,9 +468,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 + })) } /> @@ -552,7 +577,7 @@ export class App extends React.Component<{}, AppState> { } if (!element.isSelected) { - clearSelection(elements); + elements = clearSelection([...elements]); element.isSelected = true; this.forceUpdate(); } @@ -660,14 +685,15 @@ export class App extends React.Component<{}, AppState> { } else { // We unselect every other elements unless shift is pressed if (!e.shiftKey) { - clearSelection(elements); + elements = clearSelection([...elements]); } } // No matter what, we select it hitElement.isSelected = true; // 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)); @@ -675,11 +701,11 @@ export class App extends React.Component<{}, AppState> { } return duplicates; }, [] as typeof elements) - ); + ]; } } else { // If we don't click on anything, let's remove all the selected elements - clearSelection(elements); + elements = clearSelection([...elements]); } isDraggingElements = someElementIsSelected(elements); @@ -714,8 +740,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" @@ -725,14 +750,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 }); } @@ -883,7 +908,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(); @@ -901,7 +926,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; } @@ -910,7 +935,7 @@ export class App extends React.Component<{}, AppState> { if (isDraggingElements) { isDraggingElements = false; } - elements.pop(); + elements = elements.slice(0, -1); } else { draggingElement.isSelected = true; } @@ -998,8 +1023,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" @@ -1031,7 +1055,7 @@ 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]); if (x == null) x = 10 - this.state.scrollX; if (y == null) y = 10 - this.state.scrollY; @@ -1040,12 +1064,15 @@ export class App extends React.Component<{}, AppState> { const dx = x - minX; const dy = y - minY; - parsedElements.forEach(parsedElement => { - const duplicate = duplicateElement(parsedElement); - duplicate.x += dx; - duplicate.y += dy; - elements.push(duplicate); - }); + elements = [ + ...elements, + ...parsedElements.map(parsedElement => { + const duplicate = duplicateElement(parsedElement); + duplicate.x += dx; + duplicate.y += dy; + return duplicate; + }) + ]; this.forceUpdate(); } }; diff --git a/src/scene/selection.ts b/src/scene/selection.ts index b1fbc13ee..640d63542 100644 --- a/src/scene/selection.ts +++ b/src/scene/selection.ts @@ -25,20 +25,20 @@ export function setSelection( selectionX2 >= elementX2 && selectionY2 >= elementY2; }); + + return elements; } export function clearSelection(elements: ExcalidrawElement[]) { elements.forEach(element => { element.isSelected = false; }); + + return elements; } export function deleteSelectedElements(elements: ExcalidrawElement[]) { - for (let i = elements.length - 1; i >= 0; --i) { - if (elements[i].isSelected) { - elements.splice(i, 1); - } - } + return elements.filter(el => !el.isSelected); } export function getSelectedIndices(elements: ExcalidrawElement[]) { diff --git a/src/zindex.ts b/src/zindex.ts index 9ea5850b9..bc7232949 100644 --- a/src/zindex.ts +++ b/src/zindex.ts @@ -17,6 +17,8 @@ export function moveOneLeft(elements: T[], indicesToMove: number[]) { } swap(elements, index - 1, index); }); + + return elements; } export function moveOneRight(elements: T[], indicesToMove: number[]) { @@ -35,6 +37,7 @@ export function moveOneRight(elements: T[], indicesToMove: number[]) { } swap(elements, index + 1, index); }); + return elements; } // Let's go through an example @@ -112,6 +115,8 @@ export function moveAllLeft(elements: T[], indicesToMove: number[]) { leftMostElements.forEach((element, i) => { elements[i] = element; }); + + return elements; } // Let's go through an example @@ -190,4 +195,6 @@ export function moveAllRight(elements: T[], indicesToMove: number[]) { rightMostElements.forEach((element, i) => { elements[elements.length - i - 1] = element; }); + + return elements; }