Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

JavaScript Object-Oriented Drag and Drop Implementation Patterns

Tech Apr 23 9

The Core Challenge: Event Handler Context Binding

When implementing drag functionality using event listeners, the primary obstacle is maintaining correct this context. The bind() method creates a new function on each call, which prevents proper event listener removal since the bound function added to the element differs from the one targeted for removal.

Solution: Pre-binding Event Handlers

class Draggable {
    constructor(element) {
        this.element = element;
        // Store bound references to ensure addEventListener and removeEventListener use the same function
        this.boundMove = this.handleMove.bind(this);
        this.boundEnd = this.handleEnd.bind(this);
        this.init();
    }

    init() {
        this.element.addEventListener('mousedown', this.handleStart.bind(this));
    }

    handleStart(event) {
        this.offsetX = event.offsetX;
        this.offsetY = event.offsetY;
        document.addEventListener('mousemove', this.boundMove);
        document.addEventListener('mouseup', this.boundEnd);
        event.preventDefault();
    }

    handleMove(event) {
        this.element.style.left = (event.clientX - this.offsetX) + 'px';
        this.element.style.top = (event.clientY - this.offsetY) + 'px';
    }

    handleEnd() {
        document.removeEventListener('mousemove', this.boundMove);
        document.removeEventListener('mouseup', this.boundEnd);
    }
}

const container = document.querySelector('.container');
new Draggable(container);

Prototype Inheritance Approach

For scenarios requiring multiple drag implementations with shared functionality:

function createDraggable(element) {
    this.targetElement = element;
    this.moveCallback = this.onMove.bind(this);
    this.stopCallback = this.onStop.bind(this);
    this.attachEvents();
}

createDraggable.prototype.attachEvents = function() {
    this.targetElement.addEventListener('mousedown', this.startDrag.bind(this));
};

createDraggable.prototype.startDrag = function(event) {
    this.startX = event.offsetX;
    this.startY = event.offsetY;
    document.addEventListener('mousemove', this.moveCallback);
    document.addEventListener('mouseup', this.stopCallback);
    event.preventDefault();
};

createDraggable.prototype.onMove = function(event) {
    this.targetElement.style.left = (event.clientX - this.startX) + 'px';
    this.targetElement.style.top = (event.clientY - this.startY) + 'px';
};

createDraggable.prototype.onStop = function() {
    document.removeEventListener('mousemove', this.moveCallback);
    document.removeEventListener('mouseup', this.stopCallback);
};

function createBoundedDrag(element) {
    createDraggable.call(this, element);
}

for (const key in createDraggable.prototype) {
    if (createDraggable.prototype.hasOwnProperty(key)) {
        createBoundedDrag.prototype[key] = createDraggable.prototype[key];
    }
}

const boxA = document.querySelector('.box-a');
const boxB = document.querySelector('.box-b');

new createDraggable(boxA);
new createBoundedDrag(boxB);

ES6 Class Extension Pattern

Modern JavaScript provides cleaner inheritance through class syntax:

class BaseDrag {
    constructor(element) {
        this.target = element;
        this.handleMove = this.processMove.bind(this);
        this.handleStop = this.processStop.bind(this);
        this.setupListeners();
    }

    setupListeners() {
        this.target.addEventListener('mousedown', this.initiateDrag.bind(this));
    }

    initiateDrag(event) {
        this.deltaX = event.offsetX;
        this.deltaY = event.offsetY;
        document.addEventListener('mousemove', this.handleMove);
        document.addEventListener('mouseup', this.handleStop);
        event.preventDefault();
    }

    processMove(event) {
        this.target.style.left = (event.clientX - this.deltaX) + 'px';
        this.target.style.top = (event.clientY - this.deltaY) + 'px';
    }

    processStop() {
        document.removeEventListener('mousemove', this.handleMove);
        document.removeEventListener('mouseup', this.handleStop);
    }
}

class RestrictedDrag extends BaseDrag {
    constructor(element) {
        super(element);
    }
}

const firstBox = document.querySelector('.first');
const secondBox = document.querySelector('.second');

new BaseDrag(firstBox);
new RestrictedDrag(secondBox);

Key Implementation Details

The fundamental pattern involves storing bound function references as instance properties during construction. This ensures that when addEventListener is called with the bound functon, the same reference can be used in removeEventListener to properly detach the handler.

Inheritance becomes valuable when multiple draggable elements share core movement logic but require customization for specific behaviors. The parent clas encapsulates common functionality while subclasses override or extend methods as needed.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.