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

plugins.mddocs/

Plugins

Plugins extend draggable functionality through an extensible architecture. Shopify Draggable includes built-in plugins for mirrors, scrolling, accessibility, focus management, collision detection, and animations, plus supports custom plugin development.

Capabilities

Base Plugin Class

All plugins extend AbstractPlugin which provides the foundation for plugin functionality.

/**
 * Base class for all draggable plugins
 */
abstract class AbstractPlugin {
  constructor(draggable: Draggable);
  protected abstract attach(): void;
  protected abstract detach(): void;
}

export {AbstractPlugin as BasePlugin};

Usage Example:

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

class CustomPlugin extends AbstractPlugin {
  attach() {
    this.draggable.on('drag:start', this.onDragStart);
    this.draggable.on('drag:stop', this.onDragStop);
  }
  
  detach() {
    this.draggable.off('drag:start', this.onDragStart);
    this.draggable.off('drag:stop', this.onDragStop);
  }
  
  onDragStart = (event) => {
    // Custom drag start logic
  };
  
  onDragStop = (event) => {
    // Custom drag stop logic
  };
}

Built-in Plugins (Draggable.Plugins)

Default plugins included with every Draggable instance.

class Draggable {
  static Plugins: {
    Announcement: typeof Announcement;
    Focusable: typeof Focusable;
    Mirror: typeof Mirror;
    Scrollable: typeof Scrollable;
  };
}

Announcement Plugin

Provides accessibility announcements for screen readers.

class Announcement extends AbstractPlugin {
  options: AnnouncementOptions;
  protected attach(): void;
  protected detach(): void;
}

interface AnnouncementOptions {
  expire: number;
  [key: string]: string | (() => string) | number;
}

Usage Example:

const draggable = new Draggable(containers, {
  announcements: {
    'drag:start': (event) => `Started dragging ${event.source.textContent}`,
    'drag:stop': (event) => `Stopped dragging ${event.source.textContent}`,
    expire: 10000 // 10 seconds
  }
});

Focusable Plugin

Manages focus during drag operations for keyboard accessibility.

class Focusable extends AbstractPlugin {
  getElements(): HTMLElement[];
  protected attach(): void;
  protected detach(): void;
}

Mirror Plugin

Creates a visual representation (ghost/mirror) of the dragged element.

class Mirror extends AbstractPlugin {
  getElements(): HTMLElement[];
  protected attach(): void;
  protected detach(): void;
}

interface MirrorOptions {
  xAxis?: boolean;
  yAxis?: boolean;
  constrainDimensions?: boolean;
  cursorOffsetX?: number;
  cursorOffsetY?: number;
  appendTo?: string | HTMLElement | ((source: HTMLElement) => HTMLElement);
}

Usage Example:

const draggable = new Draggable(containers, {
  mirror: {
    constrainDimensions: true,
    cursorOffsetX: 10,
    cursorOffsetY: 10,
    appendTo: document.body
  }
});

// Listen to mirror events
draggable.on('mirror:created', (event) => {
  console.log('Mirror created:', event.mirror);
  event.mirror.style.opacity = '0.8';
});

Scrollable Plugin

Provides auto-scrolling when dragging near container edges.

class Scrollable extends AbstractPlugin {
  protected attach(): void;
  protected detach(): void;
}

interface ScrollableOptions {
  speed?: number;
  sensitivity?: number;
  scrollableElements?: HTMLElement[];
}

Usage Example:

const draggable = new Draggable(containers, {
  scrollable: {
    speed: 20,
    sensitivity: 40,
    scrollableElements: [document.documentElement]
  }
});

Additional Plugins

Extended plugins available through the Plugins export.

const Plugins: {
  Collidable: typeof Collidable;
  SwapAnimation: typeof SwapAnimation;
  SortAnimation: typeof SortAnimation;
  ResizeMirror: typeof ResizeMirror;
  Snappable: typeof Snappable;
};

Collidable Plugin

Detects collisions between dragged elements and other elements.

class Collidable extends AbstractPlugin {
  protected attach(): void;
  protected detach(): void;
}

type Collidables = string | NodeList | HTMLElement[] | (() => NodeList | HTMLElement[]);

// Events
class CollidableEvent extends AbstractEvent {
  readonly dragEvent: DragEvent;
  readonly collidingElement: HTMLElement;
}

class CollidableInEvent extends CollidableEvent {}
class CollidableOutEvent extends CollidableEvent {}

type CollidableEventNames = 'collidable:in' | 'collidable:out';

Usage Example:

import { Draggable, Plugins } from "@shopify/draggable";

const draggable = new Draggable(containers, {
  plugins: [Plugins.Collidable],
  collidables: '.obstacle, .barrier'
});

draggable.on('collidable:in', (event) => {
  console.log('Collision detected with:', event.collidingElement);
  event.collidingElement.classList.add('collision-active');
});

draggable.on('collidable:out', (event) => {
  event.collidingElement.classList.remove('collision-active');
});

Snappable Plugin

Provides snap-to-grid or snap-to-element functionality.

class Snappable extends AbstractPlugin {
  protected attach(): void;
  protected detach(): void;
}

// Events
class SnapEvent extends AbstractEvent {
  readonly dragEvent: DragEvent;
  readonly snappable: HTMLElement;
}

class SnapInEvent extends SnapEvent {}
class SnapOutEvent extends SnapEvent {}

type SnappableEventNames = 'snap:in' | 'snap:out';

ResizeMirror Plugin

Automatically resizes the mirror to match the drop target.

class ResizeMirror extends AbstractPlugin {
  protected attach(): void;
  protected detach(): void;
}

SwapAnimation Plugin

Provides smooth animations for element swapping.

class SwapAnimation extends AbstractPlugin {
  protected attach(): void;
  protected detach(): void;
}

interface SwapAnimationOptions {
  duration: number;
  easingFunction: string;
  horizontal: boolean;
}

SortAnimation Plugin

Provides smooth animations for sorting operations.

class SortAnimation extends AbstractPlugin {
  protected attach(): void;
  protected detach(): void;
}

interface SortAnimationOptions {
  duration?: number;
  easingFunction?: string;
}

Animation Plugin Example:

import { Sortable, Plugins } from "@shopify/draggable";

const sortable = new Sortable(containers, {
  plugins: [Plugins.SortAnimation],
  sortAnimation: {
    duration: 300,
    easingFunction: 'ease-in-out'
  }
});

Plugin Management

Methods for dynamically managing plugins on draggable instances.

/**
 * Add plugins to a draggable instance
 * @param plugins - Plugin classes to add
 */
addPlugin(...plugins: (typeof AbstractPlugin)[]): this;

/**
 * Remove plugins from a draggable instance
 * @param plugins - Plugin classes to remove
 */
removePlugin(...plugins: (typeof AbstractPlugin)[]): this;

Usage Example:

const draggable = new Draggable(containers);

// Add plugins after initialization
draggable.addPlugin(Plugins.Collidable, Plugins.Snappable);

// Remove default plugins
draggable.removePlugin(Draggable.Plugins.Mirror);

// Exclude plugins during initialization
const draggable2 = new Draggable(containers, {
  exclude: {
    plugins: [Draggable.Plugins.Scrollable]
  }
});

Custom Plugin Development

class LoggingPlugin extends AbstractPlugin {
  constructor(draggable) {
    super(draggable);
    this.logCount = 0;
  }

  attach() {
    // Add event listeners
    this.draggable.on('drag:start', this.onDragStart);
    this.draggable.on('drag:move', this.onDragMove);
    this.draggable.on('drag:stop', this.onDragStop);
    console.log('LoggingPlugin attached');
  }

  detach() {
    // Remove event listeners
    this.draggable.off('drag:start', this.onDragStart);
    this.draggable.off('drag:move', this.onDragMove);
    this.draggable.off('drag:stop', this.onDragStop);
    console.log('LoggingPlugin detached');
  }

  onDragStart = (event) => {
    this.logCount++;
    console.log(`Drag #${this.logCount} started:`, {
      source: event.source.id || event.source.className,
      timestamp: Date.now()
    });
  };

  onDragMove = (event) => {
    console.log('Drag move:', {
      x: event.sensorEvent.clientX,
      y: event.sensorEvent.clientY
    });
  };

  onDragStop = (event) => {
    console.log(`Drag #${this.logCount} ended:`, {
      duration: Date.now() - this.startTime,
      finalPosition: {
        x: event.sensorEvent.clientX,
        y: event.sensorEvent.clientY
      }
    });
  };
}

// Use custom plugin
const draggable = new Draggable(containers, {
  plugins: [LoggingPlugin]
});

Advanced Plugin Example

class SmartMirrorPlugin extends AbstractPlugin {
  constructor(draggable) {
    super(draggable);
    this.mirrors = new Map();
  }

  attach() {
    this.draggable.on('mirror:create', this.onMirrorCreate);
    this.draggable.on('mirror:created', this.onMirrorCreated);
    this.draggable.on('drag:over', this.onDragOver);
    this.draggable.on('mirror:destroy', this.onMirrorDestroy);
  }

  detach() {
    this.draggable.off('mirror:create', this.onMirrorCreate);
    this.draggable.off('mirror:created', this.onMirrorCreated);
    this.draggable.off('drag:over', this.onDragOver);
    this.draggable.off('mirror:destroy', this.onMirrorDestroy);
  }

  onMirrorCreate = (event) => {
    // Enhance mirror creation
    console.log('Creating smart mirror for:', event.source);
  };

  onMirrorCreated = (event) => {
    const mirror = event.mirror;
    const source = event.source;
    
    // Store mirror reference
    this.mirrors.set(source, mirror);
    
    // Add smart features
    mirror.classList.add('smart-mirror');
    mirror.style.boxShadow = '0 5px 15px rgba(0,0,0,0.3)';
    mirror.style.transform = 'rotate(5deg) scale(1.05)';
    
    // Add drag count badge
    const badge = document.createElement('div');
    badge.className = 'drag-count-badge';
    badge.textContent = (this.dragCount || 0) + 1;
    mirror.appendChild(badge);
  };

  onDragOver = (event) => {
    const mirror = this.mirrors.get(event.originalSource);
    if (!mirror) return;

    // Change mirror appearance based on drop target
    if (event.over) {
      mirror.style.borderColor = event.over.dataset.category === 'valid' ? 'green' : 'red';
      mirror.style.opacity = '1';
    } else {
      mirror.style.borderColor = 'transparent';
      mirror.style.opacity = '0.8';
    }
  };

  onMirrorDestroy = (event) => {
    const source = event.originalSource;
    this.mirrors.delete(source);
    this.dragCount = (this.dragCount || 0) + 1;
  };
}

// Use the smart mirror plugin
const draggable = new Draggable(containers, {
  plugins: [SmartMirrorPlugin]
});

Plugin Configuration

// Configure built-in plugins
const draggable = new Draggable(containers, {
  // Mirror configuration
  mirror: {
    constrainDimensions: true,
    xAxis: true,
    yAxis: true,
    cursorOffsetX: 20,
    cursorOffsetY: 20
  },
  
  // Scrollable configuration
  scrollable: {
    speed: 25,
    sensitivity: 60,
    scrollableElements: [document.querySelector('.scroll-container')]
  },
  
  // Announcement configuration
  announcements: {
    'drag:start': 'Picked up item',
    'drag:stop': 'Dropped item',
    expire: 5000
  },
  
  // Add additional plugins
  plugins: [Plugins.Collidable, Plugins.SortAnimation],
  
  // Plugin-specific options
  collidables: '.obstacle',
  sortAnimation: {
    duration: 200,
    easingFunction: 'cubic-bezier(0.4, 0.0, 0.2, 1)'
  }
});

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