diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 3bf80d9fef..3926eee93f 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -6596,8 +6596,94 @@ class App extends React.Component { pointerDownState.origin.y, event.shiftKey, ); - // Block drag until next pointerdown if a lasso selection is made - pointerDownState.drag.blockDragAfterLasso = true; + } + + // For lasso tool, if we hit an element, select it immediately like normal selection + if (pointerDownState.hit.element && !hitSelectedElement) { + this.setState((prevState) => { + let nextSelectedElementIds: { [id: string]: true } = { + ...prevState.selectedElementIds, + [pointerDownState.hit.element!.id]: true, + }; + + const previouslySelectedElements: ExcalidrawElement[] = []; + + Object.keys(prevState.selectedElementIds).forEach((id) => { + const element = this.scene.getElement(id); + element && previouslySelectedElements.push(element); + }); + + const hitElement = pointerDownState.hit.element!; + + // if hitElement is frame-like, deselect all of its elements + // if they are selected + if (isFrameLikeElement(hitElement)) { + getFrameChildren(previouslySelectedElements, hitElement.id).forEach( + (element) => { + delete nextSelectedElementIds[element.id]; + }, + ); + } else if (hitElement.frameId) { + // if hitElement is in a frame and its frame has been selected + // disable selection for the given element + if (nextSelectedElementIds[hitElement.frameId]) { + delete nextSelectedElementIds[hitElement.id]; + } + } else { + // hitElement is neither a frame nor an element in a frame + // but since hitElement could be in a group with some frames + // this means selecting hitElement will have the frames selected as well + // because we want to keep the invariant: + // - frames and their elements are not selected at the same time + // we deselect elements in those frames that were previously selected + + const groupIds = hitElement.groupIds; + const framesInGroups = new Set( + groupIds + .flatMap((gid) => + getElementsInGroup(this.scene.getNonDeletedElements(), gid), + ) + .filter((element) => isFrameLikeElement(element)) + .map((frame) => frame.id), + ); + + if (framesInGroups.size > 0) { + previouslySelectedElements.forEach((element) => { + if (element.frameId && framesInGroups.has(element.frameId)) { + // deselect element and groups containing the element + delete nextSelectedElementIds[element.id]; + element.groupIds + .flatMap((gid) => + getElementsInGroup( + this.scene.getNonDeletedElements(), + gid, + ), + ) + .forEach((element) => { + delete nextSelectedElementIds[element.id]; + }); + } + }); + } + } + + return { + ...selectGroupsForSelectedElements( + { + editingGroupId: prevState.editingGroupId, + selectedElementIds: nextSelectedElementIds, + }, + this.scene.getNonDeletedElements(), + prevState, + this, + ), + showHyperlinkPopup: + hitElement.link || isEmbeddableElement(hitElement) + ? "info" + : false, + }; + }); + pointerDownState.hit.wasAddedToSelection = true; } } else if (this.state.activeTool.type === "text") { this.handleTextOnPointerDown(event, pointerDownState); @@ -6978,7 +7064,6 @@ class App extends React.Component { hasOccurred: false, offset: null, origin: { ...origin }, - blockDragAfterLasso: false, }, eventListeners: { onMove: null, @@ -8289,12 +8374,14 @@ class App extends React.Component { pointerDownState.hit.element?.id; if ( (hasHitASelectedElement || - pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) && + pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements || + (this.state.activeTool.type === "lasso" && + pointerDownState.hit.element)) && !isSelectingPointsInLineEditor && (this.state.activeTool.type !== "lasso" || pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements || - hasHitASelectedElement) && - !pointerDownState.drag.blockDragAfterLasso + hasHitASelectedElement || + pointerDownState.hit.element) ) { const selectedElements = this.scene.getSelectedElements(this.state); @@ -8319,6 +8406,11 @@ class App extends React.Component { // if elements should be deselected on pointerup pointerDownState.drag.hasOccurred = true; + // Clear lasso trail when starting to drag with lasso tool + if (this.state.activeTool.type === "lasso") { + this.lassoTrail.endPath(); + } + // prevent dragging even if we're no longer holding cmd/ctrl otherwise // it would have weird results (stuff jumping all over the screen) // Checking for editingTextElement to avoid jump while editing on mobile #6503 @@ -8915,7 +9007,6 @@ class App extends React.Component { ): (event: PointerEvent) => void { return withBatchedUpdates((childEvent: PointerEvent) => { this.removePointer(childEvent); - pointerDownState.drag.blockDragAfterLasso = false; if (pointerDownState.eventListeners.onMove) { pointerDownState.eventListeners.onMove.flush(); } diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index b48ab25abb..7981e7b7f4 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -782,8 +782,6 @@ export type PointerDownState = Readonly<{ // by default same as PointerDownState.origin. On alt-duplication, reset // to current pointer position at time of duplication. origin: { x: number; y: number }; - // used to block drag after lasso selection until next pointerdown - blockDragAfterLasso: boolean; }; // We need to have these in the state so that we can unsubscribe them eventListeners: {