preface

Drag and drop sorting should be familiar to your friends. In your daily work, you may choose to use open source libraries such as sortable.js to implement requirements. But after you’ve done your requirements, have you ever wondered how drag and drop sort works? I spent some time researching it and share it with you today.

implementation

HTML

<ul class="column">
    <li class="column-item">item1</li>
    <li class="column-item">item2</li>
    <li class="column-item">item3</li>
    <li class="column-item">item4</li>
    <li class="column-item">item5</li>
    <li class="column-item">item6</li>
    <li class="column-item">item7</li>
    <li class="column-item">item8</li>
    <li class="column-item">item9</li>
    <li class="column-item">item10</li>
</ul>
Copy the code

js

ES6 Class is used

class Draggable {
    constructor(options) {
        this.parent = options.element; // Parent element
        this.cloneElementClassName = options.cloneElementClassName; // Clone the element class name
        this.isPointerdown = false;
        this.diff = { x: 0.y: 0 }; // The difference from the last move
        this.drag = { element: null.index: 0.lastIndex: 0 }; // Drag and drop elements
        this.drop = { element: null.index: 0.lastIndex: 0 }; // Release the element
        this.clone = { element: null.x: 0.y: 0 };
        this.lastPointermove = { x: 0.y: 0 };
        this.rectList = []; // Used to save the data obtained by the drag item getBoundingClientRect()
        this.init();
    }
    init() {
        this.getRect();
        this.bindEventListener();
    }
    // Get element position information
    getRect() {
        this.rectList.length = 0;
        for (const item of this.parent.children) {
            this.rectList.push(item.getBoundingClientRect()); }}handlePointerdown(e) {
        // If the mouse clicks, only the left button responds
        if (e.pointerType === 'mouse'&& e.button ! = =0) {
            return;
        }
        if (e.target === this.parent) {
            return;
        }
        this.isPointerdown = true;
        this.parent.setPointerCapture(e.pointerId);
        this.lastPointermove.x = e.clientX;
        this.lastPointermove.y = e.clientY;
        this.drag.element = e.target;
        this.drag.element.classList.add('active');
        this.clone.element = this.drag.element.cloneNode(true);
        this.clone.element.className = this.cloneElementClassName;
        this.clone.element.style.transition = 'none';
        const i = [].indexOf.call(this.parent.children, this.drag.element);
        this.clone.x = this.rectList[i].left;
        this.clone.y = this.rectList[i].top;
        this.drag.index = i;
        this.drag.lastIndex = i;
        this.clone.element.style.transform = 'translate3d(' + this.clone.x + 'px, ' + this.clone.y + 'px, 0)';
        document.body.appendChild(this.clone.element);
    }
    handlePointermove(e) {
        if (this.isPointerdown) {
            this.diff.x = e.clientX - this.lastPointermove.x;
            this.diff.y = e.clientY - this.lastPointermove.y;
            this.lastPointermove.x = e.clientX;
            this.lastPointermove.y = e.clientY;
            this.clone.x += this.diff.x;
            this.clone.y += this.diff.y;
            this.clone.element.style.transform = 'translate3d(' + this.clone.x + 'px, ' + this.clone.y + 'px, 0)';
            for (let i = 0; i < this.rectList.length; i++) {
                // Collision detection
                if (e.clientX > this.rectList[i].left && e.clientX < this.rectList[i].right &&
                    e.clientY > this.rectList[i].top && e.clientY < this.rectList[i].bottom) {
                    this.drop.element = this.parent.children[i];
                    this.drop.lastIndex = i;
                    if (this.drag.element ! = =this.drop.element) {
                        if (this.drag.index < i) {
                            this.parent.insertBefore(this.drag.element, this.drop.element.nextElementSibling);
                            this.drop.index = i - 1;
                        } else {
                            this.parent.insertBefore(this.drag.element, this.drop.element);
                            this.drop.index = i + 1;
                        }
                        this.drag.index = i;
                        const dragRect = this.rectList[this.drag.index];
                        const lastDragRect = this.rectList[this.drag.lastIndex];
                        const dropRect = this.rectList[this.drop.index];
                        const lastDropRect = this.rectList[this.drop.lastIndex];
                        this.drag.lastIndex = i;
                        this.drag.element.style.transition = 'none';
                        this.drop.element.style.transition = 'none';
                        this.drag.element.style.transform = 'translate3d(' + (lastDragRect.left - dragRect.left) + 'px, ' + (lastDragRect.top - dragRect.top) + 'px, 0)';
                        this.drop.element.style.transform = 'translate3d(' + (lastDropRect.left - dropRect.left) + 'px, ' + (lastDropRect.top - dropRect.top) + 'px, 0)';
                        this.drag.element.offsetLeft; // Trigger redraw
                        this.drag.element.style.transition = 'transform 150ms';
                        this.drop.element.style.transition = 'transform 150ms';
                        this.drag.element.style.transform = 'translate3d(0px, 0px, 0px)';
                        this.drop.element.style.transform = 'translate3d(0px, 0px, 0px)';
                    }
                    break; }}}}handlePointerup(e) {
        if (this.isPointerdown) {
            this.isPointerdown = false;
            this.drag.element.classList.remove('active');
            this.clone.element.remove(); }}handlePointercancel(e) {
        if (this.isPointerdown) {
            this.isPointerdown = false;
            this.drag.element.classList.remove('active');
            this.clone.element.remove(); }}bindEventListener() {
        this.handlePointerdown = this.handlePointerdown.bind(this);
        this.handlePointermove = this.handlePointermove.bind(this);
        this.handlePointerup = this.handlePointerup.bind(this);
        this.handlePointercancel = this.handlePointercancel.bind(this);
        this.getRect = this.getRect.bind(this);
        this.parent.addEventListener('pointerdown'.this.handlePointerdown);
        this.parent.addEventListener('pointermove'.this.handlePointermove);
        this.parent.addEventListener('pointerup'.this.handlePointerup);
        this.parent.addEventListener('pointercancel'.this.handlePointercancel);
        window.addEventListener('scroll'.this.getRect);
        window.addEventListener('resize'.this.getRect);
        window.addEventListener('orientationchange'.this.getRect);
    }
    unbindEventListener() {
        this.parent.removeEventListener('pointerdown'.this.handlePointerdown);
        this.parent.removeEventListener('pointermove'.this.handlePointermove);
        this.parent.removeEventListener('pointerup'.this.handlePointerup);
        this.parent.removeEventListener('pointercancel'.this.handlePointercancel);
        window.removeEventListener('scroll'.this.getRect);
        window.removeEventListener('resize'.this.getRect);
        window.removeEventListener('orientationchange'.this.getRect); }}/ / instantiate
new Draggable({
    element: document.querySelector('.column'),
    cloneElementClassName: 'clone-column-item'
});
Copy the code

Demo: jsdemo. Codeman. Top/HTML/dragga…

Why not use the HTML drag and drop API implementation?

Since the native HTML drag and drop API is not available on the mobile side, the PointerEvent event is used to implement the drag logic for compatibility between PC and mobile.

Write in the last

So far, the basic function of drag and drop sort has been realized, but there are still many deficiencies. Features like nested drags, cross-list drags, and bottom-scrolling are not implemented. Interested partners can refer to the open source library for their own research.