actionDeleteSelected.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { isSomeElementSelected } from "../scene";
  2. import { KEYS } from "../keys";
  3. import { ToolButton } from "../components/ToolButton";
  4. import { trash } from "../components/icons";
  5. import { t } from "../i18n";
  6. import { register } from "./register";
  7. import { getNonDeletedElements } from "../element";
  8. import { ExcalidrawElement } from "../element/types";
  9. import { AppState } from "../types";
  10. import { newElementWith } from "../element/mutateElement";
  11. import { getElementsInGroup } from "../groups";
  12. import { LinearElementEditor } from "../element/linearElementEditor";
  13. import { fixBindingsAfterDeletion } from "../element/binding";
  14. import { isBoundToContainer } from "../element/typeChecks";
  15. import { updateActiveTool } from "../utils";
  16. const deleteSelectedElements = (
  17. elements: readonly ExcalidrawElement[],
  18. appState: AppState,
  19. ) => {
  20. return {
  21. elements: elements.map((el) => {
  22. if (appState.selectedElementIds[el.id]) {
  23. return newElementWith(el, { isDeleted: true });
  24. }
  25. if (
  26. isBoundToContainer(el) &&
  27. appState.selectedElementIds[el.containerId]
  28. ) {
  29. return newElementWith(el, { isDeleted: true });
  30. }
  31. return el;
  32. }),
  33. appState: {
  34. ...appState,
  35. selectedElementIds: {},
  36. },
  37. };
  38. };
  39. const handleGroupEditingState = (
  40. appState: AppState,
  41. elements: readonly ExcalidrawElement[],
  42. ): AppState => {
  43. if (appState.editingGroupId) {
  44. const siblingElements = getElementsInGroup(
  45. getNonDeletedElements(elements),
  46. appState.editingGroupId!,
  47. );
  48. if (siblingElements.length) {
  49. return {
  50. ...appState,
  51. selectedElementIds: { [siblingElements[0].id]: true },
  52. };
  53. }
  54. }
  55. return appState;
  56. };
  57. export const actionDeleteSelected = register({
  58. name: "deleteSelectedElements",
  59. trackEvent: { category: "element", action: "delete" },
  60. perform: (elements, appState) => {
  61. if (appState.editingLinearElement) {
  62. const {
  63. elementId,
  64. selectedPointsIndices,
  65. startBindingElement,
  66. endBindingElement,
  67. } = appState.editingLinearElement;
  68. const element = LinearElementEditor.getElement(elementId);
  69. if (!element) {
  70. return false;
  71. }
  72. if (
  73. // case: no point selected → delete whole element
  74. selectedPointsIndices == null ||
  75. // case: deleting last remaining point
  76. element.points.length < 2
  77. ) {
  78. const nextElements = elements.filter((el) => el.id !== element.id);
  79. const nextAppState = handleGroupEditingState(appState, nextElements);
  80. return {
  81. elements: nextElements,
  82. appState: {
  83. ...nextAppState,
  84. editingLinearElement: null,
  85. },
  86. commitToHistory: false,
  87. };
  88. }
  89. // We cannot do this inside `movePoint` because it is also called
  90. // when deleting the uncommitted point (which hasn't caused any binding)
  91. const binding = {
  92. startBindingElement: selectedPointsIndices?.includes(0)
  93. ? null
  94. : startBindingElement,
  95. endBindingElement: selectedPointsIndices?.includes(
  96. element.points.length - 1,
  97. )
  98. ? null
  99. : endBindingElement,
  100. };
  101. LinearElementEditor.deletePoints(element, selectedPointsIndices);
  102. return {
  103. elements,
  104. appState: {
  105. ...appState,
  106. editingLinearElement: {
  107. ...appState.editingLinearElement,
  108. ...binding,
  109. selectedPointsIndices:
  110. selectedPointsIndices?.[0] > 0
  111. ? [selectedPointsIndices[0] - 1]
  112. : [0],
  113. },
  114. },
  115. commitToHistory: true,
  116. };
  117. }
  118. let { elements: nextElements, appState: nextAppState } =
  119. deleteSelectedElements(elements, appState);
  120. fixBindingsAfterDeletion(
  121. nextElements,
  122. elements.filter(({ id }) => appState.selectedElementIds[id]),
  123. );
  124. nextAppState = handleGroupEditingState(nextAppState, nextElements);
  125. return {
  126. elements: nextElements,
  127. appState: {
  128. ...nextAppState,
  129. activeTool: updateActiveTool(appState, { type: "selection" }),
  130. multiElement: null,
  131. },
  132. commitToHistory: isSomeElementSelected(
  133. getNonDeletedElements(elements),
  134. appState,
  135. ),
  136. };
  137. },
  138. contextItemLabel: "labels.delete",
  139. keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
  140. PanelComponent: ({ elements, appState, updateData }) => (
  141. <ToolButton
  142. type="button"
  143. icon={trash}
  144. title={t("labels.delete")}
  145. aria-label={t("labels.delete")}
  146. onClick={() => updateData(null)}
  147. visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
  148. />
  149. ),
  150. });