2024-08-31 00:30:30 +00:00
|
|
|
/**
|
2024-08-31 14:55:45 +00:00
|
|
|
* Creates a reorderable list from any `container` and viable list item selector.
|
|
|
|
*
|
2024-08-31 00:30:30 +00:00
|
|
|
* This function is absolute magic and I love it
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
* ```html
|
|
|
|
* <ul id="list">
|
|
|
|
* <li>Item 1</li>
|
|
|
|
* <li>Item 2</li>
|
|
|
|
* <li>Item 3</li>
|
|
|
|
* </ul>
|
|
|
|
* ```
|
|
|
|
* ```js
|
|
|
|
* // javascript
|
|
|
|
* makeMagicList(document.getElementById("list"), "li");
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} container The parent container to use as a list.
|
|
|
|
* @param {string} itemSelector The selector name of list item elements.
|
|
|
|
* @param {Function} callback A function to call after each reordering.
|
|
|
|
*/
|
|
|
|
export function makeMagicList(container, itemSelector, callback) {
|
|
|
|
if (!container)
|
|
|
|
throw new Error("container not provided");
|
|
|
|
if (!itemSelector)
|
|
|
|
throw new Error("itemSelector not provided");
|
|
|
|
|
|
|
|
container.querySelectorAll(itemSelector).forEach(item => {
|
|
|
|
item.draggable = true;
|
|
|
|
item.addEventListener("dragstart", () => { item.classList.add("moving") });
|
|
|
|
item.addEventListener("dragend", () => { item.classList.remove("moving") });
|
2024-08-31 14:55:45 +00:00
|
|
|
|
|
|
|
// dragging on inputs should take priority
|
2024-08-31 00:30:30 +00:00
|
|
|
item.querySelectorAll("input").forEach(el => {
|
|
|
|
el.addEventListener("mousedown", () => { item.draggable = false });
|
|
|
|
el.addEventListener("mouseup", () => { item.draggable = true });
|
|
|
|
el.addEventListener("dragstart", e => { e.stopPropagation() });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
var lastCursorY;
|
|
|
|
container.addEventListener("dragover", event => {
|
|
|
|
const dragging = container.querySelector(itemSelector + ".moving");
|
|
|
|
if (!dragging) return;
|
|
|
|
|
|
|
|
let cursorY = event.touches ? event.touches[0].clientY : event.clientY;
|
|
|
|
|
|
|
|
// don't bother processing if we haven't moved
|
|
|
|
if (lastCursorY === cursorY) return
|
|
|
|
lastCursorY = cursorY;
|
|
|
|
|
|
|
|
// get the element positioned ahead of the cursor
|
|
|
|
const notMoving = [...container.querySelectorAll(itemSelector + ":not(.moving)")];
|
|
|
|
const afterElement = notMoving.reduce((previous, current) => {
|
|
|
|
const box = current.getBoundingClientRect();
|
|
|
|
const offset = cursorY - box.top - box.height / 2;
|
|
|
|
if (offset < 0 && offset > previous.offset)
|
|
|
|
return { offset: offset, element: current };
|
|
|
|
return previous;
|
|
|
|
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
|
|
|
|
|
|
|
if (afterElement) {
|
|
|
|
container.insertBefore(dragging, afterElement);
|
|
|
|
} else {
|
|
|
|
container.appendChild(dragging);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (callback) callback();
|
|
|
|
});
|
|
|
|
}
|