svelte-dnd-action
Advanced tools
Comparing version 0.5.3 to 0.6.0
{ | ||
"name": "svelte-dnd-action", | ||
"svelte": "src/index.js", | ||
"main": "src/index.js", | ||
"scripts": { | ||
@@ -29,3 +30,3 @@ "test": "cypress run" | ||
"description": "*An awesome drag and drop library for Svelte 3 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *", | ||
"version": "0.5.3", | ||
"version": "0.6.0", | ||
"repository": { | ||
@@ -32,0 +33,0 @@ "type": "git", |
# SVELTE DND ACTION [![Known Vulnerabilities](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action/badge.svg?targetFile=package.json)](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action?targetFile=package.json) | ||
This is an implementation of Trello-like drag and drop for Svelte using a custom action. See features list below. | ||
This is a feature-complete implementation of drag and drop for Svelte using a custom action. It supports almost every imaginable drag and drop use-case and is fully accessible. | ||
See full features list below. | ||
@@ -9,3 +10,4 @@ ![dnd_demo2](https://user-images.githubusercontent.com/20507787/81682367-267eb780-9498-11ea-8dbc-5c9582033522.gif) | ||
### Current Status | ||
The library is working well as far as I can tell, but I have not used it in production yet. It is being actively maintained. | ||
The library is working well as far as I can tell, and I am in the process of integrating it into a production system that will be used at scale. | ||
It is being actively maintained. | ||
@@ -15,3 +17,3 @@ ### Features | ||
- Supports horizontal, vertical or any other type of container (it doesn't care much about the shape) | ||
- Supports nested dnd-zones (draggable containers with other draggable elements inside) | ||
- Supports nested dnd-zones (draggable containers with other draggable elements inside, think Trello) | ||
- Rich animations (can be opted out of) | ||
@@ -23,2 +25,3 @@ - Touch support | ||
- Performant and small footprint (no external dependencies, no fluff code) | ||
- Fully accessible (alpha) - keyboard support, aria attributes and assistive instructions for screen readers | ||
@@ -44,3 +47,3 @@ ### Installation | ||
#### Basic Example: | ||
##### Basic Example: | ||
@@ -91,3 +94,3 @@ ```html | ||
#### Input: | ||
##### Input: | ||
An options-object with the following attributes: | ||
@@ -104,3 +107,3 @@ | Name | Type | Required? | Default Value | Description | | ||
#### Output: | ||
##### Output: | ||
@@ -117,2 +120,3 @@ The action dispatches two custom events: | ||
* `id`: the item id of the dragged element | ||
* `source`: will be one of the exported list of SOURCES (Please import if you plan to use): [POINTER, KEYBOARD] | ||
@@ -123,2 +127,28 @@ You have to listen for both events and update the list of items in order for this library to work correctly. | ||
### Accessibility (alpha) | ||
If you want screen-readers to tell the user which item is being dragged and which container it interacts with, **please add `aria-label` on the container and on every draggable item**. The library will take care of the rest. | ||
For example: | ||
```html | ||
<h2>{listName}</h2> | ||
<section aria-label="{listName}" use:dndzone={{items, flipDurationMs}} on:consider={handleDndConsider} on:finalize={handleDndFinalize}> | ||
{#each items as item(item.id)} | ||
<div aria-label="{item.name}" animate:flip="{{duration: flipDurationMs}}"> | ||
{item.name} | ||
</div> | ||
{/each} | ||
</section> | ||
``` | ||
If you don't provide the aria-labels everything will still work, but the messages to the user will be less informative. | ||
*Note*: in general you probably want to use semantic-html (ex: `ol` and `li` elements rather than `section` and `div`) but the library is screen readers friendly regardless (or at least that's the goal :)). | ||
##### Keyboard support | ||
- Tab into a dnd container to get a description and instructions | ||
- Tab into an item and press the *Space*/*Enter* key to enter dragging-mode. The reader will tell the user a drag has started. | ||
- Use the *arrow keys* while in dragging-mode to change the item's position in the list (down and right are the same, up and left are the same). The reader will tell the user about position changes. | ||
- Tab to another dnd container while in dragging-mode in order to move the item to it (the item will be moved to it when it gets focus). The reader will tell the user that item was added to the new list. | ||
- Press *Space*/*Enter* key while focused on an item, or the *Escape* key anywhere to exit dragging mode. The reader will tell the user that they are no longer dragging. | ||
- Clicking on another item while in drag mode will make it the new drag target. Clicking outside of any draggable will exit dragging-mode (and tell the user) | ||
- Mouse drag and drop can be preformed independently of keyboard dragging (as in an item can be dragged with the mouse while in or out of keyboard initiated dragging-mode) | ||
- Keyboard drag uses the same `consider` (only on drag start) and `finalize` (every time the item is moved) events but only has a subset of the `TRIGGERS`. The same handlers should work fine for both. | ||
### Rules/ assumptions to keep in mind | ||
@@ -125,0 +155,0 @@ * Only one element can be dragged in any given time |
@@ -1,251 +0,8 @@ | ||
import { observe, unobserve } from './helpers/observer'; | ||
import { armWindowScroller, disarmWindowScroller} from "./helpers/windowScroller"; | ||
import { | ||
createDraggedElementFrom, | ||
moveDraggedElementToWasDroppedState, | ||
morphDraggedElementToBeLike, | ||
styleDraggable, | ||
styleShadowEl, | ||
styleActiveDropZones, | ||
styleInactiveDropZones, | ||
hideOriginalDragTarget | ||
} from "./helpers/styler"; | ||
import { DRAGGED_ENTERED_EVENT_NAME, DRAGGED_LEFT_EVENT_NAME, DRAGGED_LEFT_DOCUMENT_EVENT_NAME, DRAGGED_OVER_INDEX_EVENT_NAME, dispatchConsiderEvent, dispatchFinalizeEvent } from './helpers/dispatcher'; | ||
import {areObjectsShallowEqual, toString} from "./helpers/util"; | ||
import {dndzone as pointerDndZone} from "./pointerAction"; | ||
import {dndzone as keyboardDndZone} from "./keyboardAction"; | ||
export const SHADOW_ITEM_MARKER_PROPERTY_NAME = 'isDndShadowItem'; | ||
const DEFAULT_DROP_ZONE_TYPE = '--any--'; | ||
const MIN_OBSERVATION_INTERVAL_MS = 100; | ||
const MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3; | ||
const DEFAULT_DROP_TARGET_STYLE = { | ||
outline: 'rgba(255, 255, 102, 0.7) solid 2px', | ||
}; | ||
let originalDragTarget; | ||
let draggedEl; | ||
let draggedElData; | ||
let draggedElType; | ||
let originDropZone; | ||
let originIndex; | ||
let shadowElIdx; | ||
let shadowElData; | ||
let shadowElDropZone; | ||
let dragStartMousePosition; | ||
let currentMousePosition; | ||
let isWorkingOnPreviousDrag = false; | ||
let finalizingPreviousDrag = false; | ||
// a map from type to a set of drop-zones | ||
const typeToDropZones = new Map(); | ||
// important - this is needed because otherwise the config that would be used for everyone is the config of the element that created the event listeners | ||
const dzToConfig = new Map(); | ||
// this is needed in order to be able to cleanup old listeners and avoid stale closures issues (as the listener is defined within each zone) | ||
const elToMouseDownListener = new WeakMap(); | ||
let ITEM_ID_KEY = "id"; | ||
/** | ||
* Allows using another key instead of "id" in the items data. This is global and applies to all dndzones. | ||
* Has to be called when there are no rendered dndzones whatsoever. | ||
* @param {String} newKeyName | ||
* @throws {Error} if it was called when there are rendered dndzones or if it is given the wrong type (not a string) | ||
*/ | ||
export function overrideItemIdKeyNameBeforeInitialisingDndZones(newKeyName) { | ||
if (dzToConfig.size > 0) { | ||
throw new Error("can only override the id key before initialising any dndzone"); | ||
} | ||
if (typeof newKeyName !== "string") { | ||
throw new Error("item id key has to be a string"); | ||
} | ||
console.debug("overriding item id key name", newKeyName) | ||
ITEM_ID_KEY = newKeyName; | ||
} | ||
/* drop-zones registration management */ | ||
function registerDropZone(dropZoneEl, type) { | ||
console.debug('registering drop-zone if absent') | ||
if (!typeToDropZones.has(type)) { | ||
typeToDropZones.set(type, new Set()); | ||
} | ||
if (!typeToDropZones.get(type).has(dropZoneEl)) { | ||
typeToDropZones.get(type).add(dropZoneEl); | ||
} | ||
} | ||
function unregisterDropZone(dropZoneEl, type) { | ||
typeToDropZones.get(type).delete(dropZoneEl); | ||
if (typeToDropZones.get(type).size === 0) { | ||
typeToDropZones.delete(type); | ||
} | ||
} | ||
/* functions to manage observing the dragged element and trigger custom drag-events */ | ||
function watchDraggedElement() { | ||
console.debug('watching dragged element'); | ||
armWindowScroller(); | ||
const dropZones = typeToDropZones.get(draggedElType); | ||
for (const dz of dropZones) { | ||
dz.addEventListener(DRAGGED_ENTERED_EVENT_NAME, handleDraggedEntered); | ||
dz.addEventListener(DRAGGED_LEFT_EVENT_NAME, handleDraggedLeft); | ||
dz.addEventListener(DRAGGED_OVER_INDEX_EVENT_NAME, handleDraggedIsOverIndex); | ||
} | ||
window.addEventListener(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, handleDrop); | ||
// it is important that we don't have an interval that is faster than the flip duration because it can cause elements to jump bach and forth | ||
const observationIntervalMs = Math.max(MIN_OBSERVATION_INTERVAL_MS, ...Array.from(dropZones.keys()).map(dz => dzToConfig.get(dz).dropAnimationDurationMs)); | ||
observe(draggedEl, dropZones, observationIntervalMs * 1.07); | ||
} | ||
function unWatchDraggedElement() { | ||
console.debug('unwatching dragged element'); | ||
disarmWindowScroller(); | ||
const dropZones = typeToDropZones.get(draggedElType); | ||
for (const dz of dropZones) { | ||
dz.removeEventListener(DRAGGED_ENTERED_EVENT_NAME, handleDraggedEntered); | ||
dz.removeEventListener(DRAGGED_LEFT_EVENT_NAME, handleDraggedLeft); | ||
dz.removeEventListener(DRAGGED_OVER_INDEX_EVENT_NAME, handleDraggedIsOverIndex); | ||
} | ||
window.removeEventListener(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, handleDrop); | ||
unobserve(draggedEl, dropZones); | ||
} | ||
/* custom drag-events handlers */ | ||
function handleDraggedEntered(e) { | ||
console.debug('dragged entered', e.currentTarget, e.detail); | ||
let {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); | ||
if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { | ||
console.debug('drop is currently disabled'); | ||
return; | ||
} | ||
// this deals with another svelte related race condition. in rare occasions (super rapid operations) the list hasn't updated yet | ||
items = items.filter(i => i[ITEM_ID_KEY] !== shadowElData[ITEM_ID_KEY]) | ||
console.debug(`dragged entered items ${toString(items)}`); | ||
const {index, isProximityBased} = e.detail.indexObj; | ||
shadowElIdx = (isProximityBased && index === e.currentTarget.children.length - 1)? index + 1 : index; | ||
shadowElDropZone = e.currentTarget; | ||
items.splice( shadowElIdx, 0, shadowElData); | ||
dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_ENTERED, id: draggedElData[ITEM_ID_KEY]}); | ||
} | ||
function handleDraggedLeft(e) { | ||
console.debug('dragged left', e.currentTarget, e.detail); | ||
const {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); | ||
if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { | ||
console.debug('drop is currently disabled'); | ||
return; | ||
} | ||
items.splice(shadowElIdx, 1); | ||
shadowElIdx = undefined; | ||
shadowElDropZone = undefined; | ||
dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_LEFT, id: draggedElData[ITEM_ID_KEY]}); | ||
} | ||
function handleDraggedIsOverIndex(e) { | ||
console.debug('dragged is over index', e.currentTarget, e.detail); | ||
const {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); | ||
if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { | ||
console.debug('drop is currently disabled'); | ||
return; | ||
} | ||
const {index} = e.detail.indexObj; | ||
items.splice(shadowElIdx, 1); | ||
items.splice( index, 0, shadowElData); | ||
shadowElIdx = index; | ||
dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_OVER_INDEX, id: draggedElData[ITEM_ID_KEY]}); | ||
} | ||
/* global mouse/touch-events handlers */ | ||
function handleMouseMove(e) { | ||
e.preventDefault(); | ||
const c = e.touches? e.touches[0] : e; | ||
currentMousePosition = {x: c.clientX, y: c.clientY}; | ||
draggedEl.style.transform = `translate3d(${currentMousePosition.x - dragStartMousePosition.x}px, ${currentMousePosition.y - dragStartMousePosition.y}px, 0)`; | ||
} | ||
function handleDrop() { | ||
console.debug('dropped'); | ||
finalizingPreviousDrag = true; | ||
// cleanup | ||
window.removeEventListener('mousemove', handleMouseMove); | ||
window.removeEventListener('touchmove', handleMouseMove); | ||
window.removeEventListener('mouseup', handleDrop); | ||
window.removeEventListener('touchend', handleDrop); | ||
unWatchDraggedElement(); | ||
moveDraggedElementToWasDroppedState(draggedEl); | ||
if (!!shadowElDropZone) { // it was dropped in a drop-zone | ||
console.debug('dropped in dz', shadowElDropZone); | ||
let {items, type} = dzToConfig.get(shadowElDropZone); | ||
styleInactiveDropZones(typeToDropZones.get(type), dz => dzToConfig.get(dz).dropTargetStyle); | ||
items = items.map(item => item.hasOwnProperty(SHADOW_ITEM_MARKER_PROPERTY_NAME)? draggedElData : item); | ||
function finalizeWithinZone() { | ||
dispatchFinalizeEvent(shadowElDropZone, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: draggedElData[ITEM_ID_KEY]}); | ||
if (shadowElDropZone !== originDropZone) { | ||
// letting the origin drop zone know the element was permanently taken away | ||
dispatchFinalizeEvent(originDropZone, dzToConfig.get(originDropZone).items, {trigger: TRIGGERS.DROPPED_INTO_ANOTHER, id: draggedElData[ITEM_ID_KEY]}); | ||
} | ||
shadowElDropZone.children[shadowElIdx].style.visibility = ''; | ||
cleanupPostDrop(); | ||
} | ||
animateDraggedToFinalPosition(finalizeWithinZone); | ||
} | ||
else { // it needs to return to its place | ||
console.debug('no dz available'); | ||
let {items, type} = dzToConfig.get(originDropZone); | ||
styleInactiveDropZones(typeToDropZones.get(type), dz => dzToConfig.get(dz).dropTargetStyle); | ||
items.splice(originIndex, 0, shadowElData); | ||
shadowElDropZone = originDropZone; | ||
shadowElIdx = originIndex; | ||
dispatchConsiderEvent(originDropZone, items, {trigger: TRIGGERS.DROPPED_OUTSIDE_OF_ANY, id: draggedElData[ITEM_ID_KEY]}); | ||
function finalizeBackToOrigin() { | ||
items.splice(originIndex, 1, draggedElData); | ||
dispatchFinalizeEvent(originDropZone, items, {trigger: TRIGGERS.DROPPED_OUTSIDE_OF_ANY, id: draggedElData[ITEM_ID_KEY]}); | ||
shadowElDropZone.children[shadowElIdx].style.visibility = ''; | ||
cleanupPostDrop(); | ||
} | ||
window.setTimeout(() => animateDraggedToFinalPosition(finalizeBackToOrigin), 0); | ||
} | ||
} | ||
// helper function for handleDrop | ||
function animateDraggedToFinalPosition(callback) { | ||
const shadowElRect = shadowElDropZone.children[shadowElIdx].getBoundingClientRect(); | ||
const newTransform = { | ||
x: shadowElRect.left - parseFloat(draggedEl.style.left), | ||
y: shadowElRect.top - parseFloat(draggedEl.style.top) | ||
}; | ||
const {dropAnimationDurationMs} = dzToConfig.get(shadowElDropZone); | ||
const transition = `transform ${dropAnimationDurationMs}ms ease` | ||
draggedEl.style.transition = draggedEl.style.transition? draggedEl.style.transition + "," + transition : transition; | ||
draggedEl.style.transform = `translate3d(${newTransform.x}px, ${newTransform.y}px, 0)`; | ||
window.setTimeout(callback, dropAnimationDurationMs); | ||
} | ||
/* cleanup */ | ||
function cleanupPostDrop() { | ||
draggedEl.remove(); | ||
originalDragTarget.remove(); | ||
draggedEl = undefined; | ||
originalDragTarget = undefined; | ||
draggedElData = undefined; | ||
draggedElType = undefined; | ||
originDropZone = undefined; | ||
originIndex = undefined; | ||
shadowElData = undefined; | ||
shadowElIdx = undefined; | ||
shadowElDropZone = undefined; | ||
dragStartMousePosition = undefined; | ||
currentMousePosition = undefined; | ||
isWorkingOnPreviousDrag = false; | ||
finalizingPreviousDrag = false; | ||
} | ||
export const TRIGGERS = { | ||
DRAG_STARTED: "dragStarted", | ||
DRAGGED_ENTERED: DRAGGED_ENTERED_EVENT_NAME, | ||
DRAGGED_OVER_INDEX: DRAGGED_OVER_INDEX_EVENT_NAME, | ||
DRAGGED_LEFT: DRAGGED_LEFT_EVENT_NAME, | ||
DROPPED_INTO_ZONE: "droppedIntoZone", | ||
DROPPED_INTO_ANOTHER: "droppedIntoAnother", | ||
DROPPED_OUTSIDE_OF_ANY: "droppedOutsideOfAny" | ||
}; | ||
/** | ||
* A Svelte custom action to turn any container to a dnd zone and all of its direct children to draggables | ||
* dispatches two events that the container is expected to react to by modifying its list of items, | ||
* 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 | ||
@@ -257,2 +14,6 @@ * | ||
* @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} [dropFromOthersDisabled] | ||
* @property {Object} [dragTargetStyle] | ||
* @property {Function} [transformDraggedElement] | ||
* @param {HTMLElement} node - the element to enhance | ||
@@ -263,187 +24,14 @@ * @param {Options} options | ||
export function dndzone(node, options) { | ||
const config = { | ||
items: undefined, | ||
type: DEFAULT_DROP_ZONE_TYPE, | ||
flipDurationMs: 0, | ||
dragDisabled: false, | ||
dropFromOthersDisabled: false, | ||
dropTargetStyle: DEFAULT_DROP_TARGET_STYLE, | ||
transformDraggedElement : () => {} | ||
}; | ||
console.debug(`dndzone good to go options: ${toString(options)}, config: ${toString(config)}`, {node}); | ||
let elToIdx = new Map(); | ||
function addMaybeListeners() { | ||
window.addEventListener('mousemove', handleMouseMoveMaybeDragStart, {passive: false}); | ||
window.addEventListener('touchmove', handleMouseMoveMaybeDragStart, {passive: false, capture: false}); | ||
window.addEventListener('mouseup', handleFalseAlarm, {passive: false}); | ||
window.addEventListener('touchend', handleFalseAlarm, {passive: false}); | ||
} | ||
function removeMaybeListeners() { | ||
window.removeEventListener('mousemove', handleMouseMoveMaybeDragStart); | ||
window.removeEventListener('touchmove', handleMouseMoveMaybeDragStart); | ||
window.removeEventListener('mouseup', handleFalseAlarm); | ||
window.removeEventListener('touchend', handleFalseAlarm); | ||
} | ||
function handleFalseAlarm() { | ||
removeMaybeListeners(); | ||
originalDragTarget = undefined; | ||
dragStartMousePosition = undefined; | ||
currentMousePosition = undefined; | ||
} | ||
function handleMouseMoveMaybeDragStart(e) { | ||
e.preventDefault(); | ||
const c = e.touches? e.touches[0] : e; | ||
currentMousePosition = {x: c.clientX, y: c.clientY}; | ||
if (Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX || Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX) { | ||
removeMaybeListeners(); | ||
handleDragStart(originalDragTarget); | ||
} | ||
} | ||
function handleMouseDown(e) { | ||
// prevents responding to any button but left click which equals 0 (which is falsy) | ||
if (e.button) { | ||
console.debug(`ignoring none left click button: ${e.button}`); | ||
return; | ||
} | ||
if (isWorkingOnPreviousDrag) { | ||
console.debug('cannot start a new drag before finalizing previous one'); | ||
return; | ||
} | ||
e.stopPropagation(); | ||
const c = e.touches? e.touches[0] : e; | ||
dragStartMousePosition = {x: c.clientX, y:c.clientY}; | ||
currentMousePosition = {...dragStartMousePosition}; | ||
originalDragTarget = e.currentTarget; | ||
addMaybeListeners(); | ||
} | ||
function handleDragStart() { | ||
console.debug(`drag start config: ${toString(config)}`, originalDragTarget); | ||
isWorkingOnPreviousDrag = true; | ||
// initialising globals | ||
const currentIdx = elToIdx.get(originalDragTarget); | ||
originIndex = currentIdx; | ||
originDropZone = originalDragTarget.parentElement; | ||
const {items, type} = config; | ||
draggedElData = {...items[currentIdx]}; | ||
draggedElType = type; | ||
shadowElData = {...draggedElData, [SHADOW_ITEM_MARKER_PROPERTY_NAME]: true}; | ||
// creating the draggable element | ||
draggedEl = createDraggedElementFrom(originalDragTarget); | ||
// We will keep the original dom node in the dom because touch events keep firing on it, we want to re-add it after Svelte removes it | ||
function keepOriginalElementInDom() { | ||
const {items: itemsNow} = config; | ||
if (!draggedEl.parentElement && (!itemsNow[originIndex] || draggedElData[ITEM_ID_KEY] !== itemsNow[originIndex][ITEM_ID_KEY])) { | ||
document.body.appendChild(draggedEl); | ||
watchDraggedElement(); | ||
hideOriginalDragTarget(originalDragTarget); | ||
document.body.appendChild(originalDragTarget); | ||
} else { | ||
window.requestAnimationFrame(keepOriginalElementInDom); | ||
} | ||
} | ||
window.requestAnimationFrame(keepOriginalElementInDom); | ||
styleActiveDropZones( | ||
Array.from(typeToDropZones.get(config.type)) | ||
.filter(dz => dz === originDropZone || !dzToConfig.get(dz).dropFromOthersDisabled), | ||
dz => dzToConfig.get(dz).dropTargetStyle, | ||
); | ||
// removing the original element by removing its data entry | ||
items.splice(currentIdx, 1); | ||
dispatchConsiderEvent(originDropZone, items, {trigger: TRIGGERS.DRAG_STARTED, id: draggedElData[ITEM_ID_KEY]}); | ||
// handing over to global handlers - starting to watch the element | ||
window.addEventListener('mousemove', handleMouseMove, {passive: false}); | ||
window.addEventListener('touchmove', handleMouseMove, {passive: false, capture: false}); | ||
window.addEventListener('mouseup', handleDrop, {passive: false}); | ||
window.addEventListener('touchend', handleDrop, {passive: false}); | ||
} | ||
function configure({ | ||
items = undefined, | ||
flipDurationMs:dropAnimationDurationMs = 0, | ||
type: newType = DEFAULT_DROP_ZONE_TYPE, | ||
dragDisabled = false, | ||
dropFromOthersDisabled = false, | ||
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE, | ||
transformDraggedElement = () => {}, | ||
...rest | ||
}) { | ||
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 => !item.hasOwnProperty(ITEM_ID_KEY)); | ||
if (itemWithMissingId) { | ||
throw new Error(`missing '${ITEM_ID_KEY}' property for item ${toString(itemWithMissingId)}`); | ||
} | ||
config.dropAnimationDurationMs = dropAnimationDurationMs; | ||
if (config.type && newType !== config.type) { | ||
unregisterDropZone(node, config.type); | ||
} | ||
config.type = newType; | ||
registerDropZone(node, newType); | ||
config.items = [...items]; | ||
config.dragDisabled = dragDisabled; | ||
config.transformDraggedElement = transformDraggedElement; | ||
// realtime update for dropTargetStyle | ||
if (isWorkingOnPreviousDrag && !finalizingPreviousDrag && !areObjectsShallowEqual(dropTargetStyle, config.dropTargetStyle)) { | ||
styleInactiveDropZones([node], () => config.dropTargetStyle); | ||
styleActiveDropZones([node], () => dropTargetStyle); | ||
} | ||
config.dropTargetStyle = dropTargetStyle; | ||
// realtime update for dropFromOthersDisabled | ||
if (isWorkingOnPreviousDrag && config.dropFromOthersDisabled !== dropFromOthersDisabled) { | ||
if (dropFromOthersDisabled) { | ||
styleInactiveDropZones([node], dz => dzToConfig.get(dz).dropTargetStyle); | ||
} else { | ||
styleActiveDropZones([node], dz => dzToConfig.get(dz).dropTargetStyle); | ||
} | ||
} | ||
config.dropFromOthersDisabled = dropFromOthersDisabled; | ||
dzToConfig.set(node, config); | ||
for (let idx = 0; idx < node.children.length; idx++) { | ||
const draggableEl = node.children[idx]; | ||
styleDraggable(draggableEl, dragDisabled); | ||
if (config.items[idx].hasOwnProperty(SHADOW_ITEM_MARKER_PROPERTY_NAME)) { | ||
morphDraggedElementToBeLike(draggedEl, draggableEl, currentMousePosition.x, currentMousePosition.y, () => config.transformDraggedElement(draggedEl, draggedElData, idx)); | ||
styleShadowEl(draggableEl); | ||
continue; | ||
} | ||
draggableEl.removeEventListener('mousedown', elToMouseDownListener.get(draggableEl)); | ||
draggableEl.removeEventListener('touchstart', elToMouseDownListener.get(draggableEl)); | ||
if (!dragDisabled) { | ||
draggableEl.addEventListener('mousedown', handleMouseDown); | ||
draggableEl.addEventListener('touchstart', handleMouseDown); | ||
elToMouseDownListener.set(draggableEl, handleMouseDown); | ||
} | ||
// updating the idx | ||
elToIdx.set(draggableEl, idx); | ||
} | ||
} | ||
configure(options); | ||
return ({ | ||
update: (newOptions) => { | ||
console.debug(`dndzone will update newOptions: ${toString(newOptions)}`); | ||
configure(newOptions); | ||
const pointerZone = pointerDndZone(node, options); | ||
const keyboardZone = keyboardDndZone(node, options); | ||
return { | ||
update: newOptions => { | ||
pointerZone.update(newOptions); | ||
keyboardZone.update(newOptions); | ||
}, | ||
destroy: () => { | ||
console.debug("dndzone will destroy"); | ||
unregisterDropZone(node, config.type); | ||
dzToConfig.delete(node); | ||
pointerZone.destroy(); | ||
keyboardZone.destroy(); | ||
} | ||
}); | ||
} | ||
} | ||
} |
@@ -9,2 +9,3 @@ // external events | ||
* @property {string} id | ||
* @property {string} source | ||
* @param {Node} el | ||
@@ -11,0 +12,0 @@ * @param {Array} items |
@@ -1,1 +0,2 @@ | ||
export { dndzone, TRIGGERS, SHADOW_ITEM_MARKER_PROPERTY_NAME, overrideItemIdKeyNameBeforeInitialisingDndZones } from './action.js'; | ||
export { dndzone } from './action.js'; | ||
export {TRIGGERS, SOURCES, SHADOW_ITEM_MARKER_PROPERTY_NAME, overrideItemIdKeyNameBeforeInitialisingDndZones} from './constants'; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
74437
17
1346
187