excalidraw/src/groups.ts

168 lines
4.7 KiB
TypeScript
Raw Normal View History

import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
import { AppState } from "./types";
import { getSelectedElements } from "./scene";
export function selectGroup(
groupId: GroupId,
appState: AppState,
elements: readonly NonDeleted<ExcalidrawElement>[],
): AppState {
2020-05-30 22:48:57 +02:00
const elementsInGroup = elements.filter((element) =>
element.groupIds.includes(groupId),
);
if (elementsInGroup.length < 2) {
if (
appState.selectedGroupIds[groupId] ||
appState.editingGroupId === groupId
) {
return {
...appState,
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: false },
editingGroupId: null,
};
}
return appState;
}
return {
...appState,
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
selectedElementIds: {
...appState.selectedElementIds,
...Object.fromEntries(
2020-05-30 22:48:57 +02:00
elementsInGroup.map((element) => [element.id, true]),
),
},
};
}
/**
* If the element's group is selected, don't render an individual
* selection border around it.
*/
export function isSelectedViaGroup(
appState: AppState,
element: ExcalidrawElement,
) {
return getSelectedGroupForElement(appState, element) != null;
}
export const getSelectedGroupForElement = (
appState: AppState,
element: ExcalidrawElement,
) => {
return element.groupIds
.filter((groupId) => groupId !== appState.editingGroupId)
.find((groupId) => appState.selectedGroupIds[groupId]);
};
export function getSelectedGroupIds(appState: AppState): GroupId[] {
return Object.entries(appState.selectedGroupIds)
.filter(([groupId, isSelected]) => isSelected)
.map(([groupId, isSelected]) => groupId);
}
/**
* When you select an element, you often want to actually select the whole group it's in, unless
* you're currently editing that group.
*/
export function selectGroupsForSelectedElements(
appState: AppState,
elements: readonly NonDeleted<ExcalidrawElement>[],
): AppState {
let nextAppState = { ...appState };
const selectedElements = getSelectedElements(elements, appState);
for (const selectedElement of selectedElements) {
let groupIds = selectedElement.groupIds;
if (appState.editingGroupId) {
// handle the case where a group is nested within a group
const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
if (indexOfEditingGroup > -1) {
groupIds = groupIds.slice(0, indexOfEditingGroup);
}
}
if (groupIds.length > 0) {
const groupId = groupIds[groupIds.length - 1];
nextAppState = selectGroup(groupId, nextAppState, elements);
}
}
return nextAppState;
}
export const editGroupForSelectedElement = (
appState: AppState,
element: NonDeleted<ExcalidrawElement>,
): AppState => {
return {
...appState,
editingGroupId: element.groupIds.length ? element.groupIds[0] : null,
selectedGroupIds: {},
selectedElementIds: {
[element.id]: true,
},
};
};
export function isElementInGroup(element: ExcalidrawElement, groupId: string) {
return element.groupIds.includes(groupId);
}
export function getElementsInGroup(
elements: readonly ExcalidrawElement[],
groupId: string,
) {
return elements.filter((element) => isElementInGroup(element, groupId));
}
export function getSelectedGroupIdForElement(
element: ExcalidrawElement,
selectedGroupIds: { [groupId: string]: boolean },
) {
return element.groupIds.find((groupId) => selectedGroupIds[groupId]);
}
export function getNewGroupIdsForDuplication(
2020-05-30 22:48:57 +02:00
groupIds: ExcalidrawElement["groupIds"],
editingGroupId: AppState["editingGroupId"],
mapper: (groupId: GroupId) => GroupId,
) {
const copy = [...groupIds];
const positionOfEditingGroupId = editingGroupId
? groupIds.indexOf(editingGroupId)
: -1;
const endIndex =
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
for (let i = 0; i < endIndex; i++) {
copy[i] = mapper(copy[i]);
}
return copy;
}
export function addToGroup(
2020-05-30 22:48:57 +02:00
prevGroupIds: ExcalidrawElement["groupIds"],
newGroupId: GroupId,
2020-05-30 22:48:57 +02:00
editingGroupId: AppState["editingGroupId"],
) {
// insert before the editingGroupId, or push to the end.
const groupIds = [...prevGroupIds];
const positionOfEditingGroupId = editingGroupId
? groupIds.indexOf(editingGroupId)
: -1;
const positionToInsert =
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
groupIds.splice(positionToInsert, 0, newGroupId);
return groupIds;
}
export function removeFromSelectedGroups(
2020-05-30 22:48:57 +02:00
groupIds: ExcalidrawElement["groupIds"],
selectedGroupIds: { [groupId: string]: boolean },
) {
return groupIds.filter((groupId) => !selectedGroupIds[groupId]);
}