diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index c797c6e8c..4ccf723f8 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -18,13 +18,22 @@ export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1; export const isSafari = !isChrome && navigator.userAgent.indexOf("Safari") !== -1; export const isIOS = - /iPad|iPhone/.test(navigator.platform) || + /iPad|iPhone/i.test(navigator.platform) || // iPadOS 13+ (navigator.userAgent.includes("Mac") && "ontouchend" in document); // keeping function so it can be mocked in test export const isBrave = () => (navigator as any).brave?.isBrave?.name === "isBrave"; +export const isMobile = + isIOS || + /android|webos|ipod|blackberry|iemobile|opera mini/i.test( + navigator.userAgent.toLowerCase(), + ) || + /android|ios|ipod|blackberry|windows phone/i.test( + navigator.platform.toLowerCase(), + ); + export const supportsResizeObserver = typeof window !== "undefined" && "ResizeObserver" in window; diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index 80a9eedaa..535d96c7d 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -121,7 +121,7 @@ export const actionClearCanvas = register({ pasteDialog: appState.pasteDialog, activeTool: appState.activeTool.type === "image" - ? { ...appState.activeTool, type: "selection" } + ? { ...appState.activeTool, type: app.defaultSelectionTool } : appState.activeTool, }, captureUpdate: CaptureUpdateAction.IMMEDIATELY, @@ -494,13 +494,13 @@ export const actionToggleEraserTool = register({ name: "toggleEraserTool", label: "toolBar.eraser", trackEvent: { category: "toolbar" }, - perform: (elements, appState) => { + perform: (elements, appState, _, app) => { let activeTool: AppState["activeTool"]; if (isEraserActive(appState)) { activeTool = updateActiveTool(appState, { ...(appState.activeTool.lastActiveTool || { - type: "selection", + type: app.defaultSelectionTool, }), lastActiveToolBeforeEraser: null, }); @@ -530,6 +530,9 @@ export const actionToggleLassoTool = register({ label: "toolBar.lasso", icon: LassoIcon, trackEvent: { category: "toolbar" }, + predicate: (elements, appState, props, app) => { + return app.defaultSelectionTool !== "lasso"; + }, perform: (elements, appState, _, app) => { let activeTool: AppState["activeTool"]; diff --git a/packages/excalidraw/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx index a9281ce84..78a346568 100644 --- a/packages/excalidraw/actions/actionDeleteSelected.tsx +++ b/packages/excalidraw/actions/actionDeleteSelected.tsx @@ -298,7 +298,9 @@ export const actionDeleteSelected = register({ elements: nextElements, appState: { ...nextAppState, - activeTool: updateActiveTool(appState, { type: "selection" }), + activeTool: updateActiveTool(appState, { + type: app.defaultSelectionTool, + }), multiElement: null, activeEmbeddable: null, selectedLinearElement: null, diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index f9ff6e79f..877c817ad 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -261,13 +261,13 @@ export const actionFinalize = register({ if (appState.activeTool.type === "eraser") { activeTool = updateActiveTool(appState, { ...(appState.activeTool.lastActiveTool || { - type: "selection", + type: app.defaultSelectionTool, }), lastActiveToolBeforeEraser: null, }); } else { activeTool = updateActiveTool(appState, { - type: "selection", + type: app.defaultSelectionTool, }); } diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 5c9d59ada..91bef0e05 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -46,7 +46,7 @@ import { hasStrokeWidth, } from "../scene"; -import { SHAPES } from "./shapes"; +import { getToolbarTools } from "./shapes"; import "./Actions.scss"; @@ -295,7 +295,8 @@ export const ShapesSwitcher = ({ const frameToolSelected = activeTool.type === "frame"; const laserToolSelected = activeTool.type === "laser"; - const lassoToolSelected = activeTool.type === "lasso"; + const lassoToolSelected = + activeTool.type === "lasso" && app.defaultSelectionTool !== "lasso"; const embeddableToolSelected = activeTool.type === "embeddable"; @@ -303,63 +304,68 @@ export const ShapesSwitcher = ({ return ( <> - {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { - if ( - UIOptions.tools?.[ - value as Extract - ] === false - ) { - return null; - } + {getToolbarTools(app).map( + ({ value, icon, key, numericKey, fillable }, index) => { + if ( + UIOptions.tools?.[ + value as Extract< + typeof value, + keyof AppProps["UIOptions"]["tools"] + > + ] === false + ) { + return null; + } - const label = t(`toolBar.${value}`); - const letter = - key && capitalizeString(typeof key === "string" ? key : key[0]); - const shortcut = letter - ? `${letter} ${t("helpDialog.or")} ${numericKey}` - : `${numericKey}`; + const label = t(`toolBar.${value}`); + const letter = + key && capitalizeString(typeof key === "string" ? key : key[0]); + const shortcut = letter + ? `${letter} ${t("helpDialog.or")} ${numericKey}` + : `${numericKey}`; - return ( - { - if (!appState.penDetected && pointerType === "pen") { - app.togglePenMode(true); - } - - if (value === "selection") { - if (appState.activeTool.type === "selection") { - app.setActiveTool({ type: "lasso" }); - } else { - app.setActiveTool({ type: "selection" }); + return ( + { + if (!appState.penDetected && pointerType === "pen") { + app.togglePenMode(true); } - } - }} - onChange={({ pointerType }) => { - if (appState.activeTool.type !== value) { - trackEvent("toolbar", value, "ui"); - } - if (value === "image") { - app.setActiveTool({ - type: value, - }); - } else { - app.setActiveTool({ type: value }); - } - }} - /> - ); - })} + + if (value === "selection") { + if (appState.activeTool.type === "selection") { + app.setActiveTool({ type: "lasso" }); + } else { + app.setActiveTool({ type: "selection" }); + } + } + }} + onChange={({ pointerType }) => { + if (appState.activeTool.type !== value) { + trackEvent("toolbar", value, "ui"); + } + if (value === "image") { + app.setActiveTool({ + type: value, + }); + } else { + app.setActiveTool({ type: value }); + } + }} + /> + ); + }, + )}
@@ -418,14 +424,16 @@ export const ShapesSwitcher = ({ > {t("toolBar.laser")} - app.setActiveTool({ type: "lasso" })} - icon={LassoIcon} - data-testid="toolbar-lasso" - selected={lassoToolSelected} - > - {t("toolBar.lasso")} - + {app.defaultSelectionTool !== "lasso" && ( + app.setActiveTool({ type: "lasso" })} + icon={LassoIcon} + data-testid="toolbar-lasso" + selected={lassoToolSelected} + > + {t("toolBar.lasso")} + + )}
Generate
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index c91f1c1b9..d0fe01db9 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -100,6 +100,7 @@ import { randomInteger, CLASSES, Emitter, + isMobile, MINIMUM_ARROW_SIZE, } from "@excalidraw/common"; @@ -653,9 +654,14 @@ class App extends React.Component { >(); onRemoveEventListenersEmitter = new Emitter<[]>(); + defaultSelectionTool: "selection" | "lasso" = "selection"; + constructor(props: AppProps) { super(props); const defaultAppState = getDefaultAppState(); + this.defaultSelectionTool = this.isMobileOrTablet() + ? ("lasso" as const) + : ("selection" as const); const { excalidrawAPI, viewModeEnabled = false, @@ -1606,7 +1612,8 @@ class App extends React.Component { renderWelcomeScreen={ !this.state.isLoading && this.state.showWelcomeScreen && - this.state.activeTool.type === "selection" && + this.state.activeTool.type === + this.defaultSelectionTool && !this.state.zenModeEnabled && !this.scene.getElementsIncludingDeleted().length } @@ -2350,6 +2357,7 @@ class App extends React.Component { repairBindings: true, deleteInvisibleElements: true, }); + const activeTool = scene.appState.activeTool; scene.appState = { ...scene.appState, theme: this.props.theme || scene.appState.theme, @@ -2359,8 +2367,13 @@ class App extends React.Component { // with a library install link, which should auto-open the library) openSidebar: scene.appState?.openSidebar || this.state.openSidebar, activeTool: - scene.appState.activeTool.type === "image" - ? { ...scene.appState.activeTool, type: "selection" } + activeTool.type === "image" || + activeTool.type === "lasso" || + activeTool.type === "selection" + ? { + ...activeTool, + type: this.defaultSelectionTool, + } : scene.appState.activeTool, isLoading: false, toast: this.state.toast, @@ -2399,6 +2412,16 @@ class App extends React.Component { } }; + private isMobileOrTablet = (): boolean => { + const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0; + const hasCoarsePointer = + "matchMedia" in window && + window?.matchMedia("(pointer: coarse)")?.matches; + const isTouchMobile = hasTouch && hasCoarsePointer; + + return isMobile || isTouchMobile; + }; + private isMobileBreakpoint = (width: number, height: number) => { return ( width < MQ_MAX_WIDTH_PORTRAIT || @@ -3117,7 +3140,7 @@ class App extends React.Component { this.addElementsFromPasteOrLibrary({ elements, files: data.files || null, - position: "cursor", + position: this.isMobileOrTablet() ? "center" : "cursor", retainSeed: isPlainPaste, }); } else if (data.text) { @@ -3135,7 +3158,7 @@ class App extends React.Component { this.addElementsFromPasteOrLibrary({ elements, files, - position: "cursor", + position: this.isMobileOrTablet() ? "center" : "cursor", }); return; @@ -3195,7 +3218,7 @@ class App extends React.Component { } this.addTextFromPaste(data.text, isPlainPaste); } - this.setActiveTool({ type: "selection" }); + this.setActiveTool({ type: this.defaultSelectionTool }, true); event?.preventDefault(); }, ); @@ -3341,7 +3364,7 @@ class App extends React.Component { } }, ); - this.setActiveTool({ type: "selection" }); + this.setActiveTool({ type: this.defaultSelectionTool }, true); if (opts.fitToContent) { this.scrollToContent(duplicatedElements, { @@ -3587,7 +3610,7 @@ class App extends React.Component { ...updateActiveTool( this.state, prevState.activeTool.locked - ? { type: "selection" } + ? { type: this.defaultSelectionTool } : prevState.activeTool, ), locked: !prevState.activeTool.locked, @@ -4500,7 +4523,7 @@ class App extends React.Component { !this.state.selectionElement && !this.state.selectedElementsAreBeingDragged ) { - const shape = findShapeByKey(event.key); + const shape = findShapeByKey(event.key, this); if (shape) { if (this.state.activeTool.type !== shape) { trackEvent( @@ -4593,7 +4616,7 @@ class App extends React.Component { if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) { if (this.state.activeTool.type === "laser") { - this.setActiveTool({ type: "selection" }); + this.setActiveTool({ type: this.defaultSelectionTool }); } else { this.setActiveTool({ type: "laser" }); } @@ -5438,7 +5461,7 @@ class App extends React.Component { return; } // we should only be able to double click when mode is selection - if (this.state.activeTool.type !== "selection") { + if (this.state.activeTool.type !== this.defaultSelectionTool) { return; } @@ -6050,6 +6073,7 @@ class App extends React.Component { if ( hasDeselectedButton || (this.state.activeTool.type !== "selection" && + this.state.activeTool.type !== "lasso" && this.state.activeTool.type !== "text" && this.state.activeTool.type !== "eraser") ) { @@ -6212,7 +6236,12 @@ class App extends React.Component { !isElbowArrow(hitElement) || !(hitElement.startBinding || hitElement.endBinding) ) { - setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); + if ( + this.state.activeTool.type !== "lasso" || + selectedElements.length > 0 + ) { + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); + } if (this.state.activeEmbeddable?.state === "hover") { this.setState({ activeEmbeddable: null }); } @@ -6329,7 +6358,12 @@ class App extends React.Component { !isElbowArrow(element) || !(element.startBinding || element.endBinding) ) { - setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); + if ( + this.state.activeTool.type !== "lasso" || + Object.keys(this.state.selectedElementIds).length > 0 + ) { + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); + } } } } else if (this.hitElement(scenePointerX, scenePointerY, element)) { @@ -6338,7 +6372,12 @@ class App extends React.Component { !isElbowArrow(element) || !(element.startBinding || element.endBinding) ) { - setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); + if ( + this.state.activeTool.type !== "lasso" || + Object.keys(this.state.selectedElementIds).length > 0 + ) { + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); + } } } @@ -6600,11 +6639,119 @@ class App extends React.Component { } if (this.state.activeTool.type === "lasso") { - this.lassoTrail.startPath( - pointerDownState.origin.x, - pointerDownState.origin.y, - event.shiftKey, - ); + const hitSelectedElement = + pointerDownState.hit.element && + this.isASelectedElement(pointerDownState.hit.element); + + const isMobileOrTablet = this.isMobileOrTablet(); + + if ( + !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements && + !pointerDownState.resize.handleType && + !hitSelectedElement + ) { + this.lassoTrail.startPath( + pointerDownState.origin.x, + pointerDownState.origin.y, + event.shiftKey, + ); + + // block dragging after lasso selection on PCs until the next pointer down + // (on mobile or tablet, we want to allow user to drag immediately) + pointerDownState.drag.blockDragging = !isMobileOrTablet; + } + + // only for mobile or tablet, if we hit an element, select it immediately like normal selection + if ( + isMobileOrTablet && + pointerDownState.hit.element && + !hitSelectedElement + ) { + this.setState((prevState) => { + const 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); } else if ( @@ -6984,6 +7131,7 @@ class App extends React.Component { hasOccurred: false, offset: null, origin: { ...origin }, + blockDragging: false, }, eventListeners: { onMove: null, @@ -7059,7 +7207,10 @@ class App extends React.Component { event: React.PointerEvent, pointerDownState: PointerDownState, ): boolean => { - if (this.state.activeTool.type === "selection") { + if ( + this.state.activeTool.type === "selection" || + this.state.activeTool.type === "lasso" + ) { const elements = this.scene.getNonDeletedElements(); const elementsMap = this.scene.getNonDeletedElementsMap(); const selectedElements = this.scene.getSelectedElements(this.state); @@ -7266,7 +7417,18 @@ class App extends React.Component { // on CMD/CTRL, drill down to hit element regardless of groups etc. if (event[KEYS.CTRL_OR_CMD]) { if (event.altKey) { - // ctrl + alt means we're lasso selecting + // ctrl + alt means we're lasso selecting - start lasso trail and switch to lasso tool + + // Close any open dialogs that might interfere with lasso selection + if (this.state.openDialog?.name === "elementLinkSelector") { + this.setOpenDialog(null); + } + this.lassoTrail.startPath( + pointerDownState.origin.x, + pointerDownState.origin.y, + event.shiftKey, + ); + this.setActiveTool({ type: "lasso", fromSelection: true }); return false; } if (!this.state.selectedElementIds[hitElement.id]) { @@ -7487,7 +7649,9 @@ class App extends React.Component { resetCursor(this.interactiveCanvas); if (!this.state.activeTool.locked) { this.setState({ - activeTool: updateActiveTool(this.state, { type: "selection" }), + activeTool: updateActiveTool(this.state, { + type: this.defaultSelectionTool, + }), }); } }; @@ -8271,15 +8435,18 @@ class App extends React.Component { event.shiftKey && this.state.selectedLinearElement.elementId === pointerDownState.hit.element?.id; + if ( (hasHitASelectedElement || pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) && !isSelectingPointsInLineEditor && - this.state.activeTool.type !== "lasso" + !pointerDownState.drag.blockDragging ) { const selectedElements = this.scene.getSelectedElements(this.state); - - if (selectedElements.every((element) => element.locked)) { + if ( + selectedElements.length > 0 && + selectedElements.every((element) => element.locked) + ) { return; } @@ -8300,6 +8467,29 @@ class App extends React.Component { // if elements should be deselected on pointerup pointerDownState.drag.hasOccurred = true; + // prevent immediate dragging during lasso selection to avoid element displacement + // only allow dragging if we're not in the middle of lasso selection + // (on mobile, allow dragging if we hit an element) + if ( + this.state.activeTool.type === "lasso" && + this.lassoTrail.hasCurrentTrail && + !(this.isMobileOrTablet() && pointerDownState.hit.element) && + !this.state.activeTool.fromSelection + ) { + return; + } + + // Clear lasso trail when starting to drag selected elements with lasso tool + // Only clear if we're actually dragging (not during lasso selection) + if ( + this.state.activeTool.type === "lasso" && + selectedElements.length > 0 && + pointerDownState.drag.hasOccurred && + !this.state.activeTool.fromSelection + ) { + 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 @@ -8894,6 +9084,7 @@ class App extends React.Component { ): (event: PointerEvent) => void { return withBatchedUpdates((childEvent: PointerEvent) => { this.removePointer(childEvent); + pointerDownState.drag.blockDragging = false; if (pointerDownState.eventListeners.onMove) { pointerDownState.eventListeners.onMove.flush(); } @@ -9182,7 +9373,7 @@ class App extends React.Component { this.setState((prevState) => ({ newElement: null, activeTool: updateActiveTool(this.state, { - type: "selection", + type: this.defaultSelectionTool, }), selectedElementIds: makeNextSelectedElementIds( { @@ -9798,7 +9989,9 @@ class App extends React.Component { this.setState({ newElement: null, suggestedBindings: [], - activeTool: updateActiveTool(this.state, { type: "selection" }), + activeTool: updateActiveTool(this.state, { + type: this.defaultSelectionTool, + }), }); } else { this.setState({ @@ -10092,7 +10285,9 @@ class App extends React.Component { this.setState( { newElement: null, - activeTool: updateActiveTool(this.state, { type: "selection" }), + activeTool: updateActiveTool(this.state, { + type: this.defaultSelectionTool, + }), }, () => { this.actionManager.executeAction(actionFinalize); @@ -10465,7 +10660,7 @@ class App extends React.Component { event.nativeEvent.pointerType === "pen" && // always allow if user uses a pen secondary button event.button !== POINTER_BUTTON.SECONDARY)) && - this.state.activeTool.type !== "selection" + this.state.activeTool.type !== this.defaultSelectionTool ) { return; } diff --git a/packages/excalidraw/components/shapes.tsx b/packages/excalidraw/components/shapes.tsx index 7411a9e25..56c85bcd4 100644 --- a/packages/excalidraw/components/shapes.tsx +++ b/packages/excalidraw/components/shapes.tsx @@ -13,6 +13,8 @@ import { EraserIcon, } from "./icons"; +import type { AppClassProperties } from "../types"; + export const SHAPES = [ { icon: SelectionIcon, @@ -86,8 +88,23 @@ export const SHAPES = [ }, ] as const; -export const findShapeByKey = (key: string) => { - const shape = SHAPES.find((shape, index) => { +export const getToolbarTools = (app: AppClassProperties) => { + return app.defaultSelectionTool === "lasso" + ? ([ + { + value: "lasso", + icon: SelectionIcon, + key: KEYS.V, + numericKey: KEYS["1"], + fillable: true, + }, + ...SHAPES.slice(1), + ] as const) + : SHAPES; +}; + +export const findShapeByKey = (key: string, app: AppClassProperties) => { + const shape = getToolbarTools(app).find((shape, index) => { return ( (shape.numericKey != null && key === shape.numericKey.toString()) || (shape.key && diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 5f1ba06d5..cb4e8af6b 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -169,8 +169,14 @@ export const isSnappingEnabled = ({ selectedElements: NonDeletedExcalidrawElement[]; }) => { if (event) { + // Allow snapping for lasso tool when dragging selected elements + // but not during lasso selection phase + const isLassoDragging = + app.state.activeTool.type === "lasso" && + app.state.selectedElementsAreBeingDragged; + return ( - app.state.activeTool.type !== "lasso" && + (app.state.activeTool.type !== "lasso" || isLassoDragging) && ((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) || (!app.state.objectsSnapModeEnabled && event[KEYS.CTRL_OR_CMD] && diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index e7c3c68d3..a7fe59644 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -3682,14 +3682,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1116226695, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 81784553, "width": 20, "x": 20, "y": 30, @@ -3714,14 +3714,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 401146281, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 6694c8810..5f62999e0 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -731,6 +731,8 @@ export type AppClassProperties = { onPointerUpEmitter: App["onPointerUpEmitter"]; updateEditorAtom: App["updateEditorAtom"]; + + defaultSelectionTool: "selection" | "lasso"; }; export type PointerDownState = Readonly<{ @@ -780,6 +782,10 @@ 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 }; + // Whether to block drag after lasso selection + // this is meant to be used to block dragging after lasso selection on PCs + // until the next pointer down + blockDragging: boolean; }; // We need to have these in the state so that we can unsubscribe them eventListeners: {