diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index a7c34c5ac..53b9aa296 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -170,6 +170,7 @@ export const actionFinalize = register({ : activeTool, activeEmbeddable: null, draggingElement: null, + selectionElement: null, multiElement: null, editingElement: null, startBoundElement: null, @@ -196,7 +197,9 @@ export const actionFinalize = register({ keyTest: (event, appState) => (event.key === KEYS.ESCAPE && (appState.editingLinearElement !== null || - (!appState.draggingElement && appState.multiElement === null))) || + (!appState.selectionElement && + !appState.draggingElement && + appState.multiElement === null))) || ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && appState.multiElement !== null), PanelComponent: ({ appState, updateData, data }) => ( diff --git a/src/actions/actionHistory.tsx b/src/actions/actionHistory.tsx index 2e0f4c091..1e1d9b80b 100644 --- a/src/actions/actionHistory.tsx +++ b/src/actions/actionHistory.tsx @@ -21,6 +21,7 @@ const writeData = ( !appState.multiElement && !appState.resizingElement && !appState.editingElement && + !appState.selectionElement && !appState.draggingElement ) { const data = updater(); diff --git a/src/components/App.tsx b/src/components/App.tsx index 4ad7b889e..9eb6b1585 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3014,7 +3014,8 @@ class App extends React.Component { !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 { this.setState({ draggingElement: null, + selectionElement: null, editingElement: null, }); if (this.state.activeTool.locked) { @@ -4421,8 +4423,7 @@ class App extends React.Component { // 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 { } : {}), appState: { + selectionElement: null, draggingElement: null, editingElement: null, startBoundElement: null, @@ -4561,13 +4563,16 @@ class App extends React.Component { // retrieve the latest element as the state may be stale const pendingImageElement = this.state.pendingImageElementId && - this.scene.getElement(this.state.pendingImageElementId); + this.scene.getElement( + 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 { ); this.scene.addNewElement(element); this.setState({ + selectionElement: null, draggingElement: element, editingElement: element, startBoundElement: boundElement, @@ -5593,6 +5599,7 @@ class App extends React.Component { this.scene.addNewElement(element); this.setState({ + selectionElement: null, draggingElement: element, editingElement: element, startBoundElement: boundElement, @@ -5667,12 +5674,13 @@ class App extends React.Component { 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 { this.setState({ multiElement: null, + selectionElement: null, draggingElement: frame, editingElement: frame, }); @@ -5763,7 +5772,9 @@ class App extends React.Component { 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 { 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 { } } + 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 { 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, 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 { 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 { } else { this.setState((prevState) => ({ draggingElement: null, + selectionElement: null, })); } } @@ -6595,140 +6518,137 @@ class App extends React.Component { ); 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 { 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 { 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 { ); }; + 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, 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 { 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, + ), + }); } }; diff --git a/src/components/HintViewer.tsx b/src/components/HintViewer.tsx index 18dba0e11..f5de281ea 100644 --- a/src/components/HintViewer.tsx +++ b/src/components/HintViewer.tsx @@ -82,8 +82,9 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => { if (activeTool.type === "selection") { if ( - appState.draggingElement?.type === "selection" && + appState.selectionElement && !selectedElements.length && + !appState.draggingElement && !appState.editingElement && !appState.editingLinearElement ) { diff --git a/src/element/Hyperlink.tsx b/src/element/Hyperlink.tsx index 720f17824..0786149d4 100644 --- a/src/element/Hyperlink.tsx +++ b/src/element/Hyperlink.tsx @@ -210,6 +210,7 @@ export const Hyperlink = ({ }; const { x, y } = getCoordsForPopover(element, appState); if ( + appState.selectionElement || appState.draggingElement || appState.resizingElement || appState.isRotating || diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index adc5aafc4..4e4299019 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -134,10 +134,7 @@ export class LinearElementEditor { appState: AppState, setState: React.Component["setState"], ) { - if ( - !appState.editingLinearElement || - appState.draggingElement?.type !== "selection" - ) { + if (!appState.editingLinearElement || !appState.selectionElement) { return false; } const { editingLinearElement } = appState; @@ -149,7 +146,7 @@ export class LinearElementEditor { } const [selectionX1, selectionY1, selectionX2, selectionY2] = - getElementAbsoluteCoords(appState.draggingElement); + getElementAbsoluteCoords(appState.selectionElement); const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(element); diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index c990a6e03..cfebedbc6 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -5152,35 +5152,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, - "draggingElement": { - "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, - }, + "draggingElement": null, "editingElement": null, "editingFrame": null, "editingGroupId": null, @@ -5449,35 +5421,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, - "draggingElement": { - "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, - }, + "draggingElement": null, "editingElement": null, "editingFrame": null, "editingGroupId": null, @@ -5718,35 +5662,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, - "draggingElement": { - "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, - }, + "draggingElement": null, "editingElement": null, "editingFrame": null, "editingGroupId": null, @@ -16262,35 +16178,7 @@ exports[`regression tests > switches from group of selected elements to another "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, - "draggingElement": { - "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, - }, + "draggingElement": null, "editingElement": null, "editingFrame": null, "editingGroupId": null, @@ -16662,35 +16550,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, - "draggingElement": { - "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, - }, + "draggingElement": null, "editingElement": null, "editingFrame": null, "editingGroupId": null, diff --git a/src/types.ts b/src/types.ts index 8b05ba40a..eff96b7d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ import { StrokeRoundness, ExcalidrawFrameElement, ExcalidrawEmbeddableElement, + ExcalidrawSelectionElement, } from "./element/types"; import { Point as RoughPoint } from "roughjs/bin/geometry"; import { LinearElementEditor } from "./element/linearElementEditor"; @@ -182,10 +183,25 @@ export type AppState = { element: NonDeletedExcalidrawElement; state: "hover" | "active"; } | 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; + /** multi-point linear element when it's being created */ multiElement: NonDeleted | 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; startBoundElement: NonDeleted | null; suggestedBindings: SuggestedBinding[]; @@ -198,9 +214,14 @@ export type AppState = { }; editingFrame: string | null; elementsToHighlight: NonDeleted[] | 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; + /** + * Linear element that's being edited (when in the linear element editor). + * Not set when creating multi-point linear element. + */ editingLinearElement: LinearElementEditor | null; activeTool: { /**