Before taking a look at this example, it's recommended to visit this
post to know how we can drag and drop element in a list.
Now we can use the same technique to apply to the table rows. The basic idea is
- When user starts moving the table row, we create a list of items. Each item is cloned from each row of table.
- We show the list at the same position as table, and hide the table.
- At this step, moving row around is actually moving the list item.
- When user drags an item, we determine the index of target item within the list. And move the original dragged row to before or after the row associated with the end index.
Let's get started with the basic markup of table:
<table id="table">
...
</table>
Basic setup
mousedown
for the first cell of any row, so user can click and drag the first cell in each row
mousemove
for document
: This event triggers when user moves the row around, and we will create and insert a placeholder row depending on the direction (up or down)
mouseup
for document
: This event occurs when user drags the row.
Here is the skeleton of these event handlers:
const table = document.getElementById('table');
const mouseDownHandler = function(e) {
...
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
const mouseMoveHandler = function(e) {
...
};
const mouseUpHandler = function() {
...
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
table.querySelectorAll('tr').forEach(function(row, index) {
if (index === 0) {
return;
}
const firstCell = row.firstElementChild;
firstCell.classList.add('draggable');
firstCell.addEventListener('mousedown', mouseDownHandler);
});
Clone the table when user is moving a row
Since this task is performed once, we need a flag to track if it's executed:
let isDraggingStarted = false;
const mouseMoveHandler = function(e) {
if (!isDraggingStarted) {
isDraggingStarted = true;
cloneTable();
}
...
};
cloneTable
creates an element that has the same position as the table, and is shown right before the table:
let list;
const cloneTable = function () {
const rect = table.getBoundingClientRect();
const width = parseInt(window.getComputedStyle(table).width);
list = document.createElement('div');
list.style.position = 'absolute';
list.style.left = `${rect.left}px`;
list.style.top = `${rect.top}px`;
table.parentNode.insertBefore(list, table);
table.style.visibility = 'hidden';
};
Imagine that list
consists of items which are cloned from the table rows:
const cloneTable = function() {
...
table.querySelectorAll('tr').forEach(function(row) {
const item = document.createElement('div');
const newTable = document.createElement('table');
const newRow = document.createElement('tr');
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);
});
};
After this step, we have the following list
:
<div>
<div>
<table>
<tr>
...
</tr>
</table>
</div>
<div>
<table>
<tr>
...
</tr>
</table>
</div>
</div>
<table>
...
</table>
It's worth noting that when cloning cells in each item, we have to set the cell width same as the original cell.
So the item looks like the original row completely:
cells.forEach(function (cell) {
const newCell = cell.cloneNode(true);
newCell.style.width = `${parseInt(window.getComputedStyle(cell).width)}px`;
newRow.appendChild(newCell);
});
Determine the indexes of dragging and target rows
let draggingEle;
let draggingRowIndex;
const mouseDownHandler = function (e) {
const originalRow = e.target.parentNode;
draggingRowIndex = [].slice.call(table.querySelectorAll('tr')).indexOf(originalRow);
};
const mouseMoveHandler = function (e) {
if (!isDraggingStarted) {
cloneTable();
draggingEle = [].slice.call(list.children)[draggingRowIndex];
}
};
const mouseUpHandler = function () {
const endRowIndex = [].slice.call(list.children).indexOf(draggingEle);
};
As we have
draggingRowIndex
and
endRowIndex
, it's now easy to check if user drops to the top or bottom of table.
And we can decide how to move the target row
before or after the dragging row:
const mouseUpHandler = function () {
let rows = [].slice.call(table.querySelectorAll('tr'));
draggingRowIndex > endRowIndex
?
rows[endRowIndex].parentNode.insertBefore(rows[draggingRowIndex], rows[endRowIndex])
:
rows[endRowIndex].parentNode.insertBefore(rows[draggingRowIndex], rows[endRowIndex].nextSibling);
};
Following is the final demo. Try to drag and drop the first cell of any row.
Demo
See also