0
# Swappable Elements
1
2
Swappable extends Draggable to enable element swapping where dragging over another element exchanges their positions. Perfect for grid layouts and card arrangements where order is less important than positioning.
3
4
## Capabilities
5
6
### Swappable Constructor
7
8
Creates a swappable instance with the same interface as Draggable but with element swapping behavior.
9
10
```typescript { .api }
11
/**
12
* Creates a new swappable instance for element swapping
13
* @param containers - Elements that contain swappable items
14
* @param options - Configuration options (same as Draggable)
15
*/
16
class Swappable<T = SwappableEventNames> extends Draggable<T> {
17
constructor(containers: DraggableContainer, options?: DraggableOptions);
18
}
19
```
20
21
**Usage Example:**
22
23
```typescript
24
import { Swappable } from "@shopify/draggable";
25
26
const swappable = new Swappable(document.querySelectorAll('.card-grid'), {
27
draggable: '.card'
28
});
29
30
// Listen for swap events
31
swappable.on('swappable:swapped', (event) => {
32
console.log('Elements swapped');
33
console.log('Dragged element:', event.dragEvent.source);
34
console.log('Swapped with:', event.swappedElement);
35
});
36
```
37
38
### Swap Events
39
40
Swappable-specific events that fire during swap operations.
41
42
```typescript { .api }
43
type SwappableEventNames =
44
| 'swappable:start'
45
| 'swappable:swap'
46
| 'swappable:swapped'
47
| 'swappable:stop'
48
| DraggableEventNames;
49
```
50
51
**Event Details:**
52
53
- **swappable:start**: Fired when a swappable drag operation begins
54
- **swappable:swap**: Fired before elements are swapped (cancelable)
55
- **swappable:swapped**: Fired after elements have been swapped
56
- **swappable:stop**: Fired when the swap operation ends
57
58
**Event Handlers Example:**
59
60
```typescript
61
swappable.on('swappable:start', (event) => {
62
console.log('Started swapping');
63
// Add visual feedback
64
event.dragEvent.source.classList.add('being-swapped');
65
});
66
67
swappable.on('swappable:swap', (event) => {
68
// This fires before the swap happens - you can cancel it
69
const source = event.dragEvent.source;
70
const target = event.over;
71
72
if (shouldPreventSwap(source, target)) {
73
event.cancel();
74
return;
75
}
76
77
// Add pre-swap effects
78
target.classList.add('about-to-swap');
79
});
80
81
swappable.on('swappable:swapped', (event) => {
82
// This fires after the elements have been swapped
83
const source = event.dragEvent.source;
84
const swapped = event.swappedElement;
85
86
// Update data model
87
updateElementPositions(source, swapped);
88
89
// Add visual feedback
90
swapped.classList.add('just-swapped');
91
setTimeout(() => {
92
swapped.classList.remove('just-swapped');
93
}, 500);
94
});
95
96
swappable.on('swappable:stop', (event) => {
97
// Clean up visual states
98
document.querySelectorAll('.being-swapped, .about-to-swap').forEach(el => {
99
el.classList.remove('being-swapped', 'about-to-swap');
100
});
101
});
102
```
103
104
## Event Types
105
106
```typescript { .api }
107
class SwappableEvent extends AbstractEvent {
108
readonly dragEvent: DragEvent;
109
}
110
111
class SwappableStartEvent extends SwappableEvent {}
112
113
class SwappableSwapEvent extends SwappableEvent {
114
readonly over: HTMLElement;
115
readonly overContainer: HTMLElement;
116
}
117
118
class SwappableSwappedEvent extends SwappableEvent {
119
readonly swappedElement: HTMLElement;
120
}
121
122
class SwappableStopEvent extends SwappableEvent {}
123
```
124
125
## Swap Behavior
126
127
Swappable has unique behavior compared to other draggable types:
128
129
- **Position Exchange**: Elements physically exchange positions in the DOM
130
- **Multiple Swaps**: Dragging over multiple elements will swap with each one
131
- **Swap Reversal**: Dragging back over a previously swapped element will swap them back
132
- **Visual Feedback**: Elements move to new positions immediately during drag
133
134
## Complete Example
135
136
```typescript
137
import { Swappable } from "@shopify/draggable";
138
139
// Create swappable photo gallery
140
const photoSwappable = new Swappable(document.querySelector('.photo-grid'), {
141
draggable: '.photo-card',
142
classes: {
143
'source:dragging': 'photo-dragging',
144
'container:dragging': 'grid-active'
145
}
146
});
147
148
// Track swaps for undo functionality
149
let swapHistory = [];
150
151
photoSwappable.on('swappable:swapped', (event) => {
152
const source = event.dragEvent.source;
153
const target = event.swappedElement;
154
155
// Record swap for undo
156
swapHistory.push({
157
timestamp: Date.now(),
158
sourceId: source.dataset.photoId,
159
targetId: target.dataset.photoId,
160
sourceIndex: Array.from(source.parentNode.children).indexOf(source),
161
targetIndex: Array.from(target.parentNode.children).indexOf(target)
162
});
163
164
// Update photo metadata
165
updatePhotoOrder(source.dataset.photoId, target.dataset.photoId);
166
167
// Add swap animation
168
source.style.transform = 'scale(1.1)';
169
target.style.transform = 'scale(1.1)';
170
171
setTimeout(() => {
172
source.style.transform = '';
173
target.style.transform = '';
174
}, 200);
175
});
176
177
// Undo last swap
178
function undoLastSwap() {
179
const lastSwap = swapHistory.pop();
180
if (!lastSwap) return;
181
182
const sourceEl = document.querySelector(`[data-photo-id="${lastSwap.sourceId}"]`);
183
const targetEl = document.querySelector(`[data-photo-id="${lastSwap.targetId}"]`);
184
185
if (sourceEl && targetEl) {
186
// Swap them back
187
swapElements(sourceEl, targetEl);
188
updatePhotoOrder(lastSwap.sourceId, lastSwap.targetId);
189
}
190
}
191
192
// Utility function to swap DOM elements
193
function swapElements(el1, el2) {
194
const temp = document.createElement('div');
195
el1.parentNode.insertBefore(temp, el1);
196
el2.parentNode.insertBefore(el1, el2);
197
temp.parentNode.insertBefore(el2, temp);
198
temp.remove();
199
}
200
```
201
202
## Advanced Swapping Patterns
203
204
### Conditional Swapping
205
206
```typescript
207
const conditionalSwappable = new Swappable(containers, {
208
draggable: '.player-card'
209
});
210
211
conditionalSwappable.on('swappable:swap', (event) => {
212
const source = event.dragEvent.source;
213
const target = event.over;
214
215
// Only allow swapping within same team
216
const sourceTeam = source.dataset.team;
217
const targetTeam = target.dataset.team;
218
219
if (sourceTeam !== targetTeam) {
220
event.cancel();
221
showError('Cannot swap players between teams');
222
}
223
});
224
```
225
226
### Grid-Based Swapping
227
228
```typescript
229
const gridSwappable = new Swappable(document.querySelector('.puzzle-grid'), {
230
draggable: '.puzzle-piece',
231
classes: {
232
'source:dragging': 'piece-moving',
233
'draggable:over': 'piece-target'
234
}
235
});
236
237
// Track grid positions
238
gridSwappable.on('swappable:swapped', (event) => {
239
const source = event.dragEvent.source;
240
const target = event.swappedElement;
241
242
// Update grid coordinates
243
const sourcePos = getGridPosition(source);
244
const targetPos = getGridPosition(target);
245
246
setGridPosition(source, targetPos);
247
setGridPosition(target, sourcePos);
248
249
// Check if puzzle is solved
250
if (isPuzzleSolved()) {
251
celebrateSolution();
252
}
253
});
254
255
function getGridPosition(element) {
256
return {
257
row: parseInt(element.dataset.row),
258
col: parseInt(element.dataset.col)
259
};
260
}
261
262
function setGridPosition(element, position) {
263
element.dataset.row = position.row;
264
element.dataset.col = position.col;
265
element.style.gridArea = `${position.row} / ${position.col}`;
266
}
267
```
268
269
### Multi-Container Swapping
270
271
```typescript
272
const multiSwappable = new Swappable([
273
document.querySelector('.team-a'),
274
document.querySelector('.team-b'),
275
document.querySelector('.bench')
276
], {
277
draggable: '.player'
278
});
279
280
multiSwappable.on('swappable:swapped', (event) => {
281
const source = event.dragEvent.source;
282
const target = event.swappedElement;
283
const sourceContainer = source.closest('.team-a, .team-b, .bench');
284
const targetContainer = target.closest('.team-a, .team-b, .bench');
285
286
// Handle cross-team swaps
287
if (sourceContainer !== targetContainer) {
288
updatePlayerTeam(source.dataset.playerId, targetContainer.dataset.team);
289
updatePlayerTeam(target.dataset.playerId, sourceContainer.dataset.team);
290
}
291
292
// Update team rosters
293
updateTeamRosters();
294
});
295
```