actionDeleteSelected.tsx 4.3 KB

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