excalidraw/src/actions/actionGroup.tsx

192 lines
6.1 KiB
TypeScript
Raw Normal View History

import React from "react";
import { CODES, KEYS } from "../keys";
import { t } from "../i18n";
import { getShortcutKey } from "../utils";
import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import {
getSelectedGroupIds,
selectGroup,
selectGroupsForSelectedElements,
getElementsInGroup,
addToGroup,
removeFromSelectedGroups,
2020-05-26 13:56:22 -07:00
isElementInGroup,
} from "../groups";
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
if (elements.length >= 2) {
const groupIds = elements[0].groupIds;
for (const groupId of groupIds) {
if (
elements.reduce(
(acc, element) => acc && isElementInGroup(element, groupId),
true,
)
) {
return true;
}
}
}
return false;
};
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
);
};
export const actionGroup = register({
name: "group",
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
if (selectedElements.length < 2) {
// nothing to group
return { appState, elements, commitToHistory: false };
}
// if everything is already grouped into 1 group, there is nothing to do
const selectedGroupIds = getSelectedGroupIds(appState);
if (selectedGroupIds.length === 1) {
const selectedGroupId = selectedGroupIds[0];
const elementIdsInGroup = new Set(
getElementsInGroup(elements, selectedGroupId).map(
(element) => element.id,
),
);
const selectedElementIds = new Set(
selectedElements.map((element) => element.id),
);
const combinedSet = new Set([
...Array.from(elementIdsInGroup),
...Array.from(selectedElementIds),
]);
if (combinedSet.size === elementIdsInGroup.size) {
// no incremental ids in the selected ids
return { appState, elements, commitToHistory: false };
}
}
const newGroupId = randomId();
const updatedElements = elements.map((element) => {
if (!appState.selectedElementIds[element.id]) {
return element;
}
return newElementWith(element, {
groupIds: addToGroup(
element.groupIds,
newGroupId,
appState.editingGroupId,
),
});
});
2020-05-26 13:56:22 -07:00
// keep the z order within the group the same, but move them
// to the z order of the highest element in the layer stack
const elementsInGroup = getElementsInGroup(updatedElements, newGroupId);
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
const lastGroupElementIndex = updatedElements.lastIndexOf(
lastElementInGroup,
);
const elementsAfterGroup = updatedElements.slice(lastGroupElementIndex + 1);
const elementsBeforeGroup = updatedElements
.slice(0, lastGroupElementIndex)
.filter(
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
);
const updatedElementsInOrder = [
...elementsBeforeGroup,
...elementsInGroup,
...elementsAfterGroup,
];
return {
appState: selectGroup(
newGroupId,
{ ...appState, selectedGroupIds: {} },
2020-05-26 13:56:22 -07:00
getNonDeletedElements(updatedElementsInOrder),
),
2020-05-26 13:56:22 -07:00
elements: updatedElementsInOrder,
commitToHistory: true,
};
},
contextItemLabel: "labels.group",
contextItemPredicate: (elements, appState) =>
enableActionGroup(elements, appState),
keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<GroupIcon appearance={appState.appearance} />}
onClick={() => updateData(null)}
title={`${t("labels.group")}${getShortcutKey("CtrlOrCmd+G")}`}
aria-label={t("labels.group")}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
></ToolButton>
),
});
export const actionUngroup = register({
name: "ungroup",
perform: (elements, appState) => {
const groupIds = getSelectedGroupIds(appState);
if (groupIds.length === 0) {
return { appState, elements, commitToHistory: false };
}
const nextElements = elements.map((element) => {
const nextGroupIds = removeFromSelectedGroups(
element.groupIds,
appState.selectedGroupIds,
);
if (nextGroupIds.length === element.groupIds.length) {
return element;
}
return newElementWith(element, {
groupIds: nextGroupIds,
});
});
return {
appState: selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
),
elements: nextElements,
commitToHistory: true,
};
},
keyTest: (event) =>
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
contextItemLabel: "labels.ungroup",
contextItemPredicate: (elements, appState) =>
getSelectedGroupIds(appState).length > 0,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
hidden={getSelectedGroupIds(appState).length === 0}
icon={<UngroupIcon appearance={appState.appearance} />}
onClick={() => updateData(null)}
title={`${t("labels.ungroup")}${getShortcutKey("CtrlOrCmd+Shift+G")}`}
aria-label={t("labels.ungroup")}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
></ToolButton>
),
});