I have taken some Javascript code for dragging and dropping rows in a HTML table from this link below and adapted it to my personal project. But after some time, I came to notice an unexpected behaviour when the table is too large, in fact when it is vertically larger than the viewport size.
The unexpected behaviour is that when both these conditions are met:
the table has many rows
you have scrolled down
Then, the row being dragged is not under the cursor, but at some distance which I presumably suspect equal to the difference between the top of the page and the top of the viewport.
An image will illustrate the problem here:
As you can see the row maintains a vertical distance some pixels above the cursor in every moment while being dragged.
I believe this line to be the cause of the problem:
draggingEle.style.top = `${draggingEle.offsetTop + e.clientY - y}px`;
given that offsetTop is relative to only the viewport dimensions, but I can't find any attribute similar relative to the whole page and anything I'm trying does not work out (tried things like top, offsetHeight, pageY, screenY in several elements there).
The code is this:
document.addEventListener('DOMContentLoaded', function () {
const table = document.getElementById('table');
let draggingEle;
let draggingRowIndex;
let placeholder;
let list;
let isDraggingStarted = false;
// The current position of mouse relative to the dragging element
let x = 0;
let y = 0;
// Swap two nodes
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);
};
// Check if `nodeA` is above `nodeB`
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 cloneTable = function () {
const rect = table.getBoundingClientRect();
const width = parseInt(window.getComputedStyle(table).width);
list = document.createElement('div');
list.classList.add('clone-list');
list.style.position = 'absolute';
list.style.left = `${rect.left}px`;
list.style.top = `${rect.top}px`;
table.parentNode.insertBefore(list, table);
// Hide the original table
table.style.visibility = 'hidden';
table.querySelectorAll('tr').forEach(function (row) {
// Create a new table from given row
const item = document.createElement('div');
item.classList.add('draggable');
const newTable = document.createElement('table');
newTable.setAttribute('class', 'clone-table');
newTable.style.width = `${width}px`;
const newRow = document.createElement('tr');
const cells = [].slice.call(row.children);
cells.forEach(function (cell) {
const newCell = cell.cloneNode(true);
newCell.style.width = `${parseInt(window.getComputedStyle(cell).width)}px`;
newRow.appendChild(newCell);
});
newTable.appendChild(newRow);
item.appendChild(newTable);
list.appendChild(item);
});
};
const mouseDownHandler = function (e) {
// Get the original row
const originalRow = e.target.parentNode;
draggingRowIndex = [].slice.call(table.querySelectorAll('tr')).indexOf(originalRow);
// Determine the mouse position
x = e.clientX;
y = e.clientY;
// Attach the listeners to `document`
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
const mouseMoveHandler = function (e) {
if (!isDraggingStarted) {
isDraggingStarted = true;
cloneTable();
draggingEle = [].slice.call(list.children)[draggingRowIndex];
draggingEle.classList.add('dragging');
// 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);
placeholder.style.height = `${draggingEle.offsetHeight}px`;
}
// Set position for dragging element
draggingEle.style.position = 'absolute';
draggingEle.style.top = `${draggingEle.offsetTop + e.clientY - y}px`;
draggingEle.style.left = `${draggingEle.offsetLeft + e.clientX - x}px`;
// Reassign the position of mouse
x = e.clientX;
y = e.clientY;
// The current order
// prevEle
// draggingEle
// placeholder
// nextEle
const prevEle = draggingEle.previousElementSibling;
const nextEle = placeholder.nextElementSibling;
// The dragging element is above the previous element
// User moves the dragging element to the top
// We don't allow to drop above the header
// (which doesn't have `previousElementSibling`)
if (prevEle && prevEle.previousElementSibling && isAbove(draggingEle, prevEle)) {
// The current order -> The new order
// prevEle -> placeholder
// draggingEle -> draggingEle
// placeholder -> prevEle
swap(placeholder, draggingEle);
swap(placeholder, prevEle);
return;
}
// The dragging element is below the next element
// 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);
}
};
const mouseUpHandler = function () {
// Remove the placeholder
placeholder && placeholder.parentNode.removeChild(placeholder);
draggingEle.classList.remove('dragging');
draggingEle.style.removeProperty('top');
draggingEle.style.removeProperty('left');
draggingEle.style.removeProperty('position');
// Get the end index
const endRowIndex = [].slice.call(list.children).indexOf(draggingEle);
isDraggingStarted = false;
// Remove the `list` element
list.parentNode.removeChild(list);
// Move the dragged row to `endRowIndex`
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
);
// Bring back the table
table.style.removeProperty('visibility');
// Remove the handlers of `mousemove` and `mouseup`
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
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;
}
const firstCell = row.firstElementChild;
firstCell.classList.add('draggable');
firstCell.addEventListener('mousedown', mouseDownHandler);
});
});
.table {
border: 1px solid #ccc;
border-collapse: collapse;
}
.table th,
.table td {
border: 1px solid #ccc;
}
.table th,
.table td {
padding: 0.5rem;
}
.draggable {
cursor: move;
user-select: none;
}
.placeholder {
background-color: #edf2f7;
border: 2px dashed #cbd5e0;
}
.clone-list {
border-top: 1px solid #ccc;
}
.clone-table {
border-collapse: collapse;
border: none;
}
.clone-table th,
.clone-table td {
border: 1px solid #ccc;
border-top: none;
padding: 0.5rem;
}
.dragging {
background: #fff;
border-top: 1px solid #ccc;
z-index: 999;
}
<html lang="en">
<head>
<meta charset="utf-8" />
<title>HTML DOM - Drag and drop table row</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/demo.css" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter&family=Source+Code+Pro&display=swap"
/>
</head>
<body>
<div style="padding: 4rem 0">
<table id="table" class="table">
<thead>
<tr>
<th data-type="number">No.</th>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
<tr>
<td>1</td>
<td>Andrea</td>
<td>Ross</td>
</tr>
<tr>
<td>2</td>
<td>Penelope</td>
<td>Mills</td>
</tr>
<tr>
<td>3</td>
<td>Sarah</td>
<td>Grant</td>
</tr>
<tr>
<td>4</td>
<td>Vanessa</td>
<td>Roberts</td>
</tr>
<tr>
<td>5</td>
<td>Oliver</td>
<td>Alsop</td>
</tr>
<tr>
<td>6</td>
<td>Jennifer</td>
<td>Forsyth</td>
</tr>
<tr>
<td>7</td>
<td>Michelle</td>
<td>King</td>
</tr>
<tr>
<td>8</td>
<td>Steven</td>
<td>Kelly</td>
</tr>
<tr>
<td>9</td>
<td>Julian</td>
<td>Ferguson</td>
</tr>
<tr>
<td>10</td>
<td>Chloe</td>
<td>Ince</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
