svelte-reorderable-list
Version:
A simple and accessible reorderable list component for Svelte 5.
130 lines (129 loc) • 4.08 kB
JavaScript
/**
* Creates a visual clone of an element for drag operations
*/
export function createDragClone(originalElement, clientX, clientY, offset) {
const rect = originalElement.getBoundingClientRect();
const clone = originalElement.cloneNode(true);
clone.classList.add("drag-clone");
// Get the computed margin-left to preserve tree indentation
// const computedStyle = window.getComputedStyle(originalElement);
// const marginLeft = computedStyle.marginLeft;
const x = clientX - offset.x;
const y = clientY - offset.y;
// console.log("clone", "x,y", x, y, "offset", offset);
// console.log("marginLeft", marginLeft);
clone.style.position = "fixed";
clone.style.left = `${x}px`;
clone.style.top = `${y}px`;
clone.style.width = `${rect.width}px`;
clone.style.height = `${rect.height}px`;
// clone.style.marginLeft = marginLeft; // Preserve the original margin-left
clone.style.margin = "0px";
clone.style.pointerEvents = "none";
clone.style.zIndex = "1000";
clone.style.transition = "none";
document.body.appendChild(clone);
return clone;
}
/**
* Updates the position of a drag clone element
*/
export function updateDragClonePosition(dragClone, currentPosition, offset) {
const x = currentPosition.x - offset.x;
const y = currentPosition.y - offset.y;
dragClone.style.left = `${x}px`;
dragClone.style.top = `${y}px`;
}
/**
* Gets the center point of a drag clone element
*/
export function getDragCloneCenter(dragClone) {
const rect = dragClone.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
}
/**
* Finds the closest item element from a target
*/
export function getItemElement(target, itemSelector = "[data-reorderable-item], [data-tree-item]") {
return target.closest(itemSelector);
}
/**
* Extracts item key from an element's data attribute
*/
export function getItemKeyFromElement(element) {
return element.getAttribute("data-item-key");
}
/**
* Determines if a drag operation should be allowed based on target and config
*/
export function shouldAllowDrag(target, config) {
if (config.disabled)
return false;
if (config.cssSelectorHandle) {
return !!target.closest(config.cssSelectorHandle);
}
// Don't start drag on interactive elements unless they have the handle class
const interactiveElements = ["input", "textarea", "select", "button", "a"];
if (interactiveElements.includes(target.tagName.toLowerCase())) {
return false;
}
return true;
}
/**
* Calculates the offset from cursor to element origin
*/
export function calculateDragOffset(clientX, clientY, element) {
const rect = element.getBoundingClientRect();
return {
x: clientX - rect.left,
y: clientY - rect.top,
};
}
/**
* Cleans up drag operation by removing clone and resetting state
*/
export function finalizeDragOperation(dragState) {
if (dragState.dragClone) {
document.body.removeChild(dragState.dragClone);
dragState.dragClone = null;
}
dragState.isDragging = false;
dragState.draggedItemKey = null;
}
/**
* Common focus management functions
*/
export const focusManager = {
handleFocus: (itemKey, state) => {
state.focusedItemKey = itemKey;
},
handleBlur: (state) => {
state.focusedItemKey = null;
},
setKeyboardUser: (state, isKeyboard) => {
state.isKeyboardUser = isKeyboard;
}
};
/**
* Utility to check if an element is within the bounds of another element
*/
export function isPointInElement(point, element) {
const rect = element.getBoundingClientRect();
return (point.x >= rect.left &&
point.x <= rect.right &&
point.y >= rect.top &&
point.y <= rect.bottom);
}
/**
* Debounce utility for performance optimization
*/
export function debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}