|
@@ -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;
|
|
|
}
|
|
|
|