mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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
11 changed files with 239 additions and 157 deletions
|
@ -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) {
|
||||
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));
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue