groups.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
  2. import { AppState } from "./types";
  3. import { getSelectedElements } from "./scene";
  4. export function 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 function isSelectedViaGroup(
  41. appState: AppState,
  42. element: ExcalidrawElement,
  43. ) {
  44. return getSelectedGroupForElement(appState, element) != null;
  45. }
  46. export const getSelectedGroupForElement = (
  47. appState: AppState,
  48. element: ExcalidrawElement,
  49. ) => {
  50. return element.groupIds
  51. .filter((groupId) => groupId !== appState.editingGroupId)
  52. .find((groupId) => appState.selectedGroupIds[groupId]);
  53. };
  54. export function getSelectedGroupIds(appState: AppState): GroupId[] {
  55. return Object.entries(appState.selectedGroupIds)
  56. .filter(([groupId, isSelected]) => isSelected)
  57. .map(([groupId, isSelected]) => groupId);
  58. }
  59. /**
  60. * When you select an element, you often want to actually select the whole group it's in, unless
  61. * you're currently editing that group.
  62. */
  63. export function selectGroupsForSelectedElements(
  64. appState: AppState,
  65. elements: readonly NonDeleted<ExcalidrawElement>[],
  66. ): AppState {
  67. let nextAppState = { ...appState };
  68. const selectedElements = getSelectedElements(elements, appState);
  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 function isElementInGroup(element: ExcalidrawElement, groupId: string) {
  99. return element.groupIds.includes(groupId);
  100. }
  101. export function getElementsInGroup(
  102. elements: readonly ExcalidrawElement[],
  103. groupId: string,
  104. ) {
  105. return elements.filter((element) => isElementInGroup(element, groupId));
  106. }
  107. export function getSelectedGroupIdForElement(
  108. element: ExcalidrawElement,
  109. selectedGroupIds: { [groupId: string]: boolean },
  110. ) {
  111. return element.groupIds.find((groupId) => selectedGroupIds[groupId]);
  112. }
  113. export function getNewGroupIdsForDuplication(
  114. groupIds: ExcalidrawElement["groupIds"],
  115. editingGroupId: AppState["editingGroupId"],
  116. mapper: (groupId: GroupId) => GroupId,
  117. ) {
  118. const copy = [...groupIds];
  119. const positionOfEditingGroupId = editingGroupId
  120. ? groupIds.indexOf(editingGroupId)
  121. : -1;
  122. const endIndex =
  123. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  124. for (let i = 0; i < endIndex; i++) {
  125. copy[i] = mapper(copy[i]);
  126. }
  127. return copy;
  128. }
  129. export function addToGroup(
  130. prevGroupIds: ExcalidrawElement["groupIds"],
  131. newGroupId: GroupId,
  132. editingGroupId: AppState["editingGroupId"],
  133. ) {
  134. // insert before the editingGroupId, or push to the end.
  135. const groupIds = [...prevGroupIds];
  136. const positionOfEditingGroupId = editingGroupId
  137. ? groupIds.indexOf(editingGroupId)
  138. : -1;
  139. const positionToInsert =
  140. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  141. groupIds.splice(positionToInsert, 0, newGroupId);
  142. return groupIds;
  143. }
  144. export function removeFromSelectedGroups(
  145. groupIds: ExcalidrawElement["groupIds"],
  146. selectedGroupIds: { [groupId: string]: boolean },
  147. ) {
  148. return groupIds.filter((groupId) => !selectedGroupIds[groupId]);
  149. }