-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: add drag-to-select for multiple webframes #2853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d20e7c6
67dbb86
3c0f9bf
0497fed
1320b58
fba3f3c
a41bcab
da04fd0
990fffa
041dac4
027714d
4087cb6
6aa6cc3
f9bf313
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,10 +4,11 @@ import { useEditorEngine } from '@/components/store/editor'; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| import { EditorAttributes } from '@onlook/constants'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { EditorMode } from '@onlook/models'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { observer } from 'mobx-react-lite'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useMemo, useRef } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Frames } from './frames'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { HotkeysArea } from './hotkeys'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Overlay } from './overlay'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DragSelectOverlay } from './overlay/drag-select'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PanOverlay } from './overlay/pan'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { RecenterCanvasButton } from './recenter-canvas-button'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,12 +26,94 @@ export const Canvas = observer(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| const containerRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scale = editorEngine.canvas.scale; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const position = editorEngine.canvas.position; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isDragSelecting, setIsDragSelecting] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [dragSelectStart, setDragSelectStart] = useState({ x: 0, y: 0 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [dragSelectEnd, setDragSelectEnd] = useState({ x: 0, y: 0 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleCanvasMouseDown = (event: React.MouseEvent<HTMLDivElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.target !== containerRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| editorEngine.clearUI(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Start drag selection only in design mode and when not holding middle mouse button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (editorEngine.state.editorMode === EditorMode.DESIGN && event.button === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rect = containerRef.current.getBoundingClientRect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const x = event.clientX - rect.left; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const y = event.clientY - rect.top; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIsDragSelecting(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setDragSelectStart({ x, y }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setDragSelectEnd({ x, y }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clear existing selections if not shift-clicking | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!event.shiftKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| editorEngine.clearUI(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| editorEngine.frames.deselectAll(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| editorEngine.clearUI(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleCanvasMouseMove = (event: React.MouseEvent<HTMLDivElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isDragSelecting || !containerRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rect = containerRef.current.getBoundingClientRect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const x = event.clientX - rect.left; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const y = event.clientY - rect.top; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setDragSelectEnd({ x, y }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleCanvasMouseMove = (event: React.MouseEvent<HTMLDivElement>) => { | |
| if (!isDragSelecting || !containerRef.current) { | |
| return; | |
| } | |
| const rect = containerRef.current.getBoundingClientRect(); | |
| const x = event.clientX - rect.left; | |
| const y = event.clientY - rect.top; | |
| setDragSelectEnd({ x, y }); | |
| }; | |
| const handleCanvasMouseMove = useCallback( | |
| throttle((event: React.MouseEvent<HTMLDivElement>) => { | |
| if (!isDragSelecting || !containerRef.current) { | |
| return; | |
| } | |
| const rect = containerRef.current.getBoundingClientRect(); | |
| const x = event.clientX - rect.left; | |
| const y = event.clientY - rect.top; | |
| setDragSelectEnd({ x, y }); | |
| }, 16), // ~60fps | |
| [isDragSelecting] | |
| ); | |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mouse leave handler will incorrectly terminate drag selection when user drags outside the canvas container but is still dragging. This breaks the expected drag selection behavior where users should be able to drag beyond the visible canvas area. The onMouseLeave should not call handleCanvasMouseUp, or should only do so when the mouse button is not pressed.
| onMouseLeave={handleCanvasMouseUp} | |
| onMouseLeave={(e) => { | |
| // Only terminate drag if no mouse button is pressed | |
| if (e.buttons === 0) { | |
| handleCanvasMouseUp(e); | |
| } | |
| }} |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use client'; | ||
|
|
||
| import { observer } from 'mobx-react-lite'; | ||
|
|
||
| interface DragSelectOverlayProps { | ||
| startX: number; | ||
| startY: number; | ||
| endX: number; | ||
| endY: number; | ||
| isSelecting: boolean; | ||
| } | ||
|
|
||
| export const DragSelectOverlay = observer(({ startX, startY, endX, endY, isSelecting }: DragSelectOverlayProps) => { | ||
| if (!isSelecting) { | ||
| return null; | ||
| } | ||
|
|
||
| const left = Math.min(startX, endX); | ||
| const top = Math.min(startY, endY); | ||
| const width = Math.abs(endX - startX); | ||
| const height = Math.abs(endY - startY); | ||
|
|
||
| return ( | ||
| <div | ||
| className="absolute border-2 border-blue-500 bg-blue-500/10 pointer-events-none" | ||
| style={{ | ||
| left: `${left}px`, | ||
| top: `${top}px`, | ||
| width: `${width}px`, | ||
| height: `${height}px`, | ||
| }} | ||
| /> | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic error: The drag selection logic only triggers when event.button === 0 (left mouse button), but the else clause at line 53-55 calls editorEngine.clearUI() for ALL other mouse buttons including right-click and middle-click. This will incorrectly clear the UI when users right-click to open context menus or middle-click to pan. The else clause should either be removed or should only handle specific cases where clearing UI is actually desired.
Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.