Poop Sheet

Pointer

Using Pointer Events

Pointer Capture: Taking Control

Pointer Capture for Drag Operations

element.addEventListener('pointerdown', (e) => {
    element.setPointerCapture(e.pointerId);
    // Element now receives all events for this pointer
});

Pointer capture allows an element to continue receiving events even when the pointer moves outside its boundaries:

setPointerCapture()

const slider = document.getElementById('slider');
const thumb = slider.querySelector('.thumb');
let isDragging = false;

thumb.addEventListener('pointerdown', (e) => {
    isDragging = true;
    thumb.setPointerCapture(e.pointerId);
    e.preventDefault();
});
thumb.addEventListener('pointermove', (e) => {
    if (!isDragging) return;
    
    const sliderRect = slider.getBoundingClientRect();
    const thumbWidth = thumb.offsetWidth;
    let newLeft = e.clientX - sliderRect.left - (thumbWidth / 2);
    
    // Constrain to slider bounds
    newLeft = Math.max(0, Math.min(newLeft, sliderRect.width - thumbWidth));
    thumb.style.left = newLeft + 'px';
    
    // Calculate value (0-100)
    const value = (newLeft / (sliderRect.width - thumbWidth)) * 100;
    console.log('Slider value:', Math.round(value));
});
thumb.addEventListener('pointerup', (e) => {
    isDragging = false;
    thumb.releasePointerCapture(e.pointerId);
});
thumb.addEventListener('lostpointercapture', () => {
    isDragging = false;
});

Building a Complete Draggable Component

class DraggableElement {
    constructor(element) {
        this.element = element;
        this.isDragging = false;
        this.currentPointerId = null;
        this.startX = 0;
        this.startY = 0;
        this.offsetX = 0;
        this.offsetY = 0;
        
        this.init();
    }
    
    init() {
        this.element.style.touchAction = 'none';
        this.element.style.userSelect = 'none';
        
        this.element.addEventListener('pointerdown', this.onPointerDown.bind(this));
        this.element.addEventListener('pointermove', this.onPointerMove.bind(this));
        this.element.addEventListener('pointerup', this.onPointerUp.bind(this));
        this.element.addEventListener('pointercancel', this.onPointerUp.bind(this));
    }
    
    onPointerDown(e) {
        if (this.isDragging) return;
        
        this.isDragging = true;
        this.currentPointerId = e.pointerId;
        this.element.setPointerCapture(e.pointerId);
        
        const rect = this.element.getBoundingClientRect();
        this.startX = e.clientX - rect.left;
        this.startY = e.clientY - rect.top;
        this.offsetX = rect.left;
        this.offsetY = rect.top;
        
        this.element.classList.add('dragging');
        console.log(`Started dragging with ${e.pointerType}`);
    }
    
    onPointerMove(e) {
        if (!this.isDragging || e.pointerId !== this.currentPointerId) return;
        
        const newX = e.clientX - this.startX;
        const newY = e.clientY - this.startY;
        
        this.element.style.position = 'fixed';
        this.element.style.left = newX + 'px';
        this.element.style.top = newY + 'px';
        
        // Provide haptic feedback for touch (if supported)
        if (e.pointerType === 'touch' && 'vibrate' in navigator) {
            navigator.vibrate(1);
        }
    }
    
    onPointerUp(e) {
        if (e.pointerId !== this.currentPointerId) return;
        
        this.isDragging = false;
        this.element.releasePointerCapture(e.pointerId);
        this.element.classList.remove('dragging');
        
        console.log('Drag ended');
        this.currentPointerId = null;
    }
}

// Usage
const draggable = new DraggableElement(document.getElementById('myElement'));