mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-26 10:49:57 +02:00

Introducing independent change detection for appState and elements Generalizing object change, cleanup, refactoring, comments, solving typing issues Shaping increment, change, delta hierarchy Structural clone of elements Introducing store and incremental API Disabling buttons for canvas actions, smaller store and changes improvements Update history entry based on latest changes, iterate through the stack for visible changes to limit empty commands Solving concurrency issues, solving (partly) linear element issues, introducing commitToStore breaking change Fixing existing tests, updating snapshots Trying to be smarter on the appstate change detection Extending collab test, refactoring action / updateScene params, bugfixes Resetting snapshots Resetting snapshots UI / API tests for history - WIP Changing actions related to the observed appstate to at least update the store snapshot - WIP Adding skipping of snapshot update flag for most no-breaking changes compatible solution Ignoring uncomitted elements from local async actions, updating store directly in updateScene Bound element issues - WIP
188 lines
5.6 KiB
TypeScript
188 lines
5.6 KiB
TypeScript
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
|
import { KEYS } from "../keys";
|
|
import { ToolButton } from "../components/ToolButton";
|
|
import { t } from "../i18n";
|
|
import { register } from "./register";
|
|
import { getNonDeletedElements } from "../element";
|
|
import { ExcalidrawElement } from "../element/types";
|
|
import { AppState } from "../types";
|
|
import { newElementWith } from "../element/mutateElement";
|
|
import { getElementsInGroup } from "../groups";
|
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
import { fixBindingsAfterDeletion } from "../element/binding";
|
|
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
|
|
import { updateActiveTool } from "../utils";
|
|
import { TrashIcon } from "../components/icons";
|
|
import { StoreAction } from "./types";
|
|
|
|
const deleteSelectedElements = (
|
|
elements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
) => {
|
|
const framesToBeDeleted = new Set(
|
|
getSelectedElements(
|
|
elements.filter((el) => isFrameLikeElement(el)),
|
|
appState,
|
|
).map((el) => el.id),
|
|
);
|
|
|
|
return {
|
|
elements: elements.map((el) => {
|
|
if (appState.selectedElementIds[el.id]) {
|
|
return newElementWith(el, { isDeleted: true });
|
|
}
|
|
|
|
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
|
|
return newElementWith(el, { isDeleted: true });
|
|
}
|
|
|
|
if (
|
|
isBoundToContainer(el) &&
|
|
appState.selectedElementIds[el.containerId]
|
|
) {
|
|
return newElementWith(el, { isDeleted: true });
|
|
}
|
|
return el;
|
|
}),
|
|
appState: {
|
|
...appState,
|
|
selectedElementIds: {},
|
|
selectedGroupIds: {},
|
|
},
|
|
};
|
|
};
|
|
|
|
const handleGroupEditingState = (
|
|
appState: AppState,
|
|
elements: readonly ExcalidrawElement[],
|
|
): AppState => {
|
|
if (appState.editingGroupId) {
|
|
const siblingElements = getElementsInGroup(
|
|
getNonDeletedElements(elements),
|
|
appState.editingGroupId!,
|
|
);
|
|
if (siblingElements.length) {
|
|
return {
|
|
...appState,
|
|
selectedElementIds: { [siblingElements[0].id]: true },
|
|
};
|
|
}
|
|
}
|
|
return appState;
|
|
};
|
|
|
|
export const actionDeleteSelected = register({
|
|
name: "deleteSelectedElements",
|
|
trackEvent: { category: "element", action: "delete" },
|
|
perform: (elements, appState) => {
|
|
if (appState.editingLinearElement) {
|
|
const {
|
|
elementId,
|
|
selectedPointsIndices,
|
|
startBindingElement,
|
|
endBindingElement,
|
|
} = appState.editingLinearElement;
|
|
const element = LinearElementEditor.getElement(elementId);
|
|
if (!element) {
|
|
return false;
|
|
}
|
|
// case: no point selected → do nothing, as deleting the whole element
|
|
// is most likely a mistake, where you wanted to delete a specific point
|
|
// but failed to select it (or you thought it's selected, while it was
|
|
// only in a hover state)
|
|
if (selectedPointsIndices == null) {
|
|
return false;
|
|
}
|
|
|
|
// case: deleting last remaining point
|
|
if (element.points.length < 2) {
|
|
const nextElements = elements.map((el) => {
|
|
if (el.id === element.id) {
|
|
return newElementWith(el, { isDeleted: true });
|
|
}
|
|
return el;
|
|
});
|
|
const nextAppState = handleGroupEditingState(appState, nextElements);
|
|
|
|
return {
|
|
elements: nextElements,
|
|
appState: {
|
|
...nextAppState,
|
|
editingLinearElement: null,
|
|
},
|
|
storeAction: StoreAction.UPDATE,
|
|
};
|
|
}
|
|
|
|
// We cannot do this inside `movePoint` because it is also called
|
|
// when deleting the uncommitted point (which hasn't caused any binding)
|
|
const binding = {
|
|
startBindingElement: selectedPointsIndices?.includes(0)
|
|
? null
|
|
: startBindingElement,
|
|
endBindingElement: selectedPointsIndices?.includes(
|
|
element.points.length - 1,
|
|
)
|
|
? null
|
|
: endBindingElement,
|
|
};
|
|
|
|
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
|
|
|
return {
|
|
elements,
|
|
appState: {
|
|
...appState,
|
|
editingLinearElement: {
|
|
...appState.editingLinearElement,
|
|
...binding,
|
|
selectedPointsIndices:
|
|
selectedPointsIndices?.[0] > 0
|
|
? [selectedPointsIndices[0] - 1]
|
|
: [0],
|
|
},
|
|
},
|
|
storeAction: StoreAction.CAPTURE,
|
|
};
|
|
}
|
|
let { elements: nextElements, appState: nextAppState } =
|
|
deleteSelectedElements(elements, appState);
|
|
fixBindingsAfterDeletion(
|
|
nextElements,
|
|
elements.filter(({ id }) => appState.selectedElementIds[id]),
|
|
);
|
|
|
|
nextAppState = handleGroupEditingState(nextAppState, nextElements);
|
|
|
|
return {
|
|
elements: nextElements,
|
|
appState: {
|
|
...nextAppState,
|
|
activeTool: updateActiveTool(appState, { type: "selection" }),
|
|
multiElement: null,
|
|
activeEmbeddable: null,
|
|
},
|
|
storeAction: isSomeElementSelected(
|
|
getNonDeletedElements(elements),
|
|
appState,
|
|
)
|
|
? StoreAction.CAPTURE
|
|
: StoreAction.NONE,
|
|
};
|
|
},
|
|
contextItemLabel: "labels.delete",
|
|
keyTest: (event, appState, elements) =>
|
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
|
|
!event[KEYS.CTRL_OR_CMD],
|
|
PanelComponent: ({ elements, appState, updateData }) => (
|
|
<ToolButton
|
|
type="button"
|
|
icon={TrashIcon}
|
|
title={t("labels.delete")}
|
|
aria-label={t("labels.delete")}
|
|
onClick={() => updateData(null)}
|
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
|
/>
|
|
),
|
|
});
|