groups.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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 !!element.groupIds
  45. .filter((groupId) => groupId !== appState.editingGroupId)
  46. .find((groupId) => appState.selectedGroupIds[groupId]);
  47. }
  48. export function getSelectedGroupIds(appState: AppState): GroupId[] {
  49. return Object.entries(appState.selectedGroupIds)
  50. .filter(([groupId, isSelected]) => isSelected)
  51. .map(([groupId, isSelected]) => groupId);
  52. }
  53. /**
  54. * When you select an element, you often want to actually select the whole group it's in, unless
  55. * you're currently editing that group.
  56. */
  57. export function selectGroupsForSelectedElements(
  58. appState: AppState,
  59. elements: readonly NonDeleted<ExcalidrawElement>[],
  60. ): AppState {
  61. let nextAppState = { ...appState };
  62. const selectedElements = getSelectedElements(elements, appState);
  63. for (const selectedElement of selectedElements) {
  64. let groupIds = selectedElement.groupIds;
  65. if (appState.editingGroupId) {
  66. // handle the case where a group is nested within a group
  67. const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
  68. if (indexOfEditingGroup > -1) {
  69. groupIds = groupIds.slice(0, indexOfEditingGroup);
  70. }
  71. }
  72. if (groupIds.length > 0) {
  73. const groupId = groupIds[groupIds.length - 1];
  74. nextAppState = selectGroup(groupId, nextAppState, elements);
  75. }
  76. }
  77. return nextAppState;
  78. }
  79. export function isElementInGroup(element: ExcalidrawElement, groupId: string) {
  80. return element.groupIds.includes(groupId);
  81. }
  82. export function getElementsInGroup(
  83. elements: readonly ExcalidrawElement[],
  84. groupId: string,
  85. ) {
  86. return elements.filter((element) => isElementInGroup(element, groupId));
  87. }
  88. export function getSelectedGroupIdForElement(
  89. element: ExcalidrawElement,
  90. selectedGroupIds: { [groupId: string]: boolean },
  91. ) {
  92. return element.groupIds.find((groupId) => selectedGroupIds[groupId]);
  93. }
  94. export function getNewGroupIdsForDuplication(
  95. groupIds: ExcalidrawElement["groupIds"],
  96. editingGroupId: AppState["editingGroupId"],
  97. mapper: (groupId: GroupId) => GroupId,
  98. ) {
  99. const copy = [...groupIds];
  100. const positionOfEditingGroupId = editingGroupId
  101. ? groupIds.indexOf(editingGroupId)
  102. : -1;
  103. const endIndex =
  104. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  105. for (let i = 0; i < endIndex; i++) {
  106. copy[i] = mapper(copy[i]);
  107. }
  108. return copy;
  109. }
  110. export function addToGroup(
  111. prevGroupIds: ExcalidrawElement["groupIds"],
  112. newGroupId: GroupId,
  113. editingGroupId: AppState["editingGroupId"],
  114. ) {
  115. // insert before the editingGroupId, or push to the end.
  116. const groupIds = [...prevGroupIds];
  117. const positionOfEditingGroupId = editingGroupId
  118. ? groupIds.indexOf(editingGroupId)
  119. : -1;
  120. const positionToInsert =
  121. positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
  122. groupIds.splice(positionToInsert, 0, newGroupId);
  123. return groupIds;
  124. }
  125. export function removeFromSelectedGroups(
  126. groupIds: ExcalidrawElement["groupIds"],
  127. selectedGroupIds: { [groupId: string]: boolean },
  128. ) {
  129. return groupIds.filter((groupId) => !selectedGroupIds[groupId]);
  130. }