Make all operations on elements array immutable (#283)
* Make scene functions return array instead of mutate array - Not all functions were changes; so the given argument was a new array to some * Make data restoration functions immutable - Make mutations in App component * Make history actions immutable * Fix an issue in change property that was causing elements to be removed * mark elements params as readonly & remove unnecessary copying * Make `clearSelection` return a new array * Perform Id comparisons instead of reference comparisons in onDoubleClick * Allow deselecting items with SHIFT key - Refactor hit detection code * Fix a bug in element selection and revert drag functionality Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
parent
1ea72e9134
commit
862231da4f
@ -1,6 +1,5 @@
|
||||
# Contributing
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
### Option 1 - Manual
|
||||
@ -17,7 +16,6 @@
|
||||
> git fetch upstream
|
||||
> git branch --set-upstream-to=upstream/master master
|
||||
> ```
|
||||
>
|
||||
|
||||
### Option 2 - Codesandbox
|
||||
|
||||
|
@ -34,7 +34,7 @@ export function resizeTest(
|
||||
}
|
||||
|
||||
export function getElementWithResizeHandler(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
{ x, y }: { x: number; y: number },
|
||||
{ scrollX, scrollY }: SceneScroll
|
||||
) {
|
||||
|
@ -5,7 +5,7 @@ class SceneHistory {
|
||||
private stateHistory: string[] = [];
|
||||
private redoStack: string[] = [];
|
||||
|
||||
generateCurrentEntry(elements: ExcalidrawElement[]) {
|
||||
generateCurrentEntry(elements: readonly ExcalidrawElement[]) {
|
||||
return JSON.stringify(
|
||||
elements.map(element => ({ ...element, isSelected: false }))
|
||||
);
|
||||
@ -22,30 +22,33 @@ class SceneHistory {
|
||||
this.stateHistory.push(newEntry);
|
||||
}
|
||||
|
||||
restoreEntry(elements: ExcalidrawElement[], entry: string) {
|
||||
const newElements = JSON.parse(entry);
|
||||
elements.splice(0, elements.length);
|
||||
newElements.forEach((newElement: ExcalidrawElement) => {
|
||||
elements.push(newElement);
|
||||
});
|
||||
restoreEntry(entry: string) {
|
||||
// When restoring, we shouldn't add an history entry otherwise we'll be stuck with it and can't go back
|
||||
this.skipRecording();
|
||||
|
||||
try {
|
||||
return JSON.parse(entry);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
clearRedoStack() {
|
||||
this.redoStack.splice(0, this.redoStack.length);
|
||||
}
|
||||
|
||||
redoOnce(elements: ExcalidrawElement[]) {
|
||||
redoOnce(elements: readonly ExcalidrawElement[]) {
|
||||
const currentEntry = this.generateCurrentEntry(elements);
|
||||
const entryToRestore = this.redoStack.pop();
|
||||
if (entryToRestore !== undefined) {
|
||||
this.restoreEntry(elements, entryToRestore);
|
||||
this.stateHistory.push(currentEntry);
|
||||
}
|
||||
return this.restoreEntry(entryToRestore);
|
||||
}
|
||||
|
||||
undoOnce(elements: ExcalidrawElement[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
undoOnce(elements: readonly ExcalidrawElement[]) {
|
||||
const currentEntry = this.generateCurrentEntry(elements);
|
||||
let entryToRestore = this.stateHistory.pop();
|
||||
|
||||
@ -54,9 +57,11 @@ class SceneHistory {
|
||||
entryToRestore = this.stateHistory.pop();
|
||||
}
|
||||
if (entryToRestore !== undefined) {
|
||||
this.restoreEntry(elements, entryToRestore);
|
||||
this.redoStack.push(currentEntry);
|
||||
return this.restoreEntry(entryToRestore);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
isRecording() {
|
||||
|
214
src/index.tsx
214
src/index.tsx
@ -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 => {
|
||||
elements = [
|
||||
...elements,
|
||||
...parsedElements.map(parsedElement => {
|
||||
const duplicate = duplicateElement(parsedElement);
|
||||
duplicate.x += dx - minX;
|
||||
duplicate.y += dy - minY;
|
||||
elements.push(duplicate);
|
||||
});
|
||||
|
||||
duplicate.x += dx;
|
||||
duplicate.y += dy;
|
||||
return duplicate;
|
||||
})
|
||||
];
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
import { renderElement } from "./renderElement";
|
||||
|
||||
export function renderScene(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
rc: RoughCanvas,
|
||||
canvas: HTMLCanvasElement,
|
||||
sceneState: SceneState,
|
||||
|
@ -2,7 +2,7 @@ import { ExcalidrawElement } from "../element/types";
|
||||
import { hitTest } from "../element/collision";
|
||||
import { getElementAbsoluteCoords } from "../element";
|
||||
|
||||
export const hasBackground = (elements: ExcalidrawElement[]) =>
|
||||
export const hasBackground = (elements: readonly ExcalidrawElement[]) =>
|
||||
elements.some(
|
||||
element =>
|
||||
element.isSelected &&
|
||||
@ -11,7 +11,7 @@ export const hasBackground = (elements: ExcalidrawElement[]) =>
|
||||
element.type === "diamond")
|
||||
);
|
||||
|
||||
export const hasStroke = (elements: ExcalidrawElement[]) =>
|
||||
export const hasStroke = (elements: readonly ExcalidrawElement[]) =>
|
||||
elements.some(
|
||||
element =>
|
||||
element.isSelected &&
|
||||
@ -21,11 +21,11 @@ export const hasStroke = (elements: ExcalidrawElement[]) =>
|
||||
element.type === "arrow")
|
||||
);
|
||||
|
||||
export const hasText = (elements: ExcalidrawElement[]) =>
|
||||
export const hasText = (elements: readonly ExcalidrawElement[]) =>
|
||||
elements.some(element => element.isSelected && element.type === "text");
|
||||
|
||||
export function getElementAtPosition(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
@ -42,7 +42,7 @@ export function getElementAtPosition(
|
||||
}
|
||||
|
||||
export function getElementContainingPosition(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
export const createScene = () => {
|
||||
const elements = Array.of<ExcalidrawElement>();
|
||||
const elements: readonly ExcalidrawElement[] = [];
|
||||
return { elements };
|
||||
};
|
||||
|
@ -22,7 +22,15 @@ function saveFile(name: string, data: string) {
|
||||
link.remove();
|
||||
}
|
||||
|
||||
export function saveAsJSON(elements: ExcalidrawElement[], name: string) {
|
||||
interface DataState {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
appState: any;
|
||||
}
|
||||
|
||||
export function saveAsJSON(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
name: string
|
||||
) {
|
||||
const serialized = JSON.stringify({
|
||||
version: 1,
|
||||
source: window.location.origin,
|
||||
@ -35,7 +43,7 @@ export function saveAsJSON(elements: ExcalidrawElement[], name: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function loadFromJSON(elements: ExcalidrawElement[]) {
|
||||
export function loadFromJSON() {
|
||||
const input = document.createElement("input");
|
||||
const reader = new FileReader();
|
||||
input.type = "file";
|
||||
@ -52,19 +60,24 @@ export function loadFromJSON(elements: ExcalidrawElement[]) {
|
||||
|
||||
input.click();
|
||||
|
||||
return new Promise(resolve => {
|
||||
return new Promise<DataState>(resolve => {
|
||||
reader.onloadend = () => {
|
||||
if (reader.readyState === FileReader.DONE) {
|
||||
let elements = [];
|
||||
try {
|
||||
const data = JSON.parse(reader.result as string);
|
||||
restore(elements, data.elements, null);
|
||||
resolve();
|
||||
elements = data.elements || [];
|
||||
} catch (e) {
|
||||
// Do nothing because elements array is already empty
|
||||
}
|
||||
resolve(restore(elements, null));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function exportAsPNG(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
canvas: HTMLCanvasElement,
|
||||
{
|
||||
exportBackground,
|
||||
@ -130,47 +143,52 @@ export function exportAsPNG(
|
||||
}
|
||||
|
||||
function restore(
|
||||
elements: ExcalidrawElement[],
|
||||
savedElements: string | ExcalidrawElement[] | null,
|
||||
savedState: string | null
|
||||
) {
|
||||
try {
|
||||
if (savedElements) {
|
||||
elements.splice(
|
||||
0,
|
||||
elements.length,
|
||||
...(typeof savedElements === "string"
|
||||
? JSON.parse(savedElements)
|
||||
: savedElements)
|
||||
);
|
||||
elements.forEach((element: ExcalidrawElement) => {
|
||||
element.id = element.id || nanoid();
|
||||
element.fillStyle = element.fillStyle || "hachure";
|
||||
element.strokeWidth = element.strokeWidth || 1;
|
||||
element.roughness = element.roughness || 1;
|
||||
element.opacity =
|
||||
savedElements: readonly ExcalidrawElement[],
|
||||
savedState: any
|
||||
): DataState {
|
||||
return {
|
||||
elements: savedElements.map(element => ({
|
||||
...element,
|
||||
id: element.id || nanoid(),
|
||||
fillStyle: element.fillStyle || "hachure",
|
||||
strokeWidth: element.strokeWidth || 1,
|
||||
roughness: element.roughness || 1,
|
||||
opacity:
|
||||
element.opacity === null || element.opacity === undefined
|
||||
? 100
|
||||
: element.opacity;
|
||||
});
|
||||
: element.opacity
|
||||
})),
|
||||
appState: savedState
|
||||
};
|
||||
}
|
||||
|
||||
return savedState ? JSON.parse(savedState) : null;
|
||||
} catch (e) {
|
||||
elements.splice(0, elements.length);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreFromLocalStorage(elements: ExcalidrawElement[]) {
|
||||
export function restoreFromLocalStorage() {
|
||||
const savedElements = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
const savedState = localStorage.getItem(LOCAL_STORAGE_KEY_STATE);
|
||||
|
||||
return restore(elements, savedElements, savedState);
|
||||
let elements = [];
|
||||
if (savedElements) {
|
||||
try {
|
||||
elements = JSON.parse(savedElements);
|
||||
} catch (e) {
|
||||
// Do nothing because elements array is already empty
|
||||
}
|
||||
}
|
||||
|
||||
let appState = null;
|
||||
if (savedState) {
|
||||
try {
|
||||
appState = JSON.parse(savedState);
|
||||
} catch (e) {
|
||||
// Do nothing because appState is already null
|
||||
}
|
||||
}
|
||||
|
||||
return restore(elements, appState);
|
||||
}
|
||||
|
||||
export function saveToLocalStorage(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
state: AppState
|
||||
) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
|
||||
|
@ -7,7 +7,7 @@ export const SCROLLBAR_WIDTH = 6;
|
||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||
|
||||
export function getScrollBars(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
scrollX: number,
|
||||
@ -76,7 +76,7 @@ export function getScrollBars(
|
||||
}
|
||||
|
||||
export function isOverScrollBars(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
x: number,
|
||||
y: number,
|
||||
canvasWidth: number,
|
||||
|
@ -2,7 +2,7 @@ import { ExcalidrawElement } from "../element/types";
|
||||
import { getElementAbsoluteCoords } from "../element";
|
||||
|
||||
export function setSelection(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
selection: ExcalidrawElement
|
||||
) {
|
||||
const [
|
||||
@ -25,23 +25,25 @@ export function setSelection(
|
||||
selectionX2 >= elementX2 &&
|
||||
selectionY2 >= elementY2;
|
||||
});
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
export function clearSelection(elements: ExcalidrawElement[]) {
|
||||
elements.forEach(element => {
|
||||
export function clearSelection(elements: readonly ExcalidrawElement[]) {
|
||||
const newElements = [...elements];
|
||||
|
||||
newElements.forEach(element => {
|
||||
element.isSelected = false;
|
||||
});
|
||||
|
||||
return newElements;
|
||||
}
|
||||
|
||||
export function deleteSelectedElements(elements: ExcalidrawElement[]) {
|
||||
for (let i = elements.length - 1; i >= 0; --i) {
|
||||
if (elements[i].isSelected) {
|
||||
elements.splice(i, 1);
|
||||
}
|
||||
}
|
||||
export function deleteSelectedElements(elements: readonly ExcalidrawElement[]) {
|
||||
return elements.filter(el => !el.isSelected);
|
||||
}
|
||||
|
||||
export function getSelectedIndices(elements: ExcalidrawElement[]) {
|
||||
export function getSelectedIndices(elements: readonly ExcalidrawElement[]) {
|
||||
const selectedIndices: number[] = [];
|
||||
elements.forEach((element, index) => {
|
||||
if (element.isSelected) {
|
||||
@ -51,11 +53,11 @@ export function getSelectedIndices(elements: ExcalidrawElement[]) {
|
||||
return selectedIndices;
|
||||
}
|
||||
|
||||
export const someElementIsSelected = (elements: ExcalidrawElement[]) =>
|
||||
export const someElementIsSelected = (elements: readonly ExcalidrawElement[]) =>
|
||||
elements.some(element => element.isSelected);
|
||||
|
||||
export function getSelectedAttribute<T>(
|
||||
elements: ExcalidrawElement[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
getAttribute: (element: ExcalidrawElement) => T
|
||||
): T | null {
|
||||
const attributes = Array.from(
|
||||
|
@ -17,6 +17,8 @@ export function moveOneLeft<T>(elements: T[], indicesToMove: number[]) {
|
||||
}
|
||||
swap(elements, index - 1, index);
|
||||
});
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
|
||||
@ -35,6 +37,7 @@ export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
|
||||
}
|
||||
swap(elements, index + 1, index);
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
|
||||
// Let's go through an example
|
||||
@ -112,6 +115,8 @@ export function moveAllLeft<T>(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<T>(elements: T[], indicesToMove: number[]) {
|
||||
rightMostElements.forEach((element, i) => {
|
||||
elements[elements.length - i - 1] = element;
|
||||
});
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user