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 class="container" id="container">
<p class="draggable" draggable="true">1</p>
<p class="draggable" draggable="true">2</p>
<p class="draggable" draggable="true">3</p>
<p class="draggable" draggable="true">4</p>
<p class="draggable" draggable="true">5</p>
<p class="draggable" draggable="true">6</p>
<p class="draggable" draggable="true">7</p>
<p class="draggable" draggable="true">8</p>
<p class="draggable" draggable="true">9</p>
<p class="draggable" draggable="true">10</p>
<p class="draggable" draggable="true">11</p>
<p class="draggable" draggable="true">13</p>
<p class="draggable" draggable="true">14</p>
<p class="draggable" draggable="true">15</p>
</div>