🎯 Core Questions of This Chapter

How to achieve a smooth and intuitive drag-and-drop interaction experience?

Challenge Pain Points of Traditional Solutions Our Solution
Event Loss Drag breaks when mouse leaves element Listen on document instead of element itself
Position Disorder Pixel coordinates are not aligned, components overlap Grid snapping system
Performance Lag Excessive DOM operations cause frame drops CSS transform + GPU acceleration
State Sync Position lost after page refresh post-drag Instant persistence to database on mouseup
Collision Issues Components can overlap each other Planned: AABB collision detection

📐 Architecture Overview

Large Screen Drag-and-Drop Interaction System Architecture

Large Screen Drag-and-Drop Interaction System Architecture

Core Modules

Drag Interaction System Core Modules

Event Handling, Grid, Composable & API Persistence


🖱️ I. Drag Event Lifecycle (3 Phases)

1.1 Complete Flow Diagram

Drag Three-Phase Sequence

mousedown initiation → mousemove real-time update → mouseup confirm save

1.2 Why Listen on the document?

Classic Bug Scenario:

1
2
3
4
5
6
7
// ❌ Wrong: bound to the element
widget.addEventListener('mousedown', () => {
widget.addEventListener('mousemove', handleMove)
})

// Problem: When the mouse quickly moves out of the widget area,
// the mousemove event no longer fires → drag "gets stuck"
1
2
3
4
5
6
// ✅ Correct: bound to the document
widget.addEventListener('mousedown', () => {
// Captures events no matter where the mouse is
document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleUp)
})

Principle:

The target of an event and the listener are two different concepts.
Even after the mouse leaves the widget element, document can still capture global mousemove/mouseup events.


💻 II. Core Code Implementation: useDraggable Composable

composables/useDraggable.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import { ref, reactive, onUnmounted } from 'vue'

interface DragOptions {
widgetId: number
initialX: number
initialY: number
width: number // Widget's grid width
height: number // Widget's grid height
gridSize: {
cellWidth: number // Pixel width of each grid cell (e.g., 80px)
cellHeight: number // Pixel height of each grid cell (e.g., 80px)
cols: number // Total columns (12)
rows: number // Total rows (unlimited)
}
onPositionChange: (x: number, y: number) => void // Callback: called when position changes
onCollision?: (collidingWidgetId: number) => void // Callback: called on collision
}

interface DragState {
isDragging: boolean
startX: number // Screen X coordinate on mousedown
startY: number // Screen Y coordinate on mousedown
startGridX: number // Grid X on mousedown
startGridY: number // Grid Y on mousedown
currentGridX: number // Current Grid X (updated in real-time)
currentGridY: number // Current Grid Y (updated in real-time)
}

export function useDraggable(options: DragOptions) {
const {
widgetId,
initialX,
initialY,
width,
height,
gridSize,
onPositionChange,
onCollision,
} = options

const state = reactive<DragState>({
isDragging: false,
startX: 0,
startY: 0,
startGridX: initialX,
startGridY: initialY,
currentGridX: initialX,
currentGridY: initialY,
})

/**
* Boundary constraint function
* Ensures grid coordinates stay within valid range
*/
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value))
}

/**
* Pixel coordinates → Grid coordinates conversion
*/
function pixelToGrid(pixelX: number, pixelY: number): { x: number; y: number } {
const gridX = Math.round(pixelX / gridSize.cellWidth)
const gridY = Math.round(pixelY / gridSize.cellHeight)

return {
x: clamp(gridX + state.startGridX, 0, gridSize.cols - width),
y: clamp(gridY + state.startGridY, 0, Infinity), // Y direction can extend infinitely
}
}

/**
* Phase 1: mousedown handling
*/
function handleMouseDown(event: MouseEvent) {
// Only respond to left-click
if (event.button !== 0) return

event.preventDefault() // Prevent text selection

state.isDragging = true
state.startX = event.clientX
state.startY = event.clientY
state.startGridX = state.currentGridX
state.startGridY = state.currentGridY

// 🔑 Key: Bind to document, not the current element
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)

// Add dragging style class (for visual feedback)
;(event.target as HTMLElement).classList.add('dragging')
}

/**
* Phase 2: mousemove handling (high-frequency trigger)
*/
function handleMouseMove(event: MouseEvent) {
if (!state.isDragging) return

// Calculate delta (offset relative to start)
const deltaX = event.clientX - state.startX
const deltaY = event.clientY - state.startY

// Convert to Grid coordinates
const newCoords = pixelToGrid(deltaX, deltaY)

// Only update when coordinates actually change (reduce unnecessary renders)
if (
newCoords.x !== state.currentGridX ||
newCoords.y !== state.currentGridY
) {
// TODO: collision detection logic can be added here
// if (checkCollision(newCoords)) {
// onCollision?.(collidingId)
// return
// }

state.currentGridX = newCoords.x
state.currentGridY = newCoords.y
}
}

/**
* Phase 3: mouseup handling
*/
async function handleMouseUp(event: MouseEvent) {
if (!state.isDragging) return

state.isDragging = false

// Remove document listeners (prevent memory leaks)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)

// Remove dragging style
;(event.target as HTMLElement).classList.remove('dragging')

// If position changed, trigger callback (save to backend)
if (
state.currentGridX !== state.startGridX ||
state.currentGridY !== state.startGridY
) {
await onPositionChange(state.currentGridX, state.currentGridY)
}
}

// Clean up on component unmount (prevent memory leaks)
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
})

return {
state,
handleMouseDown,
}
}

📐 III. Grid Layout System Explained

3.1 What is Grid Layout?

Analogy:

Like an Excel spreadsheet, divide the canvas into a grid of 12 columns × N rows.
Each Widget occupies several “cells”, positioned by coordinates (x, y, w, h).

Visual Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Columns: 0   1   2   3   4   5   6   7   8   9   10  11
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
Row 0 │ Widget A (x=0,y=0,w=6,h=2) │ Widget B │
│ [Today's Sales KPI] │(x=6,y=0, │
├───┴───┴───┴───┴───┴───┼───┴───┴───┴───┤w=6,h=2) │
Row 1 │ │[Trend Line]│
├───┬───┬───┬───┬───┬───┴───┬───┬───┬───┴───┴───┤
Row 2 │ Widget C (x=0,y=2,w=12,h=3) │
│ [Category Sales TOP10 Bar Chart] │
│ │
Row 3 │ │
│ │
├───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┤
Row 4 │ │
│ (Empty area for new Widgets) │
...

3.2 Coordinate Conversion Math Formulas

Pixel → Grid (for real-time calculation during drag)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Converts screen pixel coordinates to Grid coordinates
*
* @param pixelX Screen pixel X (offset relative to container)
* @param pixelY Screen pixel Y
* @returns Grid coordinates {x, y}
*
* Example:
* cellWidth = 80px, gap = 16px
* pixelX = 176px
* → gridX = Math.round(176 / 96) = Math.round(1.83) = 2
*/
function pixelToGrid(
pixelX: number,
pixelY: number,
cellWidth: number = 80,
cellHeight: number = 80,
gap: number = 16
): { x: number; y: number } {
// Actual occupied width = cell width + gap
const effectiveWidth = cellWidth + gap
const effectiveHeight = cellHeight + gap

return {
x: Math.round(pixelX / effectiveWidth),
y: Math.round(pixelY / effectiveHeight),
}
}

Grid → Pixel (for rendering positioning)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Converts Grid coordinates to CSS pixel values
*
* @param gridX Grid X coordinate
* @param gridY Grid Y coordinate
* @param width Grid width (number of columns occupied)
* @param height Grid height (number of rows occupied)
* @returns CSS style object
*/
function gridToPixel(
gridX: number,
gridY: number,
width: number,
height: number,
cellWidth: number = 80,
cellHeight: number = 80,
gap: number = 16
): React.CSSProperties {
return {
left: `${gridX * (cellWidth + gap)}px`,
top: `${gridY * (cellHeight + gap)}px`,
width: `${width * cellWidth + (width - 1) * gap}px`,
height: `${height * cellHeight + (height - 1) * gap}px`,
}
}

// Usage example
const style = gridToPixel(x=2, y=1, width=6, height=2)
// → { left: '192px', top: '96px', width: '520px', height: '176px' }

3.3 Application in Vue Template

components/dashboard/WidgetWrapper.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<template>
<div
class="widget-wrapper"
:class="{ dragging: state.isDragging }"
:style="widgetStyle"
@mousedown="handleMouseDown"
>
<!-- Resize handle (bottom-right) -->
<div
v-if="!state.isDragging"
class="resize-handle"
@mousedown.stop="handleResizeStart"
>⋔</div>

<!-- Slot: actual Widget content -->
<slot />
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useDraggable } from '@/composables/useDraggable'

const props = defineProps<{
widgetId: number
x: number
y: number
width: number
height: number
}>()

const emit = defineEmits<{
(e: 'positionChange', x: number, y: number): void
(e: 'resize', width: number, height: number): void
}>()

// Use useDraggable composable
const { state, handleMouseDown } = useDraggable({
widgetId: props.widgetId,
initialX: props.x,
initialY: props.y,
width: props.width,
height: props.height,
gridSize: {
cellWidth: 80,
cellHeight: 80,
cols: 12,
rows: Infinity,
},
onPositionChange: async (newX, newY) => {
// Save to backend immediately
emit('positionChange', newX, newY)
await fetch(`/api/widgets/${props.widgetId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: newX, y: newY }),
})
},
})

// Dynamically compute CSS styles
const widgetStyle = computed(() => ({
position: 'absolute' as const,
left: `${state.currentGridX * 96}px`, // 80 + 16(gap)
top: `${state.currentGridY * 96}px`,
width: `${props.width * 80 + (props.width - 1) * 16}px`,
height: `${props.height * 80 + (props.height - 1) * 16}px`,
// 🔥 Key: use transform to trigger GPU acceleration
transform: state.isDragging
? `translate(${Math.random() * 0.01}px)` // Slight offset to force repaint
: undefined,
zIndex: state.isDragging ? 1000 : 1, // Elevate layer when dragging
transition: state.isDragging ? 'none' : 'all 0.3s ease', // Disable animation when dragging
}))
</script>

<style scoped>
.widget-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
cursor: grab;
user-select: none; /* Prevent text selection during drag */
}

.widget-wrapper.dragging {
cursor: grabbing;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
opacity: 0.9;
}

.resize-handle {
position: absolute;
right: 4px;
bottom: 4px;
width: 16px;
height: 16px;
cursor: nwse-resize;
opacity: 0.5;
font-size: 14px;
line-height: 16px;
text-align: center;
}

.resize-handle:hover {
opacity: 1;
}
</style>

💥 IV. Collision Detection Algorithm (AABB)

4.1 Why Collision Detection?

Problem without collision detection:

1
2
3
4
5
6
7
Before drag:                  After drag (incorrect):
┌─────────┐ ┌─────────┐
│ Widget A │ │Widget A │ ← Overlapped!
└─────────┘ ├─────────┤┌─────────┐
┌─────────┐ │Widget A ││Widget B │
│ Widget B │ └─────────┘└─────────┘
└─────────┘

4.2 AABB (Axis-Aligned Bounding Box) Algorithm

utils/collision.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
interface Rect {
x: number
y: number
width: number
height: number
}

/**
* AABB Collision Detection
*
* Principle: If two rectangles do not overlap on any axis, they do not intersect.
* Conversely, if they overlap on all axes, a collision occurs.
*
* Visualization:
* Non-collision cases (4 types):
* 1. A to the left of B 2. A to the right of B
* ┌──┐ ┌──┐
* │A │ │B │
* └──┘ ┌──┐ ┌──┐ └──┘
* │B │ │A │
* └──┘ └──┘
*
* 3. A above B 4. A below B
* ┌──┐
* │A │
* └──┘
* ┌──┐ ┌──┐
* │B │ │B │
* └──┘ └──┘
* ┌──┐
* │A │
* └──┘
*/
export function isColliding(a: Rect, b: Rect): boolean {
// If any of the following conditions are true, definitely no collision
const noCollision =
a.x + a.width <= b.x || // A's right ≤ B's left (A is left of B)
b.x + b.width <= a.x || // B's right ≤ A's left (A is right of B)
a.y + a.height <= b.y || // A's bottom ≤ B's top (A is above B)
b.y + b.height <= a.y // B's bottom ≤ A's top (A is below B)

// Invert: only if none of the above conditions are met, a collision has occurred
return !noCollision
}

/**
* Check if a Widget collides with any other Widget in a list
*/
export function checkCollisionWithOthers(
target: Rect,
others: Array<{ id: number } & Rect>
): number | null {
for (const other of others) {
if (isColliding(target, other)) {
return other.id // Return the colliding Widget's ID
}
}
return null // No collision
}

4.3 Integrating Collision Detection into Drag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Add to handleMouseMove in useDraggable.ts
function handleMouseMove(event: MouseEvent) {
if (!state.isDragging) return

const deltaX = event.clientX - state.startX
const deltaY = event.clientY - state.startY
const newCoords = pixelToGrid(deltaX, deltaY)

// Build target rectangle
const targetRect: Rect = {
x: newCoords.x,
y: newCoords.y,
width: options.width,
height: options.height,
}

// Get positions of all other widgets (from Pinia store or props)
const otherWidgets = dashboardStore.widgets
.filter(w => w.id !== widgetId)
.map(w => ({ id: w.id, x: w.x, y: w.y, width: w.width, height: w.height }))

// Collision detection
const collidingId = checkCollisionWithOthers(targetRect, otherWidgets)

if (collidingId) {
// Collision occurred, options:
// Option 1: Prevent movement (keep current position)
// Option 2: Swap positions (advanced)
// Option 3: Show warning but allow overlap (simple mode)

onCollision?.(collidingId)
return // Do not update coordinates
}

// No collision, update normally
state.currentGridX = newCoords.x
state.currentGridY = newCoords.y
}

↔️ V. Resize Functionality Implementation

5.1 Resize Handle Design

A special drag area appears in the bottom-right corner of each Widget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div class="resize-handle" @mousedown.stop="handleResizeStart">
⤡ <!-- Or use an SVG icon -->
</div>
</template>

<style scoped>
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: nwse-resize; /* Diagonal resize cursor */
opacity: 0;
transition: opacity 0.2s;
}

.widget-wrapper:hover .resize-handle {
opacity: 0.6;
}

.resize-handle:hover {
opacity: 1;
background: linear-gradient(
135deg,
transparent 50%,
#1890ff 50%
); /* Diagonal line visual effect */
}
</style>

5.2 Resize Logic Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function handleResizeStart(event: MouseEvent) {
event.preventDefault()
event.stopPropagation() // Prevent triggering parent drag

const startX = event.clientX
const startY = event.clientY
const startWidth = props.width
const startHeight = props.height

const handleResizeMove = (e: MouseEvent) => {
const deltaX = e.clientX - startX
const deltaY = e.clientY - startY

// Pixel → Grid units
const deltaGridX = Math.round(deltaX / 96) // cellWidth + gap
const deltaGridY = Math.round(deltaY / 96)

// Apply minimum size constraint
const newWidth = Math.max(2, startWidth + deltaGridX) // Minimum 2 columns
const newHeight = Math.max(1, startHeight + deltaGridY) // Minimum 1 row

// Apply maximum size constraint
const maxWidth = 12 - props.x // Cannot exceed right boundary
const clampedWidth = Math.min(newWidth, maxWidth)

emit('resize', clampedWidth, newHeight)
}

const handleResizeEnd = () => {
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)

// Save to backend
saveResizeToBackend()
}

document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', handleResizeEnd)
}

⚡ VI. Performance Optimization Strategies

6.1 GPU Acceleration (Key Optimization)

1
2
3
4
5
6
7
8
9
10
11
12
13
.widget-wrapper {
/* ❌ Causes full page reflow/repaint */
/* left: 100px; top: 200px; */

/* ✅ Use transform to trigger GPU compositing layer */
will-change: transform; /* Hint browser to optimize early */
transform: translate3d(0, 0, 0); /* Force GPU acceleration */
}

/* Disable transition animation while dragging (ensures responsiveness) */
.widget-wrapper.dragging {
transition: none !important;
}

Why is transform faster?

Property Operation Triggered Performance Impact
top/left Layout (reflow) + Paint (repaint) ⚠️ Slow
transform Composite (composition) ✅ Fast (GPU only)

6.2 Debounce and Throttle

1
2
3
4
5
6
import { throttle } from 'lodash-es'

// Use throttle to limit mousemove trigger frequency (~60fps is sufficient)
const throttledMouseMove = throttle(handleMouseMove, 16) // 16ms ≈ 60fps

document.addEventListener('mousemove', throttledMouseMove)

6.3 Virtual Scrolling for Long Lists

If the dashboard has many widgets (>20), consider virtual scrolling:

1
2
3
4
5
6
7
8
9
<!-- Only render widgets in the visible area -->
<RecycleScroller
:items="widgets"
:item-size="200"
key-field="id"
v-slot="{ item }"
>
<WidgetWrapper :widget="item" />
</RecycleScroller>

🎯 VII. Complete Usage Example

7.1 Using in Dashboard Page

views/dashboard/DashboardEditor.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<template>
<div class="dashboard-editor">
<!-- Toolbar -->
<div class="toolbar">
<button @click="addWidget">+ Add Widget</button>
<button @click="saveLayout">💾 Save Layout</button>
<span class="hint">Drag widgets to adjust position, resize from bottom-right</span>
</div>

<!-- Grid Canvas -->
<div
ref="canvasRef"
class="grid-canvas"
:style="{
gridTemplateColumns: `repeat(${layout.cols}, 1fr)`,
gridAutoRows: 'minmax(80px, auto)',
gap: '16px',
}"
>
<!-- Render all widgets -->
<WidgetWrapper
v-for="widget in widgets"
:key="widget.id"
:widget-id="widget.id"
:x="widget.x"
:y="widget.y"
:width="widget.width"
:height="widget.height"
@position-change="handlePositionChange"
@resize="handleResize"
>
<!-- Dynamic component -->
<component
:is="getComponentType(widget.type)"
:title="widget.title"
:data="widgetData[widget.id]"
/>
</WidgetWrapper>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import WidgetWrapper from '@/components/dashboard/WidgetWrapper.vue'
import KpiCard from '@/components/dashboard/KpiCard.vue'
import LineChart from '@/components/dashboard/LineChart.vue'

const canvasRef = ref<HTMLDivElement>()
const widgets = ref<any[]>([])
const widgetData = ref<Record<number, any>>({})

onMounted(async () => {
// Load Dashboard config from API
const res = await fetch(`/api/dashboards/${dashboardId}`)
const data = await res.json()
widgets.value = data.widgets

// Load data
await loadWidgetData()
})

async function handlePositionChange(widgetId: number, newX: number, newY: number) {
// Update local state (optimistic update)
const widget = widgets.value.find(w => w.id === widgetId)
if (widget) {
widget.x = newX
widget.y = newY
}

// Backend is already handled inside WidgetWrapper, additional logic can be added here
console.log(`Widget ${widgetId} moved to (${newX}, ${newY})`)
}

function getComponentType(type: string) {
const map: Record<string, any> = {
kpi_card: KpiCard,
line_chart: LineChart,
bar_chart: BarChart,
pie_chart: PieChart,
table: DataTable,
}
return map[type] || DataTable
}
</script>

<style scoped>
.grid-canvas {
display: grid;
min-height: 800px;
padding: 16px;
background-color: #f0f2f5;
background-image:
linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
background-size: 96px 96px; /* 80(cell) + 16(gap) */
border-radius: 8px;
position: relative;
}
</style>

🎯 VIII. Best Practices Summary

✅ What We Achieved

  1. Composable Architecture: Encapsulated drag logic in useDraggable(), highly reusable
  2. Document-Level Listening: Completely solved the classic issue of event loss when mouse leaves element
  3. Grid Snapping System: Automatically snaps to grid integers, ensuring clean and organized layout
  4. GPU Accelerated Rendering: Uses CSS transform to achieve 60fps smooth dragging
  5. AABB Collision Detection: Prevents components from overlapping, O(n) time complexity
  6. Instant Persistence: Saves to database immediately on mouseup, no loss on refresh
  7. Complete Resize Support: Bottom-right handle + minimum size constraints

📚 Best Practices Checklist

  • Use Composition API to encapsulate reusable drag logic
  • Bind event listeners to document instead of the element itself
  • All coordinate calculations based on Grid system (not absolute pixels)
  • Use transform instead of top/left to trigger GPU acceleration
  • Disable CSS transition while dragging to ensure responsiveness
  • Implement AABB collision detection algorithm
  • Call API immediately on mouseup to save new position
  • Clean up event listeners on component unmount (prevent memory leaks)
  • Set user-select: none to prevent text selection during drag
  • Set minimum size constraints for resizing (width ≥ 2, height ≥ 1)

🚀 Advanced Extension Directions

  1. Undo/Redo: Maintain an operation history stack, Ctrl+Z to undo
  2. Snap to Edge: Auto-align near edges of other widgets
  3. Keyboard Shortcuts: Arrow keys for fine-tuning position, Delete to remove components
  4. Multi-Select Drag: Ctrl+click to select multiple components, move them as a batch
  5. Responsive Adaptation: Dynamically adjust grid columns based on screen size (12 columns on desktop, 8 on tablet, 4 on phone)

Related Code Files:

The final article will dive into Production Deployment & Performance Optimization — asynchronous architecture design, caching strategies, monitoring & alerting, and other operational core content!

Stay tuned! 🚀