svelte-dnd-action
Version:
*An awesome drag and drop library for Svelte 3 and 4 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *
113 lines (102 loc) • 5.38 kB
JavaScript
import {getAbsoluteRectNoTransforms, isPointInsideRect, calcDistanceFromPointToCenter} from "./intersection";
import {printDebug, SHADOW_ELEMENT_ATTRIBUTE_NAME} from "../constants";
let dzToShadowIndexToRect;
/**
* Resets the cache that allows for smarter "would be index" resolution. Should be called after every drag operation
*/
export function resetIndexesCache() {
printDebug(() => "resetting indexes cache");
dzToShadowIndexToRect = new Map();
}
resetIndexesCache();
/**
* Caches the coordinates of the shadow element when it's in a certain index in a certain dropzone.
* Helpful in order to determine "would be index" more effectively
* @param {HTMLElement} dz
* @return {number} - the shadow element index
*/
function cacheShadowRect(dz) {
const shadowElIndex = Array.from(dz.children).findIndex(child => child.getAttribute(SHADOW_ELEMENT_ATTRIBUTE_NAME));
if (shadowElIndex >= 0) {
if (!dzToShadowIndexToRect.has(dz)) {
dzToShadowIndexToRect.set(dz, new Map());
}
dzToShadowIndexToRect.get(dz).set(shadowElIndex, getAbsoluteRectNoTransforms(dz.children[shadowElIndex]));
return shadowElIndex;
}
return undefined;
}
/**
* @typedef {Object} Index
* @property {number} index - the would be index
* @property {boolean} isProximityBased - false if the element is actually over the index, true if it is not over it but this index is the closest
*/
/**
* Find the index for the dragged element in the list it is dragged over
* @param {{x: number, y: number}} referencePoint - The reference point for detection (cursor position or element center)
* @param {HTMLElement} collectionBelowEl
* @returns {Index|null} - if the element is over the container the Index object otherwise null
*/
export function findWouldBeIndex(referencePoint, collectionBelowEl) {
const collectionRect = getAbsoluteRectNoTransforms(collectionBelowEl);
if (!isPointInsideRect(referencePoint, collectionRect)) {
return null;
}
const children = collectionBelowEl.children;
// the container is empty, floating element should be the first
if (children.length === 0) {
return {index: 0, isProximityBased: true};
}
const shadowElIndex = cacheShadowRect(collectionBelowEl);
// the search could be more efficient but keeping it simple for now
// a possible improvement: pass in the lastIndex it was found in and check there first, then expand from there
for (let i = 0; i < children.length; i++) {
const childRect = getAbsoluteRectNoTransforms(children[i]);
if (isPointInsideRect(referencePoint, childRect)) {
const cachedShadowRect = dzToShadowIndexToRect.has(collectionBelowEl) && dzToShadowIndexToRect.get(collectionBelowEl).get(i);
if (cachedShadowRect) {
if (!isPointInsideRect(referencePoint, cachedShadowRect)) {
return {index: shadowElIndex, isProximityBased: false};
}
}
return {index: i, isProximityBased: false};
}
}
// this can happen if there is space around the children so the floating element has
//entered the container but not any of the children, in this case we will find the nearest child
let minDistanceSoFar = Number.MAX_VALUE;
let indexOfMin = undefined;
// we are checking all of them because we don't know whether we are dealing with a horizontal or vertical container and where the floating element entered from
for (let i = 0; i < children.length; i++) {
const distance = calcDistanceFromPointToCenter(referencePoint, children[i]);
if (distance < minDistanceSoFar) {
minDistanceSoFar = distance;
indexOfMin = i;
}
}
// -------- Phantom slot check --------
// Regardless of layout (simple vertical list, flex-wrap, grid, floats …) the
// visually closest drop target can be *after* the current last **real** child.
// In simple layouts the would be index from the existing children would always be the last index
// but in more complex layouts (flex-wrap, grid, floats …) it can be any index.
// The problem is we can't predict where an additional element would be rendered in the general case,
// We therefore create a temporary, invisible clone of that last element, let
// the browser position it, measure the distance, and remove it immediately
// (same task → no paint). This leaves `children` back in its original state
// before we exit the function, so existing index-caching logic and shadow-
// element bookkeeping continue to work unchanged.
if (children.length > 0) {
const originalLen = children.length; // before we append the phantom
const template = children[originalLen - 1];
const phantom = template.cloneNode(false); // shallow clone is enough for size
phantom.style.visibility = "hidden";
phantom.style.pointerEvents = "none";
collectionBelowEl.appendChild(phantom);
const phantomDistance = calcDistanceFromPointToCenter(referencePoint, phantom);
if (phantomDistance < minDistanceSoFar) {
indexOfMin = originalLen; // index of phantom slot in original list
}
collectionBelowEl.removeChild(phantom);
}
return {index: indexOfMin, isProximityBased: true};
}