|
|
|
@ -3014,7 +3014,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
!event.ctrlKey &&
|
|
|
|
|
!event.altKey &&
|
|
|
|
|
!event.metaKey &&
|
|
|
|
|
this.state.draggingElement === null
|
|
|
|
|
!this.state.draggingElement &&
|
|
|
|
|
!this.state.selectionElement
|
|
|
|
|
) {
|
|
|
|
|
const shape = findShapeByKey(event.key);
|
|
|
|
|
if (shape) {
|
|
|
|
@ -3343,6 +3344,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
editingElement: null,
|
|
|
|
|
});
|
|
|
|
|
if (this.state.activeTool.locked) {
|
|
|
|
@ -4421,8 +4423,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
// finger is lifted
|
|
|
|
|
if (
|
|
|
|
|
event.pointerType === "touch" &&
|
|
|
|
|
this.state.draggingElement &&
|
|
|
|
|
this.state.draggingElement.type === "freedraw"
|
|
|
|
|
this.state.draggingElement?.type === "freedraw"
|
|
|
|
|
) {
|
|
|
|
|
const element = this.state.draggingElement as ExcalidrawFreeDrawElement;
|
|
|
|
|
this.updateScene({
|
|
|
|
@ -4434,6 +4435,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
appState: {
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
editingElement: null,
|
|
|
|
|
startBoundElement: null,
|
|
|
|
@ -4561,13 +4563,16 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
// retrieve the latest element as the state may be stale
|
|
|
|
|
const pendingImageElement =
|
|
|
|
|
this.state.pendingImageElementId &&
|
|
|
|
|
this.scene.getElement(this.state.pendingImageElementId);
|
|
|
|
|
this.scene.getElement<ExcalidrawImageElement>(
|
|
|
|
|
this.state.pendingImageElementId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!pendingImageElement) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
draggingElement: pendingImageElement,
|
|
|
|
|
editingElement: pendingImageElement,
|
|
|
|
|
pendingImageElementId: null,
|
|
|
|
@ -5374,6 +5379,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
);
|
|
|
|
|
this.scene.addNewElement(element);
|
|
|
|
|
this.setState({
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
draggingElement: element,
|
|
|
|
|
editingElement: element,
|
|
|
|
|
startBoundElement: boundElement,
|
|
|
|
@ -5593,6 +5599,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
|
|
|
|
this.scene.addNewElement(element);
|
|
|
|
|
this.setState({
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
draggingElement: element,
|
|
|
|
|
editingElement: element,
|
|
|
|
|
startBoundElement: boundElement,
|
|
|
|
@ -5667,12 +5674,13 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
if (element.type === "selection") {
|
|
|
|
|
this.setState({
|
|
|
|
|
selectionElement: element,
|
|
|
|
|
draggingElement: element,
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.scene.addNewElement(element);
|
|
|
|
|
this.setState({
|
|
|
|
|
multiElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
draggingElement: element,
|
|
|
|
|
editingElement: element,
|
|
|
|
|
});
|
|
|
|
@ -5705,6 +5713,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
multiElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
draggingElement: frame,
|
|
|
|
|
editingElement: frame,
|
|
|
|
|
});
|
|
|
|
@ -5763,7 +5772,9 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
|
if (!this.maybeUpdateSelectionElement(pointerDownState, event)) {
|
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -5776,7 +5787,9 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
|
if (!this.maybeUpdateSelectionElement(pointerDownState, event)) {
|
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -6132,6 +6145,13 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
|
|
|
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
|
|
|
|
|
|
|
|
|
if (this.maybeHandleBoxSelection(pointerDownState, event)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// It is very important to read this.state within each move event,
|
|
|
|
|
// otherwise we would read a stale one!
|
|
|
|
|
const draggingElement = this.state.draggingElement;
|
|
|
|
@ -6199,105 +6219,6 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.state.activeTool.type === "selection") {
|
|
|
|
|
pointerDownState.boxSelection.hasOccurred = true;
|
|
|
|
|
|
|
|
|
|
const elements = this.scene.getNonDeletedElements();
|
|
|
|
|
|
|
|
|
|
// box-select line editor points
|
|
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
|
LinearElementEditor.handleBoxSelection(
|
|
|
|
|
event,
|
|
|
|
|
this.state,
|
|
|
|
|
this.setState.bind(this),
|
|
|
|
|
);
|
|
|
|
|
// regular box-select
|
|
|
|
|
} else {
|
|
|
|
|
let shouldReuseSelection = true;
|
|
|
|
|
|
|
|
|
|
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
|
|
|
|
if (
|
|
|
|
|
pointerDownState.withCmdOrCtrl &&
|
|
|
|
|
pointerDownState.hit.element
|
|
|
|
|
) {
|
|
|
|
|
this.setState((prevState) =>
|
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
|
{
|
|
|
|
|
...prevState,
|
|
|
|
|
selectedElementIds: {
|
|
|
|
|
[pointerDownState.hit.element!.id]: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
this.scene.getNonDeletedElements(),
|
|
|
|
|
prevState,
|
|
|
|
|
this,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
shouldReuseSelection = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const elementsWithinSelection = getElementsWithinSelection(
|
|
|
|
|
elements,
|
|
|
|
|
draggingElement,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.setState((prevState) => {
|
|
|
|
|
const nextSelectedElementIds = {
|
|
|
|
|
...(shouldReuseSelection && prevState.selectedElementIds),
|
|
|
|
|
...elementsWithinSelection.reduce(
|
|
|
|
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
|
|
|
|
acc[element.id] = true;
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{},
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (pointerDownState.hit.element) {
|
|
|
|
|
// if using ctrl/cmd, select the hitElement only if we
|
|
|
|
|
// haven't box-selected anything else
|
|
|
|
|
if (!elementsWithinSelection.length) {
|
|
|
|
|
nextSelectedElementIds[pointerDownState.hit.element.id] = true;
|
|
|
|
|
} else {
|
|
|
|
|
delete nextSelectedElementIds[pointerDownState.hit.element.id];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prevState = !shouldReuseSelection
|
|
|
|
|
? { ...prevState, selectedGroupIds: {}, editingGroupId: null }
|
|
|
|
|
: prevState;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...selectGroupsForSelectedElements(
|
|
|
|
|
{
|
|
|
|
|
editingGroupId: prevState.editingGroupId,
|
|
|
|
|
selectedElementIds: nextSelectedElementIds,
|
|
|
|
|
},
|
|
|
|
|
this.scene.getNonDeletedElements(),
|
|
|
|
|
prevState,
|
|
|
|
|
this,
|
|
|
|
|
),
|
|
|
|
|
// select linear element only when we haven't box-selected anything else
|
|
|
|
|
selectedLinearElement:
|
|
|
|
|
elementsWithinSelection.length === 1 &&
|
|
|
|
|
isLinearElement(elementsWithinSelection[0])
|
|
|
|
|
? new LinearElementEditor(
|
|
|
|
|
elementsWithinSelection[0],
|
|
|
|
|
this.scene,
|
|
|
|
|
)
|
|
|
|
|
: null,
|
|
|
|
|
showHyperlinkPopup:
|
|
|
|
|
elementsWithinSelection.length === 1 &&
|
|
|
|
|
(elementsWithinSelection[0].link ||
|
|
|
|
|
isEmbeddableElement(elementsWithinSelection[0]))
|
|
|
|
|
? "info"
|
|
|
|
|
: false,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -6558,6 +6479,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
resetCursor(this.interactiveCanvas);
|
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
activeTool: updateActiveTool(this.state, {
|
|
|
|
|
type: "selection",
|
|
|
|
|
}),
|
|
|
|
@ -6576,6 +6498,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
} else {
|
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -6595,140 +6518,137 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
);
|
|
|
|
|
this.setState({
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (draggingElement) {
|
|
|
|
|
if (pointerDownState.drag.hasOccurred) {
|
|
|
|
|
const sceneCoords = viewportCoordsToSceneCoords(
|
|
|
|
|
childEvent,
|
|
|
|
|
this.state,
|
|
|
|
|
);
|
|
|
|
|
if (pointerDownState.drag.hasOccurred) {
|
|
|
|
|
const sceneCoords = viewportCoordsToSceneCoords(childEvent, this.state);
|
|
|
|
|
|
|
|
|
|
// when editing the points of a linear element, we check if the
|
|
|
|
|
// linear element still is in the frame afterwards
|
|
|
|
|
// if not, the linear element will be removed from its frame (if any)
|
|
|
|
|
if (
|
|
|
|
|
this.state.selectedLinearElement &&
|
|
|
|
|
this.state.selectedLinearElement.isDragging
|
|
|
|
|
) {
|
|
|
|
|
const linearElement = this.scene.getElement(
|
|
|
|
|
this.state.selectedLinearElement.elementId,
|
|
|
|
|
);
|
|
|
|
|
// when editing the points of a linear element, we check if the
|
|
|
|
|
// linear element still is in the frame afterwards
|
|
|
|
|
// if not, the linear element will be removed from its frame (if any)
|
|
|
|
|
if (
|
|
|
|
|
this.state.selectedLinearElement &&
|
|
|
|
|
this.state.selectedLinearElement.isDragging
|
|
|
|
|
) {
|
|
|
|
|
const linearElement = this.scene.getElement(
|
|
|
|
|
this.state.selectedLinearElement.elementId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (linearElement?.frameId) {
|
|
|
|
|
const frame = getContainingFrame(linearElement);
|
|
|
|
|
if (linearElement?.frameId) {
|
|
|
|
|
const frame = getContainingFrame(linearElement);
|
|
|
|
|
|
|
|
|
|
if (frame && linearElement) {
|
|
|
|
|
if (!elementOverlapsWithFrame(linearElement, frame)) {
|
|
|
|
|
// remove the linear element from all groups
|
|
|
|
|
// before removing it from the frame as well
|
|
|
|
|
mutateElement(linearElement, {
|
|
|
|
|
groupIds: [],
|
|
|
|
|
});
|
|
|
|
|
if (frame && linearElement) {
|
|
|
|
|
if (!elementOverlapsWithFrame(linearElement, frame)) {
|
|
|
|
|
// remove the linear element from all groups
|
|
|
|
|
// before removing it from the frame as well
|
|
|
|
|
mutateElement(linearElement, {
|
|
|
|
|
groupIds: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(
|
|
|
|
|
removeElementsFromFrame(
|
|
|
|
|
this.scene.getElementsIncludingDeleted(),
|
|
|
|
|
[linearElement],
|
|
|
|
|
this.state,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
this.scene.replaceAllElements(
|
|
|
|
|
removeElementsFromFrame(
|
|
|
|
|
this.scene.getElementsIncludingDeleted(),
|
|
|
|
|
[linearElement],
|
|
|
|
|
this.state,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// update the relationships between selected elements and frames
|
|
|
|
|
const topLayerFrame =
|
|
|
|
|
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
|
|
|
|
|
|
|
|
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
|
|
|
|
let nextElements = this.scene.getElementsIncludingDeleted();
|
|
|
|
|
|
|
|
|
|
const updateGroupIdsAfterEditingGroup = (
|
|
|
|
|
elements: ExcalidrawElement[],
|
|
|
|
|
) => {
|
|
|
|
|
if (elements.length > 0) {
|
|
|
|
|
for (const element of elements) {
|
|
|
|
|
const index = element.groupIds.indexOf(
|
|
|
|
|
this.state.editingGroupId!,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// update the relationships between selected elements and frames
|
|
|
|
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
|
|
|
|
|
|
|
|
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
|
|
|
|
let nextElements = this.scene.getElementsIncludingDeleted();
|
|
|
|
|
|
|
|
|
|
const updateGroupIdsAfterEditingGroup = (
|
|
|
|
|
elements: ExcalidrawElement[],
|
|
|
|
|
) => {
|
|
|
|
|
if (elements.length > 0) {
|
|
|
|
|
for (const element of elements) {
|
|
|
|
|
const index = element.groupIds.indexOf(
|
|
|
|
|
this.state.editingGroupId!,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
mutateElement(
|
|
|
|
|
element,
|
|
|
|
|
{
|
|
|
|
|
groupIds: element.groupIds.slice(0, index),
|
|
|
|
|
},
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nextElements.forEach((element) => {
|
|
|
|
|
if (
|
|
|
|
|
element.groupIds.length &&
|
|
|
|
|
getElementsInGroup(
|
|
|
|
|
nextElements,
|
|
|
|
|
element.groupIds[element.groupIds.length - 1],
|
|
|
|
|
).length < 2
|
|
|
|
|
) {
|
|
|
|
|
mutateElement(
|
|
|
|
|
element,
|
|
|
|
|
{
|
|
|
|
|
groupIds: element.groupIds.slice(0, index),
|
|
|
|
|
groupIds: [],
|
|
|
|
|
},
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
nextElements.forEach((element) => {
|
|
|
|
|
if (
|
|
|
|
|
element.groupIds.length &&
|
|
|
|
|
getElementsInGroup(
|
|
|
|
|
nextElements,
|
|
|
|
|
element.groupIds[element.groupIds.length - 1],
|
|
|
|
|
).length < 2
|
|
|
|
|
) {
|
|
|
|
|
mutateElement(
|
|
|
|
|
element,
|
|
|
|
|
{
|
|
|
|
|
groupIds: [],
|
|
|
|
|
},
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
editingGroupId: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
topLayerFrame &&
|
|
|
|
|
!this.state.selectedElementIds[topLayerFrame.id]
|
|
|
|
|
) {
|
|
|
|
|
const elementsToAdd = selectedElements.filter(
|
|
|
|
|
(element) =>
|
|
|
|
|
element.frameId !== topLayerFrame.id &&
|
|
|
|
|
isElementInFrame(element, nextElements, this.state),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (this.state.editingGroupId) {
|
|
|
|
|
updateGroupIdsAfterEditingGroup(elementsToAdd);
|
|
|
|
|
}
|
|
|
|
|
this.setState({
|
|
|
|
|
editingGroupId: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nextElements = addElementsToFrame(
|
|
|
|
|
nextElements,
|
|
|
|
|
elementsToAdd,
|
|
|
|
|
topLayerFrame,
|
|
|
|
|
);
|
|
|
|
|
} else if (!topLayerFrame) {
|
|
|
|
|
if (this.state.editingGroupId) {
|
|
|
|
|
const elementsToRemove = selectedElements.filter(
|
|
|
|
|
(element) =>
|
|
|
|
|
element.frameId &&
|
|
|
|
|
!isElementInFrame(element, nextElements, this.state),
|
|
|
|
|
);
|
|
|
|
|
if (
|
|
|
|
|
topLayerFrame &&
|
|
|
|
|
!this.state.selectedElementIds[topLayerFrame.id]
|
|
|
|
|
) {
|
|
|
|
|
const elementsToAdd = selectedElements.filter(
|
|
|
|
|
(element) =>
|
|
|
|
|
element.frameId !== topLayerFrame.id &&
|
|
|
|
|
isElementInFrame(element, nextElements, this.state),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
updateGroupIdsAfterEditingGroup(elementsToRemove);
|
|
|
|
|
}
|
|
|
|
|
if (this.state.editingGroupId) {
|
|
|
|
|
updateGroupIdsAfterEditingGroup(elementsToAdd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nextElements = updateFrameMembershipOfSelectedElements(
|
|
|
|
|
nextElements = addElementsToFrame(
|
|
|
|
|
nextElements,
|
|
|
|
|
this.state,
|
|
|
|
|
this,
|
|
|
|
|
elementsToAdd,
|
|
|
|
|
topLayerFrame,
|
|
|
|
|
);
|
|
|
|
|
} else if (!topLayerFrame) {
|
|
|
|
|
if (this.state.editingGroupId) {
|
|
|
|
|
const elementsToRemove = selectedElements.filter(
|
|
|
|
|
(element) =>
|
|
|
|
|
element.frameId &&
|
|
|
|
|
!isElementInFrame(element, nextElements, this.state),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(nextElements);
|
|
|
|
|
updateGroupIdsAfterEditingGroup(elementsToRemove);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nextElements = updateFrameMembershipOfSelectedElements(
|
|
|
|
|
nextElements,
|
|
|
|
|
this.state,
|
|
|
|
|
this,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(nextElements);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (draggingElement) {
|
|
|
|
|
if (draggingElement.type === "frame") {
|
|
|
|
|
const elementsInsideFrame = getElementsInNewFrame(
|
|
|
|
|
this.scene.getElementsIncludingDeleted(),
|
|
|
|
@ -7032,8 +6952,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
if (
|
|
|
|
|
!activeTool.locked &&
|
|
|
|
|
activeTool.type !== "freedraw" &&
|
|
|
|
|
draggingElement &&
|
|
|
|
|
draggingElement.type !== "selection"
|
|
|
|
|
draggingElement
|
|
|
|
|
) {
|
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
|
selectedElementIds: makeNextSelectedElementIds(
|
|
|
|
@ -7072,12 +6991,14 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
resetCursor(this.interactiveCanvas);
|
|
|
|
|
this.setState({
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
suggestedBindings: [],
|
|
|
|
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.setState({
|
|
|
|
|
draggingElement: null,
|
|
|
|
|
selectionElement: null,
|
|
|
|
|
suggestedBindings: [],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -7876,6 +7797,139 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private maybeUpdateSelectionElement = (
|
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
|
event: PointerEvent | KeyboardEvent,
|
|
|
|
|
): boolean => {
|
|
|
|
|
const { selectionElement } = this.state;
|
|
|
|
|
|
|
|
|
|
if (!selectionElement || this.state.activeTool.type !== "selection") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pointerCoords = pointerDownState.lastCoords;
|
|
|
|
|
|
|
|
|
|
dragNewElement(
|
|
|
|
|
selectionElement,
|
|
|
|
|
this.state.activeTool.type,
|
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
|
pointerCoords.x,
|
|
|
|
|
pointerCoords.y,
|
|
|
|
|
distance(pointerDownState.origin.x, pointerCoords.x),
|
|
|
|
|
distance(pointerDownState.origin.y, pointerCoords.y),
|
|
|
|
|
shouldMaintainAspectRatio(event),
|
|
|
|
|
shouldResizeFromCenter(event),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private maybeHandleBoxSelection = (
|
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
|
event: PointerEvent,
|
|
|
|
|
): boolean => {
|
|
|
|
|
const { selectionElement } = this.state;
|
|
|
|
|
|
|
|
|
|
if (!selectionElement || this.state.activeTool.type !== "selection") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.maybeUpdateSelectionElement(pointerDownState, event);
|
|
|
|
|
|
|
|
|
|
pointerDownState.boxSelection.hasOccurred = true;
|
|
|
|
|
|
|
|
|
|
const elements = this.scene.getNonDeletedElements();
|
|
|
|
|
|
|
|
|
|
// box-select line editor points
|
|
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
|
LinearElementEditor.handleBoxSelection(
|
|
|
|
|
event,
|
|
|
|
|
this.state,
|
|
|
|
|
this.setState.bind(this),
|
|
|
|
|
);
|
|
|
|
|
// regular box-select
|
|
|
|
|
} else {
|
|
|
|
|
let shouldReuseSelection = true;
|
|
|
|
|
|
|
|
|
|
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
|
|
|
|
if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
|
|
|
|
|
this.setState((prevState) =>
|
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
|
{
|
|
|
|
|
...prevState,
|
|
|
|
|
selectedElementIds: {
|
|
|
|
|
[pointerDownState.hit.element!.id]: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
this.scene.getNonDeletedElements(),
|
|
|
|
|
prevState,
|
|
|
|
|
this,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
shouldReuseSelection = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const elementsWithinSelection = getElementsWithinSelection(
|
|
|
|
|
elements,
|
|
|
|
|
selectionElement,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.setState((prevState) => {
|
|
|
|
|
const nextSelectedElementIds = {
|
|
|
|
|
...(shouldReuseSelection && prevState.selectedElementIds),
|
|
|
|
|
...elementsWithinSelection.reduce(
|
|
|
|
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
|
|
|
|
acc[element.id] = true;
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{},
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (pointerDownState.hit.element) {
|
|
|
|
|
// if using ctrl/cmd, select the hitElement only if we
|
|
|
|
|
// haven't box-selected anything else
|
|
|
|
|
if (!elementsWithinSelection.length) {
|
|
|
|
|
nextSelectedElementIds[pointerDownState.hit.element.id] = true;
|
|
|
|
|
} else {
|
|
|
|
|
delete nextSelectedElementIds[pointerDownState.hit.element.id];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prevState = !shouldReuseSelection
|
|
|
|
|
? { ...prevState, selectedGroupIds: {}, editingGroupId: null }
|
|
|
|
|
: prevState;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...selectGroupsForSelectedElements(
|
|
|
|
|
{
|
|
|
|
|
editingGroupId: prevState.editingGroupId,
|
|
|
|
|
selectedElementIds: nextSelectedElementIds,
|
|
|
|
|
},
|
|
|
|
|
this.scene.getNonDeletedElements(),
|
|
|
|
|
prevState,
|
|
|
|
|
this,
|
|
|
|
|
),
|
|
|
|
|
// select linear element only when we haven't box-selected anything else
|
|
|
|
|
selectedLinearElement:
|
|
|
|
|
elementsWithinSelection.length === 1 &&
|
|
|
|
|
isLinearElement(elementsWithinSelection[0])
|
|
|
|
|
? new LinearElementEditor(elementsWithinSelection[0], this.scene)
|
|
|
|
|
: null,
|
|
|
|
|
showHyperlinkPopup:
|
|
|
|
|
elementsWithinSelection.length === 1 &&
|
|
|
|
|
(elementsWithinSelection[0].link ||
|
|
|
|
|
isEmbeddableElement(elementsWithinSelection[0]))
|
|
|
|
|
? "info"
|
|
|
|
|
: false,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private maybeDragNewGenericElement = (
|
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
|
event: MouseEvent | KeyboardEvent,
|
|
|
|
@ -7885,93 +7939,73 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
if (!draggingElement) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
draggingElement.type === "selection" &&
|
|
|
|
|
this.state.activeTool.type !== "eraser"
|
|
|
|
|
) {
|
|
|
|
|
dragNewElement(
|
|
|
|
|
draggingElement,
|
|
|
|
|
this.state.activeTool.type,
|
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
|
pointerCoords.x,
|
|
|
|
|
pointerCoords.y,
|
|
|
|
|
distance(pointerDownState.origin.x, pointerCoords.x),
|
|
|
|
|
distance(pointerDownState.origin.y, pointerCoords.y),
|
|
|
|
|
shouldMaintainAspectRatio(event),
|
|
|
|
|
shouldResizeFromCenter(event),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
let [gridX, gridY] = getGridPoint(
|
|
|
|
|
pointerCoords.x,
|
|
|
|
|
pointerCoords.y,
|
|
|
|
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
|
|
|
|
);
|
|
|
|
|
let [gridX, gridY] = getGridPoint(
|
|
|
|
|
pointerCoords.x,
|
|
|
|
|
pointerCoords.y,
|
|
|
|
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const image =
|
|
|
|
|
isInitializedImageElement(draggingElement) &&
|
|
|
|
|
this.imageCache.get(draggingElement.fileId)?.image;
|
|
|
|
|
const aspectRatio =
|
|
|
|
|
image && !(image instanceof Promise)
|
|
|
|
|
? image.width / image.height
|
|
|
|
|
: null;
|
|
|
|
|
const image =
|
|
|
|
|
isInitializedImageElement(draggingElement) &&
|
|
|
|
|
this.imageCache.get(draggingElement.fileId)?.image;
|
|
|
|
|
const aspectRatio =
|
|
|
|
|
image && !(image instanceof Promise) ? image.width / image.height : null;
|
|
|
|
|
|
|
|
|
|
this.maybeCacheReferenceSnapPoints(event, [draggingElement]);
|
|
|
|
|
this.maybeCacheReferenceSnapPoints(event, [draggingElement]);
|
|
|
|
|
|
|
|
|
|
const { snapOffset, snapLines } = snapNewElement(
|
|
|
|
|
draggingElement,
|
|
|
|
|
this.state,
|
|
|
|
|
event,
|
|
|
|
|
{
|
|
|
|
|
x:
|
|
|
|
|
pointerDownState.originInGrid.x +
|
|
|
|
|
(this.state.originSnapOffset?.x ?? 0),
|
|
|
|
|
y:
|
|
|
|
|
pointerDownState.originInGrid.y +
|
|
|
|
|
(this.state.originSnapOffset?.y ?? 0),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
x: gridX - pointerDownState.originInGrid.x,
|
|
|
|
|
y: gridY - pointerDownState.originInGrid.y,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
const { snapOffset, snapLines } = snapNewElement(
|
|
|
|
|
draggingElement,
|
|
|
|
|
this.state,
|
|
|
|
|
event,
|
|
|
|
|
{
|
|
|
|
|
x:
|
|
|
|
|
pointerDownState.originInGrid.x +
|
|
|
|
|
(this.state.originSnapOffset?.x ?? 0),
|
|
|
|
|
y:
|
|
|
|
|
pointerDownState.originInGrid.y +
|
|
|
|
|
(this.state.originSnapOffset?.y ?? 0),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
x: gridX - pointerDownState.originInGrid.x,
|
|
|
|
|
y: gridY - pointerDownState.originInGrid.y,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
gridX += snapOffset.x;
|
|
|
|
|
gridY += snapOffset.y;
|
|
|
|
|
gridX += snapOffset.x;
|
|
|
|
|
gridY += snapOffset.y;
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
snapLines,
|
|
|
|
|
});
|
|
|
|
|
this.setState({
|
|
|
|
|
snapLines,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dragNewElement(
|
|
|
|
|
draggingElement,
|
|
|
|
|
this.state.activeTool.type,
|
|
|
|
|
pointerDownState.originInGrid.x,
|
|
|
|
|
pointerDownState.originInGrid.y,
|
|
|
|
|
gridX,
|
|
|
|
|
gridY,
|
|
|
|
|
distance(pointerDownState.originInGrid.x, gridX),
|
|
|
|
|
distance(pointerDownState.originInGrid.y, gridY),
|
|
|
|
|
isImageElement(draggingElement)
|
|
|
|
|
? !shouldMaintainAspectRatio(event)
|
|
|
|
|
: shouldMaintainAspectRatio(event),
|
|
|
|
|
shouldResizeFromCenter(event),
|
|
|
|
|
aspectRatio,
|
|
|
|
|
this.state.originSnapOffset,
|
|
|
|
|
);
|
|
|
|
|
dragNewElement(
|
|
|
|
|
draggingElement,
|
|
|
|
|
this.state.activeTool.type,
|
|
|
|
|
pointerDownState.originInGrid.x,
|
|
|
|
|
pointerDownState.originInGrid.y,
|
|
|
|
|
gridX,
|
|
|
|
|
gridY,
|
|
|
|
|
distance(pointerDownState.originInGrid.x, gridX),
|
|
|
|
|
distance(pointerDownState.originInGrid.y, gridY),
|
|
|
|
|
isImageElement(draggingElement)
|
|
|
|
|
? !shouldMaintainAspectRatio(event)
|
|
|
|
|
: shouldMaintainAspectRatio(event),
|
|
|
|
|
shouldResizeFromCenter(event),
|
|
|
|
|
aspectRatio,
|
|
|
|
|
this.state.originSnapOffset,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.maybeSuggestBindingForAll([draggingElement]);
|
|
|
|
|
this.maybeSuggestBindingForAll([draggingElement]);
|
|
|
|
|
|
|
|
|
|
// highlight elements that are to be added to frames on frames creation
|
|
|
|
|
if (this.state.activeTool.type === "frame") {
|
|
|
|
|
this.setState({
|
|
|
|
|
elementsToHighlight: getElementsInResizingFrame(
|
|
|
|
|
this.scene.getNonDeletedElements(),
|
|
|
|
|
draggingElement as ExcalidrawFrameElement,
|
|
|
|
|
this.state,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// highlight elements that are to be added to frames on frames creation
|
|
|
|
|
if (this.state.activeTool.type === "frame") {
|
|
|
|
|
this.setState({
|
|
|
|
|
elementsToHighlight: getElementsInResizingFrame(
|
|
|
|
|
this.scene.getNonDeletedElements(),
|
|
|
|
|
draggingElement as ExcalidrawFrameElement,
|
|
|
|
|
this.state,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|