CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-shopify--draggable

The JavaScript Drag & Drop library your grandparents warned you about.

Pending
Overview
Eval results
Files

swappable.mddocs/

Swappable Elements

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.

Capabilities

Swappable Constructor

Creates a swappable instance with the same interface as Draggable but with element swapping behavior.

/**
 * Creates a new swappable instance for element swapping
 * @param containers - Elements that contain swappable items
 * @param options - Configuration options (same as Draggable)
 */
class Swappable<T = SwappableEventNames> extends Draggable<T> {
  constructor(containers: DraggableContainer, options?: DraggableOptions);
}

Usage Example:

import { Swappable } from "@shopify/draggable";

const swappable = new Swappable(document.querySelectorAll('.card-grid'), {
  draggable: '.card'
});

// Listen for swap events
swappable.on('swappable:swapped', (event) => {
  console.log('Elements swapped');
  console.log('Dragged element:', event.dragEvent.source);
  console.log('Swapped with:', event.swappedElement);
});

Swap Events

Swappable-specific events that fire during swap operations.

type SwappableEventNames =
  | 'swappable:start'
  | 'swappable:swap'
  | 'swappable:swapped'
  | 'swappable:stop'
  | DraggableEventNames;

Event Details:

  • swappable:start: Fired when a swappable drag operation begins
  • swappable:swap: Fired before elements are swapped (cancelable)
  • swappable:swapped: Fired after elements have been swapped
  • swappable:stop: Fired when the swap operation ends

Event Handlers Example:

swappable.on('swappable:start', (event) => {
  console.log('Started swapping');
  // Add visual feedback
  event.dragEvent.source.classList.add('being-swapped');
});

swappable.on('swappable:swap', (event) => {
  // This fires before the swap happens - you can cancel it
  const source = event.dragEvent.source;
  const target = event.over;
  
  if (shouldPreventSwap(source, target)) {
    event.cancel();
    return;
  }
  
  // Add pre-swap effects
  target.classList.add('about-to-swap');
});

swappable.on('swappable:swapped', (event) => {
  // This fires after the elements have been swapped
  const source = event.dragEvent.source;
  const swapped = event.swappedElement;
  
  // Update data model
  updateElementPositions(source, swapped);
  
  // Add visual feedback
  swapped.classList.add('just-swapped');
  setTimeout(() => {
    swapped.classList.remove('just-swapped');
  }, 500);
});

swappable.on('swappable:stop', (event) => {
  // Clean up visual states
  document.querySelectorAll('.being-swapped, .about-to-swap').forEach(el => {
    el.classList.remove('being-swapped', 'about-to-swap');
  });
});

Event Types

class SwappableEvent extends AbstractEvent {
  readonly dragEvent: DragEvent;
}

class SwappableStartEvent extends SwappableEvent {}

class SwappableSwapEvent extends SwappableEvent {
  readonly over: HTMLElement;
  readonly overContainer: HTMLElement;
}

class SwappableSwappedEvent extends SwappableEvent {
  readonly swappedElement: HTMLElement;
}

class SwappableStopEvent extends SwappableEvent {}

Swap Behavior

Swappable has unique behavior compared to other draggable types:

  • Position Exchange: Elements physically exchange positions in the DOM
  • Multiple Swaps: Dragging over multiple elements will swap with each one
  • Swap Reversal: Dragging back over a previously swapped element will swap them back
  • Visual Feedback: Elements move to new positions immediately during drag

Complete Example

import { Swappable } from "@shopify/draggable";

// Create swappable photo gallery
const photoSwappable = new Swappable(document.querySelector('.photo-grid'), {
  draggable: '.photo-card',
  classes: {
    'source:dragging': 'photo-dragging',
    'container:dragging': 'grid-active'
  }
});

// Track swaps for undo functionality
let swapHistory = [];

photoSwappable.on('swappable:swapped', (event) => {
  const source = event.dragEvent.source;
  const target = event.swappedElement;
  
  // Record swap for undo
  swapHistory.push({
    timestamp: Date.now(),
    sourceId: source.dataset.photoId,
    targetId: target.dataset.photoId,
    sourceIndex: Array.from(source.parentNode.children).indexOf(source),
    targetIndex: Array.from(target.parentNode.children).indexOf(target)
  });
  
  // Update photo metadata
  updatePhotoOrder(source.dataset.photoId, target.dataset.photoId);
  
  // Add swap animation
  source.style.transform = 'scale(1.1)';
  target.style.transform = 'scale(1.1)';
  
  setTimeout(() => {
    source.style.transform = '';
    target.style.transform = '';
  }, 200);
});

// Undo last swap
function undoLastSwap() {
  const lastSwap = swapHistory.pop();
  if (!lastSwap) return;
  
  const sourceEl = document.querySelector(`[data-photo-id="${lastSwap.sourceId}"]`);
  const targetEl = document.querySelector(`[data-photo-id="${lastSwap.targetId}"]`);
  
  if (sourceEl && targetEl) {
    // Swap them back
    swapElements(sourceEl, targetEl);
    updatePhotoOrder(lastSwap.sourceId, lastSwap.targetId);
  }
}

// Utility function to swap DOM elements
function swapElements(el1, el2) {
  const temp = document.createElement('div');
  el1.parentNode.insertBefore(temp, el1);
  el2.parentNode.insertBefore(el1, el2);
  temp.parentNode.insertBefore(el2, temp);
  temp.remove();
}

Advanced Swapping Patterns

Conditional Swapping

const conditionalSwappable = new Swappable(containers, {
  draggable: '.player-card'
});

conditionalSwappable.on('swappable:swap', (event) => {
  const source = event.dragEvent.source;
  const target = event.over;
  
  // Only allow swapping within same team
  const sourceTeam = source.dataset.team;
  const targetTeam = target.dataset.team;
  
  if (sourceTeam !== targetTeam) {
    event.cancel();
    showError('Cannot swap players between teams');
  }
});

Grid-Based Swapping

const gridSwappable = new Swappable(document.querySelector('.puzzle-grid'), {
  draggable: '.puzzle-piece',
  classes: {
    'source:dragging': 'piece-moving',
    'draggable:over': 'piece-target'
  }
});

// Track grid positions
gridSwappable.on('swappable:swapped', (event) => {
  const source = event.dragEvent.source;
  const target = event.swappedElement;
  
  // Update grid coordinates
  const sourcePos = getGridPosition(source);
  const targetPos = getGridPosition(target);
  
  setGridPosition(source, targetPos);
  setGridPosition(target, sourcePos);
  
  // Check if puzzle is solved
  if (isPuzzleSolved()) {
    celebrateSolution();
  }
});

function getGridPosition(element) {
  return {
    row: parseInt(element.dataset.row),
    col: parseInt(element.dataset.col)
  };
}

function setGridPosition(element, position) {
  element.dataset.row = position.row;
  element.dataset.col = position.col;
  element.style.gridArea = `${position.row} / ${position.col}`;
}

Multi-Container Swapping

const multiSwappable = new Swappable([
  document.querySelector('.team-a'),
  document.querySelector('.team-b'),
  document.querySelector('.bench')
], {
  draggable: '.player'
});

multiSwappable.on('swappable:swapped', (event) => {
  const source = event.dragEvent.source;
  const target = event.swappedElement;
  const sourceContainer = source.closest('.team-a, .team-b, .bench');
  const targetContainer = target.closest('.team-a, .team-b, .bench');
  
  // Handle cross-team swaps
  if (sourceContainer !== targetContainer) {
    updatePlayerTeam(source.dataset.playerId, targetContainer.dataset.team);
    updatePlayerTeam(target.dataset.playerId, sourceContainer.dataset.team);
  }
  
  // Update team rosters
  updateTeamRosters();
});

Install with Tessl CLI

npx tessl i tessl/npm-shopify--draggable

docs

core-draggable.md

droppable.md

events.md

index.md

plugins.md

sensors.md

sortable.md

swappable.md

tile.json