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:
Gasim Gasimzada
2020-01-09 19:22:04 +04:00
committed by David Luzar
parent 1ea72e9134
commit 862231da4f
11 changed files with 239 additions and 157 deletions

View File

@ -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) {
const data = JSON.parse(reader.result as string);
restore(elements, data.elements, null);
resolve();
let elements = [];
try {
const data = JSON.parse(reader.result as string);
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 =
element.opacity === null || element.opacity === undefined
? 100
: element.opacity;
});
}
return savedState ? JSON.parse(savedState) : null;
} catch (e) {
elements.splice(0, elements.length);
return null;
}
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
})),
appState: savedState
};
}
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));