or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

core-draggable.mddroppable.mdevents.mdindex.mdplugins.mdsensors.mdsortable.mdswappable.md

swappable.mddocs/

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

```