groups.ts 5.5 KB

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