groups.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
  2. import { AppState } from "./types";
  3. import { getSelectedElements } from "./scene";
  4. export const selectGroup = (
  5. groupId: GroupId,
  6. appState: AppState,
  7. elements: readonly NonDeleted<ExcalidrawElement>[],
  8. ): AppState => {
  9. const elementsInGroup = elements.filter((element) =>
  10. element.groupIds.includes(groupId),
  11. );
  12. if (elementsInGroup.length < 2) {
  13. if (
  14. appState.selectedGroupIds[groupId] ||
  15. appState.editingGroupId === groupId
  16. ) {
  17. return {
  18. ...appState,
  19. selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: false },
  20. editingGroupId: null,
  21. };
  22. }
  23. return appState;
  24. }
  25. return {
  26. ...appState,
  27. selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
  28. selectedElementIds: {
  29. ...appState.selectedElementIds,
  30. ...Object.fromEntries(
  31. elementsInGroup.map((element) => [element.id, true]),
  32. ),
  33. },
  34. };
  35. };
  36. /**
  37. * If the element's group is selected, don't render an individual
  38. * selection border around it.
  39. */
  40. export const isSelectedViaGroup = (
  41. appState: AppState,
  42. element: ExcalidrawElement,
  43. ) => getSelectedGroupForElement(appState, element) != null;
  44. export const getSelectedGroupForElement = (
  45. appState: AppState,
  46. element: ExcalidrawElement,
  47. ) =>
  48. element.groupIds
  49. .filter((groupId) => groupId !== appState.editingGroupId)
  50. .find((groupId) => appState.selectedGroupIds[groupId]);
  51. export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
  52. Object.entries(appState.selectedGroupIds)
  53. .filter(([groupId, isSelected]) => isSelected)
  54. .map(([groupId, isSelected]) => groupId);
  55. /**
  56. * When you select an element, you often want to actually select the whole group it's in, unless
  57. * you're currently editing that group.
  58. */
  59. export const selectGroupsForSelectedElements = (
  60. appState: AppState,
  61. elements: readonly NonDeleted<ExcalidrawElement>[],
  62. ): AppState => {
  63. let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
  64. const selectedElements = getSelectedElements(elements, appState);
  65. if (!selectedElements.length) {
  66. return { ...nextAppState, editingGroupId: null };
  67. }
  68. for (const selectedElement of selectedElements) {
  69. let groupIds = selectedElement.groupIds;
  70. if (appState.editingGroupId) {
  71. // handle the case where a group is nested within a group
  72. const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
  73. if (indexOfEditingGroup > -1) {
  74. groupIds = groupIds.slice(0, indexOfEditingGroup);
  75. }
  76. }
  77. if (groupIds.length > 0) {
  78. const groupId = groupIds[groupIds.length - 1];
  79. nextAppState = selectGroup(groupId, nextAppState, elements);
  80. }
  81. }
  82. return nextAppState;
  83. };
  84. export const editGroupForSelectedElement = (
  85. appState: AppState,
  86. element: NonDeleted<ExcalidrawElement>,
  87. ): AppState => {
  88. return {
  89. ...appState,
  90. editingGroupId: element.groupIds.length ? element.groupIds[0] : null,
  91. selectedGroupIds: {},
  92. selectedElementIds: {
  93. [element.id]: true,
  94. },
  95. };
  96. };
  97. export const isElementInGroup = (element: ExcalidrawElement, groupId: string) =>
  98. element.groupIds.includes(groupId);
  99. export const getElementsInGroup = (
  100. elements: readonly ExcalidrawElement[],
  101. groupId: string,
  102. ) => elements.filter((element) => isElementInGroup(element, groupId));
  103. export const getSelectedGroupIdForElement = (
  104. element: ExcalidrawElement,
  105. selectedGroupIds: { [groupId: string]: boolean },
  106. ) => element.groupIds.find((groupId) => selectedGroupIds[groupId]);
  107. export const getNewGroupIdsForDuplication = (
  108. groupIds: ExcalidrawElement["groupIds"],
  109. editingGroupId: AppState["editingGroupId"],
  110. mapper: (groupId: GroupId) => GroupId,
  111. ) => {
  112. const copy = [...groupIds];
  113. const positionOfEditingGroupId = editingGroupId
  114. ? groupIds.indexOf(editingGroupId)
  115. : -1;
  116. const endIndex =
  117. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  118. for (let index = 0; index < endIndex; index++) {
  119. copy[index] = mapper(copy[index]);
  120. }
  121. return copy;
  122. };
  123. export const addToGroup = (
  124. prevGroupIds: ExcalidrawElement["groupIds"],
  125. newGroupId: GroupId,
  126. editingGroupId: AppState["editingGroupId"],
  127. ) => {
  128. // insert before the editingGroupId, or push to the end.
  129. const groupIds = [...prevGroupIds];
  130. const positionOfEditingGroupId = editingGroupId
  131. ? groupIds.indexOf(editingGroupId)
  132. : -1;
  133. const positionToInsert =
  134. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  135. groupIds.splice(positionToInsert, 0, newGroupId);
  136. return groupIds;
  137. };
  138. export const removeFromSelectedGroups = (
  139. groupIds: ExcalidrawElement["groupIds"],
  140. selectedGroupIds: { [groupId: string]: boolean },
  141. ) => groupIds.filter((groupId) => !selectedGroupIds[groupId]);