|
@@ -56,7 +56,7 @@ import { Panel } from "./components/Panel";
|
|
|
import "./styles.scss";
|
|
|
import { getElementWithResizeHandler } from "./element/resizeTest";
|
|
|
|
|
|
-const { elements } = createScene();
|
|
|
+let { 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();
|
|
|
}
|
|
|
};
|