Group/ungroup (#1648)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Pete Hunt 2020-05-26 13:07:46 -07:00 committed by GitHub
parent 5252726307
commit 61e5b66dac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 964 additions and 86 deletions

View file

@ -12,15 +12,21 @@ import { getShortcutKey } from "../utils";
export const actionDuplicateSelection = register({
name: "duplicateSelection",
perform: (elements, appState) => {
const groupIdMap = new Map();
return {
appState,
elements: elements.reduce(
(acc: Array<ExcalidrawElement>, element: ExcalidrawElement) => {
if (appState.selectedElementIds[element.id]) {
const newElement = duplicateElement(element, {
x: element.x + 10,
y: element.y + 10,
});
const newElement = duplicateElement(
appState.editingGroupId,
groupIdMap,
element,
{
x: element.x + 10,
y: element.y + 10,
},
);
appState.selectedElementIds[newElement.id] = true;
delete appState.selectedElementIds[element.id];
return acc.concat([element, newElement]);

119
src/actions/actionGroup.ts Normal file
View file

@ -0,0 +1,119 @@
import { KEYS } from "../keys";
import { register } from "./register";
import nanoid from "nanoid";
import { newElementWith } from "../element/mutateElement";
import { getSelectedElements } from "../scene";
import {
getSelectedGroupIds,
selectGroup,
selectGroupsForSelectedElements,
getElementsInGroup,
addToGroup,
removeFromSelectedGroups,
} from "../groups";
import { getNonDeletedElements } from "../element";
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 = nanoid();
const updatedElements = elements.map((element) => {
if (!appState.selectedElementIds[element.id]) {
return element;
}
return newElementWith(element, {
groupIds: addToGroup(
element.groupIds,
newGroupId,
appState.editingGroupId,
),
});
});
return {
appState: selectGroup(
newGroupId,
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(updatedElements),
),
elements: updatedElements,
commitToHistory: true,
};
},
contextMenuOrder: 4,
contextItemLabel: "labels.group",
keyTest: (event) => {
return (
!event.shiftKey &&
event[KEYS.CTRL_OR_CMD] &&
event.keyCode === KEYS.G_KEY_CODE
);
},
});
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) => {
return (
event.shiftKey &&
event[KEYS.CTRL_OR_CMD] &&
event.keyCode === KEYS.G_KEY_CODE
);
},
contextMenuOrder: 5,
contextItemLabel: "labels.ungroup",
});

View file

@ -1,19 +1,25 @@
import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements } from "../element";
export const actionSelectAll = register({
name: "selectAll",
perform: (elements, appState) => {
return {
appState: {
...appState,
selectedElementIds: elements.reduce((map, element) => {
if (!element.isDeleted) {
map[element.id] = true;
}
return map;
}, {} as any),
},
appState: selectGroupsForSelectedElements(
{
...appState,
editingGroupId: null,
selectedElementIds: elements.reduce((map, element) => {
if (!element.isDeleted) {
map[element.id] = true;
}
return map;
}, {} as any),
},
getNonDeletedElements(elements),
),
commitToHistory: true,
};
},

View file

@ -44,3 +44,5 @@ export {
actionFullScreen,
actionShortcuts,
} from "./actionMenu";
export { actionGroup, actionUngroup } from "./actionGroup";

View file

@ -55,7 +55,9 @@ export type ActionName =
| "changeFontFamily"
| "changeTextAlign"
| "toggleFullScreen"
| "toggleShortcuts";
| "toggleShortcuts"
| "group"
| "ungroup";
export interface Action {
name: ActionName;