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 *
211 lines (197 loc) • 8.32 kB
JavaScript
import {SHADOW_ELEMENT_ATTRIBUTE_NAME, DRAGGED_ELEMENT_ID} from "../constants";
import {findCenter} from "./intersection";
import {svelteNodeClone} from "./svelteNodeClone";
import {getFeatureFlag, FEATURE_FLAG_NAMES} from "../featureFlags";
const TRANSITION_DURATION_SECONDS = 0.2;
/**
* private helper function - creates a transition string for a property
* @param {string} property
* @return {string} - the transition string
*/
function trs(property) {
return `${property} ${TRANSITION_DURATION_SECONDS}s ease`;
}
/**
* clones the given element and applies proper styles and transitions to the dragged element
* @param {HTMLElement} originalElement
* @param {Point} [positionCenterOnXY]
* @return {Node} - the cloned, styled element
*/
export function createDraggedElementFrom(originalElement, positionCenterOnXY) {
const rect = originalElement.getBoundingClientRect();
const draggedEl = svelteNodeClone(originalElement);
copyStylesFromTo(originalElement, draggedEl);
draggedEl.id = DRAGGED_ELEMENT_ID;
draggedEl.style.position = "fixed";
let elTopPx = rect.top;
let elLeftPx = rect.left;
draggedEl.style.top = `${elTopPx}px`;
draggedEl.style.left = `${elLeftPx}px`;
if (positionCenterOnXY) {
const center = findCenter(rect);
elTopPx -= center.y - positionCenterOnXY.y;
elLeftPx -= center.x - positionCenterOnXY.x;
window.setTimeout(() => {
draggedEl.style.top = `${elTopPx}px`;
draggedEl.style.left = `${elLeftPx}px`;
}, 0);
}
draggedEl.style.margin = "0";
// we can't have relative or automatic height and width or it will break the illusion
draggedEl.style.boxSizing = "border-box";
draggedEl.style.height = `${rect.height}px`;
draggedEl.style.width = `${rect.width}px`;
draggedEl.style.transition = `${trs("top")}, ${trs("left")}, ${trs("background-color")}, ${trs("opacity")}, ${trs("color")} `;
// this is a workaround for a strange browser bug that causes the right border to disappear when all the transitions are added at the same time
window.setTimeout(() => (draggedEl.style.transition += `, ${trs("width")}, ${trs("height")}`), 0);
draggedEl.style.zIndex = "9999";
draggedEl.style.cursor = "grabbing";
return draggedEl;
}
/**
* styles the dragged element to a 'dropped' state
* @param {HTMLElement} draggedEl
*/
export function moveDraggedElementToWasDroppedState(draggedEl) {
draggedEl.style.cursor = "grab";
}
/**
* Morphs the dragged element style, maintains the mouse pointer within the element
* @param {HTMLElement} draggedEl
* @param {HTMLElement} copyFromEl - the element the dragged element should look like, typically the shadow element
* @param {number} currentMouseX
* @param {number} currentMouseY
*/
export function morphDraggedElementToBeLike(draggedEl, copyFromEl, currentMouseX, currentMouseY) {
copyStylesFromTo(copyFromEl, draggedEl);
const newRect = copyFromEl.getBoundingClientRect();
const draggedElRect = draggedEl.getBoundingClientRect();
const widthChange = newRect.width - draggedElRect.width;
const heightChange = newRect.height - draggedElRect.height;
if (widthChange || heightChange) {
const relativeDistanceOfMousePointerFromDraggedSides = {
left: (currentMouseX - draggedElRect.left) / draggedElRect.width,
top: (currentMouseY - draggedElRect.top) / draggedElRect.height
};
if (!getFeatureFlag(FEATURE_FLAG_NAMES.USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT)) {
draggedEl.style.height = `${newRect.height}px`;
draggedEl.style.width = `${newRect.width}px`;
}
draggedEl.style.left = `${parseFloat(draggedEl.style.left) - relativeDistanceOfMousePointerFromDraggedSides.left * widthChange}px`;
draggedEl.style.top = `${parseFloat(draggedEl.style.top) - relativeDistanceOfMousePointerFromDraggedSides.top * heightChange}px`;
}
}
/**
* @param {HTMLElement} copyFromEl
* @param {HTMLElement} copyToEl
*/
function copyStylesFromTo(copyFromEl, copyToEl) {
const computedStyle = window.getComputedStyle(copyFromEl);
Array.from(computedStyle)
.filter(
s =>
s.startsWith("background") ||
s.startsWith("padding") ||
s.startsWith("font") ||
s.startsWith("text") ||
s.startsWith("align") ||
s.startsWith("justify") ||
s.startsWith("display") ||
s.startsWith("flex") ||
s.startsWith("border") ||
s === "opacity" ||
s === "color" ||
s === "list-style-type" ||
// copying with and height to make up for rect update timing issues in some browsers
(getFeatureFlag(FEATURE_FLAG_NAMES.USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT) && (s === "width" || s === "height"))
)
.forEach(s => copyToEl.style.setProperty(s, computedStyle.getPropertyValue(s), computedStyle.getPropertyPriority(s)));
}
/**
* makes the element compatible with being draggable
* @param {HTMLElement} draggableEl
* @param {boolean} dragDisabled
*/
export function styleDraggable(draggableEl, dragDisabled) {
draggableEl.draggable = false;
draggableEl.ondragstart = () => false;
if (!dragDisabled) {
draggableEl.style.userSelect = "none";
draggableEl.style.WebkitUserSelect = "none";
draggableEl.style.cursor = "grab";
} else {
draggableEl.style.userSelect = "";
draggableEl.style.WebkitUserSelect = "";
draggableEl.style.cursor = "";
}
}
/**
* Hides the provided element so that it can stay in the dom without interrupting
* @param {HTMLElement} dragTarget
*/
export function hideElement(dragTarget) {
dragTarget.style.display = "none";
dragTarget.style.position = "fixed";
dragTarget.style.zIndex = "-5";
}
/**
* styles the shadow element
* @param {HTMLElement} shadowEl
*/
export function decorateShadowEl(shadowEl) {
shadowEl.style.visibility = "hidden";
shadowEl.setAttribute(SHADOW_ELEMENT_ATTRIBUTE_NAME, "true");
}
/**
* undo the styles the shadow element
* @param {HTMLElement} shadowEl
*/
export function unDecorateShadowElement(shadowEl) {
shadowEl.style.visibility = "";
shadowEl.removeAttribute(SHADOW_ELEMENT_ATTRIBUTE_NAME);
}
/**
* will mark the given dropzones as visually active
* @param {Array<HTMLElement>} dropZones
* @param {Function} getStyles - maps a dropzone to a styles object (so the styles can be removed)
* @param {Function} getClasses - maps a dropzone to a classList
*/
export function styleActiveDropZones(dropZones, getStyles = () => {}, getClasses = () => []) {
dropZones.forEach(dz => {
const styles = getStyles(dz);
Object.keys(styles).forEach(style => {
dz.style[style] = styles[style];
});
getClasses(dz).forEach(c => dz.classList.add(c));
});
}
/**
* will remove the 'active' styling from given dropzones
* @param {Array<HTMLElement>} dropZones
* @param {Function} getStyles - maps a dropzone to a styles object
* @param {Function} getClasses - maps a dropzone to a classList
*/
export function styleInactiveDropZones(dropZones, getStyles = () => {}, getClasses = () => []) {
dropZones.forEach(dz => {
const styles = getStyles(dz);
Object.keys(styles).forEach(style => {
dz.style[style] = "";
});
getClasses(dz).forEach(c => dz.classList.contains(c) && dz.classList.remove(c));
});
}
/**
* will prevent the provided element from shrinking by setting its minWidth and minHeight to the current width and height values
* @param {HTMLElement} el
* @return {function(): void} - run this function to undo the operation and restore the original values
*/
export function preventShrinking(el) {
const originalMinHeight = el.style.minHeight;
el.style.minHeight = window.getComputedStyle(el).getPropertyValue("height");
const originalMinWidth = el.style.minWidth;
el.style.minWidth = window.getComputedStyle(el).getPropertyValue("width");
return function undo() {
el.style.minHeight = originalMinHeight;
el.style.minWidth = originalMinWidth;
};
}