在查看此示例之前,建议访问此帖子以了解我们如何在列表中拖放元素。

现在我们可以使用相同的技术应用于表格行。基本思想是

  • 当用户开始移动表行时,我们创建一个项目列表。每个项目都是从表的每一行克隆的。
  • 我们在与表格相同的位置显示列表,并隐藏表格。
  • 在这一步,移动行实际上是移动列表项。
  • 当用户拖动一个项目时,我们确定目标项目在列表中的索引。并将原始拖动的行移动到与结束索引关联的行之前或之后。

让我们从表格的基本标记开始:

<table id="table">
    ...
</table>

基本设置

正如在列表中拖放元素示例中提到的,我们需要处理三个事件:

  • mousedown对于任何行的第一个单元格,因此用户可以单击并拖动每行中的第一个单元格
  • mousemovefor document:当用户移动行时触发此事件,我们将根据方向(向上或向下)创建并插入占位符行
  • mouseupfor document:当用户拖动行时发生此事件。

以下是这些事件处理程序的框架:

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

const mouseDownHandler = function(e) {
    ...

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

const mouseMoveHandler = function(e) {
    ...
};

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

// Query all rows
table.querySelectorAll('tr').forEach(function(row, index) {
    // Ignore the header
    // We don't want user to change the order of header
    if (index === 0) {
        return;
    }

    // Get the first cell of row
    const firstCell = row.firstElementChild;
    firstCell.classList.add('draggable');

    // Attach event handler
    firstCell.addEventListener('mousedown', mouseDownHandler);
});

当用户移动一行时克隆表

由于此任务执行一次,我们需要一个标志来跟踪它是否已执行:

let isDraggingStarted = false;

const mouseMoveHandler = function(e) {
    if (!isDraggingStarted) {
        isDraggingStarted = true;

        cloneTable();
    }
    ...
};

cloneTable创建一个与表格具有相同位置的元素,并显示在表格之前:

let list;

const cloneTable = function () {
    // Get the bounding rectangle of table
    const rect = table.getBoundingClientRect();

    // Get the width of table
    const width = parseInt(window.getComputedStyle(table).width);

    // Create new element
    list = document.createElement('div');

    // Set the same position as table
    list.style.position = 'absolute';
    list.style.left = `${rect.left}px`;
    list.style.top = `${rect.top}px`;

    // Insert it before the table
    table.parentNode.insertBefore(list, table);

    // Hide the table
    table.style.visibility = 'hidden';
};

想象一下,它list由从表行中克隆的项目组成:

const cloneTable = function() {
    ...

    // Loop over the rows
    table.querySelectorAll('tr').forEach(function(row) {
        const item = document.createElement('div');

        const newTable = document.createElement('table');
        const newRow = document.createElement('tr');

        // Query the cells of row
        const cells = [].slice.call(row.children);
        cells.forEach(function(cell) {
            const newCell = cell.cloneNode(true);
            newRow.appendChild(newCell);
        });

        newTable.appendChild(newRow);
        item.appendChild(newTable);

        list.appendChild(item);
    });
};

在这一步之后,我们有以下内容list

<!-- The list -->
<div>
    <!-- First item -->
    <div>
        <table>
            <!-- The first row of original table -->
            <tr>
                ...
            </tr>
        </table>
    </div>

    <!-- Second item -->
    <div>
        <table>
            <!-- The second row of original table -->
            <tr>
                ...
            </tr>
        </table>
    </div>

    <!-- ... -->
</div>

<!-- The original table -->
<table>
    ...
</table>

值得注意的是,在克隆每个项目中的单元格时,我们必须将单元格宽度设置为与原始单元格相同。所以该项目看起来完全像原始行:

cells.forEach(function (cell) {
    const newCell = cell.cloneNode(true);
    // Set the width as the original cell
    newCell.style.width = `${parseInt(window.getComputedStyle(cell).width)}px`;
    newRow.appendChild(newCell);
});

确定拖动和目标行的索引

let draggingEle; // The dragging element
let draggingRowIndex; // The index of dragging row

const mouseDownHandler = function (e) {
    // Get the original row
    const originalRow = e.target.parentNode;
    draggingRowIndex = [].slice.call(table.querySelectorAll('tr')).indexOf(originalRow);
};

const mouseMoveHandler = function (e) {
    if (!isDraggingStarted) {
        cloneTable();

        // Query the dragging element
        draggingEle = [].slice.call(list.children)[draggingRowIndex];
    }
};

const mouseUpHandler = function () {
    // Get the end index
    const endRowIndex = [].slice.call(list.children).indexOf(draggingEle);
};

正如我们有draggingRowIndexendRowIndex,现在很容易检查用户是掉到表的顶部还是底部。我们可以决定如何在拖动行之前或之后移动目标行

const mouseUpHandler = function () {
    // Move the dragged row to `endRowIndex`
    let rows = [].slice.call(table.querySelectorAll('tr'));
    draggingRowIndex > endRowIndex
        ? // User drops to the top
          rows[endRowIndex].parentNode.insertBefore(rows[draggingRowIndex], rows[endRowIndex])
        : // User drops to the bottom
          rows[endRowIndex].parentNode.insertBefore(rows[draggingRowIndex], rows[endRowIndex].nextSibling);
};

下面是最后的demo。尝试拖放任意行的第一个单元格。

演示

https://htmldom.dev/drag-and-drop-table-row/

拖放表格行