feat: stop storing selection element into `appState.draggingElement`

dwelle/dragginEement-rewrite
dwelle
parent 44d9d5fcac
commit add575a419

@ -170,6 +170,7 @@ export const actionFinalize = register({
: activeTool, : activeTool,
activeEmbeddable: null, activeEmbeddable: null,
draggingElement: null, draggingElement: null,
selectionElement: null,
multiElement: null, multiElement: null,
editingElement: null, editingElement: null,
startBoundElement: null, startBoundElement: null,
@ -196,7 +197,9 @@ export const actionFinalize = register({
keyTest: (event, appState) => keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE && (event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null || (appState.editingLinearElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) || (!appState.selectionElement &&
!appState.draggingElement &&
appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null), appState.multiElement !== null),
PanelComponent: ({ appState, updateData, data }) => ( PanelComponent: ({ appState, updateData, data }) => (

@ -21,6 +21,7 @@ const writeData = (
!appState.multiElement && !appState.multiElement &&
!appState.resizingElement && !appState.resizingElement &&
!appState.editingElement && !appState.editingElement &&
!appState.selectionElement &&
!appState.draggingElement !appState.draggingElement
) { ) {
const data = updater(); const data = updater();

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

@ -82,8 +82,9 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
if (activeTool.type === "selection") { if (activeTool.type === "selection") {
if ( if (
appState.draggingElement?.type === "selection" && appState.selectionElement &&
!selectedElements.length && !selectedElements.length &&
!appState.draggingElement &&
!appState.editingElement && !appState.editingElement &&
!appState.editingLinearElement !appState.editingLinearElement
) { ) {

@ -210,6 +210,7 @@ export const Hyperlink = ({
}; };
const { x, y } = getCoordsForPopover(element, appState); const { x, y } = getCoordsForPopover(element, appState);
if ( if (
appState.selectionElement ||
appState.draggingElement || appState.draggingElement ||
appState.resizingElement || appState.resizingElement ||
appState.isRotating || appState.isRotating ||

@ -134,10 +134,7 @@ export class LinearElementEditor {
appState: AppState, appState: AppState,
setState: React.Component<any, AppState>["setState"], setState: React.Component<any, AppState>["setState"],
) { ) {
if ( if (!appState.editingLinearElement || !appState.selectionElement) {
!appState.editingLinearElement ||
appState.draggingElement?.type !== "selection"
) {
return false; return false;
} }
const { editingLinearElement } = appState; const { editingLinearElement } = appState;
@ -149,7 +146,7 @@ export class LinearElementEditor {
} }
const [selectionX1, selectionY1, selectionX2, selectionY2] = const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(appState.draggingElement); getElementAbsoluteCoords(appState.selectionElement);
const pointsSceneCoords = const pointsSceneCoords =
LinearElementEditor.getPointsGlobalCoordinates(element); LinearElementEditor.getPointsGlobalCoordinates(element);

@ -5152,35 +5152,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"draggingElement": { "draggingElement": null,
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id3",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 500,
"y": 500,
},
"editingElement": null, "editingElement": null,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
@ -5449,35 +5421,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"draggingElement": { "draggingElement": null,
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id3",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 50,
"y": 50,
},
"editingElement": null, "editingElement": null,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
@ -5718,35 +5662,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"draggingElement": { "draggingElement": null,
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 2019559783,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 110,
"y": 110,
},
"editingElement": null, "editingElement": null,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
@ -16262,35 +16178,7 @@ exports[`regression tests > switches from group of selected elements to another
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"draggingElement": { "draggingElement": null,
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id4",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 493213705,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 0,
"y": 0,
},
"editingElement": null, "editingElement": null,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
@ -16662,35 +16550,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"draggingElement": { "draggingElement": null,
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id2",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 0,
"y": 0,
},
"editingElement": null, "editingElement": null,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,

@ -17,6 +17,7 @@ import {
StrokeRoundness, StrokeRoundness,
ExcalidrawFrameElement, ExcalidrawFrameElement,
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
ExcalidrawSelectionElement,
} from "./element/types"; } from "./element/types";
import { Point as RoughPoint } from "roughjs/bin/geometry"; import { Point as RoughPoint } from "roughjs/bin/geometry";
import { LinearElementEditor } from "./element/linearElementEditor"; import { LinearElementEditor } from "./element/linearElementEditor";
@ -182,10 +183,25 @@ export type AppState = {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
state: "hover" | "active"; state: "hover" | "active";
} | null; } | null;
draggingElement: NonDeletedExcalidrawElement | null; /** element that's being dragged or created */
draggingElement: Exclude<
NonDeletedExcalidrawElement,
ExcalidrawSelectionElement
> | null;
/**
* Element that's being resized.
* NOTE not set when resizing a group or linear element
*/
resizingElement: NonDeletedExcalidrawElement | null; resizingElement: NonDeletedExcalidrawElement | null;
/** multi-point linear element when it's being created */
multiElement: NonDeleted<ExcalidrawLinearElement> | null; multiElement: NonDeleted<ExcalidrawLinearElement> | null;
selectionElement: NonDeletedExcalidrawElement | null; /**
* The selection box (we currently use an excalidraw element).
*
* Checking for this attribute is a good way to determine whether the user is
* selecting.
*/
selectionElement: ExcalidrawSelectionElement | null;
isBindingEnabled: boolean; isBindingEnabled: boolean;
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null; startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
suggestedBindings: SuggestedBinding[]; suggestedBindings: SuggestedBinding[];
@ -198,9 +214,14 @@ export type AppState = {
}; };
editingFrame: string | null; editingFrame: string | null;
elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null; elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
// element being edited, but not necessarily added to elements array yet /**
// (e.g. text element when typing into the input) * Text that's being element, or new element being created.
*/
editingElement: NonDeletedExcalidrawElement | null; editingElement: NonDeletedExcalidrawElement | null;
/**
* Linear element that's being edited (when in the linear element editor).
* Not set when creating multi-point linear element.
*/
editingLinearElement: LinearElementEditor | null; editingLinearElement: LinearElementEditor | null;
activeTool: { activeTool: {
/** /**

Loading…
Cancel
Save