fix: eraser removed deleted elements (#5155)

* fix: eraser removed deleted elements

* rename `getElements` API

* fix one more case of not including deleted elements
This commit is contained in:
David Luzar 2022-05-07 21:01:37 +02:00 committed by GitHub
parent 3d56ceb794
commit a524eeb66e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 135 additions and 125 deletions

View File

@ -468,7 +468,7 @@ class App extends React.Component<AppProps, AppState> {
public render() { public render() {
const { zenModeEnabled, viewModeEnabled } = this.state; const { zenModeEnabled, viewModeEnabled } = this.state;
const selectedElement = getSelectedElements( const selectedElement = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
const { const {
@ -501,7 +501,7 @@ class App extends React.Component<AppProps, AppState> {
files={this.files} files={this.files}
setAppState={this.setAppState} setAppState={this.setAppState}
actionManager={this.actionManager} actionManager={this.actionManager}
elements={this.scene.getElements()} elements={this.scene.getNonDeletedElements()}
onCollabButtonClick={onCollabButtonClick} onCollabButtonClick={onCollabButtonClick}
onLockToggle={this.toggleLock} onLockToggle={this.toggleLock}
onPenModeToggle={this.togglePenMode} onPenModeToggle={this.togglePenMode}
@ -549,7 +549,7 @@ class App extends React.Component<AppProps, AppState> {
<Stats <Stats
appState={this.state} appState={this.state}
setAppState={this.setAppState} setAppState={this.setAppState}
elements={this.scene.getElements()} elements={this.scene.getNonDeletedElements()}
onClose={this.toggleStats} onClose={this.toggleStats}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
/> />
@ -578,7 +578,7 @@ class App extends React.Component<AppProps, AppState> {
}; };
public getSceneElements = () => { public getSceneElements = () => {
return this.scene.getElements(); return this.scene.getNonDeletedElements();
}; };
private syncActionResult = withBatchedUpdates( private syncActionResult = withBatchedUpdates(
@ -1195,7 +1195,9 @@ class App extends React.Component<AppProps, AppState> {
); );
cursorButton[socketId] = user.button; cursorButton[socketId] = user.button;
}); });
const renderingElements = this.scene.getElements().filter((element) => { const renderingElements = this.scene
.getNonDeletedElements()
.filter((element) => {
if (isImageElement(element)) { if (isImageElement(element)) {
if ( if (
// not placed on canvas yet (but in elements array) // not placed on canvas yet (but in elements array)
@ -1525,7 +1527,7 @@ class App extends React.Component<AppProps, AppState> {
}, {} as any), }, {} as any),
selectedGroupIds: {}, selectedGroupIds: {},
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
() => { () => {
if (opts.files) { if (opts.files) {
@ -1626,7 +1628,7 @@ class App extends React.Component<AppProps, AppState> {
scrollToContent = ( scrollToContent = (
target: target:
| ExcalidrawElement | ExcalidrawElement
| readonly ExcalidrawElement[] = this.scene.getElements(), | readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
) => { ) => {
this.setState({ this.setState({
...calculateScrollCenter( ...calculateScrollCenter(
@ -1671,7 +1673,7 @@ class App extends React.Component<AppProps, AppState> {
this.files = { ...this.files, ...Object.fromEntries(filesMap) }; this.files = { ...this.files, ...Object.fromEntries(filesMap) };
this.scene.getElements().forEach((element) => { this.scene.getNonDeletedElements().forEach((element) => {
if ( if (
isInitializedImageElement(element) && isInitializedImageElement(element) &&
filesMap.has(element.fileId) filesMap.has(element.fileId)
@ -1816,7 +1818,7 @@ class App extends React.Component<AppProps, AppState> {
: ELEMENT_TRANSLATE_AMOUNT); : ELEMENT_TRANSLATE_AMOUNT);
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
true, true,
); );
@ -1850,7 +1852,7 @@ class App extends React.Component<AppProps, AppState> {
event.preventDefault(); event.preventDefault();
} else if (event.key === KEYS.ENTER) { } else if (event.key === KEYS.ENTER) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
@ -1918,7 +1920,7 @@ class App extends React.Component<AppProps, AppState> {
!event[KEYS.CTRL_OR_CMD] !event[KEYS.CTRL_OR_CMD]
) { ) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
if ( if (
@ -1965,7 +1967,7 @@ class App extends React.Component<AppProps, AppState> {
} }
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
isBindingEnabled(this.state) isBindingEnabled(this.state)
@ -2115,7 +2117,9 @@ class App extends React.Component<AppProps, AppState> {
})); }));
} }
if (isDeleted) { if (isDeleted) {
fixBindingsAfterDeletion(this.scene.getElements(), [element]); fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
element,
]);
} }
if (!isDeleted || isExistingElement) { if (!isDeleted || isExistingElement) {
this.history.resumeRecording(); this.history.resumeRecording();
@ -2217,9 +2221,9 @@ class App extends React.Component<AppProps, AppState> {
): NonDeleted<ExcalidrawElement>[] { ): NonDeleted<ExcalidrawElement>[] {
const elements = const elements =
includeBoundTextElement && includeLockedElements includeBoundTextElement && includeLockedElements
? this.scene.getElements() ? this.scene.getNonDeletedElements()
: this.scene : this.scene
.getElements() .getNonDeletedElements()
.filter( .filter(
(element) => (element) =>
(includeLockedElements || !element.locked) && (includeLockedElements || !element.locked) &&
@ -2260,7 +2264,7 @@ class App extends React.Component<AppProps, AppState> {
let container: ExcalidrawTextContainer | null = null; let container: ExcalidrawTextContainer | null = null;
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
@ -2284,7 +2288,7 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
container = getTextBindableContainerAtPosition( container = getTextBindableContainerAtPosition(
this.scene this.scene
.getElements() .getNonDeletedElements()
.filter( .filter(
(ele) => (ele) =>
isTextBindableContainer(ele, false) && !getBoundTextElement(ele), isTextBindableContainer(ele, false) && !getBoundTextElement(ele),
@ -2388,7 +2392,7 @@ class App extends React.Component<AppProps, AppState> {
} }
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
@ -2433,7 +2437,7 @@ class App extends React.Component<AppProps, AppState> {
selectedElementIds: { [hitElement!.id]: true }, selectedElementIds: { [hitElement!.id]: true },
selectedGroupIds: {}, selectedGroupIds: {},
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
); );
return; return;
@ -2443,7 +2447,7 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.canvas); resetCursor(this.canvas);
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) { if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
@ -2469,7 +2473,7 @@ class App extends React.Component<AppProps, AppState> {
): ExcalidrawElement | undefined => { ): ExcalidrawElement | undefined => {
// Reversing so we traverse the elements in decreasing order // Reversing so we traverse the elements in decreasing order
// of z-index // of z-index
const elements = this.scene.getElements().slice().reverse(); const elements = this.scene.getNonDeletedElements().slice().reverse();
let hitElementIndex = Infinity; let hitElementIndex = Infinity;
return elements.find((element, index) => { return elements.find((element, index) => {
@ -2731,7 +2735,7 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const elements = this.scene.getElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
if ( if (
@ -2905,7 +2909,7 @@ class App extends React.Component<AppProps, AppState> {
point.y = nextY; point.y = nextY;
} }
const elements = this.scene.getElements().map((ele) => { const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
const id = const id =
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId) isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
? ele.containerId ? ele.containerId
@ -3287,7 +3291,7 @@ class App extends React.Component<AppProps, AppState> {
): PointerDownState { ): PointerDownState {
const origin = viewportCoordsToSceneCoords(event, this.state); const origin = viewportCoordsToSceneCoords(event, this.state);
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements); const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
@ -3305,7 +3309,9 @@ class App extends React.Component<AppProps, AppState> {
), ),
// we need to duplicate because we'll be updating this state // we need to duplicate because we'll be updating this state
lastCoords: { ...origin }, lastCoords: { ...origin },
originalElements: this.scene.getElements().reduce((acc, element) => { originalElements: this.scene
.getNonDeletedElements()
.reduce((acc, element) => {
acc.set(element.id, deepCopyElement(element)); acc.set(element.id, deepCopyElement(element));
return acc; return acc;
}, new Map() as PointerDownState["originalElements"]), }, new Map() as PointerDownState["originalElements"]),
@ -3405,7 +3411,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
): boolean => { ): boolean => {
if (this.state.activeTool.type === "selection") { if (this.state.activeTool.type === "selection") {
const elements = this.scene.getElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithTransformHandleType = const elementWithTransformHandleType =
@ -3578,7 +3584,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
showHyperlinkPopup: hitElement.link ? "info" : false, showHyperlinkPopup: hitElement.link ? "info" : false,
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
); );
}); });
pointerDownState.hit.wasAddedToSelection = true; pointerDownState.hit.wasAddedToSelection = true;
@ -3928,7 +3934,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.offset === null) { if (pointerDownState.drag.offset === null) {
pointerDownState.drag.offset = tupleToCoors( pointerDownState.drag.offset = tupleToCoors(
getDragOffsetXY( getDragOffsetXY(
getSelectedElements(this.scene.getElements(), this.state), getSelectedElements(this.scene.getNonDeletedElements(), this.state),
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
), ),
@ -4023,7 +4029,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.hasHitElementInside) pointerDownState.hit.hasHitElementInside)
) { ) {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
if (selectedElements.every((element) => element.locked)) { if (selectedElements.every((element) => element.locked)) {
@ -4192,7 +4198,7 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.activeTool.type === "selection") { if (this.state.activeTool.type === "selection") {
pointerDownState.boxSelection.hasOccurred = true; pointerDownState.boxSelection.hasOccurred = true;
const elements = this.scene.getElements(); const elements = this.scene.getNonDeletedElements();
if ( if (
!event.shiftKey && !event.shiftKey &&
// allows for box-selecting points (without shift) // allows for box-selecting points (without shift)
@ -4208,7 +4214,7 @@ class App extends React.Component<AppProps, AppState> {
[pointerDownState.hit.element!.id]: true, [pointerDownState.hit.element!.id]: true,
}, },
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
); );
} else { } else {
@ -4257,7 +4263,7 @@ class App extends React.Component<AppProps, AppState> {
? "info" ? "info"
: false, : false,
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
); );
} }
@ -4578,7 +4584,10 @@ class App extends React.Component<AppProps, AppState> {
// hitElement is part of // hitElement is part of
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
.flatMap((groupId) => .flatMap((groupId) =>
getElementsInGroup(this.scene.getElements(), groupId), getElementsInGroup(
this.scene.getNonDeletedElements(),
groupId,
),
) )
.map((element) => ({ [element.id]: false })) .map((element) => ({ [element.id]: false }))
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {}); .reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
@ -4607,7 +4616,7 @@ class App extends React.Component<AppProps, AppState> {
[hitElement!.id]: false, [hitElement!.id]: false,
}, },
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
); );
} }
@ -4629,7 +4638,7 @@ class App extends React.Component<AppProps, AppState> {
...prevState, ...prevState,
selectedElementIds: { [hitElement.id]: true }, selectedElementIds: { [hitElement.id]: true },
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
})); }));
} }
@ -4674,7 +4683,7 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
activeTool.type !== "selection" || activeTool.type !== "selection" ||
isSomeElementSelected(this.scene.getElements(), this.state) isSomeElementSelected(this.scene.getNonDeletedElements(), this.state)
) { ) {
this.history.resumeRecording(); this.history.resumeRecording();
} }
@ -4683,7 +4692,7 @@ class App extends React.Component<AppProps, AppState> {
(isBindingEnabled(this.state) (isBindingEnabled(this.state)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements
: unbindLinearElements)( : unbindLinearElements)(
getSelectedElements(this.scene.getElements(), this.state), getSelectedElements(this.scene.getNonDeletedElements(), this.state),
); );
} }
@ -4706,7 +4715,7 @@ class App extends React.Component<AppProps, AppState> {
private restoreReadyToEraseElements = ( private restoreReadyToEraseElements = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
) => { ) => {
const elements = this.scene.getElements().map((ele) => { const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
if ( if (
pointerDownState.elementIdsToErase[ele.id] && pointerDownState.elementIdsToErase[ele.id] &&
pointerDownState.elementIdsToErase[ele.id].erase pointerDownState.elementIdsToErase[ele.id].erase
@ -4730,7 +4739,7 @@ class App extends React.Component<AppProps, AppState> {
}; };
private eraseElements = (pointerDownState: PointerDownState) => { private eraseElements = (pointerDownState: PointerDownState) => {
const elements = this.scene.getElements().map((ele) => { const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
if ( if (
pointerDownState.elementIdsToErase[ele.id] && pointerDownState.elementIdsToErase[ele.id] &&
pointerDownState.elementIdsToErase[ele.id].erase pointerDownState.elementIdsToErase[ele.id].erase
@ -5099,7 +5108,7 @@ class App extends React.Component<AppProps, AppState> {
/** adds new images to imageCache and re-renders if needed */ /** adds new images to imageCache and re-renders if needed */
private addNewImagesToImageCache = async ( private addNewImagesToImageCache = async (
imageElements: InitializedExcalidrawImageElement[] = getInitializedImageElements( imageElements: InitializedExcalidrawImageElement[] = getInitializedImageElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
files: BinaryFiles = this.files, files: BinaryFiles = this.files,
) => { ) => {
@ -5392,7 +5401,7 @@ class App extends React.Component<AppProps, AppState> {
...this.state, ...this.state,
selectedElementIds: { [element.id]: true }, selectedElementIds: { [element.id]: true },
}, },
this.scene.getElements(), this.scene.getNonDeletedElements(),
), ),
() => { () => {
this._openContextMenu({ top, left }, type); this._openContextMenu({ top, left }, type);
@ -5468,7 +5477,7 @@ class App extends React.Component<AppProps, AppState> {
event: MouseEvent | KeyboardEvent, event: MouseEvent | KeyboardEvent,
): boolean => { ): boolean => {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );
const transformHandleType = pointerDownState.resize.handleType; const transformHandleType = pointerDownState.resize.handleType;
@ -5555,10 +5564,10 @@ class App extends React.Component<AppProps, AppState> {
const separator = "separator"; const separator = "separator";
const elements = this.scene.getElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
this.scene.getElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
); );

View File

@ -47,6 +47,20 @@ export const isBindingEnabled = (appState: AppState): boolean => {
return appState.isBindingEnabled; return appState.isBindingEnabled;
}; };
const getNonDeletedElements = (
scene: Scene,
ids: readonly ExcalidrawElement["id"][],
): NonDeleted<ExcalidrawElement>[] => {
const result: NonDeleted<ExcalidrawElement>[] = [];
ids.forEach((id) => {
const element = scene.getNonDeletedElement(id);
if (element != null) {
result.push(element);
}
});
return result;
};
export const bindOrUnbindLinearElement = ( export const bindOrUnbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startBindingElement: ExcalidrawBindableElement | null | "keep", startBindingElement: ExcalidrawBindableElement | null | "keep",
@ -74,16 +88,17 @@ export const bindOrUnbindLinearElement = (
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
(id) => !boundToElementIds.has(id), (id) => !boundToElementIds.has(id),
); );
Scene.getScene(linearElement)!
.getNonDeletedElements(onlyUnbound) getNonDeletedElements(Scene.getScene(linearElement)!, onlyUnbound).forEach(
.forEach((element) => { (element) => {
mutateElement(element, { mutateElement(element, {
boundElements: element.boundElements?.filter( boundElements: element.boundElements?.filter(
(element) => (element) =>
element.type !== "arrow" || element.id !== linearElement.id, element.type !== "arrow" || element.id !== linearElement.id,
), ),
}); });
}); },
);
}; };
const bindOrUnbindLinearElementEdge = ( const bindOrUnbindLinearElementEdge = (
@ -253,7 +268,7 @@ export const getHoveredElementForBinding = (
scene: Scene, scene: Scene,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
const hoveredElement = getElementAtPosition( const hoveredElement = getElementAtPosition(
scene.getElements(), scene.getNonDeletedElements(),
(element) => (element) =>
isBindableElement(element, false) && isBindableElement(element, false) &&
bindingBorderTest(element, pointerCoords), bindingBorderTest(element, pointerCoords),
@ -305,9 +320,11 @@ export const updateBoundElements = (
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
Scene.getScene(changedElement)!
.getNonDeletedElements(boundLinearElements.map((el) => el.id)) getNonDeletedElements(
.forEach((element) => { Scene.getScene(changedElement)!,
boundLinearElements.map((el) => el.id),
).forEach((element) => {
if (!isLinearElement(element)) { if (!isLinearElement(element)) {
return; return;
} }
@ -507,7 +524,7 @@ const getElligibleElementsForBindableElementAndWhere = (
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
): SuggestedPointBinding[] => { ): SuggestedPointBinding[] => {
return Scene.getScene(bindableElement)! return Scene.getScene(bindableElement)!
.getElements() .getNonDeletedElements()
.map((element) => { .map((element) => {
if (!isBindingElement(element, false)) { if (!isBindingElement(element, false)) {
return null; return null;

View File

@ -52,13 +52,11 @@ class Scene {
private elements: readonly ExcalidrawElement[] = []; private elements: readonly ExcalidrawElement[] = [];
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>(); private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
// TODO: getAllElementsIncludingDeleted
getElementsIncludingDeleted() { getElementsIncludingDeleted() {
return this.elements; return this.elements;
} }
// TODO: getAllNonDeletedElements getNonDeletedElements(): readonly NonDeletedExcalidrawElement[] {
getElements(): readonly NonDeletedExcalidrawElement[] {
return this.nonDeletedElements; return this.nonDeletedElements;
} }
@ -76,20 +74,6 @@ class Scene {
return null; return null;
} }
// TODO: Rename methods here, this is confusing
getNonDeletedElements(
ids: readonly ExcalidrawElement["id"][],
): NonDeleted<ExcalidrawElement>[] {
const result: NonDeleted<ExcalidrawElement>[] = [];
ids.forEach((id) => {
const element = this.getNonDeletedElement(id);
if (element != null) {
result.push(element);
}
});
return result;
}
replaceAllElements(nextElements: readonly ExcalidrawElement[]) { replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
this.elements = nextElements; this.elements = nextElements;
this.elementsMap.clear(); this.elementsMap.clear();