dnd-manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +769 -0
- package/dist/index.d.mts +131 -0
- package/dist/index.mjs +200 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Javi Aguilar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
# dnd-manager
|
|
2
|
+
|
|
3
|
+
Simple, performant, and data-driven Drag-and-Drop library for vanilla JS, and compatible with any framework.
|
|
4
|
+
|
|
5
|
+
This document provides practical examples of using `DragDropManager` in both vanilla
|
|
6
|
+
JavaScript and React applications.
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [Vanilla JavaScript Example](#vanilla-javascript-example)
|
|
11
|
+
2. [React Example](#react-example)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Vanilla JavaScript Example
|
|
16
|
+
|
|
17
|
+
This example creates a draggable grid with cells that can be reordered. Clicking a cell logs its
|
|
18
|
+
data to the console, and dragging shows a preview element following the cursor.
|
|
19
|
+
|
|
20
|
+
**HTML:**
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<!DOCTYPE html>
|
|
24
|
+
<html lang="en">
|
|
25
|
+
<head>
|
|
26
|
+
<meta charset="UTF-8" />
|
|
27
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
28
|
+
<title>Vanilla JS DragDrop Grid</title>
|
|
29
|
+
<style>
|
|
30
|
+
body {
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 20px;
|
|
33
|
+
font-family:
|
|
34
|
+
system-ui,
|
|
35
|
+
-apple-system,
|
|
36
|
+
sans-serif;
|
|
37
|
+
background: #f9fafb;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.grid {
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-columns: repeat(3, 120px);
|
|
43
|
+
gap: 8px;
|
|
44
|
+
padding: 20px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.cell {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
padding: 20px;
|
|
54
|
+
min-height: 80px;
|
|
55
|
+
transition:
|
|
56
|
+
transform 0.2s,
|
|
57
|
+
opacity 0.2s,
|
|
58
|
+
box-shadow 0.2s;
|
|
59
|
+
user-select: none;
|
|
60
|
+
touch-action: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.cell:not([data-empty]) {
|
|
64
|
+
color: white;
|
|
65
|
+
cursor: grab;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.cell[data-empty] {
|
|
69
|
+
background: #f3f4f6;
|
|
70
|
+
border: 2px dashed #d1d5db;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Dragging states */
|
|
74
|
+
.cell[data-dragging='true'] {
|
|
75
|
+
opacity: 0.4;
|
|
76
|
+
cursor: grabbing !important;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.cell[data-hovered='true'] {
|
|
80
|
+
transform: scale(1.05);
|
|
81
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Drag preview */
|
|
85
|
+
#drag-preview {
|
|
86
|
+
position: fixed;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
z-index: 9999;
|
|
89
|
+
opacity: 0;
|
|
90
|
+
transform: translate(-50%, -50%);
|
|
91
|
+
transition: opacity 0.2s;
|
|
92
|
+
padding: 20px;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
color: white;
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<h1>Vanilla JS Drag & Drop Grid</h1>
|
|
102
|
+
<p>Drag cells to reorder, click to log to console</p>
|
|
103
|
+
|
|
104
|
+
<div id="grid-container" class="grid">
|
|
105
|
+
<!-- Row 0 -->
|
|
106
|
+
<div
|
|
107
|
+
class="cell"
|
|
108
|
+
data-kind="cell"
|
|
109
|
+
data-row="0"
|
|
110
|
+
data-col="0"
|
|
111
|
+
data-item-id="1"
|
|
112
|
+
data-name="Item 1"
|
|
113
|
+
style="background: #ef4444"
|
|
114
|
+
>
|
|
115
|
+
Item 1
|
|
116
|
+
</div>
|
|
117
|
+
<div
|
|
118
|
+
class="cell"
|
|
119
|
+
data-kind="cell"
|
|
120
|
+
data-row="0"
|
|
121
|
+
data-col="1"
|
|
122
|
+
data-item-id="2"
|
|
123
|
+
data-name="Item 2"
|
|
124
|
+
style="background: #3b82f6"
|
|
125
|
+
>
|
|
126
|
+
Item 2
|
|
127
|
+
</div>
|
|
128
|
+
<div
|
|
129
|
+
class="cell"
|
|
130
|
+
data-kind="cell"
|
|
131
|
+
data-row="0"
|
|
132
|
+
data-col="2"
|
|
133
|
+
data-item-id="3"
|
|
134
|
+
data-name="Item 3"
|
|
135
|
+
style="background: #10b981"
|
|
136
|
+
>
|
|
137
|
+
Item 3
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Row 1 -->
|
|
141
|
+
<div
|
|
142
|
+
class="cell"
|
|
143
|
+
data-kind="cell"
|
|
144
|
+
data-row="1"
|
|
145
|
+
data-col="0"
|
|
146
|
+
data-item-id="4"
|
|
147
|
+
data-name="Item 4"
|
|
148
|
+
style="background: #f59e0b"
|
|
149
|
+
>
|
|
150
|
+
Item 4
|
|
151
|
+
</div>
|
|
152
|
+
<div class="cell" data-kind="cell" data-row="1" data-col="1" data-empty></div>
|
|
153
|
+
<div
|
|
154
|
+
class="cell"
|
|
155
|
+
data-kind="cell"
|
|
156
|
+
data-row="1"
|
|
157
|
+
data-col="2"
|
|
158
|
+
data-item-id="5"
|
|
159
|
+
data-name="Item 5"
|
|
160
|
+
style="background: #8b5cf6"
|
|
161
|
+
>
|
|
162
|
+
Item 5
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Row 2 -->
|
|
166
|
+
<div
|
|
167
|
+
class="cell"
|
|
168
|
+
data-kind="cell"
|
|
169
|
+
data-row="2"
|
|
170
|
+
data-col="0"
|
|
171
|
+
data-item-id="6"
|
|
172
|
+
data-name="Item 6"
|
|
173
|
+
style="background: #ec4899"
|
|
174
|
+
>
|
|
175
|
+
Item 6
|
|
176
|
+
</div>
|
|
177
|
+
<div
|
|
178
|
+
class="cell"
|
|
179
|
+
data-kind="cell"
|
|
180
|
+
data-row="2"
|
|
181
|
+
data-col="1"
|
|
182
|
+
data-item-id="7"
|
|
183
|
+
data-name="Item 7"
|
|
184
|
+
style="background: #14b8a6"
|
|
185
|
+
>
|
|
186
|
+
Item 7
|
|
187
|
+
</div>
|
|
188
|
+
<div class="cell" data-kind="cell" data-row="2" data-col="2" data-empty></div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Drag preview element -->
|
|
192
|
+
<div id="drag-preview"></div>
|
|
193
|
+
|
|
194
|
+
<script type="module" src="./app.ts"></script>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**TypeScript (app.ts):**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { DragDropManager, type DragDropCallbacks } from './drag-drop-manager'
|
|
203
|
+
|
|
204
|
+
// Types
|
|
205
|
+
type GridItem = {
|
|
206
|
+
id: string
|
|
207
|
+
name: string
|
|
208
|
+
color: string
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type GridPosition = {
|
|
212
|
+
row: number
|
|
213
|
+
col: number
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Initialize
|
|
217
|
+
function initDragDropGrid(): void {
|
|
218
|
+
const container = document.getElementById('grid-container')
|
|
219
|
+
const preview = document.getElementById('drag-preview')
|
|
220
|
+
|
|
221
|
+
if (!container || !preview) return
|
|
222
|
+
|
|
223
|
+
// Setup DragDropManager callbacks
|
|
224
|
+
const callbacks: DragDropCallbacks<GridItem, GridPosition> = {
|
|
225
|
+
canDrag: (element, position) => {
|
|
226
|
+
// Can only drag non-empty cells
|
|
227
|
+
return element.dataset.empty === undefined
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
getItemPosition: (element, kind) => {
|
|
231
|
+
const row = element.dataset.row
|
|
232
|
+
const col = element.dataset.col
|
|
233
|
+
if (row === undefined || col === undefined) return null
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
row: parseInt(row, 10),
|
|
237
|
+
col: parseInt(col, 10),
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
getItemData: (element, position) => {
|
|
242
|
+
const itemId = element.dataset.itemId
|
|
243
|
+
const name = element.dataset.name
|
|
244
|
+
const color = element.style.background
|
|
245
|
+
|
|
246
|
+
if (!itemId || !name || !color) return null
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
id: itemId,
|
|
250
|
+
name: name,
|
|
251
|
+
color: color,
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
onDragStart: (element, position, item) => {
|
|
256
|
+
console.log('🚀 Drag started:', item)
|
|
257
|
+
|
|
258
|
+
// Copy element dimensions and styles to preview
|
|
259
|
+
const rect = element.getBoundingClientRect()
|
|
260
|
+
preview.textContent = item.name
|
|
261
|
+
preview.style.background = item.color
|
|
262
|
+
preview.style.width = `${rect.width}px`
|
|
263
|
+
preview.style.height = `${rect.height}px`
|
|
264
|
+
preview.style.opacity = '0.9'
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
onDragMove: (pos, hoveredElement) => {
|
|
268
|
+
// Update preview position
|
|
269
|
+
preview.style.left = `${pos.x}px`
|
|
270
|
+
preview.style.top = `${pos.y}px`
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
onDrop: (sourcePos, targetPos, sourceItem) => {
|
|
274
|
+
console.log('📦 Dropped:', {
|
|
275
|
+
from: sourcePos,
|
|
276
|
+
to: targetPos,
|
|
277
|
+
item: sourceItem,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// Get source and target elements
|
|
281
|
+
const sourceElement = container.querySelector(
|
|
282
|
+
`[data-row="${sourcePos.row}"][data-col="${sourcePos.col}"]`,
|
|
283
|
+
) as HTMLElement
|
|
284
|
+
const targetElement = container.querySelector(
|
|
285
|
+
`[data-row="${targetPos.row}"][data-col="${targetPos.col}"]`,
|
|
286
|
+
) as HTMLElement
|
|
287
|
+
|
|
288
|
+
if (!sourceElement || !targetElement) return
|
|
289
|
+
|
|
290
|
+
// Swap element contents and data attributes
|
|
291
|
+
const tempContent = sourceElement.innerHTML
|
|
292
|
+
const tempId = sourceElement.dataset.itemId
|
|
293
|
+
const tempName = sourceElement.dataset.name
|
|
294
|
+
const tempColor = sourceElement.style.background
|
|
295
|
+
const tempEmpty = sourceElement.dataset.empty
|
|
296
|
+
|
|
297
|
+
// Update source with target data
|
|
298
|
+
sourceElement.innerHTML = targetElement.innerHTML
|
|
299
|
+
sourceElement.dataset.itemId = targetElement.dataset.itemId || ''
|
|
300
|
+
sourceElement.dataset.name = targetElement.dataset.name || ''
|
|
301
|
+
sourceElement.style.background = targetElement.style.background
|
|
302
|
+
if (targetElement.dataset.empty !== undefined) {
|
|
303
|
+
sourceElement.dataset.empty = ''
|
|
304
|
+
} else {
|
|
305
|
+
delete sourceElement.dataset.empty
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Update target with source data
|
|
309
|
+
targetElement.innerHTML = tempContent
|
|
310
|
+
targetElement.dataset.itemId = tempId || ''
|
|
311
|
+
targetElement.dataset.name = tempName || ''
|
|
312
|
+
targetElement.style.background = tempColor
|
|
313
|
+
if (tempEmpty !== undefined) {
|
|
314
|
+
targetElement.dataset.empty = ''
|
|
315
|
+
} else {
|
|
316
|
+
delete targetElement.dataset.empty
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
onDragEnd: () => {
|
|
321
|
+
// Hide preview
|
|
322
|
+
preview.style.opacity = '0'
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
onClick: (element, position) => {
|
|
326
|
+
const itemId = element.dataset.itemId
|
|
327
|
+
const name = element.dataset.name
|
|
328
|
+
const color = element.style.background
|
|
329
|
+
|
|
330
|
+
if (itemId && name) {
|
|
331
|
+
console.log('👆 Clicked:', { id: itemId, name, color })
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create DragDropManager instance
|
|
337
|
+
const dragDropManager = new DragDropManager<GridItem, GridPosition>(
|
|
338
|
+
container,
|
|
339
|
+
{
|
|
340
|
+
draggableKind: 'cell',
|
|
341
|
+
droppableKind: 'cell',
|
|
342
|
+
dragThreshold: 10,
|
|
343
|
+
clickThreshold: 10,
|
|
344
|
+
scrollThreshold: 100,
|
|
345
|
+
scrollSpeed: 10,
|
|
346
|
+
},
|
|
347
|
+
callbacks,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
// Cleanup on page unload
|
|
351
|
+
window.addEventListener('beforeunload', () => {
|
|
352
|
+
dragDropManager.destroy()
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Start when DOM is ready
|
|
357
|
+
document.addEventListener('DOMContentLoaded', initDragDropGrid)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## React Example
|
|
363
|
+
|
|
364
|
+
This example shows the same draggable grid implemented as a React component with TypeScript.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { useEffect, useRef, useState } from 'react'
|
|
368
|
+
import { DragDropManager, type DragDropCallbacks } from './drag-drop-manager'
|
|
369
|
+
|
|
370
|
+
// Types
|
|
371
|
+
type GridItem = {
|
|
372
|
+
id: string
|
|
373
|
+
name: string
|
|
374
|
+
color: string
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
type GridPosition = {
|
|
378
|
+
row: number
|
|
379
|
+
col: number
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Initial data
|
|
383
|
+
const INITIAL_DATA: (GridItem | null)[][] = [
|
|
384
|
+
[
|
|
385
|
+
{ id: '1', name: 'Item 1', color: '#ef4444' },
|
|
386
|
+
{ id: '2', name: 'Item 2', color: '#3b82f6' },
|
|
387
|
+
{ id: '3', name: 'Item 3', color: '#10b981' },
|
|
388
|
+
],
|
|
389
|
+
[
|
|
390
|
+
{ id: '4', name: 'Item 4', color: '#f59e0b' },
|
|
391
|
+
null,
|
|
392
|
+
{ id: '5', name: 'Item 5', color: '#8b5cf6' },
|
|
393
|
+
],
|
|
394
|
+
[
|
|
395
|
+
{ id: '6', name: 'Item 6', color: '#ec4899' },
|
|
396
|
+
{ id: '7', name: 'Item 7', color: '#14b8a6' },
|
|
397
|
+
null,
|
|
398
|
+
],
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
// Cell component
|
|
402
|
+
type CellProps = {
|
|
403
|
+
item: GridItem | null
|
|
404
|
+
row: number
|
|
405
|
+
col: number
|
|
406
|
+
} & React.ComponentProps<'div'>
|
|
407
|
+
|
|
408
|
+
function GridCell({ item, row, col, ...props }: CellProps) {
|
|
409
|
+
if (item) {
|
|
410
|
+
return (
|
|
411
|
+
<div
|
|
412
|
+
data-kind="cell"
|
|
413
|
+
data-row={row}
|
|
414
|
+
data-col={col}
|
|
415
|
+
data-item-id={item.id}
|
|
416
|
+
className="grid-cell grid-cell--filled"
|
|
417
|
+
style={{
|
|
418
|
+
background: item.color,
|
|
419
|
+
}}
|
|
420
|
+
{...props}
|
|
421
|
+
>
|
|
422
|
+
{item.name}
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div
|
|
429
|
+
data-kind="cell"
|
|
430
|
+
data-row={row}
|
|
431
|
+
data-col={col}
|
|
432
|
+
data-empty="true"
|
|
433
|
+
className="grid-cell grid-cell--empty"
|
|
434
|
+
{...props}
|
|
435
|
+
/>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Drag preview component
|
|
440
|
+
type DragPreviewProps = {
|
|
441
|
+
item: GridItem | null
|
|
442
|
+
position: { x: number; y: number } | null
|
|
443
|
+
width?: number
|
|
444
|
+
height?: number
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function DragPreview({ item, position, width, height }: DragPreviewProps) {
|
|
448
|
+
if (!item || !position) return null
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div
|
|
452
|
+
className="drag-preview"
|
|
453
|
+
style={{
|
|
454
|
+
left: position.x,
|
|
455
|
+
top: position.y,
|
|
456
|
+
background: item.color,
|
|
457
|
+
width: width ? `${width}px` : undefined,
|
|
458
|
+
height: height ? `${height}px` : undefined,
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
{item.name}
|
|
462
|
+
</div>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Main component
|
|
467
|
+
export function DraggableGrid() {
|
|
468
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
469
|
+
const [gridData, setGridData] = useState<(GridItem | null)[][]>(INITIAL_DATA)
|
|
470
|
+
const [dragPreview, setDragPreview] = useState<{
|
|
471
|
+
item: GridItem | null
|
|
472
|
+
position: { x: number; y: number } | null
|
|
473
|
+
width?: number
|
|
474
|
+
height?: number
|
|
475
|
+
}>({ item: null, position: null })
|
|
476
|
+
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
if (!containerRef.current) return
|
|
479
|
+
|
|
480
|
+
// Setup callbacks
|
|
481
|
+
const callbacks: DragDropCallbacks<GridItem, GridPosition> = {
|
|
482
|
+
canDrag: (element, position) => {
|
|
483
|
+
return element.dataset.empty === undefined
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
getItemPosition: (element, kind) => {
|
|
487
|
+
const row = element.dataset.row
|
|
488
|
+
const col = element.dataset.col
|
|
489
|
+
if (row === undefined || col === undefined) return null
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
row: parseInt(row, 10),
|
|
493
|
+
col: parseInt(col, 10),
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
getItemData: (element, position) => {
|
|
498
|
+
return gridData[position.row][position.col]
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
onDragStart: (element, position, item) => {
|
|
502
|
+
console.log('🚀 Drag started:', item)
|
|
503
|
+
|
|
504
|
+
// Store element dimensions for preview
|
|
505
|
+
const rect = element.getBoundingClientRect()
|
|
506
|
+
setDragPreview({
|
|
507
|
+
item,
|
|
508
|
+
position: null,
|
|
509
|
+
width: rect.width,
|
|
510
|
+
height: rect.height,
|
|
511
|
+
})
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
onDragMove: (pos, hoveredElement) => {
|
|
515
|
+
setDragPreview((prev) => ({
|
|
516
|
+
...prev,
|
|
517
|
+
position: pos,
|
|
518
|
+
}))
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
onDrop: (sourcePos, targetPos, sourceItem) => {
|
|
522
|
+
console.log('📦 Dropped:', {
|
|
523
|
+
from: sourcePos,
|
|
524
|
+
to: targetPos,
|
|
525
|
+
item: sourceItem,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// Swap items
|
|
529
|
+
setGridData((prevData) => {
|
|
530
|
+
const newData = prevData.map((row) => [...row])
|
|
531
|
+
const targetItem = newData[targetPos.row][targetPos.col]
|
|
532
|
+
newData[targetPos.row][targetPos.col] = sourceItem
|
|
533
|
+
newData[sourcePos.row][sourcePos.col] = targetItem
|
|
534
|
+
return newData
|
|
535
|
+
})
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
onDragEnd: () => {
|
|
539
|
+
setDragPreview({ item: null, position: null, width: undefined, height: undefined })
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
onClick: (element, position) => {
|
|
543
|
+
const item = gridData[position.row][position.col]
|
|
544
|
+
if (item) {
|
|
545
|
+
console.log('👆 Clicked:', item)
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Create manager
|
|
551
|
+
const manager = new DragDropManager<GridItem, GridPosition>(
|
|
552
|
+
containerRef,
|
|
553
|
+
{
|
|
554
|
+
draggableKind: 'cell',
|
|
555
|
+
droppableKind: 'cell',
|
|
556
|
+
dragThreshold: 10,
|
|
557
|
+
clickThreshold: 10,
|
|
558
|
+
scrollThreshold: 100,
|
|
559
|
+
scrollSpeed: 10,
|
|
560
|
+
},
|
|
561
|
+
callbacks,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
// Cleanup
|
|
565
|
+
return () => {
|
|
566
|
+
manager.destroy()
|
|
567
|
+
}
|
|
568
|
+
}, [gridData])
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<div className="draggable-grid-container">
|
|
572
|
+
<h1>React Drag & Drop Grid</h1>
|
|
573
|
+
<p>Drag cells to reorder, click to log to console</p>
|
|
574
|
+
|
|
575
|
+
<div ref={containerRef} className="grid">
|
|
576
|
+
{gridData.map((row, rowIndex) =>
|
|
577
|
+
row.map((item, colIndex) => (
|
|
578
|
+
<GridCell key={`${rowIndex}-${colIndex}`} item={item} row={rowIndex} col={colIndex} />
|
|
579
|
+
)),
|
|
580
|
+
)}
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<DragPreview
|
|
584
|
+
item={dragPreview.item}
|
|
585
|
+
position={dragPreview.position}
|
|
586
|
+
width={dragPreview.width}
|
|
587
|
+
height={dragPreview.height}
|
|
588
|
+
/>
|
|
589
|
+
</div>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**CSS (Tailwind or plain CSS):**
|
|
595
|
+
|
|
596
|
+
```css
|
|
597
|
+
/* Container */
|
|
598
|
+
.draggable-grid-container {
|
|
599
|
+
padding: 20px;
|
|
600
|
+
font-family:
|
|
601
|
+
system-ui,
|
|
602
|
+
-apple-system,
|
|
603
|
+
sans-serif;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Grid */
|
|
607
|
+
.grid {
|
|
608
|
+
display: grid;
|
|
609
|
+
grid-template-columns: repeat(3, 120px);
|
|
610
|
+
gap: 8px;
|
|
611
|
+
padding: 20px;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* Grid cells */
|
|
615
|
+
.grid-cell {
|
|
616
|
+
display: flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
justify-content: center;
|
|
619
|
+
font-weight: 600;
|
|
620
|
+
border-radius: 8px;
|
|
621
|
+
transition:
|
|
622
|
+
transform 0.2s,
|
|
623
|
+
opacity 0.2s,
|
|
624
|
+
box-shadow 0.2s;
|
|
625
|
+
user-select: none;
|
|
626
|
+
touch-action: none;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.grid-cell--filled {
|
|
630
|
+
padding: 20px;
|
|
631
|
+
color: white;
|
|
632
|
+
cursor: grab;
|
|
633
|
+
min-height: 80px;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.grid-cell--empty {
|
|
637
|
+
background: #f3f4f6;
|
|
638
|
+
border: 2px dashed #d1d5db;
|
|
639
|
+
min-height: 80px;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* Dragging state */
|
|
643
|
+
.grid-cell[data-dragging='true'] {
|
|
644
|
+
opacity: 0.4;
|
|
645
|
+
cursor: grabbing !important;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* Hover state */
|
|
649
|
+
.grid-cell[data-hovered='true'] {
|
|
650
|
+
transform: scale(1.05);
|
|
651
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* Drag preview */
|
|
655
|
+
.drag-preview {
|
|
656
|
+
position: fixed;
|
|
657
|
+
pointer-events: none;
|
|
658
|
+
z-index: 9999;
|
|
659
|
+
transform: translate(-50%, -50%);
|
|
660
|
+
padding: 20px;
|
|
661
|
+
border-radius: 8px;
|
|
662
|
+
color: white;
|
|
663
|
+
font-weight: 600;
|
|
664
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
665
|
+
opacity: 0.9;
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**With Tailwind CSS:**
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
// Update cell component to use Tailwind classes
|
|
673
|
+
function GridCell({ item, row, col, ...props }: CellProps) {
|
|
674
|
+
if (item) {
|
|
675
|
+
return (
|
|
676
|
+
<div
|
|
677
|
+
data-kind="cell"
|
|
678
|
+
data-row={row}
|
|
679
|
+
data-col={col}
|
|
680
|
+
data-item-id={item.id}
|
|
681
|
+
className="flex items-center justify-center p-5 rounded-lg cursor-grab
|
|
682
|
+
font-semibold text-white transition-all duration-200
|
|
683
|
+
data-[dragging=true]:opacity-40 data-[dragging=true]:cursor-grabbing
|
|
684
|
+
data-[hovered=true]:scale-105 data-[hovered=true]:ring-4
|
|
685
|
+
data-[hovered=true]:ring-blue-500/50"
|
|
686
|
+
style={{ background: item.color }}
|
|
687
|
+
{...props}
|
|
688
|
+
>
|
|
689
|
+
{item.name}
|
|
690
|
+
</div>
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<div
|
|
696
|
+
data-kind="cell"
|
|
697
|
+
data-row={row}
|
|
698
|
+
data-col={col}
|
|
699
|
+
data-empty="true"
|
|
700
|
+
className="bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg min-h-[80px]"
|
|
701
|
+
{...props}
|
|
702
|
+
/>
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Drag preview with Tailwind
|
|
707
|
+
function DragPreview({ item, position, width, height }: DragPreviewProps) {
|
|
708
|
+
if (!item || !position) return null
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
<div
|
|
712
|
+
className="fixed pointer-events-none z-[9999] -translate-x-1/2 -translate-y-1/2
|
|
713
|
+
p-5 rounded-lg text-white font-semibold shadow-2xl opacity-90"
|
|
714
|
+
style={{
|
|
715
|
+
left: position.x,
|
|
716
|
+
top: position.y,
|
|
717
|
+
background: item.color,
|
|
718
|
+
width: width ? `${width}px` : undefined,
|
|
719
|
+
height: height ? `${height}px` : undefined,
|
|
720
|
+
}}
|
|
721
|
+
>
|
|
722
|
+
{item.name}
|
|
723
|
+
</div>
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Covered Features
|
|
731
|
+
|
|
732
|
+
### Both Examples Include:
|
|
733
|
+
|
|
734
|
+
1. **Grid Layout** - 3x3 grid with draggable cells
|
|
735
|
+
2. **Click Detection** - Logs item data to console on click
|
|
736
|
+
3. **Drag Preview** - Visual feedback following cursor during drag, matching original element size
|
|
737
|
+
4. **Swap Behavior** - Items swap positions on drop
|
|
738
|
+
5. **Empty Cells** - Non-draggable placeholder cells
|
|
739
|
+
6. **Visual Feedback** - Opacity changes, hover states, and scaling
|
|
740
|
+
7. **TypeScript Types** - Full type safety with generics
|
|
741
|
+
|
|
742
|
+
### DragDropManager Features Used:
|
|
743
|
+
|
|
744
|
+
- `canDrag` - Prevents dragging empty cells
|
|
745
|
+
- `getItemPosition` - Extracts grid coordinates from data attributes
|
|
746
|
+
- `getItemData` - Retrieves item data from grid state
|
|
747
|
+
- `onDragStart` - Shows preview and updates source appearance
|
|
748
|
+
- Uses `getBoundingClientRect()` to copy element dimensions to preview
|
|
749
|
+
- `onDragMove` - Updates preview position in real-time
|
|
750
|
+
- `onDrop` - Swaps items and updates state
|
|
751
|
+
- `onDragEnd` - Hides preview
|
|
752
|
+
- `onClick` - Handles click/tap events
|
|
753
|
+
|
|
754
|
+
### Configuration:
|
|
755
|
+
|
|
756
|
+
- `dragThreshold: 10` - Prevents accidental drags
|
|
757
|
+
- `clickThreshold: 10` - Distinguishes clicks from drags
|
|
758
|
+
- `scrollThreshold: 100` - Auto-scroll near viewport edges
|
|
759
|
+
- `scrollSpeed: 10` - Smooth scroll speed
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## Notes
|
|
764
|
+
|
|
765
|
+
- **Performance**: The manager uses `requestAnimationFrame` for smooth updates at 60fps
|
|
766
|
+
- **Mobile-Friendly**: Uses pointer events which work with touch, mouse, and pen
|
|
767
|
+
- **Accessibility**: Uses data attributes that don't interfere with screen readers
|
|
768
|
+
- **React Integration**: Manager is created in `useEffect` with proper cleanup
|
|
769
|
+
- **State Management**: React example uses `useState` to trigger re-renders after drops
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//#region src/drag-drop-manager.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Framework-agnostic Drag and Drop Manager
|
|
4
|
+
*
|
|
5
|
+
* A data-driven drag-and-drop utility that works with React and vanilla JS.
|
|
6
|
+
* Uses data attributes and direct DOM manipulation for optimal performance.
|
|
7
|
+
*
|
|
8
|
+
* @template TItem - The type of data associated with draggable items
|
|
9
|
+
* @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)
|
|
10
|
+
*/
|
|
11
|
+
type DragDropConfig = {
|
|
12
|
+
/**
|
|
13
|
+
* Minimum distance in pixels the pointer must move before a drag operation starts.
|
|
14
|
+
* Prevents accidental drags from small movements and allows normal scrolling on mobile.
|
|
15
|
+
* Checked during `pointermove` before drag has started.
|
|
16
|
+
* @default 10
|
|
17
|
+
* @example
|
|
18
|
+
* dragThreshold: 15 // Harder to start drag (better for mobile scroll)
|
|
19
|
+
*/
|
|
20
|
+
dragThreshold?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum distance in pixels between pointerdown and pointerup to treat as a click/tap.
|
|
23
|
+
* Used to distinguish intentional clicks from cancelled drag attempts.
|
|
24
|
+
* Only checked on `pointerup` if drag never started.
|
|
25
|
+
* @default 10
|
|
26
|
+
* @example
|
|
27
|
+
* clickThreshold: 5 // Stricter click detection
|
|
28
|
+
* clickThreshold: 15 // More forgiving for mobile taps
|
|
29
|
+
*/
|
|
30
|
+
clickThreshold?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Distance in pixels from viewport edge that triggers auto-scrolling during drag.
|
|
33
|
+
* When dragging near screen edges, the window automatically scrolls.
|
|
34
|
+
* @default 100
|
|
35
|
+
* @example
|
|
36
|
+
* scrollThreshold: 50 // Start scrolling closer to edges
|
|
37
|
+
* scrollThreshold: 150 // Larger safe zone before scrolling
|
|
38
|
+
*/
|
|
39
|
+
scrollThreshold?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Scroll speed in pixels per animation frame during auto-scroll.
|
|
42
|
+
* Higher values = faster scrolling when dragging near viewport edges.
|
|
43
|
+
* @default 10
|
|
44
|
+
* @example
|
|
45
|
+
* scrollSpeed: 5 // Slower, smoother scroll
|
|
46
|
+
* scrollSpeed: 20 // Faster scroll
|
|
47
|
+
*/
|
|
48
|
+
scrollSpeed?: number;
|
|
49
|
+
/**
|
|
50
|
+
* The `data-kind` attribute value for draggable elements.
|
|
51
|
+
* Elements with this attribute can be picked up and dragged.
|
|
52
|
+
* @example
|
|
53
|
+
* draggableKind: 'cell' // <div data-kind="cell">
|
|
54
|
+
* draggableKind: 'card' // <div data-kind="card">
|
|
55
|
+
* draggableKind: 'list-item' // <div data-kind="list-item">
|
|
56
|
+
*/
|
|
57
|
+
draggableKind: string;
|
|
58
|
+
/**
|
|
59
|
+
* The `data-kind` attribute value for droppable target elements.
|
|
60
|
+
* Elements with this attribute can receive dropped items.
|
|
61
|
+
* Often the same as `draggableKind` for reorderable lists.
|
|
62
|
+
* @example
|
|
63
|
+
* droppableKind: 'cell' // Can drop on cells
|
|
64
|
+
* droppableKind: 'zone' // Can drop on zones
|
|
65
|
+
*/
|
|
66
|
+
droppableKind: string;
|
|
67
|
+
};
|
|
68
|
+
type PointerPosition = {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
};
|
|
72
|
+
type DragDropCallbacks<TItem, TPosition> = {
|
|
73
|
+
/** Called when determining if an element can be dragged */canDrag?: (element: HTMLElement, position: TPosition) => boolean; /** Called when drag starts (after threshold is met) */
|
|
74
|
+
onDragStart?: (element: HTMLElement, position: TPosition, item: TItem) => void; /** Called continuously during drag */
|
|
75
|
+
onDragMove?: (pos: PointerPosition, hoveredElement: HTMLElement | null) => void; /** Called when item is dropped on a valid target */
|
|
76
|
+
onDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => void; /** Called when drag ends (regardless of success) */
|
|
77
|
+
onDragEnd?: () => void; /** Called on simple click/tap (no drag) */
|
|
78
|
+
onClick?: (element: HTMLElement, position: TPosition) => void; /** Called to extract item data from an element */
|
|
79
|
+
getItemData?: (element: HTMLElement, position: TPosition) => TItem | null; /** Called to get the custom position/coordinates of an element */
|
|
80
|
+
getItemPosition: (element: HTMLElement, kind: string) => TPosition | null;
|
|
81
|
+
};
|
|
82
|
+
type DragState<TItem, TPosition> = {
|
|
83
|
+
startPosition: PointerPosition | null;
|
|
84
|
+
sourceElement: HTMLElement | null;
|
|
85
|
+
sourcePosition: TPosition | null;
|
|
86
|
+
sourceItem: TItem | null;
|
|
87
|
+
isDragging: boolean;
|
|
88
|
+
lastHoveredElement: HTMLElement | null;
|
|
89
|
+
};
|
|
90
|
+
declare class DragDropManager<TItem = unknown, TPosition = unknown> {
|
|
91
|
+
private container;
|
|
92
|
+
private config;
|
|
93
|
+
private callbacks;
|
|
94
|
+
private state;
|
|
95
|
+
private rafId;
|
|
96
|
+
private lastPointerPos;
|
|
97
|
+
private boundHandlers;
|
|
98
|
+
constructor(containerRef: {
|
|
99
|
+
current: HTMLElement | null;
|
|
100
|
+
} | string | HTMLElement, config: DragDropConfig, callbacks: DragDropCallbacks<TItem, TPosition>);
|
|
101
|
+
private attachListeners;
|
|
102
|
+
private handlePointerDown;
|
|
103
|
+
private handlePointerMove;
|
|
104
|
+
private handlePointerUp;
|
|
105
|
+
private handlePointerCancel;
|
|
106
|
+
private handleAutoScroll;
|
|
107
|
+
private cleanup;
|
|
108
|
+
/**
|
|
109
|
+
* Find closest ancestor element with matching data-kind attribute
|
|
110
|
+
*/
|
|
111
|
+
private findElementByKind;
|
|
112
|
+
/**
|
|
113
|
+
* Calculate distance between two points
|
|
114
|
+
*/
|
|
115
|
+
private getDistance;
|
|
116
|
+
/**
|
|
117
|
+
* Get current drag state (read-only)
|
|
118
|
+
*/
|
|
119
|
+
getState(): Readonly<DragState<TItem, TPosition>>;
|
|
120
|
+
/**
|
|
121
|
+
* Check if currently dragging
|
|
122
|
+
*/
|
|
123
|
+
isDragging(): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Cleanup and remove event listeners
|
|
126
|
+
*/
|
|
127
|
+
destroy(): void;
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { type DragDropCallbacks, type DragDropConfig, DragDropManager, type PointerPosition };
|
|
131
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
//#region src/drag-drop-manager.ts
|
|
2
|
+
var DragDropManager = class {
|
|
3
|
+
container;
|
|
4
|
+
config;
|
|
5
|
+
callbacks;
|
|
6
|
+
state = {
|
|
7
|
+
startPosition: null,
|
|
8
|
+
sourceElement: null,
|
|
9
|
+
sourcePosition: null,
|
|
10
|
+
sourceItem: null,
|
|
11
|
+
isDragging: false,
|
|
12
|
+
lastHoveredElement: null
|
|
13
|
+
};
|
|
14
|
+
rafId = null;
|
|
15
|
+
lastPointerPos = null;
|
|
16
|
+
boundHandlers;
|
|
17
|
+
constructor(containerRef, config, callbacks) {
|
|
18
|
+
if (typeof containerRef === "string") {
|
|
19
|
+
const element = document.querySelector(containerRef);
|
|
20
|
+
if (!element) throw new Error(`DragDropManager: Could not find element with selector "${containerRef}"`);
|
|
21
|
+
this.container = element;
|
|
22
|
+
} else if (typeof containerRef === "object" && "current" in containerRef) {
|
|
23
|
+
if (!containerRef.current) throw new Error("DragDropManager: Ref.current is null");
|
|
24
|
+
this.container = containerRef.current;
|
|
25
|
+
} else this.container = containerRef;
|
|
26
|
+
this.config = {
|
|
27
|
+
dragThreshold: config.dragThreshold ?? 10,
|
|
28
|
+
clickThreshold: config.clickThreshold ?? 10,
|
|
29
|
+
scrollThreshold: config.scrollThreshold ?? 100,
|
|
30
|
+
scrollSpeed: config.scrollSpeed ?? 10,
|
|
31
|
+
draggableKind: config.draggableKind,
|
|
32
|
+
droppableKind: config.droppableKind
|
|
33
|
+
};
|
|
34
|
+
this.callbacks = callbacks;
|
|
35
|
+
this.boundHandlers = {
|
|
36
|
+
pointerDown: this.handlePointerDown.bind(this),
|
|
37
|
+
pointerMove: this.handlePointerMove.bind(this),
|
|
38
|
+
pointerUp: this.handlePointerUp.bind(this),
|
|
39
|
+
pointerCancel: this.handlePointerCancel.bind(this)
|
|
40
|
+
};
|
|
41
|
+
this.attachListeners();
|
|
42
|
+
}
|
|
43
|
+
attachListeners() {
|
|
44
|
+
this.container.addEventListener("pointerdown", this.boundHandlers.pointerDown);
|
|
45
|
+
this.container.addEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
46
|
+
this.container.addEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
47
|
+
this.container.addEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
48
|
+
}
|
|
49
|
+
handlePointerDown(e) {
|
|
50
|
+
const target = e.target;
|
|
51
|
+
const draggableElement = this.findElementByKind(target, this.config.draggableKind);
|
|
52
|
+
if (!draggableElement) return;
|
|
53
|
+
const position = this.callbacks.getItemPosition(draggableElement, this.config.draggableKind);
|
|
54
|
+
if (!position) return;
|
|
55
|
+
if (draggableElement.dataset.empty !== void 0) return;
|
|
56
|
+
if (this.callbacks.canDrag && !this.callbacks.canDrag(draggableElement, position)) return;
|
|
57
|
+
const item = this.callbacks.getItemData?.(draggableElement, position) ?? null;
|
|
58
|
+
if (!item) return;
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
this.state.startPosition = {
|
|
61
|
+
x: e.clientX,
|
|
62
|
+
y: e.clientY
|
|
63
|
+
};
|
|
64
|
+
this.state.sourceElement = draggableElement;
|
|
65
|
+
this.state.sourcePosition = position;
|
|
66
|
+
this.state.sourceItem = item;
|
|
67
|
+
}
|
|
68
|
+
handlePointerMove(e) {
|
|
69
|
+
const currentPos = {
|
|
70
|
+
x: e.clientX,
|
|
71
|
+
y: e.clientY
|
|
72
|
+
};
|
|
73
|
+
if (this.state.startPosition && !this.state.isDragging) {
|
|
74
|
+
if (this.getDistance(this.state.startPosition, currentPos) >= this.config.dragThreshold) {
|
|
75
|
+
this.state.isDragging = true;
|
|
76
|
+
if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {
|
|
77
|
+
this.state.sourceElement.setAttribute("data-dragging", "true");
|
|
78
|
+
this.callbacks.onDragStart?.(this.state.sourceElement, this.state.sourcePosition, this.state.sourceItem);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!this.state.isDragging) return;
|
|
84
|
+
this.lastPointerPos = currentPos;
|
|
85
|
+
if (this.rafId === null) this.rafId = requestAnimationFrame(() => {
|
|
86
|
+
this.rafId = null;
|
|
87
|
+
if (!this.lastPointerPos) return;
|
|
88
|
+
const pos = this.lastPointerPos;
|
|
89
|
+
const elementUnderCursor = document.elementFromPoint(pos.x, pos.y);
|
|
90
|
+
const droppableElement = this.findElementByKind(elementUnderCursor, this.config.droppableKind);
|
|
91
|
+
if (droppableElement !== this.state.lastHoveredElement) {
|
|
92
|
+
if (this.state.lastHoveredElement) this.state.lastHoveredElement.removeAttribute("data-hovered");
|
|
93
|
+
if (droppableElement) {
|
|
94
|
+
droppableElement.setAttribute("data-hovered", "true");
|
|
95
|
+
this.state.lastHoveredElement = droppableElement;
|
|
96
|
+
} else this.state.lastHoveredElement = null;
|
|
97
|
+
}
|
|
98
|
+
this.handleAutoScroll(pos);
|
|
99
|
+
this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
handlePointerUp(e) {
|
|
103
|
+
if (this.state.startPosition && !this.state.isDragging) {
|
|
104
|
+
const currentPos = {
|
|
105
|
+
x: e.clientX,
|
|
106
|
+
y: e.clientY
|
|
107
|
+
};
|
|
108
|
+
if (this.getDistance(this.state.startPosition, currentPos) < this.config.clickThreshold && this.state.sourceElement && this.state.sourcePosition) this.callbacks.onClick?.(this.state.sourceElement, this.state.sourcePosition);
|
|
109
|
+
this.cleanup();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!this.state.isDragging || !this.state.sourcePosition || !this.state.sourceItem) {
|
|
113
|
+
this.cleanup();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
117
|
+
const droppableElement = this.findElementByKind(target, this.config.droppableKind);
|
|
118
|
+
if (droppableElement) {
|
|
119
|
+
const targetPosition = this.callbacks.getItemPosition(droppableElement, this.config.droppableKind);
|
|
120
|
+
if (targetPosition) this.callbacks.onDrop?.(this.state.sourcePosition, targetPosition, this.state.sourceItem);
|
|
121
|
+
}
|
|
122
|
+
this.cleanup();
|
|
123
|
+
}
|
|
124
|
+
handlePointerCancel() {
|
|
125
|
+
this.cleanup();
|
|
126
|
+
}
|
|
127
|
+
handleAutoScroll(pos) {
|
|
128
|
+
const { scrollThreshold, scrollSpeed } = this.config;
|
|
129
|
+
const viewportWidth = window.innerWidth;
|
|
130
|
+
const viewportHeight = window.innerHeight;
|
|
131
|
+
let scrollX = 0;
|
|
132
|
+
let scrollY = 0;
|
|
133
|
+
if (pos.x < scrollThreshold) scrollX = -scrollSpeed;
|
|
134
|
+
else if (pos.x > viewportWidth - scrollThreshold) scrollX = scrollSpeed;
|
|
135
|
+
if (pos.y < scrollThreshold) scrollY = -scrollSpeed;
|
|
136
|
+
else if (pos.y > viewportHeight - scrollThreshold) scrollY = scrollSpeed;
|
|
137
|
+
if (scrollX !== 0 || scrollY !== 0) window.scrollBy(scrollX, scrollY);
|
|
138
|
+
}
|
|
139
|
+
cleanup() {
|
|
140
|
+
if (this.state.sourceElement) this.state.sourceElement.removeAttribute("data-dragging");
|
|
141
|
+
if (this.state.lastHoveredElement) this.state.lastHoveredElement.removeAttribute("data-hovered");
|
|
142
|
+
if (this.rafId !== null) {
|
|
143
|
+
cancelAnimationFrame(this.rafId);
|
|
144
|
+
this.rafId = null;
|
|
145
|
+
}
|
|
146
|
+
if (this.state.isDragging) this.callbacks.onDragEnd?.();
|
|
147
|
+
this.state = {
|
|
148
|
+
startPosition: null,
|
|
149
|
+
sourceElement: null,
|
|
150
|
+
sourcePosition: null,
|
|
151
|
+
sourceItem: null,
|
|
152
|
+
isDragging: false,
|
|
153
|
+
lastHoveredElement: null
|
|
154
|
+
};
|
|
155
|
+
this.lastPointerPos = null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Find closest ancestor element with matching data-kind attribute
|
|
159
|
+
*/
|
|
160
|
+
findElementByKind(element, kind) {
|
|
161
|
+
let current = element;
|
|
162
|
+
while (current && current !== this.container) {
|
|
163
|
+
if (current.dataset.kind === kind) return current;
|
|
164
|
+
current = current.parentElement;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Calculate distance between two points
|
|
170
|
+
*/
|
|
171
|
+
getDistance(p1, p2) {
|
|
172
|
+
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get current drag state (read-only)
|
|
176
|
+
*/
|
|
177
|
+
getState() {
|
|
178
|
+
return { ...this.state };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if currently dragging
|
|
182
|
+
*/
|
|
183
|
+
isDragging() {
|
|
184
|
+
return this.state.isDragging;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Cleanup and remove event listeners
|
|
188
|
+
*/
|
|
189
|
+
destroy() {
|
|
190
|
+
this.cleanup();
|
|
191
|
+
this.container.removeEventListener("pointerdown", this.boundHandlers.pointerDown);
|
|
192
|
+
this.container.removeEventListener("pointermove", this.boundHandlers.pointerMove);
|
|
193
|
+
this.container.removeEventListener("pointerup", this.boundHandlers.pointerUp);
|
|
194
|
+
this.container.removeEventListener("pointercancel", this.boundHandlers.pointerCancel);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
export { DragDropManager };
|
|
200
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/drag-drop-manager.ts"],"sourcesContent":["/**\n * Framework-agnostic Drag and Drop Manager\n *\n * A data-driven drag-and-drop utility that works with React and vanilla JS.\n * Uses data attributes and direct DOM manipulation for optimal performance.\n *\n * @template TItem - The type of data associated with draggable items\n * @template TPosition - The type representing position/coordinates (e.g., {x, y}, {box, cell}, etc.)\n */\n\nexport type DragDropConfig = {\n /**\n * Minimum distance in pixels the pointer must move before a drag operation starts.\n * Prevents accidental drags from small movements and allows normal scrolling on mobile.\n * Checked during `pointermove` before drag has started.\n * @default 10\n * @example\n * dragThreshold: 15 // Harder to start drag (better for mobile scroll)\n */\n dragThreshold?: number\n\n /**\n * Maximum distance in pixels between pointerdown and pointerup to treat as a click/tap.\n * Used to distinguish intentional clicks from cancelled drag attempts.\n * Only checked on `pointerup` if drag never started.\n * @default 10\n * @example\n * clickThreshold: 5 // Stricter click detection\n * clickThreshold: 15 // More forgiving for mobile taps\n */\n clickThreshold?: number\n\n /**\n * Distance in pixels from viewport edge that triggers auto-scrolling during drag.\n * When dragging near screen edges, the window automatically scrolls.\n * @default 100\n * @example\n * scrollThreshold: 50 // Start scrolling closer to edges\n * scrollThreshold: 150 // Larger safe zone before scrolling\n */\n scrollThreshold?: number\n\n /**\n * Scroll speed in pixels per animation frame during auto-scroll.\n * Higher values = faster scrolling when dragging near viewport edges.\n * @default 10\n * @example\n * scrollSpeed: 5 // Slower, smoother scroll\n * scrollSpeed: 20 // Faster scroll\n */\n scrollSpeed?: number\n\n /**\n * The `data-kind` attribute value for draggable elements.\n * Elements with this attribute can be picked up and dragged.\n * @example\n * draggableKind: 'cell' // <div data-kind=\"cell\">\n * draggableKind: 'card' // <div data-kind=\"card\">\n * draggableKind: 'list-item' // <div data-kind=\"list-item\">\n */\n draggableKind: string\n\n /**\n * The `data-kind` attribute value for droppable target elements.\n * Elements with this attribute can receive dropped items.\n * Often the same as `draggableKind` for reorderable lists.\n * @example\n * droppableKind: 'cell' // Can drop on cells\n * droppableKind: 'zone' // Can drop on zones\n */\n droppableKind: string\n}\n\nexport type PointerPosition = {\n x: number\n y: number\n}\n\nexport type DragDropCallbacks<TItem, TPosition> = {\n /** Called when determining if an element can be dragged */\n canDrag?: (element: HTMLElement, position: TPosition) => boolean\n /** Called when drag starts (after threshold is met) */\n onDragStart?: (element: HTMLElement, position: TPosition, item: TItem) => void\n /** Called continuously during drag */\n onDragMove?: (pos: PointerPosition, hoveredElement: HTMLElement | null) => void\n /** Called when item is dropped on a valid target */\n onDrop?: (sourcePosition: TPosition, targetPosition: TPosition, sourceItem: TItem) => void\n /** Called when drag ends (regardless of success) */\n onDragEnd?: () => void\n /** Called on simple click/tap (no drag) */\n onClick?: (element: HTMLElement, position: TPosition) => void\n /** Called to extract item data from an element */\n getItemData?: (element: HTMLElement, position: TPosition) => TItem | null\n /** Called to get the custom position/coordinates of an element */\n getItemPosition: (element: HTMLElement, kind: string) => TPosition | null\n}\n\ntype DragState<TItem, TPosition> = {\n startPosition: PointerPosition | null\n sourceElement: HTMLElement | null\n sourcePosition: TPosition | null\n sourceItem: TItem | null\n isDragging: boolean\n lastHoveredElement: HTMLElement | null\n}\n\nexport class DragDropManager<TItem = unknown, TPosition = unknown> {\n private container: HTMLElement\n private config: Required<DragDropConfig>\n private callbacks: DragDropCallbacks<TItem, TPosition>\n private state: DragState<TItem, TPosition> = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n private rafId: number | null = null\n private lastPointerPos: PointerPosition | null = null\n private boundHandlers: {\n pointerDown: (e: PointerEvent) => void\n pointerMove: (e: PointerEvent) => void\n pointerUp: (e: PointerEvent) => void\n pointerCancel: (e: PointerEvent) => void\n }\n\n constructor(\n containerRef: { current: HTMLElement | null } | string | HTMLElement,\n config: DragDropConfig,\n callbacks: DragDropCallbacks<TItem, TPosition>,\n ) {\n // Resolve container element\n if (typeof containerRef === 'string') {\n const element = document.querySelector<HTMLElement>(containerRef)\n if (!element) {\n throw new Error(`DragDropManager: Could not find element with selector \"${containerRef}\"`)\n }\n this.container = element\n } else if (typeof containerRef === 'object' && 'current' in containerRef) {\n if (!containerRef.current) {\n throw new Error('DragDropManager: Ref.current is null')\n }\n this.container = containerRef.current\n } else {\n this.container = containerRef as HTMLElement\n }\n\n this.config = {\n dragThreshold: config.dragThreshold ?? 10,\n clickThreshold: config.clickThreshold ?? 10,\n scrollThreshold: config.scrollThreshold ?? 100,\n scrollSpeed: config.scrollSpeed ?? 10,\n draggableKind: config.draggableKind,\n droppableKind: config.droppableKind,\n }\n\n this.callbacks = callbacks\n\n // Bind handlers to preserve context\n this.boundHandlers = {\n pointerDown: this.handlePointerDown.bind(this),\n pointerMove: this.handlePointerMove.bind(this),\n pointerUp: this.handlePointerUp.bind(this),\n pointerCancel: this.handlePointerCancel.bind(this),\n }\n\n this.attachListeners()\n }\n\n private attachListeners(): void {\n this.container.addEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.addEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.addEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.addEventListener('pointercancel', this.boundHandlers.pointerCancel)\n }\n\n private handlePointerDown(e: PointerEvent): void {\n const target = e.target as HTMLElement\n const draggableElement = this.findElementByKind(target, this.config.draggableKind)\n\n if (!draggableElement) return\n\n const position = this.callbacks.getItemPosition(draggableElement, this.config.draggableKind)\n if (!position) return\n\n // Check if element is empty (convention: data-empty=\"any-value\")\n const isEmpty = draggableElement.dataset.empty !== undefined\n if (isEmpty) return\n\n // Check canDrag callback\n if (this.callbacks.canDrag && !this.callbacks.canDrag(draggableElement, position)) {\n return\n }\n\n // Get item data\n const item = this.callbacks.getItemData?.(draggableElement, position) ?? null\n if (!item) return\n\n // Prevent text selection during potential drag\n e.preventDefault()\n\n // Store start position and element info\n this.state.startPosition = { x: e.clientX, y: e.clientY }\n this.state.sourceElement = draggableElement\n this.state.sourcePosition = position\n this.state.sourceItem = item\n }\n\n private handlePointerMove(e: PointerEvent): void {\n const currentPos: PointerPosition = { x: e.clientX, y: e.clientY }\n\n // Check if we should start dragging (threshold not yet met)\n if (this.state.startPosition && !this.state.isDragging) {\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n if (distance >= this.config.dragThreshold) {\n // Start drag\n this.state.isDragging = true\n\n if (this.state.sourceElement && this.state.sourcePosition && this.state.sourceItem) {\n // Set data-dragging attribute on source\n this.state.sourceElement.setAttribute('data-dragging', 'true')\n\n // Notify drag start\n this.callbacks.onDragStart?.(\n this.state.sourceElement,\n this.state.sourcePosition,\n this.state.sourceItem,\n )\n }\n }\n return\n }\n\n if (!this.state.isDragging) return\n\n // Store latest position for RAF\n this.lastPointerPos = currentPos\n\n // Throttle updates to 60fps using RAF\n if (this.rafId === null) {\n this.rafId = requestAnimationFrame(() => {\n this.rafId = null\n if (!this.lastPointerPos) return\n\n const pos = this.lastPointerPos\n\n // Find hovered droppable element\n const elementUnderCursor = document.elementFromPoint(pos.x, pos.y) as HTMLElement\n const droppableElement = this.findElementByKind(\n elementUnderCursor,\n this.config.droppableKind,\n )\n\n // Update hover feedback\n if (droppableElement !== this.state.lastHoveredElement) {\n // Remove hover from previous element\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Add hover to current element (including the source element).\n // This enables \"drag over itself\" states (e.g. combined data-dragging + data-hovered styling).\n if (droppableElement) {\n droppableElement.setAttribute('data-hovered', 'true')\n this.state.lastHoveredElement = droppableElement\n } else {\n this.state.lastHoveredElement = null\n }\n }\n\n // Auto-scroll\n this.handleAutoScroll(pos)\n\n // Notify drag move\n this.callbacks.onDragMove?.(pos, this.state.lastHoveredElement)\n })\n }\n }\n\n private handlePointerUp(e: PointerEvent): void {\n // Handle click/tap if we never started dragging\n if (this.state.startPosition && !this.state.isDragging) {\n const currentPos = { x: e.clientX, y: e.clientY }\n const distance = this.getDistance(this.state.startPosition, currentPos)\n\n // If distance is very small, treat as click\n if (\n distance < this.config.clickThreshold &&\n this.state.sourceElement &&\n this.state.sourcePosition\n ) {\n this.callbacks.onClick?.(this.state.sourceElement, this.state.sourcePosition)\n }\n\n this.cleanup()\n return\n }\n\n if (!this.state.isDragging || !this.state.sourcePosition || !this.state.sourceItem) {\n this.cleanup()\n return\n }\n\n // Find drop target\n const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement\n const droppableElement = this.findElementByKind(target, this.config.droppableKind)\n\n if (droppableElement) {\n const targetPosition = this.callbacks.getItemPosition(\n droppableElement,\n this.config.droppableKind,\n )\n\n if (targetPosition) {\n // Notify drop\n this.callbacks.onDrop?.(this.state.sourcePosition, targetPosition, this.state.sourceItem)\n }\n }\n\n this.cleanup()\n }\n\n private handlePointerCancel(): void {\n this.cleanup()\n }\n\n private handleAutoScroll(pos: PointerPosition): void {\n const { scrollThreshold, scrollSpeed } = this.config\n const viewportWidth = window.innerWidth\n const viewportHeight = window.innerHeight\n\n let scrollX = 0\n let scrollY = 0\n\n // Check horizontal scroll\n if (pos.x < scrollThreshold) {\n scrollX = -scrollSpeed\n } else if (pos.x > viewportWidth - scrollThreshold) {\n scrollX = scrollSpeed\n }\n\n // Check vertical scroll\n if (pos.y < scrollThreshold) {\n scrollY = -scrollSpeed\n } else if (pos.y > viewportHeight - scrollThreshold) {\n scrollY = scrollSpeed\n }\n\n if (scrollX !== 0 || scrollY !== 0) {\n window.scrollBy(scrollX, scrollY)\n }\n }\n\n private cleanup(): void {\n // Remove data-dragging attribute\n if (this.state.sourceElement) {\n this.state.sourceElement.removeAttribute('data-dragging')\n }\n\n // Remove hover attribute\n if (this.state.lastHoveredElement) {\n this.state.lastHoveredElement.removeAttribute('data-hovered')\n }\n\n // Cancel RAF\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n // Notify drag end\n if (this.state.isDragging) {\n this.callbacks.onDragEnd?.()\n }\n\n // Reset state\n this.state = {\n startPosition: null,\n sourceElement: null,\n sourcePosition: null,\n sourceItem: null,\n isDragging: false,\n lastHoveredElement: null,\n }\n this.lastPointerPos = null\n }\n\n /**\n * Find closest ancestor element with matching data-kind attribute\n */\n private findElementByKind(element: HTMLElement | null, kind: string): HTMLElement | null {\n let current = element\n while (current && current !== this.container) {\n if (current.dataset.kind === kind) {\n return current\n }\n current = current.parentElement\n }\n return null\n }\n\n /**\n * Calculate distance between two points\n */\n private getDistance(p1: PointerPosition, p2: PointerPosition): number {\n return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))\n }\n\n /**\n * Get current drag state (read-only)\n */\n public getState(): Readonly<DragState<TItem, TPosition>> {\n return { ...this.state }\n }\n\n /**\n * Check if currently dragging\n */\n public isDragging(): boolean {\n return this.state.isDragging\n }\n\n /**\n * Cleanup and remove event listeners\n */\n public destroy(): void {\n this.cleanup()\n this.container.removeEventListener('pointerdown', this.boundHandlers.pointerDown)\n this.container.removeEventListener('pointermove', this.boundHandlers.pointerMove)\n this.container.removeEventListener('pointerup', this.boundHandlers.pointerUp)\n this.container.removeEventListener('pointercancel', this.boundHandlers.pointerCancel)\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dnd-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"homepage": "https://itsjavi.com/dnd-manager",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/itsjavi/dnd-manager.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.mjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.mjs",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown src/index.ts --dts --sourcemap --format esm --clean",
|
|
27
|
+
"format": "pnpx sort-package-json && pnpm exec prettier --cache --write .",
|
|
28
|
+
"prepublishOnly": "pnpm typecheck && pnpm build && publint",
|
|
29
|
+
"test": "vitest --run",
|
|
30
|
+
"test:watch": "vitest --watch",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"prettier": "^3.8.1",
|
|
35
|
+
"publint": "^0.3.17",
|
|
36
|
+
"sort-package-json": "^3.6.1",
|
|
37
|
+
"tsdown": "^0.20.3",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vitest": "^4.0.18"
|
|
40
|
+
}
|
|
41
|
+
}
|