0
# Droppable Zones
1
2
Droppable extends Draggable to create designated drop zones where draggable elements can be placed. It provides visual feedback for valid drop targets and handles drop validation.
3
4
## Capabilities
5
6
### Droppable Constructor
7
8
Creates a droppable instance that requires dropzone configuration to define valid drop targets.
9
10
```typescript { .api }
11
/**
12
* Creates a new droppable instance with designated drop zones
13
* @param containers - Elements that contain draggable items
14
* @param options - Configuration options including required dropzone
15
*/
16
class Droppable<T = DroppableEventNames> extends Draggable<T> {
17
constructor(containers: DraggableContainer, options: DroppableOptions);
18
}
19
20
interface DroppableOptions extends DraggableOptions {
21
dropzone: string | NodeList | HTMLElement[] | (() => NodeList | HTMLElement[]);
22
classes?: {[key in DroppableClassNames]: string};
23
}
24
25
type DroppableClassNames =
26
| DraggableClassNames
27
| 'droppable:active'
28
| 'droppable:occupied';
29
```
30
31
**Usage Example:**
32
33
```typescript
34
import { Droppable } from "@shopify/draggable";
35
36
const droppable = new Droppable(document.querySelectorAll('.drag-container'), {
37
draggable: '.draggable-item',
38
dropzone: '.drop-zone',
39
classes: {
40
'droppable:active': 'drop-zone-active',
41
'droppable:occupied': 'drop-zone-occupied'
42
}
43
});
44
45
// Listen for drop events
46
droppable.on('droppable:dropped', (event) => {
47
console.log('Item dropped into:', event.dropzone);
48
console.log('Dragged item:', event.dragEvent.source);
49
});
50
```
51
52
### Class Name Management
53
54
Specialized class name management for dropzone-specific states.
55
56
```typescript { .api }
57
/**
58
* Returns CSS class name for droppable-specific class identifiers
59
* @param name - Droppable class identifier
60
* @returns CSS class name string
61
*/
62
getClassNameFor(name: DroppableClassNames): string;
63
```
64
65
### Drop Events
66
67
Droppable-specific events that fire during drop operations.
68
69
```typescript { .api }
70
type DroppableEventNames =
71
| 'droppable:start'
72
| 'droppable:dropped'
73
| 'droppable:returned'
74
| 'droppable:stop'
75
| DraggableEventNames;
76
```
77
78
**Event Details:**
79
80
- **droppable:start**: Fired when a droppable drag operation begins
81
- **droppable:dropped**: Fired when an item is dropped into a valid dropzone
82
- **droppable:returned**: Fired when an item returns to its original dropzone
83
- **droppable:stop**: Fired when the drop operation ends
84
85
**Event Handlers Example:**
86
87
```typescript
88
droppable.on('droppable:start', (event) => {
89
console.log('Started dragging from dropzone:', event.dropzone);
90
// Highlight valid drop zones
91
document.querySelectorAll('.drop-zone').forEach(zone => {
92
zone.classList.add('available-target');
93
});
94
});
95
96
droppable.on('droppable:dropped', (event) => {
97
console.log(`Item moved to new dropzone`);
98
handleItemDrop(event.dragEvent.source, event.dropzone);
99
100
// Optionally prevent the drop
101
// event.cancel();
102
});
103
104
droppable.on('droppable:returned', (event) => {
105
console.log('Item returned to original position');
106
handleItemReturn(event.dragEvent.source, event.dropzone);
107
});
108
109
droppable.on('droppable:stop', (event) => {
110
// Clean up highlighting
111
document.querySelectorAll('.drop-zone').forEach(zone => {
112
zone.classList.remove('available-target');
113
});
114
});
115
```
116
117
## Event Types
118
119
```typescript { .api }
120
class DroppableEvent extends AbstractEvent {
121
readonly dragEvent: DragEvent;
122
}
123
124
class DroppableStartEvent extends DroppableEvent {
125
dropzone: HTMLElement;
126
}
127
128
class DroppableDroppedEvent extends DroppableEvent {
129
dropzone: HTMLElement;
130
}
131
132
class DroppableReturnedEvent extends DroppableEvent {
133
dropzone: HTMLElement;
134
}
135
136
class DroppableStopEvent extends DroppableEvent {
137
dropzone: HTMLElement;
138
}
139
```
140
141
## Dropzone Configuration
142
143
The dropzone option can be specified in multiple ways:
144
145
```typescript
146
// CSS selector string
147
const droppable1 = new Droppable(containers, {
148
draggable: '.item',
149
dropzone: '.drop-target'
150
});
151
152
// NodeList or HTMLElement array
153
const dropzones = document.querySelectorAll('.zone');
154
const droppable2 = new Droppable(containers, {
155
draggable: '.item',
156
dropzone: dropzones
157
});
158
159
// Function returning NodeList or HTMLElement array
160
const droppable3 = new Droppable(containers, {
161
draggable: '.item',
162
dropzone: () => document.querySelectorAll('.dynamic-zone')
163
});
164
```
165
166
## Complete Example
167
168
```typescript
169
import { Droppable } from "@shopify/draggable";
170
171
// Create a file upload interface with drag and drop
172
const fileDroppable = new Droppable(document.querySelector('.file-area'), {
173
draggable: '.file-item',
174
dropzone: '.upload-zone, .trash-zone',
175
classes: {
176
'droppable:active': 'zone-highlight',
177
'droppable:occupied': 'zone-has-file',
178
'source:dragging': 'file-being-dragged'
179
}
180
});
181
182
// Handle different drop zones
183
fileDroppable.on('droppable:dropped', (event) => {
184
const file = event.dragEvent.source;
185
const zone = event.dropzone;
186
187
if (zone.classList.contains('upload-zone')) {
188
// Start upload process
189
uploadFile(file.dataset.fileId);
190
showUploadProgress(file);
191
} else if (zone.classList.contains('trash-zone')) {
192
// Delete file
193
deleteFile(file.dataset.fileId);
194
file.remove();
195
}
196
});
197
198
// Provide visual feedback
199
fileDroppable.on('droppable:start', (event) => {
200
// Show drop zones when dragging starts
201
document.querySelectorAll('.drop-zone').forEach(zone => {
202
zone.style.opacity = '1';
203
zone.style.transform = 'scale(1.05)';
204
});
205
});
206
207
fileDroppable.on('droppable:stop', (event) => {
208
// Hide drop zones when dragging ends
209
document.querySelectorAll('.drop-zone').forEach(zone => {
210
zone.style.opacity = '';
211
zone.style.transform = '';
212
});
213
});
214
215
// Handle validation
216
fileDroppable.on('droppable:dropped', (event) => {
217
const file = event.dragEvent.source;
218
const zone = event.dropzone;
219
220
// Check if file type is allowed in this zone
221
const allowedTypes = zone.dataset.allowedTypes?.split(',') || [];
222
const fileType = file.dataset.type;
223
224
if (allowedTypes.length > 0 && !allowedTypes.includes(fileType)) {
225
event.cancel();
226
showError(`${fileType} files not allowed in this zone`);
227
}
228
});
229
```
230
231
## Advanced Dropzone Patterns
232
233
### Conditional Drop Zones
234
235
```typescript
236
const conditionalDroppable = new Droppable(containers, {
237
draggable: '.task',
238
dropzone: () => {
239
// Only return available drop zones based on current state
240
return Array.from(document.querySelectorAll('.task-column'))
241
.filter(column => !column.classList.contains('locked'));
242
}
243
});
244
```
245
246
### Nested Drop Zones
247
248
```typescript
249
const nestedDroppable = new Droppable(document.querySelector('.workspace'), {
250
draggable: '.widget',
251
dropzone: '.panel, .sidebar, .main-area'
252
});
253
254
// Handle different drop contexts
255
nestedDroppable.on('droppable:dropped', (event) => {
256
const widget = event.dragEvent.source;
257
const target = event.dropzone;
258
259
// Configure widget based on drop target
260
if (target.classList.contains('sidebar')) {
261
widget.classList.add('sidebar-widget');
262
resizeWidget(widget, 'narrow');
263
} else if (target.classList.contains('main-area')) {
264
widget.classList.add('main-widget');
265
resizeWidget(widget, 'wide');
266
}
267
});
268
```