فهرست منبع

Add NonDeleted<ExcalidrawElement> (#1068)

* add NonDeleted

* make test:all script run tests without prompt

* rename helper

* replace with helper

* make element contructors return nonDeleted elements

* cache filtered elements where appliacable for better perf

* rename manager element getter

* remove unnecessary assertion

* fix test

* make element types in resizeElement into nonDeleted

Co-authored-by: dwelle <luzar.david@gmail.com>
Pete Hunt 5 سال پیش
والد
کامیت
df0613d8ac

+ 1 - 0
package.json

@@ -110,6 +110,7 @@
     "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
     "start": "react-scripts start",
     "test": "npm run test:app",
+    "test:all": "npm run test:typecheck && npm run test:code && npm run test:other && npm run test:app -- --watchAll=false",
     "test:update": "npm run test:app -- --updateSnapshot --watchAll=false",
     "test:app": "react-scripts test --env=jsdom --passWithNoTests",
     "test:code": "eslint --max-warnings=0 --ignore-path .gitignore --ext .js,.ts,.tsx .",

+ 6 - 2
src/actions/actionDeleteSelected.tsx

@@ -5,6 +5,7 @@ import React from "react";
 import { trash } from "../components/icons";
 import { t } from "../i18n";
 import { register } from "./register";
+import { getNonDeletedElements } from "../element";
 
 export const actionDeleteSelected = register({
   name: "deleteSelectedElements",
@@ -20,7 +21,10 @@ export const actionDeleteSelected = register({
         elementType: "selection",
         multiElement: null,
       },
-      commitToHistory: isSomeElementSelected(elements, appState),
+      commitToHistory: isSomeElementSelected(
+        getNonDeletedElements(elements),
+        appState,
+      ),
     };
   },
   contextItemLabel: "labels.delete",
@@ -33,7 +37,7 @@ export const actionDeleteSelected = register({
       title={t("labels.delete")}
       aria-label={t("labels.delete")}
       onClick={() => updateData(null)}
-      visible={isSomeElementSelected(elements, appState)}
+      visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
     />
   ),
 });

+ 2 - 2
src/actions/actionDuplicateSelection.tsx

@@ -2,7 +2,7 @@ import React from "react";
 import { KEYS } from "../keys";
 import { register } from "./register";
 import { ExcalidrawElement } from "../element/types";
-import { duplicateElement } from "../element";
+import { duplicateElement, getNonDeletedElements } from "../element";
 import { isSomeElementSelected } from "../scene";
 import { ToolButton } from "../components/ToolButton";
 import { clone } from "../components/icons";
@@ -43,7 +43,7 @@ export const actionDuplicateSelection = register({
       )}`}
       aria-label={t("labels.duplicateSelection")}
       onClick={() => updateData(null)}
-      visible={isSomeElementSelected(elements, appState)}
+      visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
     />
   ),
 });

+ 5 - 2
src/actions/actionMenu.tsx

@@ -2,7 +2,7 @@ import React from "react";
 import { menu, palette } from "../components/icons";
 import { ToolButton } from "../components/ToolButton";
 import { t } from "../i18n";
-import { showSelectedShapeActions } from "../element";
+import { showSelectedShapeActions, getNonDeletedElements } from "../element";
 import { register } from "./register";
 import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
 import { KEYS } from "../keys";
@@ -39,7 +39,10 @@ export const actionToggleEditMenu = register({
   }),
   PanelComponent: ({ elements, appState, updateData }) => (
     <ToolButton
-      visible={showSelectedShapeActions(appState, elements)}
+      visible={showSelectedShapeActions(
+        appState,
+        getNonDeletedElements(elements),
+      )}
       type="button"
       icon={palette}
       aria-label={t("buttons.edit")}

+ 12 - 3
src/actions/actionProperties.tsx

@@ -5,7 +5,11 @@ import {
   isSomeElementSelected,
 } from "../scene";
 import { ButtonSelect } from "../components/ButtonSelect";
-import { isTextElement, redrawTextBoundingBox } from "../element";
+import {
+  isTextElement,
+  redrawTextBoundingBox,
+  getNonDeletedElements,
+} from "../element";
 import { ColorPicker } from "../components/ColorPicker";
 import { AppState } from "../../src/types";
 import { t } from "../i18n";
@@ -33,10 +37,15 @@ const getFormValue = function <T>(
   defaultValue?: T,
 ): T | null {
   const editingElement = appState.editingElement;
+  const nonDeletedElements = getNonDeletedElements(elements);
   return (
     (editingElement && getAttribute(editingElement)) ??
-    (isSomeElementSelected(elements, appState)
-      ? getCommonAttributeOfSelectedElements(elements, appState, getAttribute)
+    (isSomeElementSelected(nonDeletedElements, appState)
+      ? getCommonAttributeOfSelectedElements(
+          nonDeletedElements,
+          appState,
+          getAttribute,
+        )
       : defaultValue) ??
     null
   );

+ 36 - 9
src/actions/manager.tsx

@@ -9,6 +9,7 @@ import {
 import { ExcalidrawElement } from "../element/types";
 import { AppState } from "../types";
 import { t } from "../i18n";
+import { globalSceneState } from "../scene";
 
 export class ActionManager implements ActionsManagerInterface {
   actions = {} as ActionsManagerInterface["actions"];
@@ -17,16 +18,18 @@ export class ActionManager implements ActionsManagerInterface {
 
   getAppState: () => AppState;
 
-  getElements: () => readonly ExcalidrawElement[];
+  getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
 
   constructor(
     updater: UpdaterFn,
     getAppState: () => AppState,
-    getElements: () => readonly ExcalidrawElement[],
+    getElementsIncludingDeleted: () => ReturnType<
+      typeof globalSceneState["getElementsIncludingDeleted"]
+    >,
   ) {
     this.updater = updater;
     this.getAppState = getAppState;
-    this.getElements = getElements;
+    this.getElementsIncludingDeleted = getElementsIncludingDeleted;
   }
 
   registerAction(action: Action) {
@@ -43,7 +46,11 @@ export class ActionManager implements ActionsManagerInterface {
       .filter(
         (action) =>
           action.keyTest &&
-          action.keyTest(event, this.getAppState(), this.getElements()),
+          action.keyTest(
+            event,
+            this.getAppState(),
+            this.getElementsIncludingDeleted(),
+          ),
       );
 
     if (data.length === 0) {
@@ -51,12 +58,24 @@ export class ActionManager implements ActionsManagerInterface {
     }
 
     event.preventDefault();
-    this.updater(data[0].perform(this.getElements(), this.getAppState(), null));
+    this.updater(
+      data[0].perform(
+        this.getElementsIncludingDeleted(),
+        this.getAppState(),
+        null,
+      ),
+    );
     return true;
   }
 
   executeAction(action: Action) {
-    this.updater(action.perform(this.getElements(), this.getAppState(), null));
+    this.updater(
+      action.perform(
+        this.getElementsIncludingDeleted(),
+        this.getAppState(),
+        null,
+      ),
+    );
   }
 
   getContextMenuItems(actionFilter: ActionFilterFn = (action) => action) {
@@ -72,7 +91,11 @@ export class ActionManager implements ActionsManagerInterface {
         label: action.contextItemLabel ? t(action.contextItemLabel) : "",
         action: () => {
           this.updater(
-            action.perform(this.getElements(), this.getAppState(), null),
+            action.perform(
+              this.getElementsIncludingDeleted(),
+              this.getAppState(),
+              null,
+            ),
           );
         },
       }));
@@ -84,13 +107,17 @@ export class ActionManager implements ActionsManagerInterface {
       const PanelComponent = action.PanelComponent!;
       const updateData = (formState?: any) => {
         this.updater(
-          action.perform(this.getElements(), this.getAppState(), formState),
+          action.perform(
+            this.getElementsIncludingDeleted(),
+            this.getAppState(),
+            formState,
+          ),
         );
       };
 
       return (
         <PanelComponent
-          elements={this.getElements()}
+          elements={this.getElementsIncludingDeleted()}
           appState={this.getAppState()}
           updateData={updateData}
         />

+ 5 - 2
src/clipboard.ts

@@ -1,4 +1,7 @@
-import { ExcalidrawElement } from "./element/types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+} from "./element/types";
 import { getSelectedElements } from "./scene";
 import { AppState } from "./types";
 import { SVG_EXPORT_TAG } from "./scene/export";
@@ -19,7 +22,7 @@ export const probablySupportsClipboardBlob =
   "toBlob" in HTMLCanvasElement.prototype;
 
 export async function copyToAppClipboard(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
 ) {
   CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));

+ 5 - 5
src/components/Actions.tsx

@@ -9,6 +9,7 @@ import { ToolButton } from "./ToolButton";
 import { capitalizeString, setCursorForShape } from "../utils";
 import Stack from "./Stack";
 import useIsMobile from "../is-mobile";
+import { getNonDeletedElements } from "../element";
 
 export function SelectedShapeActions({
   appState,
@@ -21,7 +22,10 @@ export function SelectedShapeActions({
   renderAction: ActionManager["renderAction"];
   elementType: ExcalidrawElement["type"];
 }) {
-  const targetElements = getTargetElement(elements, appState);
+  const targetElements = getTargetElement(
+    getNonDeletedElements(elements),
+    appState,
+  );
   const isEditing = Boolean(appState.editingElement);
   const isMobile = useIsMobile();
 
@@ -82,13 +86,9 @@ export function SelectedShapeActions({
 export function ShapesSwitcher({
   elementType,
   setAppState,
-  setElements,
-  elements,
 }: {
   elementType: ExcalidrawElement["type"];
   setAppState: any;
-  setElements: any;
-  elements: readonly ExcalidrawElement[];
 }) {
   return (
     <>

+ 63 - 70
src/components/App.tsx

@@ -20,7 +20,6 @@ import {
   getElementMap,
   getDrawingVersion,
   getSyncableElements,
-  hasNonDeletedElements,
   newLinearElement,
   ResizeArrowFnType,
   resizeElements,
@@ -185,7 +184,7 @@ export class App extends React.Component<any, AppState> {
     this.actionManager = new ActionManager(
       this.syncActionResult,
       () => this.state,
-      () => globalSceneState.getAllElements(),
+      () => globalSceneState.getElementsIncludingDeleted(),
     );
     this.actionManager.registerAll(actions);
 
@@ -209,10 +208,7 @@ export class App extends React.Component<any, AppState> {
           appState={this.state}
           setAppState={this.setAppState}
           actionManager={this.actionManager}
-          elements={globalSceneState.getAllElements().filter((element) => {
-            return !element.isDeleted;
-          })}
-          setElements={this.setElements}
+          elements={globalSceneState.getElements()}
           onRoomCreate={this.openPortal}
           onRoomDestroy={this.closePortal}
           onLockToggle={this.toggleLock}
@@ -310,7 +306,7 @@ export class App extends React.Component<any, AppState> {
     try {
       await Promise.race([
         document.fonts?.ready?.then(() => {
-          globalSceneState.getAllElements().forEach((element) => {
+          globalSceneState.getElementsIncludingDeleted().forEach((element) => {
             if (isTextElement(element)) {
               invalidateShapeForElement(element);
             }
@@ -431,7 +427,7 @@ export class App extends React.Component<any, AppState> {
   }
   private onResize = withBatchedUpdates(() => {
     globalSceneState
-      .getAllElements()
+      .getElementsIncludingDeleted()
       .forEach((element) => invalidateShapeForElement(element));
     this.setState({});
   });
@@ -439,7 +435,7 @@ export class App extends React.Component<any, AppState> {
   private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
     if (
       this.state.isCollaborating &&
-      hasNonDeletedElements(globalSceneState.getAllElements())
+      globalSceneState.getElements().length > 0
     ) {
       event.preventDefault();
       // NOTE: modern browsers no longer allow showing a custom message here
@@ -484,8 +480,9 @@ export class App extends React.Component<any, AppState> {
       );
       cursorButton[socketID] = user.button;
     });
+    const elements = globalSceneState.getElements();
     const { atLeastOneVisibleElement, scrollBars } = renderScene(
-      globalSceneState.getAllElements().filter((element) => {
+      elements.filter((element) => {
         // don't render text element that's being currently edited (it's
         //  rendered on remote only)
         return (
@@ -517,22 +514,20 @@ export class App extends React.Component<any, AppState> {
     if (scrollBars) {
       currentScrollBars = scrollBars;
     }
-    const scrolledOutside =
-      !atLeastOneVisibleElement &&
-      hasNonDeletedElements(globalSceneState.getAllElements());
+    const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
     if (this.state.scrolledOutside !== scrolledOutside) {
       this.setState({ scrolledOutside: scrolledOutside });
     }
     this.saveDebounced();
 
     if (
-      getDrawingVersion(globalSceneState.getAllElements()) >
+      getDrawingVersion(globalSceneState.getElementsIncludingDeleted()) >
       this.lastBroadcastedOrReceivedSceneVersion
     ) {
       this.broadcastScene("SCENE_UPDATE");
     }
 
-    history.record(this.state, globalSceneState.getAllElements());
+    history.record(this.state, globalSceneState.getElementsIncludingDeleted());
   }
 
   // Copy/paste
@@ -543,7 +538,7 @@ export class App extends React.Component<any, AppState> {
     }
     this.copyAll();
     const { elements: nextElements, appState } = deleteSelectedElements(
-      globalSceneState.getAllElements(),
+      globalSceneState.getElementsIncludingDeleted(),
       this.state,
     );
     globalSceneState.replaceAllElements(nextElements);
@@ -561,19 +556,16 @@ export class App extends React.Component<any, AppState> {
   });
 
   private copyAll = () => {
-    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
+    copyToAppClipboard(globalSceneState.getElements(), this.state);
   };
 
   private copyToClipboardAsPng = () => {
-    const selectedElements = getSelectedElements(
-      globalSceneState.getAllElements(),
-      this.state,
-    );
+    const elements = globalSceneState.getElements();
+
+    const selectedElements = getSelectedElements(elements, this.state);
     exportCanvas(
       "clipboard",
-      selectedElements.length
-        ? selectedElements
-        : globalSceneState.getAllElements(),
+      selectedElements.length ? selectedElements : elements,
       this.state,
       this.canvas!,
       this.state,
@@ -582,14 +574,14 @@ export class App extends React.Component<any, AppState> {
 
   private copyToClipboardAsSvg = () => {
     const selectedElements = getSelectedElements(
-      globalSceneState.getAllElements(),
+      globalSceneState.getElements(),
       this.state,
     );
     exportCanvas(
       "clipboard-svg",
       selectedElements.length
         ? selectedElements
-        : globalSceneState.getAllElements(),
+        : globalSceneState.getElements(),
       this.state,
       this.canvas!,
       this.state,
@@ -669,7 +661,7 @@ export class App extends React.Component<any, AppState> {
     );
 
     globalSceneState.replaceAllElements([
-      ...globalSceneState.getAllElements(),
+      ...globalSceneState.getElementsIncludingDeleted(),
       ...newElements,
     ]);
     history.resumeRecording();
@@ -703,7 +695,7 @@ export class App extends React.Component<any, AppState> {
     });
 
     globalSceneState.replaceAllElements([
-      ...globalSceneState.getAllElements(),
+      ...globalSceneState.getElementsIncludingDeleted(),
       element,
     ]);
     this.setState({ selectedElementIds: { [element.id]: true } });
@@ -789,15 +781,15 @@ export class App extends React.Component<any, AppState> {
         // elements with more staler versions than ours, ignore them
         // and keep ours.
         if (
-          globalSceneState.getAllElements() == null ||
-          globalSceneState.getAllElements().length === 0
+          globalSceneState.getElementsIncludingDeleted() == null ||
+          globalSceneState.getElementsIncludingDeleted().length === 0
         ) {
           globalSceneState.replaceAllElements(remoteElements);
         } else {
           // create a map of ids so we don't have to iterate
           // over the array more than once.
           const localElementMap = getElementMap(
-            globalSceneState.getAllElements(),
+            globalSceneState.getElementsIncludingDeleted(),
           );
 
           // Reconcile
@@ -982,12 +974,14 @@ export class App extends React.Component<any, AppState> {
     const data: SocketUpdateDataSource[typeof sceneType] = {
       type: sceneType,
       payload: {
-        elements: getSyncableElements(globalSceneState.getAllElements()),
+        elements: getSyncableElements(
+          globalSceneState.getElementsIncludingDeleted(),
+        ),
       },
     };
     this.lastBroadcastedOrReceivedSceneVersion = Math.max(
       this.lastBroadcastedOrReceivedSceneVersion,
-      getDrawingVersion(globalSceneState.getAllElements()),
+      getDrawingVersion(globalSceneState.getElementsIncludingDeleted()),
     );
     return this._broadcastSocketData(
       data as typeof data & { _brand: "socketUpdateData" },
@@ -1063,7 +1057,7 @@ export class App extends React.Component<any, AppState> {
         ? ELEMENT_SHIFT_TRANSLATE_AMOUNT
         : ELEMENT_TRANSLATE_AMOUNT;
       globalSceneState.replaceAllElements(
-        globalSceneState.getAllElements().map((el) => {
+        globalSceneState.getElementsIncludingDeleted().map((el) => {
           if (this.state.selectedElementIds[el.id]) {
             const update: { x?: number; y?: number } = {};
             if (event.key === KEYS.ARROW_LEFT) {
@@ -1083,7 +1077,7 @@ export class App extends React.Component<any, AppState> {
       event.preventDefault();
     } else if (event.key === KEYS.ENTER) {
       const selectedElements = getSelectedElements(
-        globalSceneState.getAllElements(),
+        globalSceneState.getElements(),
         this.state,
       );
 
@@ -1188,7 +1182,7 @@ export class App extends React.Component<any, AppState> {
 
     const deleteElement = () => {
       globalSceneState.replaceAllElements([
-        ...globalSceneState.getAllElements().map((_element) => {
+        ...globalSceneState.getElementsIncludingDeleted().map((_element) => {
           if (_element.id === element.id) {
             return newElementWith(_element, { isDeleted: true });
           }
@@ -1199,7 +1193,7 @@ export class App extends React.Component<any, AppState> {
 
     const updateElement = (text: string) => {
       globalSceneState.replaceAllElements([
-        ...globalSceneState.getAllElements().map((_element) => {
+        ...globalSceneState.getElementsIncludingDeleted().map((_element) => {
           if (_element.id === element.id) {
             return newTextElement({
               ...(_element as ExcalidrawTextElement),
@@ -1271,7 +1265,7 @@ export class App extends React.Component<any, AppState> {
     centerIfPossible?: boolean;
   }) => {
     const elementAtPosition = getElementAtPosition(
-      globalSceneState.getAllElements(),
+      globalSceneState.getElements(),
       this.state,
       x,
       y,
@@ -1326,7 +1320,7 @@ export class App extends React.Component<any, AppState> {
       });
     } else {
       globalSceneState.replaceAllElements([
-        ...globalSceneState.getAllElements(),
+        ...globalSceneState.getElementsIncludingDeleted(),
         element,
       ]);
 
@@ -1503,13 +1497,12 @@ export class App extends React.Component<any, AppState> {
       return;
     }
 
-    const selectedElements = getSelectedElements(
-      globalSceneState.getAllElements(),
-      this.state,
-    );
+    const elements = globalSceneState.getElements();
+
+    const selectedElements = getSelectedElements(elements, this.state);
     if (selectedElements.length === 1 && !isOverScrollBar) {
       const elementWithResizeHandler = getElementWithResizeHandler(
-        globalSceneState.getAllElements(),
+        elements,
         this.state,
         { x, y },
         this.state.zoom,
@@ -1538,7 +1531,7 @@ export class App extends React.Component<any, AppState> {
       }
     }
     const hitElement = getElementAtPosition(
-      globalSceneState.getAllElements(),
+      elements,
       this.state,
       x,
       y,
@@ -1737,13 +1730,11 @@ export class App extends React.Component<any, AppState> {
     let hitElement: ExcalidrawElement | null = null;
     let hitElementWasAddedToSelection = false;
     if (this.state.elementType === "selection") {
-      const selectedElements = getSelectedElements(
-        globalSceneState.getAllElements(),
-        this.state,
-      );
+      const elements = globalSceneState.getElements();
+      const selectedElements = getSelectedElements(elements, this.state);
       if (selectedElements.length === 1) {
         const elementWithResizeHandler = getElementWithResizeHandler(
-          globalSceneState.getAllElements(),
+          elements,
           this.state,
           { x, y },
           this.state.zoom,
@@ -1781,7 +1772,7 @@ export class App extends React.Component<any, AppState> {
       }
       if (!isResizingElements) {
         hitElement = getElementAtPosition(
-          globalSceneState.getAllElements(),
+          elements,
           this.state,
           x,
           y,
@@ -1809,7 +1800,7 @@ export class App extends React.Component<any, AppState> {
               },
             }));
             globalSceneState.replaceAllElements(
-              globalSceneState.getAllElements(),
+              globalSceneState.getElementsIncludingDeleted(),
             );
             hitElementWasAddedToSelection = true;
           }
@@ -1820,7 +1811,7 @@ export class App extends React.Component<any, AppState> {
             // put the duplicates where the selected elements used to be.
             const nextElements = [];
             const elementsToAppend = [];
-            for (const element of globalSceneState.getAllElements()) {
+            for (const element of globalSceneState.getElementsIncludingDeleted()) {
               if (
                 this.state.selectedElementIds[element.id] ||
                 (element.id === hitElement.id && hitElementWasAddedToSelection)
@@ -1930,7 +1921,7 @@ export class App extends React.Component<any, AppState> {
           points: [...element.points, [0, 0]],
         });
         globalSceneState.replaceAllElements([
-          ...globalSceneState.getAllElements(),
+          ...globalSceneState.getElementsIncludingDeleted(),
           element,
         ]);
         this.setState({
@@ -1958,7 +1949,7 @@ export class App extends React.Component<any, AppState> {
         });
       } else {
         globalSceneState.replaceAllElements([
-          ...globalSceneState.getAllElements(),
+          ...globalSceneState.getElementsIncludingDeleted(),
           element,
         ]);
         this.setState({
@@ -2047,7 +2038,7 @@ export class App extends React.Component<any, AppState> {
         // if elements should be deselected on pointerup
         draggingOccurred = true;
         const selectedElements = getSelectedElements(
-          globalSceneState.getAllElements(),
+          globalSceneState.getElements(),
           this.state,
         );
         if (selectedElements.length > 0) {
@@ -2123,14 +2114,12 @@ export class App extends React.Component<any, AppState> {
       }
 
       if (this.state.elementType === "selection") {
-        if (
-          !event.shiftKey &&
-          isSomeElementSelected(globalSceneState.getAllElements(), this.state)
-        ) {
+        const elements = globalSceneState.getElements();
+        if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
           this.setState({ selectedElementIds: {} });
         }
         const elementsWithinSelection = getElementsWithinSelection(
-          globalSceneState.getAllElements(),
+          elements,
           draggingElement,
         );
         this.setState((prevState) => ({
@@ -2223,7 +2212,7 @@ export class App extends React.Component<any, AppState> {
       ) {
         // remove invisible element which was added in onPointerDown
         globalSceneState.replaceAllElements(
-          globalSceneState.getAllElements().slice(0, -1),
+          globalSceneState.getElementsIncludingDeleted().slice(0, -1),
         );
         this.setState({
           draggingElement: null,
@@ -2240,7 +2229,7 @@ export class App extends React.Component<any, AppState> {
       if (resizingElement && isInvisiblySmallElement(resizingElement)) {
         globalSceneState.replaceAllElements(
           globalSceneState
-            .getAllElements()
+            .getElementsIncludingDeleted()
             .filter((el) => el.id !== resizingElement.id),
         );
       }
@@ -2285,7 +2274,7 @@ export class App extends React.Component<any, AppState> {
 
       if (
         elementType !== "selection" ||
-        isSomeElementSelected(globalSceneState.getAllElements(), this.state)
+        isSomeElementSelected(globalSceneState.getElements(), this.state)
       ) {
         history.resumeRecording();
       }
@@ -2366,8 +2355,9 @@ export class App extends React.Component<any, AppState> {
       window.devicePixelRatio,
     );
 
+    const elements = globalSceneState.getElements();
     const element = getElementAtPosition(
-      globalSceneState.getAllElements(),
+      elements,
       this.state,
       x,
       y,
@@ -2381,12 +2371,12 @@ export class App extends React.Component<any, AppState> {
             action: () => this.pasteFromClipboard(null),
           },
           probablySupportsClipboardBlob &&
-            hasNonDeletedElements(globalSceneState.getAllElements()) && {
+            elements.length > 0 && {
               label: t("labels.copyAsPng"),
               action: this.copyToClipboardAsPng,
             },
           probablySupportsClipboardWriteText &&
-            hasNonDeletedElements(globalSceneState.getAllElements()) && {
+            elements.length > 0 && {
               label: t("labels.copyAsSvg"),
               action: this.copyToClipboardAsSvg,
             },
@@ -2468,7 +2458,7 @@ export class App extends React.Component<any, AppState> {
     scale: number,
   ) {
     const elementClickedInside = getElementContainingPosition(
-      globalSceneState.getAllElements(),
+      globalSceneState.getElementsIncludingDeleted(),
       x,
       y,
     );
@@ -2522,7 +2512,10 @@ export class App extends React.Component<any, AppState> {
   }, 300);
 
   private saveDebounced = debounce(() => {
-    saveToLocalStorage(globalSceneState.getAllElements(), this.state);
+    saveToLocalStorage(
+      globalSceneState.getElementsIncludingDeleted(),
+      this.state,
+    );
   }, 300);
 }
 
@@ -2548,7 +2541,7 @@ if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") {
   Object.defineProperties(window.h, {
     elements: {
       get() {
-        return globalSceneState.getAllElements();
+        return globalSceneState.getElementsIncludingDeleted();
       },
       set(elements: ExcalidrawElement[]) {
         return globalSceneState.replaceAllElements(elements);

+ 4 - 4
src/components/ExportDialog.tsx

@@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef } from "react";
 
 import { ToolButton } from "./ToolButton";
 import { clipboard, exportFile, link } from "./icons";
-import { ExcalidrawElement } from "../element/types";
+import { NonDeletedExcalidrawElement } from "../element/types";
 import { AppState } from "../types";
 import { exportToCanvas } from "../scene/export";
 import { ActionsManagerInterface } from "../actions/types";
@@ -20,7 +20,7 @@ const scales = [1, 2, 3];
 const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
 
 export type ExportCB = (
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   scale?: number,
 ) => void;
 
@@ -35,7 +35,7 @@ function ExportModal({
   onExportToBackend,
 }: {
   appState: AppState;
-  elements: readonly ExcalidrawElement[];
+  elements: readonly NonDeletedExcalidrawElement[];
   exportPadding?: number;
   actionManager: ActionsManagerInterface;
   onExportToPng: ExportCB;
@@ -166,7 +166,7 @@ export function ExportDialog({
   onExportToBackend,
 }: {
   appState: AppState;
-  elements: readonly ExcalidrawElement[];
+  elements: readonly NonDeletedExcalidrawElement[];
   exportPadding?: number;
   actionManager: ActionsManagerInterface;
   onExportToPng: ExportCB;

+ 2 - 2
src/components/HintViewer.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { t } from "../i18n";
-import { ExcalidrawElement } from "../element/types";
+import { NonDeletedExcalidrawElement } from "../element/types";
 import { getSelectedElements } from "../scene";
 
 import "./HintViewer.scss";
@@ -9,7 +9,7 @@ import { isLinearElement } from "../element/typeChecks";
 
 interface Hint {
   appState: AppState;
-  elements: readonly ExcalidrawElement[];
+  elements: readonly NonDeletedExcalidrawElement[];
 }
 
 const getHints = ({ appState, elements }: Hint) => {

+ 2 - 7
src/components/LayerUI.tsx

@@ -4,7 +4,7 @@ import { calculateScrollCenter } from "../scene";
 import { exportCanvas } from "../data";
 
 import { AppState } from "../types";
-import { ExcalidrawElement } from "../element/types";
+import { NonDeletedExcalidrawElement } from "../element/types";
 
 import { ActionManager } from "../actions/manager";
 import { Island } from "./Island";
@@ -31,8 +31,7 @@ interface LayerUIProps {
   appState: AppState;
   canvas: HTMLCanvasElement | null;
   setAppState: any;
-  elements: readonly ExcalidrawElement[];
-  setElements: (elements: readonly ExcalidrawElement[]) => void;
+  elements: readonly NonDeletedExcalidrawElement[];
   onRoomCreate: () => void;
   onRoomDestroy: () => void;
   onLockToggle: () => void;
@@ -45,7 +44,6 @@ export const LayerUI = React.memo(
     setAppState,
     canvas,
     elements,
-    setElements,
     onRoomCreate,
     onRoomDestroy,
     onLockToggle,
@@ -96,7 +94,6 @@ export const LayerUI = React.memo(
       <MobileMenu
         appState={appState}
         elements={elements}
-        setElements={setElements}
         actionManager={actionManager}
         exportButton={renderExportDialog()}
         setAppState={setAppState}
@@ -170,8 +167,6 @@ export const LayerUI = React.memo(
                         <ShapesSwitcher
                           elementType={appState.elementType}
                           setAppState={setAppState}
-                          setElements={setElements}
-                          elements={elements}
                         />
                       </Stack.Row>
                     </Island>

+ 2 - 6
src/components/MobileMenu.tsx

@@ -5,7 +5,7 @@ import { t, setLanguage } from "../i18n";
 import Stack from "./Stack";
 import { LanguageList } from "./LanguageList";
 import { showSelectedShapeActions } from "../element";
-import { ExcalidrawElement } from "../element/types";
+import { NonDeletedExcalidrawElement } from "../element/types";
 import { FixedSideContainer } from "./FixedSideContainer";
 import { Island } from "./Island";
 import { HintViewer } from "./HintViewer";
@@ -22,8 +22,7 @@ type MobileMenuProps = {
   actionManager: ActionManager;
   exportButton: React.ReactNode;
   setAppState: any;
-  elements: readonly ExcalidrawElement[];
-  setElements: any;
+  elements: readonly NonDeletedExcalidrawElement[];
   onRoomCreate: () => void;
   onRoomDestroy: () => void;
   onLockToggle: () => void;
@@ -32,7 +31,6 @@ type MobileMenuProps = {
 export function MobileMenu({
   appState,
   elements,
-  setElements,
   actionManager,
   exportButton,
   setAppState,
@@ -54,8 +52,6 @@ export function MobileMenu({
                     <ShapesSwitcher
                       elementType={appState.elementType}
                       setAppState={setAppState}
-                      setElements={setElements}
-                      elements={elements}
                     />
                   </Stack.Row>
                 </Island>

+ 6 - 4
src/data/index.ts

@@ -1,4 +1,7 @@
-import { ExcalidrawElement } from "../element/types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
 
 import { getDefaultAppState } from "../appState";
 
@@ -16,7 +19,6 @@ import { serializeAsJSON } from "./json";
 import { ExportType } from "../scene/types";
 import { restore } from "./restore";
 import { restoreFromLocalStorage } from "./localStorage";
-import { hasNonDeletedElements } from "../element";
 
 export { loadFromBlob } from "./blob";
 export { saveAsJSON, loadFromJSON } from "./json";
@@ -283,7 +285,7 @@ export async function importFromBackend(
 
 export async function exportCanvas(
   type: ExportType,
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   canvas: HTMLCanvasElement,
   {
@@ -300,7 +302,7 @@ export async function exportCanvas(
     scale?: number;
   },
 ) {
-  if (!hasNonDeletedElements(elements)) {
+  if (elements.length === 0) {
     return window.alert(t("alerts.cannotExportEmptyCanvas"));
   }
   if (type === "svg" || type === "clipboard-svg") {

+ 3 - 3
src/element/collision.ts

@@ -1,6 +1,6 @@
 import { distanceBetweenPointAndSegment } from "../math";
 
-import { ExcalidrawElement } from "./types";
+import { NonDeletedExcalidrawElement } from "./types";
 
 import { getDiamondPoints, getElementAbsoluteCoords } from "./bounds";
 import { Point } from "../types";
@@ -11,7 +11,7 @@ import { isLinearElement } from "./typeChecks";
 import { rotate } from "../math";
 
 function isElementDraggableFromInside(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   appState: AppState,
 ): boolean {
   return (
@@ -21,7 +21,7 @@ function isElementDraggableFromInside(
 }
 
 export function hitTest(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   appState: AppState,
   x: number,
   y: number,

+ 6 - 3
src/element/index.ts

@@ -1,4 +1,4 @@
-import { ExcalidrawElement } from "./types";
+import { ExcalidrawElement, NonDeletedExcalidrawElement } from "./types";
 import { isInvisiblySmallElement } from "./sizeHelpers";
 
 export {
@@ -63,6 +63,9 @@ export function getDrawingVersion(elements: readonly ExcalidrawElement[]) {
   return elements.reduce((acc, el) => acc + el.version, 0);
 }
 
-export function hasNonDeletedElements(elements: readonly ExcalidrawElement[]) {
-  return elements.some((element) => !element.isDeleted);
+export function getNonDeletedElements(elements: readonly ExcalidrawElement[]) {
+  return (
+    elements.filter((element) => !element.isDeleted) as
+    readonly NonDeletedExcalidrawElement[]
+  );
 }

+ 5 - 5
src/element/newElement.ts

@@ -3,6 +3,7 @@ import {
   ExcalidrawTextElement,
   ExcalidrawLinearElement,
   ExcalidrawGenericElement,
+  NonDeleted,
 } from "../element/types";
 import { measureText } from "../utils";
 import { randomInteger, randomId } from "../random";
@@ -56,7 +57,7 @@ function _newElementBase<T extends ExcalidrawElement>(
     seed: rest.seed ?? randomInteger(),
     version: rest.version || 1,
     versionNonce: rest.versionNonce ?? 0,
-    isDeleted: rest.isDeleted ?? false,
+    isDeleted: false as false,
   };
 }
 
@@ -64,7 +65,7 @@ export function newElement(
   opts: {
     type: ExcalidrawGenericElement["type"];
   } & ElementConstructorOpts,
-): ExcalidrawGenericElement {
+): NonDeleted<ExcalidrawGenericElement> {
   return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
 }
 
@@ -73,13 +74,12 @@ export function newTextElement(
     text: string;
     font: string;
   } & ElementConstructorOpts,
-): ExcalidrawTextElement {
+): NonDeleted<ExcalidrawTextElement> {
   const { text, font } = opts;
   const metrics = measureText(text, font);
   const textElement = newElementWith(
     {
       ..._newElementBase<ExcalidrawTextElement>("text", opts),
-      isDeleted: false,
       text: text,
       font: font,
       // Center the text
@@ -100,7 +100,7 @@ export function newLinearElement(
     type: ExcalidrawLinearElement["type"];
     lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
   } & ElementConstructorOpts,
-): ExcalidrawLinearElement {
+): NonDeleted<ExcalidrawLinearElement> {
   return {
     ..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
     points: [],

+ 22 - 18
src/element/resizeElements.ts

@@ -3,7 +3,11 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
 import { getSelectedElements, globalSceneState } from "../scene";
 import { rescalePoints } from "../points";
 import { rotate, adjustXYWithRotation } from "../math";
-import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
+import {
+  ExcalidrawLinearElement,
+  NonDeletedExcalidrawElement,
+  NonDeleted,
+} from "./types";
 import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
 import { isLinearElement } from "./typeChecks";
 import { mutateElement } from "./mutateElement";
@@ -17,7 +21,7 @@ import {
 type ResizeTestType = ReturnType<typeof resizeTest>;
 
 export type ResizeArrowFnType = (
-  element: ExcalidrawLinearElement,
+  element: NonDeleted<ExcalidrawLinearElement>,
   pointIndex: number,
   deltaX: number,
   deltaY: number,
@@ -27,13 +31,13 @@ export type ResizeArrowFnType = (
 ) => void;
 
 const arrowResizeOrigin: ResizeArrowFnType = (
-  element: ExcalidrawLinearElement,
-  pointIndex: number,
-  deltaX: number,
-  deltaY: number,
-  pointerX: number,
-  pointerY: number,
-  perfect: boolean,
+  element,
+  pointIndex,
+  deltaX,
+  deltaY,
+  pointerX,
+  pointerY,
+  perfect,
 ) => {
   const [px, py] = element.points[pointIndex];
   let x = element.x + deltaX;
@@ -63,13 +67,13 @@ const arrowResizeOrigin: ResizeArrowFnType = (
 };
 
 const arrowResizeEnd: ResizeArrowFnType = (
-  element: ExcalidrawLinearElement,
-  pointIndex: number,
-  deltaX: number,
-  deltaY: number,
-  pointerX: number,
-  pointerY: number,
-  perfect: boolean,
+  element,
+  pointIndex,
+  deltaX,
+  deltaY,
+  pointerX,
+  pointerY,
+  perfect,
 ) => {
   const [px, py] = element.points[pointIndex];
   if (perfect) {
@@ -110,7 +114,7 @@ export function resizeElements(
     isRotating: resizeHandle === "rotation",
   });
   const selectedElements = getSelectedElements(
-    globalSceneState.getAllElements(),
+    globalSceneState.getElements(),
     appState,
   );
   if (selectedElements.length === 1) {
@@ -451,7 +455,7 @@ export function resizeElements(
 }
 
 export function canResizeMutlipleElements(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
 ) {
   return elements.every((element) =>
     ["rectangle", "diamond", "ellipse"].includes(element.type),

+ 8 - 4
src/element/resizeTest.ts

@@ -1,4 +1,8 @@
-import { ExcalidrawElement, PointerType } from "./types";
+import {
+  ExcalidrawElement,
+  PointerType,
+  NonDeletedExcalidrawElement,
+} from "./types";
 
 import {
   OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
@@ -24,7 +28,7 @@ function isInHandlerRect(
 }
 
 export function resizeTest(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   appState: AppState,
   x: number,
   y: number,
@@ -66,7 +70,7 @@ export function resizeTest(
 }
 
 export function getElementWithResizeHandler(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   { x, y }: { x: number; y: number },
   zoom: number,
@@ -78,7 +82,7 @@ export function getElementWithResizeHandler(
     }
     const resizeHandle = resizeTest(element, appState, x, y, zoom, pointerType);
     return resizeHandle ? { element, resizeHandle } : null;
-  }, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
+  }, null as { element: NonDeletedExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
 }
 
 export function getResizeHandlerFromCoords(

+ 2 - 2
src/element/showSelectedShapeActions.ts

@@ -1,10 +1,10 @@
 import { AppState } from "../types";
-import { ExcalidrawElement } from "./types";
+import { NonDeletedExcalidrawElement } from "./types";
 import { getSelectedElements } from "../scene";
 
 export const showSelectedShapeActions = (
   appState: AppState,
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
 ) =>
   Boolean(
     appState.editingElement ||

+ 6 - 0
src/element/types.ts

@@ -33,6 +33,12 @@ export type ExcalidrawElement =
   | ExcalidrawTextElement
   | ExcalidrawLinearElement;
 
+export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
+  isDeleted: false;
+};
+
+export type NonDeletedExcalidrawElement = NonDeleted<ExcalidrawElement>;
+
 export type ExcalidrawTextElement = _ExcalidrawElementBase &
   Readonly<{
     type: "text";

+ 10 - 6
src/renderer/renderElement.ts

@@ -1,4 +1,8 @@
-import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
+import {
+  ExcalidrawElement,
+  ExcalidrawTextElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
 import { isTextElement } from "../element/typeChecks";
 import {
   getDiamondPoints,
@@ -24,7 +28,7 @@ export interface ExcalidrawElementWithCanvas {
 }
 
 function generateElementCanvas(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   zoom: number,
 ): ExcalidrawElementWithCanvas {
   const canvas = document.createElement("canvas");
@@ -72,7 +76,7 @@ function generateElementCanvas(
 }
 
 function drawElementOnCanvas(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   rc: RoughCanvas,
   context: CanvasRenderingContext2D,
 ) {
@@ -133,7 +137,7 @@ export function invalidateShapeForElement(element: ExcalidrawElement) {
 }
 
 function generateElement(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   generator: RoughGenerator,
   sceneState?: SceneState,
 ) {
@@ -285,7 +289,7 @@ function drawElementFromCanvas(
 }
 
 export function renderElement(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   rc: RoughCanvas,
   context: CanvasRenderingContext2D,
   renderOptimizations: boolean,
@@ -342,7 +346,7 @@ export function renderElement(
 }
 
 export function renderElementToSvg(
-  element: ExcalidrawElement,
+  element: NonDeletedExcalidrawElement,
   rsvg: RoughSVG,
   svgRoot: SVGElement,
   offsetX?: number,

+ 7 - 6
src/renderer/renderScene.ts

@@ -2,7 +2,10 @@ import { RoughCanvas } from "roughjs/bin/canvas";
 import { RoughSVG } from "roughjs/bin/svg";
 
 import { FlooredNumber, AppState } from "../types";
-import { ExcalidrawElement } from "../element/types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
 import {
   getElementAbsoluteCoords,
   OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
@@ -74,9 +77,9 @@ function strokeCircle(
 }
 
 export function renderScene(
-  allElements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
-  selectionElement: ExcalidrawElement | null,
+  selectionElement: NonDeletedExcalidrawElement | null,
   scale: number,
   rc: RoughCanvas,
   canvas: HTMLCanvasElement,
@@ -99,8 +102,6 @@ export function renderScene(
     return { atLeastOneVisibleElement: false };
   }
 
-  const elements = allElements.filter((element) => !element.isDeleted);
-
   const context = canvas.getContext("2d")!;
   context.scale(scale, scale);
 
@@ -493,7 +494,7 @@ function isVisibleElement(
 
 // This should be only called for exporting purposes
 export function renderSceneToSvg(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   rsvg: RoughSVG,
   svgRoot: SVGElement,
   {

+ 5 - 2
src/scene/comparisons.ts

@@ -1,4 +1,7 @@
-import { ExcalidrawElement } from "../element/types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
 
 import { getElementAbsoluteCoords, hitTest } from "../element";
 import { AppState } from "../types";
@@ -16,7 +19,7 @@ export const hasStroke = (type: string) =>
 export const hasText = (type: string) => type === "text";
 
 export function getElementAtPosition(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   x: number,
   y: number,

+ 3 - 3
src/scene/export.ts

@@ -1,5 +1,5 @@
 import rough from "roughjs/bin/rough";
-import { ExcalidrawElement } from "../element/types";
+import { NonDeletedExcalidrawElement } from "../element/types";
 import { getCommonBounds } from "../element/bounds";
 import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
 import { distance, SVG_NS } from "../utils";
@@ -9,7 +9,7 @@ import { AppState } from "../types";
 export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 
 export function exportToCanvas(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   {
     exportBackground,
@@ -66,7 +66,7 @@ export function exportToCanvas(
 }
 
 export function exportToSvg(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   {
     exportBackground,
     exportPadding = 10,

+ 12 - 4
src/scene/globalScene.ts

@@ -1,4 +1,8 @@
-import { ExcalidrawElement } from "../element/types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
+import { getNonDeletedElements } from "../element";
 
 export interface SceneStateCallback {
   (): void;
@@ -8,15 +12,19 @@ export interface SceneStateCallbackRemover {
   (): void;
 }
 
-class SceneState {
+class GlobalScene {
   private callbacks: Set<SceneStateCallback> = new Set();
 
   constructor(private _elements: readonly ExcalidrawElement[] = []) {}
 
-  getAllElements() {
+  getElementsIncludingDeleted() {
     return this._elements;
   }
 
+  getElements(): readonly NonDeletedExcalidrawElement[] {
+    return getNonDeletedElements(this._elements);
+  }
+
   replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
     this._elements = nextElements;
     this.informMutation();
@@ -44,4 +52,4 @@ class SceneState {
   }
 }
 
-export const globalSceneState = new SceneState();
+export const globalSceneState = new GlobalScene();

+ 11 - 8
src/scene/selection.ts

@@ -1,11 +1,14 @@
-import { ExcalidrawElement } from "../element/types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+} from "../element/types";
 import { getElementAbsoluteCoords, getElementBounds } from "../element";
 import { AppState } from "../types";
 import { newElementWith } from "../element/mutateElement";
 
 export function getElementsWithinSelection(
-  elements: readonly ExcalidrawElement[],
-  selection: ExcalidrawElement,
+  elements: readonly NonDeletedExcalidrawElement[],
+  selection: NonDeletedExcalidrawElement,
 ) {
   const [
     selectionX1,
@@ -47,7 +50,7 @@ export function deleteSelectedElements(
 }
 
 export function isSomeElementSelected(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
 ): boolean {
   return elements.some((element) => appState.selectedElementIds[element.id]);
@@ -58,7 +61,7 @@ export function isSomeElementSelected(
  *  elements. If elements don't share the same value, returns `null`.
  */
 export function getCommonAttributeOfSelectedElements<T>(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   getAttribute: (element: ExcalidrawElement) => T,
 ): T | null {
@@ -73,14 +76,14 @@ export function getCommonAttributeOfSelectedElements<T>(
 }
 
 export function getSelectedElements(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
-): readonly ExcalidrawElement[] {
+) {
   return elements.filter((element) => appState.selectedElementIds[element.id]);
 }
 
 export function getTargetElement(
-  elements: readonly ExcalidrawElement[],
+  elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
 ) {
   return appState.editingElement

+ 2 - 1
src/tests/zindex.test.tsx

@@ -10,6 +10,7 @@ import {
   actionBringToFront,
   actionSendToBack,
 } from "../actions";
+import { ExcalidrawElement } from "../element/types";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -27,7 +28,7 @@ function populateElements(
   const selectedElementIds: any = {};
 
   h.elements = elements.map(({ id, isDeleted = false, isSelected = false }) => {
-    const element: Mutable<ReturnType<typeof newElement>> = newElement({
+    const element: Mutable<ExcalidrawElement> = newElement({
       type: "rectangle",
       x: 100,
       y: 100,

+ 7 - 6
src/types.ts

@@ -1,7 +1,8 @@
 import {
-  ExcalidrawElement,
   PointerType,
   ExcalidrawLinearElement,
+  NonDeletedExcalidrawElement,
+  NonDeleted,
 } from "./element/types";
 import { SHAPES } from "./shapes";
 import { Point as RoughPoint } from "roughjs/bin/geometry";
@@ -12,13 +13,13 @@ export type Point = Readonly<RoughPoint>;
 export type AppState = {
   isLoading: boolean;
   errorMessage: string | null;
-  draggingElement: ExcalidrawElement | null;
-  resizingElement: ExcalidrawElement | null;
-  multiElement: ExcalidrawLinearElement | null;
-  selectionElement: ExcalidrawElement | null;
+  draggingElement: NonDeletedExcalidrawElement | null;
+  resizingElement: NonDeletedExcalidrawElement | null;
+  multiElement: NonDeleted<ExcalidrawLinearElement> | null;
+  selectionElement: NonDeletedExcalidrawElement | null;
   // element being edited, but not necessarily added to elements array yet
   //  (e.g. text element when typing into the input)
-  editingElement: ExcalidrawElement | null;
+  editingElement: NonDeletedExcalidrawElement | null;
   elementType: typeof SHAPES[number]["value"];
   elementLocked: boolean;
   exportBackground: boolean;