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

@ -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 (
<>

View file

@ -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);

View file

@ -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;

View file

@ -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) => {

View file

@ -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>

View file

@ -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>