Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

JavaScript Object-Oriented Drag and Drop Implementation Patterns

Tech 1

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...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

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...

Leave a Comment

Anonymous

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