groups.ts 5.8 KB

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