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>
This commit is contained in:
Pete Hunt 2020-04-08 09:49:52 -07:00 committed by GitHub
parent c714c778ab
commit df0613d8ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 260 additions and 189 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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