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

@ -131,6 +131,12 @@ import {
} from "../data/localStorage";
import throttle from "lodash.throttle";
import {
getSelectedGroupIds,
selectGroupsForSelectedElements,
isElementInGroup,
getSelectedGroupIdForElement,
} from "../groups";
/**
* @param func handler taking at most single parameter (event).
@ -704,9 +710,10 @@ class App extends React.Component<any, AppState> {
const dx = x - elementsCenterX;
const dy = y - elementsCenterY;
const groupIdMap = new Map();
const newElements = clipboardElements.map((element) =>
duplicateElement(element, {
duplicateElement(this.state.editingGroupId, groupIdMap, element, {
x: element.x + dx - minX,
y: element.y + dy - minY,
}),
@ -1212,7 +1219,11 @@ class App extends React.Component<any, AppState> {
resetCursor();
} else {
setCursorForShape(this.state.elementType);
this.setState({ selectedElementIds: {} });
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
isHoldingSpace = false;
}
@ -1226,7 +1237,12 @@ class App extends React.Component<any, AppState> {
document.activeElement.blur();
}
if (elementType !== "selection") {
this.setState({ elementType, selectedElementIds: {} });
this.setState({
elementType,
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
} else {
this.setState({ elementType });
}
@ -1337,7 +1353,11 @@ class App extends React.Component<any, AppState> {
}),
});
// deselect all other elements when inserting text
this.setState({ selectedElementIds: {} });
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
// do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote)
@ -1459,8 +1479,6 @@ class App extends React.Component<any, AppState> {
return;
}
resetCursor();
const { x, y } = viewportCoordsToSceneCoords(
event,
this.state,
@ -1468,6 +1486,40 @@ class App extends React.Component<any, AppState> {
window.devicePixelRatio,
);
const selectedGroupIds = getSelectedGroupIds(this.state);
if (selectedGroupIds.length > 0) {
const elements = globalSceneState.getElements();
const hitElement = getElementAtPosition(
elements,
this.state,
x,
y,
this.state.zoom,
);
const selectedGroupId =
hitElement &&
getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds);
if (selectedGroupId) {
this.setState((prevState) =>
selectGroupsForSelectedElements(
{
...prevState,
editingGroupId: selectedGroupId,
selectedElementIds: { [hitElement!.id]: true },
selectedGroupIds: {},
},
globalSceneState.getElements(),
),
);
return;
}
}
resetCursor();
this.startTextEditing({
x: x,
y: y,
@ -1942,7 +1994,16 @@ class App extends React.Component<any, AppState> {
!(hitElement && this.state.selectedElementIds[hitElement.id]) &&
!event.shiftKey
) {
this.setState({ selectedElementIds: {} });
this.setState((prevState) => ({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId:
prevState.editingGroupId &&
hitElement &&
isElementInGroup(hitElement, prevState.editingGroupId)
? prevState.editingGroupId
: null,
}));
}
// If we click on something
@ -1952,12 +2013,32 @@ class App extends React.Component<any, AppState> {
// otherwise, it will trigger selection based on current
// state of the box
if (!this.state.selectedElementIds[hitElement.id]) {
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
[hitElement!.id]: true,
},
}));
// if we are currently editing a group, treat all selections outside of the group
// as exiting editing mode.
if (
this.state.editingGroupId &&
!isElementInGroup(hitElement, this.state.editingGroupId)
) {
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
return;
}
this.setState((prevState) => {
return selectGroupsForSelectedElements(
{
...prevState,
selectedElementIds: {
...prevState.selectedElementIds,
[hitElement!.id]: true,
},
},
globalSceneState.getElements(),
);
});
// TODO: this is strange...
globalSceneState.replaceAllElements(
globalSceneState.getElementsIncludingDeleted(),
);
@ -1966,7 +2047,11 @@ class App extends React.Component<any, AppState> {
}
}
} else {
this.setState({ selectedElementIds: {} });
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
if (this.state.elementType === "text") {
@ -2218,6 +2303,7 @@ class App extends React.Component<any, AppState> {
const nextElements = [];
const elementsToAppend = [];
const groupIdMap = new Map();
for (const element of globalSceneState.getElementsIncludingDeleted()) {
if (
this.state.selectedElementIds[element.id] ||
@ -2225,7 +2311,11 @@ class App extends React.Component<any, AppState> {
// updated yet by the time this mousemove event is fired
(element.id === hitElement.id && hitElementWasAddedToSelection)
) {
const duplicatedElement = duplicateElement(element);
const duplicatedElement = duplicateElement(
this.state.editingGroupId,
groupIdMap,
element,
);
mutateElement(duplicatedElement, {
x: duplicatedElement.x + (originX - lastX),
y: duplicatedElement.y + (originY - lastY),
@ -2316,21 +2406,31 @@ class App extends React.Component<any, AppState> {
if (this.state.elementType === "selection") {
const elements = globalSceneState.getElements();
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
this.setState({ selectedElementIds: {} });
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
const elementsWithinSelection = getElementsWithinSelection(
elements,
draggingElement,
);
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
...elementsWithinSelection.reduce((map, element) => {
map[element.id] = true;
return map;
}, {} as any),
},
}));
this.setState((prevState) =>
selectGroupsForSelectedElements(
{
...prevState,
selectedElementIds: {
...prevState.selectedElementIds,
...elementsWithinSelection.reduce((map, element) => {
map[element.id] = true;
return map;
}, {} as any),
},
},
globalSceneState.getElements(),
),
);
}
});
@ -2445,7 +2545,12 @@ class App extends React.Component<any, AppState> {
// If click occurred and elements were dragged or some element
// was added to selection (on pointerdown phase) we need to keep
// selection unchanged
if (hitElement && !draggingOccurred && !hitElementWasAddedToSelection) {
if (
getSelectedGroupIds(this.state).length === 0 &&
hitElement &&
!draggingOccurred &&
!hitElementWasAddedToSelection
) {
if (childEvent.shiftKey) {
this.setState((prevState) => ({
selectedElementIds: {
@ -2462,7 +2567,11 @@ class App extends React.Component<any, AppState> {
if (draggingElement === null) {
// if no element is clicked, clear the selection and redraw
this.setState({ selectedElementIds: {} });
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
return;
}

View file

@ -318,6 +318,14 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("buttons.redo")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Z")]}
/>
<Shortcut
label={t("labels.group")}
shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
/>
<Shortcut
label={t("labels.ungroup")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
/>
</ShortcutIsland>
</Column>
</Columns>