alternatvie: keep lasso drag to only mobile

This commit is contained in:
Ryan Di
2025-07-17 17:29:32 +10:00
parent a1b95c47a7
commit 85dc55c718
2 changed files with 53 additions and 13 deletions

View File

@@ -25,6 +25,18 @@ export const isIOS =
export const isBrave = () => export const isBrave = () =>
(navigator as any).brave?.isBrave?.name === "isBrave"; (navigator as any).brave?.isBrave?.name === "isBrave";
// Mobile user agent detection
export const isMobileUA =
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
navigator.userAgent.toLowerCase(),
);
// Mobile platform detection
export const isMobilePlatform =
/android|ios|iphone|ipad|ipod|blackberry|windows phone/i.test(
navigator.platform.toLowerCase(),
);
export const supportsResizeObserver = export const supportsResizeObserver =
typeof window !== "undefined" && "ResizeObserver" in window; typeof window !== "undefined" && "ResizeObserver" in window;

View File

@@ -100,6 +100,8 @@ import {
randomInteger, randomInteger,
CLASSES, CLASSES,
Emitter, Emitter,
isMobileUA,
isMobilePlatform,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@@ -2386,6 +2388,19 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private isMobileOrTablet = (): boolean => {
// Touch + pointer accuracy
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
const hasCoarsePointer = window.matchMedia("(pointer: coarse)").matches;
const isTouchMobile = hasTouch && hasCoarsePointer;
// At least two indicators should be true
const indicators = [isMobileUA, isTouchMobile, isMobilePlatform];
const hasMultipleIndicators = indicators.filter(Boolean).length >= 2;
return hasMultipleIndicators;
};
private isMobileBreakpoint = (width: number, height: number) => { private isMobileBreakpoint = (width: number, height: number) => {
return ( return (
width < MQ_MAX_WIDTH_PORTRAIT || width < MQ_MAX_WIDTH_PORTRAIT ||
@@ -6013,7 +6028,7 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
hasDeselectedButton || hasDeselectedButton ||
(this.state.activeTool.type !== "selection" && (this.state.activeTool.type !== "selection" &&
this.state.activeTool.type !== "lasso" && (this.state.activeTool.type !== "lasso" || !this.isMobileOrTablet()) &&
this.state.activeTool.type !== "text" && this.state.activeTool.type !== "text" &&
this.state.activeTool.type !== "eraser") this.state.activeTool.type !== "eraser")
) { ) {
@@ -6187,7 +6202,7 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
if ( if (
this.state.activeTool.type !== "lasso" || this.state.activeTool.type !== "lasso" ||
selectedElements.length > 0 (selectedElements.length > 0 && this.isMobileOrTablet())
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
} }
@@ -6301,7 +6316,8 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
if ( if (
this.state.activeTool.type !== "lasso" || this.state.activeTool.type !== "lasso" ||
Object.keys(this.state.selectedElementIds).length > 0 (Object.keys(this.state.selectedElementIds).length > 0 &&
this.isMobileOrTablet())
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
} }
@@ -6315,7 +6331,8 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
if ( if (
this.state.activeTool.type !== "lasso" || this.state.activeTool.type !== "lasso" ||
Object.keys(this.state.selectedElementIds).length > 0 (Object.keys(this.state.selectedElementIds).length > 0 &&
this.isMobileOrTablet())
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
} }
@@ -6583,13 +6600,16 @@ class App extends React.Component<AppProps, AppState> {
const hitSelectedElement = const hitSelectedElement =
pointerDownState.hit.element && pointerDownState.hit.element &&
this.isASelectedElement(pointerDownState.hit.element); this.isASelectedElement(pointerDownState.hit.element);
const isMobileOrTablet = this.isMobileOrTablet();
// Start a new lasso ONLY if we're not interacting with an existing // On PCs, we always want to start a new lasso path even when we're hitting some elements
// Otherwise, start a new lasso ONLY if we're not interacting with an existing
// selection (move/resize/rotate). // selection (move/resize/rotate).
if ( if (
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements && !isMobileOrTablet ||
(!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
!pointerDownState.resize.handleType && !pointerDownState.resize.handleType &&
!hitSelectedElement !hitSelectedElement)
) { ) {
this.lassoTrail.startPath( this.lassoTrail.startPath(
pointerDownState.origin.x, pointerDownState.origin.x,
@@ -6598,8 +6618,13 @@ class App extends React.Component<AppProps, AppState> {
); );
} }
// For lasso tool, if we hit an element, select it immediately like normal selection // When mobile & tablet, for lasso tool
if (pointerDownState.hit.element && !hitSelectedElement) { // if we hit an element, select it immediately like normal selection
if (
isMobileOrTablet &&
pointerDownState.hit.element &&
!hitSelectedElement
) {
this.setState((prevState) => { this.setState((prevState) => {
const nextSelectedElementIds: { [id: string]: true } = { const nextSelectedElementIds: { [id: string]: true } = {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
@@ -7141,7 +7166,7 @@ class App extends React.Component<AppProps, AppState> {
): boolean => { ): boolean => {
if ( if (
this.state.activeTool.type === "selection" || this.state.activeTool.type === "selection" ||
this.state.activeTool.type === "lasso" (this.state.activeTool.type === "lasso" && this.isMobileOrTablet())
) { ) {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const elementsMap = this.scene.getNonDeletedElementsMap(); const elementsMap = this.scene.getNonDeletedElementsMap();
@@ -8387,7 +8412,8 @@ class App extends React.Component<AppProps, AppState> {
(hasHitASelectedElement || (hasHitASelectedElement ||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements || pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements ||
(this.state.activeTool.type === "lasso" && (this.state.activeTool.type === "lasso" &&
pointerDownState.hit.element)) && pointerDownState.hit.element &&
this.isMobileOrTablet())) &&
!isSelectingPointsInLineEditor && !isSelectingPointsInLineEditor &&
(this.state.activeTool.type !== "lasso" || (this.state.activeTool.type !== "lasso" ||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements || pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements ||
@@ -8437,7 +8463,9 @@ class App extends React.Component<AppProps, AppState> {
selectedElements.length > 0 && selectedElements.length > 0 &&
!pointerDownState.withCmdOrCtrl && !pointerDownState.withCmdOrCtrl &&
!this.state.editingTextElement && !this.state.editingTextElement &&
this.state.activeEmbeddable?.state !== "active" this.state.activeEmbeddable?.state !== "active" &&
// for lasso tool, only allow dragging on mobile or tablet devices
(this.state.activeTool.type !== "lasso" || this.isMobileOrTablet())
) { ) {
const dragOffset = { const dragOffset = {
x: pointerCoords.x - pointerDownState.drag.origin.x, x: pointerCoords.x - pointerDownState.drag.origin.x,