groups.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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 };
  64. const selectedElements = getSelectedElements(elements, appState);
  65. for (const selectedElement of selectedElements) {
  66. let groupIds = selectedElement.groupIds;
  67. if (appState.editingGroupId) {
  68. // handle the case where a group is nested within a group
  69. const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
  70. if (indexOfEditingGroup > -1) {
  71. groupIds = groupIds.slice(0, indexOfEditingGroup);
  72. }
  73. }
  74. if (groupIds.length > 0) {
  75. const groupId = groupIds[groupIds.length - 1];
  76. nextAppState = selectGroup(groupId, nextAppState, elements);
  77. }
  78. }
  79. return nextAppState;
  80. };
  81. export const editGroupForSelectedElement = (
  82. appState: AppState,
  83. element: NonDeleted<ExcalidrawElement>,
  84. ): AppState => {
  85. return {
  86. ...appState,
  87. editingGroupId: element.groupIds.length ? element.groupIds[0] : null,
  88. selectedGroupIds: {},
  89. selectedElementIds: {
  90. [element.id]: true,
  91. },
  92. };
  93. };
  94. export const isElementInGroup = (element: ExcalidrawElement, groupId: string) =>
  95. element.groupIds.includes(groupId);
  96. export const getElementsInGroup = (
  97. elements: readonly ExcalidrawElement[],
  98. groupId: string,
  99. ) => elements.filter((element) => isElementInGroup(element, groupId));
  100. export const getSelectedGroupIdForElement = (
  101. element: ExcalidrawElement,
  102. selectedGroupIds: { [groupId: string]: boolean },
  103. ) => element.groupIds.find((groupId) => selectedGroupIds[groupId]);
  104. export const getNewGroupIdsForDuplication = (
  105. groupIds: ExcalidrawElement["groupIds"],
  106. editingGroupId: AppState["editingGroupId"],
  107. mapper: (groupId: GroupId) => GroupId,
  108. ) => {
  109. const copy = [...groupIds];
  110. const positionOfEditingGroupId = editingGroupId
  111. ? groupIds.indexOf(editingGroupId)
  112. : -1;
  113. const endIndex =
  114. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  115. for (let index = 0; index < endIndex; index++) {
  116. copy[index] = mapper(copy[index]);
  117. }
  118. return copy;
  119. };
  120. export const addToGroup = (
  121. prevGroupIds: ExcalidrawElement["groupIds"],
  122. newGroupId: GroupId,
  123. editingGroupId: AppState["editingGroupId"],
  124. ) => {
  125. // insert before the editingGroupId, or push to the end.
  126. const groupIds = [...prevGroupIds];
  127. const positionOfEditingGroupId = editingGroupId
  128. ? groupIds.indexOf(editingGroupId)
  129. : -1;
  130. const positionToInsert =
  131. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  132. groupIds.splice(positionToInsert, 0, newGroupId);
  133. return groupIds;
  134. };
  135. export const removeFromSelectedGroups = (
  136. groupIds: ExcalidrawElement["groupIds"],
  137. selectedGroupIds: { [groupId: string]: boolean },
  138. ) => groupIds.filter((groupId) => !selectedGroupIds[groupId]);