在此示例中,我们将创建一个可排序列表,其项目可以在其中拖放:

<div id="list">
    <div class="draggable">A</div>
    <div class="draggable">B</div>
    <div class="draggable">C</div>
    <div class="draggable">D</div>
    <div class="draggable">E</div>
</div>

draggable每个项目都有指示用户可以拖动它的类:

.draggable {
    cursor: move;
    user-select: none;
}

使项目可拖动

通过使用制作可拖动元素帖子中提到的类似方法,我们可以将每个项目变成可拖动元素:

// The current dragging item
let draggingEle;

// The current position of mouse relative to the dragging element
let x = 0;
let y = 0;

const mouseDownHandler = function (e) {
    draggingEle = e.target;

    // Calculate the mouse position
    const rect = draggingEle.getBoundingClientRect();
    x = e.pageX - rect.left;
    y = e.pageY - rect.top;

    // Attach the listeners to `document`
    document.addEventListener('mousemove', mouseMoveHandler);
    document.addEventListener('mouseup', mouseUpHandler);
};

const mouseMoveHandler = function (e) {
    // Set position for dragging element
    draggingEle.style.position = 'absolute';
    draggingEle.style.top = `${e.pageY - y}px`;
    draggingEle.style.left = `${e.pageX - x}px`;
};

事件处理程序mouseup将删除拖动项目的位置样式并清理事件处理程序:

const mouseUpHandler = function () {
    // Remove the position styles
    draggingEle.style.removeProperty('top');
    draggingEle.style.removeProperty('left');
    draggingEle.style.removeProperty('position');

    x = null;
    y = null;
    draggingEle = null;

    // Remove the handlers of `mousemove` and `mouseup`
    document.removeEventListener('mousemove', mouseMoveHandler);
    document.removeEventListener('mouseup', mouseUpHandler);
};

现在我们可以通过遍历项目列表将mousedown事件附加到每个项目:

// Query the list element
const list = document.getElementById('list');

// Query all items
[].slice.call(list.querySelectorAll('.draggable')).forEach(function (item) {
    item.addEventListener('mousedown', mouseDownHandler);
});

添加占位符

让我们再看一下项目列表:

A B C D E

C例如,当我们拖动一个项目时,下一个项目 ( D) 将向上移动到顶部并占据拖动元素 ( ) 的区域C。为了解决这个问题,我们创建了一个动态占位符元素并将其插入到拖动元素之前。占位符的高度必须与拖动元素的高度相同。

占位符在鼠标移动期间创建一次,因此我们添加一个新标志isDraggingStarted来跟踪它:

let placeholder;
let isDraggingStarted = false;

const mouseMoveHandler = function(e) {
    const draggingRect = draggingEle.getBoundingClientRect();

    if (!isDraggingStarted) {
        // Update the flag
        isDraggingStarted = true;

        // Let the placeholder take the height of dragging element
        // So the next element won't move up
        placeholder = document.createElement('div');
        placeholder.classList.add('placeholder');
        draggingEle.parentNode.insertBefore(
            placeholder,
            draggingEle.nextSibling
        );

        // Set the placeholder's height
        placeholder.style.height = `${draggingRect.height}px`;
    }

    ...
}

一旦用户放下项目,占位符将被删除:

const mouseUpHandler = function() {
    // Remove the placeholder
    placeholder && placeholder.parentNode.removeChild(placeholder);
    // Reset the flag
    isDraggingStarted = false;

    ...
};

这是用户拖动和移动项目时元素的顺序:

A B placeholder <- The dynamic placeholder C <- The dragging item D E

确定用户是向上还是向下移动项目

首先,我们需要一个辅助函数来检查一个项目是在另一个项目之上还是之下。

如果 A 的水平中心点小于 ,则AnodeA被视为上面的。节点的中心点可以通过取其顶部和高度的一半的总和来计算:nodeBnodeAnodeB

const isAbove = function (nodeA, nodeB) {
    // Get the bounding rectangle of nodes
    const rectA = nodeA.getBoundingClientRect();
    const rectB = nodeB.getBoundingClientRect();

    return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2;
};

当用户四处移动项目时,我们定义上一个和下一个兄弟项目

const mouseMoveHandler = function (e) {
    // The current order:
    // prevEle
    // draggingEle
    // placeholder
    // nextEle
    const prevEle = draggingEle.previousElementSibling;
    const nextEle = placeholder.nextElementSibling;
};

如果用户将项目移动到顶部,我们将交换占位符和上一个项目:

const mouseMoveHandler = function(e) {
    ...

    // User moves item to the top
    if (prevEle && isAbove(draggingEle, prevEle)) {
        // The current order    -> The new order
        // prevEle              -> placeholder
        // draggingEle          -> draggingEle
        // placeholder          -> prevEle
        swap(placeholder, draggingEle);
        swap(placeholder, prevEle);
        return;
    }
};

同样,如果我们检测到用户将项目向下移动到底部,我们将交换下一个和拖动的项目:

const mouseMoveHandler = function(e) {
    ...

    // User moves the dragging element to the bottom
    if (nextEle && isAbove(nextEle, draggingEle)) {
        // The current order    -> The new order
        // draggingEle          -> nextEle
        // placeholder          -> placeholder
        // nextEle              -> draggingEle
        swap(nextEle, placeholder);
        swap(nextEle, draggingEle);
    }
};

这是一个用于交换两个节点swap的小函数:

const swap = function (nodeA, nodeB) {
    const parentA = nodeA.parentNode;
    const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;

    // Move `nodeA` to before the `nodeB`
    nodeB.parentNode.insertBefore(nodeA, nodeB);

    // Move `nodeB` to before the sibling of `nodeA`
    parentA.insertBefore(nodeB, siblingA);
};

下面是最后的demo。尝试拖放任何项目!

演示

演示
https://htmldom.dev/drag-and-drop-element-in-a-list/

拖放列表中的元素