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

sensors.mddocs/

Sensors

Sensors detect and handle different types of input events (mouse, touch, keyboard, native drag) and convert them into unified sensor events that the draggable system can process. They provide a pluggable architecture for supporting various input methods.

Capabilities

Base Sensor Class

All sensors extend the base Sensor class which provides common functionality.

/**
 * Base sensor class for input detection and handling
 */
class Sensor {
  constructor(containers: HTMLElement | HTMLElement[] | NodeList, options?: SensorOptions);
  attach(): this;
  detach(): this;
  addContainer(...containers: HTMLElement[]): void;
  removeContainer(...containers: HTMLElement[]): void;
  trigger(element: HTMLElement, sensorEvent: SensorEvent): SensorEvent;
}

interface SensorOptions {
  delay?: number | DelayOptions;
}

Usage Example:

import { MouseSensor, TouchSensor } from "@shopify/draggable";

// Create custom sensor configuration
const containers = document.querySelectorAll('.container');
const mouseSensor = new MouseSensor(containers, {
  delay: { mouse: 100 }
});

const touchSensor = new TouchSensor(containers, {
  delay: { touch: 200 }
});

// Use with draggable
const draggable = new Draggable(containers, {
  sensors: [mouseSensor, touchSensor]
});

Built-in Sensors

Draggable includes several built-in sensor implementations.

// Available through Draggable.Sensors static property
interface Sensors {
  DragSensor: typeof DragSensor;
  MouseSensor: typeof MouseSensor;
  TouchSensor: typeof TouchSensor;
  ForceTouchSensor: typeof ForceTouchSensor;
}

class MouseSensor extends Sensor {}
class TouchSensor extends Sensor {}
class DragSensor extends Sensor {}
class ForceTouchSensor extends Sensor {}

Built-in Sensors:

  • MouseSensor: Handles mouse click and drag events
  • TouchSensor: Handles touch events for mobile devices
  • DragSensor: Handles native HTML5 drag and drop events
  • ForceTouchSensor: Handles force touch/pressure-sensitive input

Usage Example:

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

// Access built-in sensors
const { MouseSensor, TouchSensor, ForceTouchSensor } = Draggable.Sensors;

const draggable = new Draggable(containers, {
  sensors: [MouseSensor, TouchSensor, ForceTouchSensor],
  exclude: {
    sensors: [TouchSensor] // Exclude touch on desktop
  }
});

Sensor Events

Sensors generate unified sensor events that contain input information.

class SensorEvent extends AbstractEvent {
  readonly originalEvent: Event;
  readonly clientX: number;
  readonly clientY: number;
  readonly target: HTMLElement;
  readonly container: HTMLElement;
  readonly pressure: number;
}

class DragStartSensorEvent extends SensorEvent {}
class DragMoveSensorEvent extends SensorEvent {}
class DragStopSensorEvent extends SensorEvent {}
class DragPressureSensorEvent extends SensorEvent {}

Usage Example:

draggable.on('drag:move', (event) => {
  const sensorEvent = event.sensorEvent;
  console.log('Mouse/touch position:', sensorEvent.clientX, sensorEvent.clientY);
  console.log('Pressure:', sensorEvent.pressure);
  console.log('Original browser event:', sensorEvent.originalEvent);
});

Sensor Management

Methods for managing sensors on draggable instances.

/**
 * Add sensors to a draggable instance
 * @param sensors - Sensor classes to add
 */
addSensor(...sensors: (typeof Sensor)[]): this;

/**
 * Remove sensors from a draggable instance
 * @param sensors - Sensor classes to remove
 */
removeSensor(...sensors: (typeof Sensor)[]): this;

Usage Example:

const draggable = new Draggable(containers);

// Add additional sensors
draggable.addSensor(Draggable.Sensors.ForceTouchSensor);

// Remove default sensors
draggable.removeSensor(Draggable.Sensors.TouchSensor);

// Add custom sensor
class KeyboardSensor extends Sensor {
  attach() {
    // Custom keyboard handling
    document.addEventListener('keydown', this.onKeyDown);
    return this;
  }
  
  detach() {
    document.removeEventListener('keydown', this.onKeyDown);
    return this;
  }
  
  onKeyDown = (event) => {
    // Custom keyboard drag logic
  }
}

draggable.addSensor(KeyboardSensor);

Container Management

Sensors can dynamically manage which containers they monitor.

/**
 * Add containers to sensor monitoring
 * @param containers - Container elements to add
 */
addContainer(...containers: HTMLElement[]): void;

/**
 * Remove containers from sensor monitoring
 * @param containers - Container elements to remove
 */
removeContainer(...containers: HTMLElement[]): void;

Usage Example:

const mouseSensor = new MouseSensor([container1, container2]);

// Add more containers later
mouseSensor.addContainer(container3, container4);

// Remove a container
mouseSensor.removeContainer(container1);

Custom Sensor Implementation

class CustomSensor extends Sensor {
  constructor(containers, options = {}) {
    super(containers, options);
    this.handleStart = this.handleStart.bind(this);
    this.handleMove = this.handleMove.bind(this);
    this.handleEnd = this.handleEnd.bind(this);
  }

  attach() {
    // Add event listeners to containers
    this.containers.forEach(container => {
      container.addEventListener('customstart', this.handleStart);
      container.addEventListener('custommove', this.handleMove);
      container.addEventListener('customend', this.handleEnd);
    });
    return this;
  }

  detach() {
    // Remove event listeners
    this.containers.forEach(container => {
      container.removeEventListener('customstart', this.handleStart);
      container.removeEventListener('custommove', this.handleMove);
      container.removeEventListener('customend', this.handleEnd);
    });
    return this;
  }

  handleStart(event) {
    const sensorEvent = new DragStartSensorEvent({
      originalEvent: event,
      clientX: event.clientX || 0,
      clientY: event.clientY || 0,
      target: event.target,
      container: event.currentTarget,
      pressure: 0
    });

    this.trigger(event.target, sensorEvent);
  }

  handleMove(event) {
    const sensorEvent = new DragMoveSensorEvent({
      originalEvent: event,
      clientX: event.clientX || 0,
      clientY: event.clientY || 0,
      target: event.target,
      container: event.currentTarget,
      pressure: 0
    });

    this.trigger(event.target, sensorEvent);
  }

  handleEnd(event) {
    const sensorEvent = new DragStopSensorEvent({
      originalEvent: event,
      clientX: event.clientX || 0,
      clientY: event.clientY || 0,
      target: event.target,
      container: event.currentTarget,
      pressure: 0
    });

    this.trigger(event.target, sensorEvent);
  }
}

// Use custom sensor
const draggable = new Draggable(containers, {
  sensors: [CustomSensor]
});

Sensor Configuration

// Configure sensor delays
const draggable = new Draggable(containers, {
  delay: {
    mouse: 150,    // 150ms delay for mouse
    touch: 200,    // 200ms delay for touch
    drag: 0        // No delay for native drag
  }
});

// Or configure per sensor
const mouseSensor = new MouseSensor(containers, {
  delay: { mouse: 100 }
});

const touchSensor = new TouchSensor(containers, {
  delay: { touch: 300 }
});

Complete Sensor Example

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

// Custom gesture sensor for advanced touch interactions
class GestureSensor extends Sensor {
  constructor(containers, options) {
    super(containers, options);
    this.touches = new Map();
    this.gestureThreshold = options.gestureThreshold || 50;
  }

  attach() {
    this.containers.forEach(container => {
      container.addEventListener('touchstart', this.onTouchStart, { passive: false });
      container.addEventListener('touchmove', this.onTouchMove, { passive: false });
      container.addEventListener('touchend', this.onTouchEnd);
    });
    return this;
  }

  detach() {
    this.containers.forEach(container => {
      container.removeEventListener('touchstart', this.onTouchStart);
      container.removeEventListener('touchmove', this.onTouchMove);
      container.removeEventListener('touchend', this.onTouchEnd);
    });
    return this;
  }

  onTouchStart = (event) => {
    // Track multiple touches for gesture detection
    Array.from(event.touches).forEach(touch => {
      this.touches.set(touch.identifier, {
        startX: touch.clientX,
        startY: touch.clientY,
        currentX: touch.clientX,
        currentY: touch.clientY
      });
    });

    if (event.touches.length === 1) {
      // Single touch - start drag
      const touch = event.touches[0];
      const sensorEvent = new DragStartSensorEvent({
        originalEvent: event,
        clientX: touch.clientX,
        clientY: touch.clientY,
        target: event.target,
        container: event.currentTarget,
        pressure: touch.force || 0
      });

      this.trigger(event.target, sensorEvent);
    }
  };

  onTouchMove = (event) => {
    if (event.touches.length === 1) {
      // Single touch - continue drag
      const touch = event.touches[0];
      const sensorEvent = new DragMoveSensorEvent({
        originalEvent: event,
        clientX: touch.clientX,
        clientY: touch.clientY,
        target: event.target,
        container: event.currentTarget,
        pressure: touch.force || 0
      });

      this.trigger(event.target, sensorEvent);
    } else if (event.touches.length === 2) {
      // Two touches - handle pinch/zoom gesture
      this.handlePinchGesture(event);
    }
  };

  onTouchEnd = (event) => {
    // Clean up touch tracking
    Array.from(event.changedTouches).forEach(touch => {
      this.touches.delete(touch.identifier);
    });

    if (event.touches.length === 0) {
      // No more touches - end drag
      const touch = event.changedTouches[0];
      const sensorEvent = new DragStopSensorEvent({
        originalEvent: event,
        clientX: touch.clientX,
        clientY: touch.clientY,
        target: event.target,
        container: event.currentTarget,
        pressure: 0
      });

      this.trigger(event.target, sensorEvent);
    }
  };

  handlePinchGesture(event) {
    // Custom pinch gesture logic
    const [touch1, touch2] = event.touches;
    const distance = Math.sqrt(
      Math.pow(touch2.clientX - touch1.clientX, 2) +
      Math.pow(touch2.clientY - touch1.clientY, 2)
    );

    // Emit custom gesture event
    const gestureEvent = new CustomEvent('pinch', {
      detail: { distance, touches: [touch1, touch2] }
    });
    event.target.dispatchEvent(gestureEvent);
  }
}

// Use the custom sensor
const draggable = new Draggable(containers, {
  sensors: [GestureSensor],
  gestureThreshold: 75
});

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