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 *
125 lines (120 loc) • 5.24 kB
JavaScript
import {dndzone as pointerDndZone} from "./pointerAction";
import {dndzone as keyboardDndZone} from "./keyboardAction";
import {ITEM_ID_KEY, SHADOW_ELEMENT_HINT_ATTRIBUTE_NAME} from "./constants";
import {toString} from "./helpers/util";
/**
* A custom action to turn any container to a dnd zone and all of its direct children to draggables
* Supports mouse, touch and keyboard interactions.
* Dispatches two events that the container is expected to react to by modifying its list of items,
* which will then feed back in to this action via the update function
*
* @typedef {object} Options
* @property {array} items - the list of items that was used to generate the children of the given node (the list used in the #each block
* @property {string} [type] - the type of the dnd zone. children dragged from here can only be dropped in other zones of the same type, default to a base type
* @property {number} [flipDurationMs] - if the list animated using flip (recommended), specifies the flip duration such that everything syncs with it without conflict, defaults to zero
* @property {boolean} [dragDisabled]
* @property {boolean} [morphDisabled] - whether dragged element should morph to zone dimensions
* @property {boolean} [dropFromOthersDisabled]
* @property {number} [zoneTabIndex] - set the tabindex of the list container when not dragging
* @property {number} [zoneItemTabIndex] - set the tabindex of the list container items when not dragging
* @property {object} [dropTargetStyle]
* @property {string[]} [dropTargetClasses]
* @property {boolean|number} [delayTouchStart] - On touch devices, wait this long before converting the gesture to a drag.
* `true` uses the built-in default (80 ms).
* @property {boolean} [dropAnimationDisabled] - cancels the drop animation to place
* @property {function} [transformDraggedElement]
* @param {HTMLElement} node - the element to enhance
* @param {Options} options
* @return {{update: function, destroy: function}}
*/
export function dndzone(node, options) {
if (shouldIgnoreZone(node)) {
return {
update: () => {},
destroy: () => {}
};
}
validateOptions(options);
const pointerZone = pointerDndZone(node, options);
const keyboardZone = keyboardDndZone(node, options);
return {
update: newOptions => {
validateOptions(newOptions);
pointerZone.update(newOptions);
keyboardZone.update(newOptions);
},
destroy: () => {
pointerZone.destroy();
keyboardZone.destroy();
}
};
}
/**
* If the user marked something in the ancestry of our node as shadow element, we can ignore it
* We need the user to mark it for us because svelte updates the action from deep to shallow (but renders top down)
* @param {HTMLElement} node
* @return {boolean}
*/
function shouldIgnoreZone(node) {
return !!node.closest(`[${SHADOW_ELEMENT_HINT_ATTRIBUTE_NAME}="true"]`);
}
function validateOptions(options) {
/*eslint-disable*/
const {
items,
flipDurationMs,
type,
dragDisabled,
morphDisabled,
dropFromOthersDisabled,
zoneTabIndex,
zoneItemTabIndex,
dropTargetStyle,
dropTargetClasses,
transformDraggedElement,
autoAriaDisabled,
centreDraggedOnCursor,
delayTouchStart,
dropAnimationDisabled,
...rest
} = options;
/*eslint-enable*/
if (Object.keys(rest).length > 0) {
console.warn(`dndzone will ignore unknown options`, rest);
}
if (!items) {
throw new Error("no 'items' key provided to dndzone");
}
const itemWithMissingId = items.find(item => !{}.hasOwnProperty.call(item, ITEM_ID_KEY));
if (itemWithMissingId) {
throw new Error(`missing '${ITEM_ID_KEY}' property for item ${toString(itemWithMissingId)}`);
}
if (dropTargetClasses && !Array.isArray(dropTargetClasses)) {
throw new Error(`dropTargetClasses should be an array but instead it is a ${typeof dropTargetClasses}, ${toString(dropTargetClasses)}`);
}
if (zoneTabIndex && !isInt(zoneTabIndex)) {
throw new Error(`zoneTabIndex should be a number but instead it is a ${typeof zoneTabIndex}, ${toString(zoneTabIndex)}`);
}
if (zoneItemTabIndex && !isInt(zoneItemTabIndex)) {
throw new Error(`zoneItemTabIndex should be a number but instead it is a ${typeof zoneItemTabIndex}, ${toString(zoneItemTabIndex)}`);
}
if (delayTouchStart !== undefined && delayTouchStart !== false) {
const validBoolean = delayTouchStart === true;
const validNumber = typeof delayTouchStart === "number" && isFinite(delayTouchStart) && delayTouchStart >= 0;
if (!validBoolean && !validNumber) {
throw new Error(
`delayTouchStart should be a boolean (true/false) or a non-negative number but instead it is a ${typeof delayTouchStart}, ${toString(
delayTouchStart
)}`
);
}
}
}
function isInt(value) {
return (
!isNaN(value) &&
(function (x) {
return (x | 0) === x;
})(parseFloat(value))
);
}