Refactor ExcalidrawElement (#874)

* Get rid of isSelected, canvas, canvasZoom, canvasOffsetX and canvasOffsetY on ExcalidrawElement.

* Fix most unit tests. Fix cmd a. Fix alt drag

* Focus on paste

* shift select should include previously selected items

* Fix last test

* Move this.shape out of ExcalidrawElement and into a WeakMap
This commit is contained in:
Pete Hunt 2020-03-08 10:20:55 -07:00 committed by GitHub
parent 8ecb4201db
commit ccbbdb75a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 416 additions and 306 deletions

View file

@ -10,22 +10,23 @@ export const actionDeleteSelected = register({
name: "deleteSelectedElements",
perform: (elements, appState) => {
return {
elements: deleteSelectedElements(elements),
elements: deleteSelectedElements(elements, appState),
appState: { ...appState, elementType: "selection", multiElement: null },
};
},
contextItemLabel: "labels.delete",
contextMenuOrder: 3,
commitToHistory: (_, elements) => isSomeElementSelected(elements),
commitToHistory: (appState, elements) =>
isSomeElementSelected(elements, appState),
keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
PanelComponent: ({ elements, updateData }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
icon={trash}
title={t("labels.delete")}
aria-label={t("labels.delete")}
onClick={() => updateData(null)}
visible={isSomeElementSelected(elements)}
visible={isSomeElementSelected(elements, appState)}
/>
),
});

View file

@ -1,5 +1,4 @@
import { KEYS } from "../keys";
import { clearSelection } from "../scene";
import { isInvisiblySmallElement } from "../element";
import { resetCursor } from "../utils";
import React from "react";
@ -7,11 +6,12 @@ import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons";
import { t } from "../i18n";
import { register } from "./register";
import { invalidateShapeForElement } from "../renderer/renderElement";
export const actionFinalize = register({
name: "finalize",
perform: (elements, appState) => {
let newElements = clearSelection(elements);
let newElements = elements;
if (window.document.activeElement instanceof HTMLElement) {
window.document.activeElement.blur();
}
@ -26,9 +26,9 @@ export const actionFinalize = register({
if (isInvisiblySmallElement(appState.multiElement)) {
newElements = newElements.slice(0, -1);
}
appState.multiElement.shape = null;
invalidateShapeForElement(appState.multiElement);
if (!appState.elementLocked) {
appState.multiElement.isSelected = true;
appState.selectedElementIds[appState.multiElement.id] = true;
}
}
if (!appState.elementLocked || !appState.multiElement) {
@ -44,6 +44,7 @@ export const actionFinalize = register({
: "selection",
draggingElement: null,
multiElement: null,
selectedElementIds: {},
},
};
},

View file

@ -14,10 +14,11 @@ import { register } from "./register";
const changeProperty = (
elements: readonly ExcalidrawElement[],
appState: AppState,
callback: (element: ExcalidrawElement) => ExcalidrawElement,
) => {
return elements.map(element => {
if (element.isSelected) {
if (appState.selectedElementIds[element.id]) {
return callback(element);
}
return element;
@ -25,15 +26,16 @@ const changeProperty = (
};
const getFormValue = function<T>(
editingElement: AppState["editingElement"],
elements: readonly ExcalidrawElement[],
appState: AppState,
getAttribute: (element: ExcalidrawElement) => T,
defaultValue?: T,
): T | null {
const editingElement = appState.editingElement;
return (
(editingElement && getAttribute(editingElement)) ??
(isSomeElementSelected(elements)
? getCommonAttributeOfSelectedElements(elements, getAttribute)
(isSomeElementSelected(elements, appState)
? getCommonAttributeOfSelectedElements(elements, appState, getAttribute)
: defaultValue) ??
null
);
@ -43,9 +45,8 @@ export const actionChangeStrokeColor = register({
name: "changeStrokeColor",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => ({
elements: changeProperty(elements, appState, el => ({
...el,
shape: null,
strokeColor: value,
})),
appState: { ...appState, currentItemStrokeColor: value },
@ -59,8 +60,8 @@ export const actionChangeStrokeColor = register({
type="elementStroke"
label={t("labels.stroke")}
color={getFormValue(
appState.editingElement,
elements,
appState,
element => element.strokeColor,
appState.currentItemStrokeColor,
)}
@ -74,9 +75,8 @@ export const actionChangeBackgroundColor = register({
name: "changeBackgroundColor",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => ({
elements: changeProperty(elements, appState, el => ({
...el,
shape: null,
backgroundColor: value,
})),
appState: { ...appState, currentItemBackgroundColor: value },
@ -90,8 +90,8 @@ export const actionChangeBackgroundColor = register({
type="elementBackground"
label={t("labels.background")}
color={getFormValue(
appState.editingElement,
elements,
appState,
element => element.backgroundColor,
appState.currentItemBackgroundColor,
)}
@ -105,9 +105,8 @@ export const actionChangeFillStyle = register({
name: "changeFillStyle",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => ({
elements: changeProperty(elements, appState, el => ({
...el,
shape: null,
fillStyle: value,
})),
appState: { ...appState, currentItemFillStyle: value },
@ -125,8 +124,8 @@ export const actionChangeFillStyle = register({
]}
group="fill"
value={getFormValue(
appState.editingElement,
elements,
appState,
element => element.fillStyle,
appState.currentItemFillStyle,
)}
@ -142,9 +141,8 @@ export const actionChangeStrokeWidth = register({
name: "changeStrokeWidth",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => ({
elements: changeProperty(elements, appState, el => ({
...el,
shape: null,
strokeWidth: value,
})),
appState: { ...appState, currentItemStrokeWidth: value },
@ -162,8 +160,8 @@ export const actionChangeStrokeWidth = register({
{ value: 4, text: t("labels.extraBold") },
]}
value={getFormValue(
appState.editingElement,
elements,
appState,
element => element.strokeWidth,
appState.currentItemStrokeWidth,
)}
@ -177,9 +175,8 @@ export const actionChangeSloppiness = register({
name: "changeSloppiness",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => ({
elements: changeProperty(elements, appState, el => ({
...el,
shape: null,
roughness: value,
})),
appState: { ...appState, currentItemRoughness: value },
@ -197,8 +194,8 @@ export const actionChangeSloppiness = register({
{ value: 2, text: t("labels.cartoonist") },
]}
value={getFormValue(
appState.editingElement,
elements,
appState,
element => element.roughness,
appState.currentItemRoughness,
)}
@ -212,9 +209,8 @@ export const actionChangeOpacity = register({
name: "changeOpacity",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => ({
elements: changeProperty(elements, appState, el => ({
...el,
shape: null,
opacity: value,
})),
appState: { ...appState, currentItemOpacity: value },
@ -246,8 +242,8 @@ export const actionChangeOpacity = register({
}}
value={
getFormValue(
appState.editingElement,
elements,
appState,
element => element.opacity,
appState.currentItemOpacity,
) ?? undefined
@ -261,11 +257,10 @@ export const actionChangeFontSize = register({
name: "changeFontSize",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => {
elements: changeProperty(elements, appState, el => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = {
...el,
shape: null,
font: `${value}px ${el.font.split("px ")[1]}`,
};
redrawTextBoundingBox(element);
@ -295,8 +290,8 @@ export const actionChangeFontSize = register({
{ value: 36, text: t("labels.veryLarge") },
]}
value={getFormValue(
appState.editingElement,
elements,
appState,
element => isTextElement(element) && +element.font.split("px ")[0],
+(appState.currentItemFont || DEFAULT_FONT).split("px ")[0],
)}
@ -310,11 +305,10 @@ export const actionChangeFontFamily = register({
name: "changeFontFamily",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, el => {
elements: changeProperty(elements, appState, el => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = {
...el,
shape: null,
font: `${el.font.split("px ")[0]}px ${value}`,
};
redrawTextBoundingBox(element);
@ -343,8 +337,8 @@ export const actionChangeFontFamily = register({
{ value: "Cascadia", text: t("labels.code") },
]}
value={getFormValue(
appState.editingElement,
elements,
appState,
element => isTextElement(element) && element.font.split("px ")[1],
(appState.currentItemFont || DEFAULT_FONT).split("px ")[1],
)}

View file

@ -3,9 +3,14 @@ import { register } from "./register";
export const actionSelectAll = register({
name: "selectAll",
perform: elements => {
perform: (elements, appState) => {
return {
elements: elements.map(elem => ({ ...elem, isSelected: true })),
appState: {
...appState,
selectedElementIds: Object.fromEntries(
elements.map(element => [element.id, true]),
),
},
};
},
contextItemLabel: "labels.selectAll",

View file

@ -11,8 +11,8 @@ let copiedStyles: string = "{}";
export const actionCopyStyles = register({
name: "copyStyles",
perform: elements => {
const element = elements.find(el => el.isSelected);
perform: (elements, appState) => {
const element = elements.find(el => appState.selectedElementIds[el.id]);
if (element) {
copiedStyles = JSON.stringify(element);
}
@ -25,17 +25,16 @@ export const actionCopyStyles = register({
export const actionPasteStyles = register({
name: "pasteStyles",
perform: elements => {
perform: (elements, appState) => {
const pastedElement = JSON.parse(copiedStyles);
if (!isExcalidrawElement(pastedElement)) {
return { elements };
}
return {
elements: elements.map(element => {
if (element.isSelected) {
if (appState.selectedElementIds[element.id]) {
const newElement = {
...element,
shape: null,
backgroundColor: pastedElement?.backgroundColor,
strokeWidth: pastedElement?.strokeWidth,
strokeColor: pastedElement?.strokeColor,

View file

@ -20,7 +20,10 @@ export const actionSendBackward = register({
name: "sendBackward",
perform: (elements, appState) => {
return {
elements: moveOneLeft([...elements], getSelectedIndices(elements)),
elements: moveOneLeft(
[...elements],
getSelectedIndices(elements, appState),
),
appState,
};
},
@ -44,7 +47,10 @@ export const actionBringForward = register({
name: "bringForward",
perform: (elements, appState) => {
return {
elements: moveOneRight([...elements], getSelectedIndices(elements)),
elements: moveOneRight(
[...elements],
getSelectedIndices(elements, appState),
),
appState,
};
},
@ -68,7 +74,10 @@ export const actionSendToBack = register({
name: "sendToBack",
perform: (elements, appState) => {
return {
elements: moveAllLeft([...elements], getSelectedIndices(elements)),
elements: moveAllLeft(
[...elements],
getSelectedIndices(elements, appState),
),
appState,
};
},
@ -91,7 +100,10 @@ export const actionBringToFront = register({
name: "bringToFront",
perform: (elements, appState) => {
return {
elements: moveAllRight([...elements], getSelectedIndices(elements)),
elements: moveAllRight(
[...elements],
getSelectedIndices(elements, appState),
),
appState,
};
},