The JavaScript Drag & Drop library your grandparents warned you about.
—
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.
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]
});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:
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
}
});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);
});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);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);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]
});// 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 }
});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