actionDeleteSelected.tsx 5.5 KB

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