Drag and drop table row
Written byPhuoc Nguyen
Category
Level 3 — Advanced
Created
02 Apr, 2020
Last updated
07 May, 2021
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
As mentioned in the Drag and drop element in a list example, we need handle three events:
`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:
// 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);
});
#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 () {
// 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';
};
Imagine that
`list`
consists of items which are cloned from the table rows: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);
});
};
After this step, we have the following
`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>
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);
// Set the width as the original cell
newCell.style.width = `${parseInt(window.getComputedStyle(cell).width)}px`;
newRow.appendChild(newCell);
});
#Determine the indexes of dragging and target rows
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);
};
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 () {
// 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);
};
Following is the final demo. Try to drag and drop the first cell of any row.
#Demo
#See also
- Add or remove class from an element
- Attach or detach an event handler
- Calculate the mouse position relative to an element
- Clone an element
- Create an element
- Drag and drop element in a list
- Drag and drop table column
- Drag to scroll
- Get siblings of an element
- Insert an element after or before other element
- Loop over a nodelist
- Make a draggable element
- Remove an element
- Select an element or list of elements
- Set css style for an element
Questions? 🙋
Do you have any questions? Not just about this specific post, but about any topic in front-end development that you'd like to learn more about? If so, feel free to send me a message on Twitter or send me an email. You can find them at the bottom of this page.
I have a long list of upcoming posts, but your questions or ideas for the next one will be my top priority. Let's learn together! Sharing knowledge is the best way to grow 🥷.
Recent posts ⚡
Copy the content of an element to your clipboard
01 Oct, 2023
Make a text area fit its content automatically
30 Sep, 2023
Quickly insert alternate characters while typing
30 Sep, 2023
Zebra-like background
30 Sep, 2023
Add autocomplete to your text area
28 Sep, 2023
Linear scale of a number between two ranges
28 Sep, 2023
Highlight the current line in a text area
27 Sep, 2023
Create your own custom cursor in a text area
27 Sep, 2023
Mirror a text area for improving user experience
26 Sep, 2023
Display the line numbers in a text area
24 Sep, 2023
Select a given line in a text area
24 Sep, 2023
Newsletter 🔔
If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn