2020-12-01 23:36:06 +02:00
|
|
|
import { CODES, KEYS } from "../keys";
|
2020-07-26 00:42:06 +02:00
|
|
|
import { t } from "../i18n";
|
|
|
|
import { getShortcutKey } from "../utils";
|
2020-05-26 13:07:46 -07:00
|
|
|
import { register } from "./register";
|
2020-08-13 04:35:31 -07:00
|
|
|
import { UngroupIcon, GroupIcon } from "../components/icons";
|
2020-05-26 13:07:46 -07:00
|
|
|
import { newElementWith } from "../element/mutateElement";
|
2020-07-26 00:42:06 +02:00
|
|
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
2020-05-26 13:07:46 -07:00
|
|
|
import {
|
|
|
|
getSelectedGroupIds,
|
|
|
|
selectGroup,
|
|
|
|
selectGroupsForSelectedElements,
|
|
|
|
getElementsInGroup,
|
|
|
|
addToGroup,
|
|
|
|
removeFromSelectedGroups,
|
2020-05-26 13:56:22 -07:00
|
|
|
isElementInGroup,
|
2020-05-26 13:07:46 -07:00
|
|
|
} from "../groups";
|
|
|
|
import { getNonDeletedElements } from "../element";
|
2020-05-28 01:56:18 -07:00
|
|
|
import { randomId } from "../random";
|
2020-07-26 00:42:06 +02:00
|
|
|
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)
|
|
|
|
);
|
|
|
|
};
|
2020-05-26 13:07:46 -07:00
|
|
|
|
|
|
|
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 };
|
|
|
|
}
|
|
|
|
}
|
2020-05-28 01:56:18 -07:00
|
|
|
const newGroupId = randomId();
|
2020-05-26 13:07:46 -07:00
|
|
|
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,
|
|
|
|
];
|
|
|
|
|
2020-05-26 13:07:46 -07:00
|
|
|
return {
|
|
|
|
appState: selectGroup(
|
|
|
|
newGroupId,
|
|
|
|
{ ...appState, selectedGroupIds: {} },
|
2020-05-26 13:56:22 -07:00
|
|
|
getNonDeletedElements(updatedElementsInOrder),
|
2020-05-26 13:07:46 -07:00
|
|
|
),
|
2020-05-26 13:56:22 -07:00
|
|
|
elements: updatedElementsInOrder,
|
2020-05-26 13:07:46 -07:00
|
|
|
commitToHistory: true,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
contextItemLabel: "labels.group",
|
2020-07-09 22:32:27 +02:00
|
|
|
contextItemPredicate: (elements, appState) =>
|
2020-07-26 00:42:06 +02:00
|
|
|
enableActionGroup(elements, appState),
|
2020-12-01 23:36:06 +02:00
|
|
|
keyTest: (event) =>
|
|
|
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
2020-07-26 00:42:06 +02:00
|
|
|
PanelComponent: ({ elements, appState, updateData }) => (
|
|
|
|
<ToolButton
|
|
|
|
hidden={!enableActionGroup(elements, appState)}
|
|
|
|
type="button"
|
2021-03-13 18:58:06 +05:30
|
|
|
icon={<GroupIcon theme={appState.theme} />}
|
2020-07-26 00:42:06 +02:00
|
|
|
onClick={() => updateData(null)}
|
|
|
|
title={`${t("labels.group")} — ${getShortcutKey("CtrlOrCmd+G")}`}
|
|
|
|
aria-label={t("labels.group")}
|
|
|
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
|
|
|
></ToolButton>
|
|
|
|
),
|
2020-05-26 13:07:46 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
},
|
2020-12-01 23:36:06 +02:00
|
|
|
keyTest: (event) =>
|
|
|
|
event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.code === CODES.G,
|
2020-05-26 13:07:46 -07:00
|
|
|
contextItemLabel: "labels.ungroup",
|
2020-07-09 22:32:27 +02:00
|
|
|
contextItemPredicate: (elements, appState) =>
|
|
|
|
getSelectedGroupIds(appState).length > 0,
|
2020-07-26 00:42:06 +02:00
|
|
|
|
|
|
|
PanelComponent: ({ elements, appState, updateData }) => (
|
|
|
|
<ToolButton
|
|
|
|
type="button"
|
|
|
|
hidden={getSelectedGroupIds(appState).length === 0}
|
2021-03-13 18:58:06 +05:30
|
|
|
icon={<UngroupIcon theme={appState.theme} />}
|
2020-07-26 00:42:06 +02:00
|
|
|
onClick={() => updateData(null)}
|
|
|
|
title={`${t("labels.ungroup")} — ${getShortcutKey("CtrlOrCmd+Shift+G")}`}
|
|
|
|
aria-label={t("labels.ungroup")}
|
|
|
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
|
|
|
></ToolButton>
|
|
|
|
),
|
2020-05-26 13:07:46 -07:00
|
|
|
});
|