I followed a tutorial for making a list that you can drag and drop vertically. I found a way to make it work horizontally, and wanted to add flex wrap to make it look better. But now when I try to drag one of the divs it starts jittering/spasming (because the div doesn't know if it should go up or down).
How do i avoid the jittering?
here's the codepen:
https://codepen.io/cavia/pen/YzvGyXb
heres my code:
const draggables = document.querySelectorAll('.draggable')
const containers = document.querySelectorAll('.container')
draggables.forEach(draggable => {
draggable.addEventListener("dragstart", () =>{
draggable.classList.add('dragging')
})
draggable.addEventListener("dragend", () =>{
draggable.classList.remove('dragging')
})
})
containers.forEach(container =>{
container.addEventListener("dragover", e => {
e.preventDefault()
const afterElement = getDragAfterElement(container, e.clientX)
const draggable = document.querySelector('.dragging')
if (afterElement == null) {
container.appendChild(draggable)
}else{
container.insertBefore(draggable, afterElement)
}
})
})
function getDragAfterElement(container, x) {
const draggableElements = [...container.querySelectorAll('.draggable:not(.dragging)')]
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect()
const offset = x - (box.left box.width / 2)
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child }
} else {
return closest
}
}, { offset: Number.NEGATIVE_INFINITY }).element
}
*{
margin: 0px;
padding: 0px;
}
.container{
background-color: #333;
padding: 1rem;
margin-top: 1rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 300px;
}
.draggable{
padding: 1rem;
background-color: white;
border: 1px solid black;
cursor: move;
width: fit-content;
}
.draggable.dragging{
opacity: .5;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
<title></title>
</head>
<body>
<div id="container">
<p draggable="true">1</p>
<p draggable="true">2</p>
<p draggable="true">3</p>
<p draggable="true">4</p>
<p draggable="true">5</p>
<p draggable="true">6</p>
<p draggable="true">7</p>
<p draggable="true">8</p>
<p draggable="true">9</p>
<p draggable="true">10</p>
<p draggable="true">11</p>
<p draggable="true">13</p>
<p draggable="true">14</p>
<p draggable="true">15</p>
</div>
</body>
</html>
CodePudding user response:
Original algorithm
The original code works by determining the nearest box on the X axis in relation to the cursor position.
It would then insert the new box after the box it found.
When no match was found, it would default to inserting it at the end.
How Flexbox impacted the algorithm
With the introduction of flexbox, we've also introduced rows.
Each row will start a X-position 0.
The algorithm would therefor find matches on different rows.
if the X postion of a box on a different row was closest, it would attempt to drop the box there instead of the row we were on.
Algorithm modifications
We can solve this by taking the rows into account.
Each box also has an Y position.
We search within that Y-range (the top side of the box and bottom side of the box) of match the same row.
A box on a different row is excluded.
This creates a new problem though, when we reach the end of a row, the element will be inserted all the way at the end, not at the end of the row.
We can solve that by checking if there's still a box in the list AND that box is also on the next row. we can then be certain to add it to the end of the current row, instead of all the way at the end.
const draggables = document.querySelectorAll(".draggable");
const containers = document.querySelectorAll(".container");
draggables.forEach((draggable) => {
draggable.addEventListener("dragstart", () => {
draggable.classList.add("dragging");
});
draggable.addEventListener("dragend", () => {
draggable.classList.remove("dragging");
});
});
containers.forEach((container) => {
container.addEventListener("dragover", (e) => {
e.preventDefault();
const afterElement = getDragAfterElement(container, e.clientX, e.clientY);
const draggable = document.querySelector(".dragging");
if (afterElement == null) {
container.appendChild(draggable);
} else {
container.insertBefore(draggable, afterElement);
}
});
});
function getDragAfterElement(container, x, y) {
const draggableElements = [
...container.querySelectorAll(".draggable:not(.dragging)")
];
return draggableElements.reduce(
(closest, child, index) => {
const box = child.getBoundingClientRect();
const nextBox = draggableElements[index 1] && draggableElements[index 1].getBoundingClientRect();
const inRow = y - box.bottom <= 0 && y - box.top >= 0; // check if this is in the same row
const offset = x - (box.left box.width / 2);
if (inRow) {
if (offset < 0 && offset > closest.offset) {
return {
offset: offset,
element: child
};
} else {
if ( // handle row ends,
nextBox && // there is a box after this one.
y - nextBox.top <= 0 && // the next is in a new row
closest.offset === Number.NEGATIVE_INFINITY // we didn't find a fit in the current row.
) {
return {
offset: 0,
element: draggableElements[index 1]
};
}
return closest;
}
} else {
return closest;
}
}, {
offset: Number.NEGATIVE_INFINITY
}
).element;
}
* {
margin: 0px;
padding: 0px;
}
.container {
background-color: #333;
padding: 1rem;
margin-top: 1rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 300px;
}
.draggable {
padding: 1rem;
background-color: white;
border: 1px solid black;
cursor: move;
width: fit-content;
}
.draggable.dragging {
opacity: 0.5;
}
<div id="container">
<p draggable="true">1</p>
<p draggable="true">2</p>
<p draggable="true">3</p>
<p draggable="true">4</p>
<p draggable="true">5</p>
<p draggable="true">6</p>
<p draggable="true">7</p>
<p draggable="true">8</p>
<p draggable="true">9</p>
<p draggable="true">10</p>
<p draggable="true">11</p>
<p draggable="true">13</p>
<p draggable="true">14</p>
<p draggable="true">15</p>
</div>