UNPKG

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 *

1,447 lines (1,380 loc) 104 kB
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function () {}; return { s: F, n: function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function (e) { throw e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function () { it = it.call(o); }, n: function () { var step = it.next(); normalCompletion = step.done; return step; }, e: function (e) { didErr = true; err = e; }, f: function () { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } // external events var FINALIZE_EVENT_NAME = "finalize"; var CONSIDER_EVENT_NAME = "consider"; /** * @typedef {Object} Info * @property {string} trigger * @property {string} id * @property {string} source * @param {Node} el * @param {Array} items * @param {Info} info */ function dispatchFinalizeEvent(el, items, info) { el.dispatchEvent(new CustomEvent(FINALIZE_EVENT_NAME, { detail: { items: items, info: info } })); } /** * Dispatches a consider event * @param {Node} el * @param {Array} items * @param {Info} info */ function dispatchConsiderEvent(el, items, info) { el.dispatchEvent(new CustomEvent(CONSIDER_EVENT_NAME, { detail: { items: items, info: info } })); } // internal events var DRAGGED_ENTERED_EVENT_NAME = "draggedEntered"; var DRAGGED_LEFT_EVENT_NAME = "draggedLeft"; var DRAGGED_OVER_INDEX_EVENT_NAME = "draggedOverIndex"; var DRAGGED_LEFT_DOCUMENT_EVENT_NAME = "draggedLeftDocument"; var DRAGGED_LEFT_TYPES = { LEFT_FOR_ANOTHER: "leftForAnother", OUTSIDE_OF_ANY: "outsideOfAny" }; function dispatchDraggedElementEnteredContainer(containerEl, indexObj, draggedEl) { containerEl.dispatchEvent(new CustomEvent(DRAGGED_ENTERED_EVENT_NAME, { detail: { indexObj: indexObj, draggedEl: draggedEl } })); } /** * @param containerEl - the dropzone the element left * @param draggedEl - the dragged element * @param theOtherDz - the new dropzone the element entered */ function dispatchDraggedElementLeftContainerForAnother(containerEl, draggedEl, theOtherDz) { containerEl.dispatchEvent(new CustomEvent(DRAGGED_LEFT_EVENT_NAME, { detail: { draggedEl: draggedEl, type: DRAGGED_LEFT_TYPES.LEFT_FOR_ANOTHER, theOtherDz: theOtherDz } })); } function dispatchDraggedElementLeftContainerForNone(containerEl, draggedEl) { containerEl.dispatchEvent(new CustomEvent(DRAGGED_LEFT_EVENT_NAME, { detail: { draggedEl: draggedEl, type: DRAGGED_LEFT_TYPES.OUTSIDE_OF_ANY } })); } function dispatchDraggedElementIsOverIndex(containerEl, indexObj, draggedEl) { containerEl.dispatchEvent(new CustomEvent(DRAGGED_OVER_INDEX_EVENT_NAME, { detail: { indexObj: indexObj, draggedEl: draggedEl } })); } function dispatchDraggedLeftDocument(draggedEl) { window.dispatchEvent(new CustomEvent(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, { detail: { draggedEl: draggedEl } })); } var TRIGGERS = { DRAG_STARTED: "dragStarted", DRAGGED_ENTERED: DRAGGED_ENTERED_EVENT_NAME, DRAGGED_ENTERED_ANOTHER: "dragEnteredAnother", DRAGGED_OVER_INDEX: DRAGGED_OVER_INDEX_EVENT_NAME, DRAGGED_LEFT: DRAGGED_LEFT_EVENT_NAME, DRAGGED_LEFT_ALL: "draggedLeftAll", DROPPED_INTO_ZONE: "droppedIntoZone", DROPPED_INTO_ANOTHER: "droppedIntoAnother", DROPPED_OUTSIDE_OF_ANY: "droppedOutsideOfAny", DRAG_STOPPED: "dragStopped" }; var SOURCES = { POINTER: "pointer", KEYBOARD: "keyboard" }; var SHADOW_ITEM_MARKER_PROPERTY_NAME = "isDndShadowItem"; var SHADOW_ELEMENT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-internal"; var SHADOW_ELEMENT_HINT_ATTRIBUTE_NAME = "data-is-dnd-shadow-item-hint"; var SHADOW_PLACEHOLDER_ITEM_ID = "id:dnd-shadow-placeholder-0000"; var DRAGGED_ELEMENT_ID = "dnd-action-dragged-el"; var ITEM_ID_KEY = "id"; var activeDndZoneCount = 0; function incrementActiveDropZoneCount() { activeDndZoneCount++; } function decrementActiveDropZoneCount() { if (activeDndZoneCount === 0) { throw new Error("Bug! trying to decrement when there are no dropzones"); } activeDndZoneCount--; } /** * 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) */ function overrideItemIdKeyNameBeforeInitialisingDndZones(newKeyName) { if (activeDndZoneCount > 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"); } printDebug(function () { return ["overriding item id key name", newKeyName]; }); ITEM_ID_KEY = newKeyName; } var isOnServer = typeof window === "undefined"; var printDebug = function printDebug() {}; /** * Allows the user to show/hide console debug output * * @param {boolean} isDebug */ function setDebugMode(isDebug) { if (isDebug) { printDebug = function printDebug(generateMessage) { var logFunction = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : console.debug; var message = generateMessage(); if (Array.isArray(message)) { logFunction.apply(void 0, _toConsumableArray(message)); } else { logFunction(message); } }; } else { printDebug = function printDebug() {}; } } // This is based off https://stackoverflow.com/questions/27745438/how-to-compute-getboundingclientrect-without-considering-transforms/57876601#57876601 // It removes the transforms that are potentially applied by the flip animations /** * Gets the bounding rect but removes transforms (ex: flip animation) * @param {HTMLElement} el * @param {boolean} [onlyVisible] - use the visible rect defaults to true * @return {{top: number, left: number, bottom: number, right: number}} */ function getBoundingRectNoTransforms(el) { var onlyVisible = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var ta; var rect = onlyVisible ? getVisibleRectRecursive(el) : el.getBoundingClientRect(); var style = getComputedStyle(el); var tx = style.transform; if (tx) { var sx, sy, dx, dy; if (tx.startsWith("matrix3d(")) { ta = tx.slice(9, -1).split(/, /); sx = +ta[0]; sy = +ta[5]; dx = +ta[12]; dy = +ta[13]; } else if (tx.startsWith("matrix(")) { ta = tx.slice(7, -1).split(/, /); sx = +ta[0]; sy = +ta[3]; dx = +ta[4]; dy = +ta[5]; } else { return rect; } var to = style.transformOrigin; var x = rect.x - dx - (1 - sx) * parseFloat(to); var y = rect.y - dy - (1 - sy) * parseFloat(to.slice(to.indexOf(" ") + 1)); var w = sx ? rect.width / sx : el.offsetWidth; var h = sy ? rect.height / sy : el.offsetHeight; return { x: x, y: y, width: w, height: h, top: y, right: x + w, bottom: y + h, left: x }; } else { return rect; } } /** * Gets the absolute bounding rect (accounts for the window's scroll position and removes transforms) * @param {HTMLElement} el * @return {{top: number, left: number, bottom: number, right: number}} */ function getAbsoluteRectNoTransforms(el) { var rect = getBoundingRectNoTransforms(el); return { top: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY, left: rect.left + window.scrollX, right: rect.right + window.scrollX }; } /** * Gets the absolute bounding rect (accounts for the window's scroll position) * @param {HTMLElement} el * @return {{top: number, left: number, bottom: number, right: number}} */ function getAbsoluteRect(el) { var rect = el.getBoundingClientRect(); return { top: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY, left: rect.left + window.scrollX, right: rect.right + window.scrollX }; } /** * finds the center :) * @typedef {Object} Rect * @property {number} top * @property {number} bottom * @property {number} left * @property {number} right * @param {Rect} rect * @return {{x: number, y: number}} */ function findCenter(rect) { return { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 }; } /** * @typedef {Object} Point * @property {number} x * @property {number} y * @param {Point} pointA * @param {Point} pointB * @return {number} */ function calcDistance(pointA, pointB) { return Math.sqrt(Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)); } /** * @param {Point} point * @param {Rect} rect * @return {boolean|boolean} */ function isPointInsideRect(point, rect) { return point.y <= rect.bottom && point.y >= rect.top && point.x >= rect.left && point.x <= rect.right; } /** * find the absolute coordinates of the center of a dom element * @param el {HTMLElement} * @returns {{x: number, y: number}} */ function findCenterOfElement(el) { return findCenter(getAbsoluteRect(el)); } /** * @param {HTMLElement} elA * @param {HTMLElement} elB * @return {boolean} */ function isCenterOfAInsideB(elA, elB) { var centerOfA = findCenterOfElement(elA); var rectOfB = getAbsoluteRectNoTransforms(elB); return isPointInsideRect(centerOfA, rectOfB); } /** * @param {HTMLElement|ChildNode} elA * @param {HTMLElement|ChildNode} elB * @return {number} */ function calcDistanceBetweenCenters(elA, elB) { var centerOfA = findCenterOfElement(elA); var centerOfB = findCenterOfElement(elB); return calcDistance(centerOfA, centerOfB); } /** * @param {HTMLElement} el - the element to check * @returns {boolean} - true if the element in its entirety is off-screen including the scrollable area (the normal dom events look at the mouse rather than the element) */ function isElementOffDocument(el) { var rect = getAbsoluteRect(el); return rect.right < 0 || rect.left > document.documentElement.scrollWidth || rect.bottom < 0 || rect.top > document.documentElement.scrollHeight; } function getVisibleRectRecursive(element) { var rect = element.getBoundingClientRect(); var visibleRect = { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right }; // Traverse up the DOM hierarchy, checking for scrollable ancestors var parent = element.parentElement; while (parent && parent !== document.body) { var parentRect = parent.getBoundingClientRect(); // Check if the parent has a scrollable overflow var overflowY = window.getComputedStyle(parent).overflowY; var overflowX = window.getComputedStyle(parent).overflowX; var isScrollableY = overflowY === "scroll" || overflowY === "auto"; var isScrollableX = overflowX === "scroll" || overflowX === "auto"; // Constrain the visible area to the parent's visible area if (isScrollableY) { visibleRect.top = Math.max(visibleRect.top, parentRect.top); visibleRect.bottom = Math.min(visibleRect.bottom, parentRect.bottom); } if (isScrollableX) { visibleRect.left = Math.max(visibleRect.left, parentRect.left); visibleRect.right = Math.min(visibleRect.right, parentRect.right); } parent = parent.parentElement; } // Finally, constrain the visible rect to the viewport visibleRect.top = Math.max(visibleRect.top, 0); visibleRect.bottom = Math.min(visibleRect.bottom, window.innerHeight); visibleRect.left = Math.max(visibleRect.left, 0); visibleRect.right = Math.min(visibleRect.right, window.innerWidth); // Return the visible rectangle, ensuring that all values are valid return { top: visibleRect.top, bottom: visibleRect.bottom, left: visibleRect.left, right: visibleRect.right, width: Math.max(0, visibleRect.right - visibleRect.left), height: Math.max(0, visibleRect.bottom - visibleRect.top) }; } var dzToShadowIndexToRect; /** * Resets the cache that allows for smarter "would be index" resolution. Should be called after every drag operation */ function resetIndexesCache() { printDebug(function () { return "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) { var shadowElIndex = Array.from(dz.children).findIndex(function (child) { return 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 {HTMLElement} floatingAboveEl * @param {HTMLElement} collectionBelowEl * @returns {Index|null} - if the element is over the container the Index object otherwise null */ function findWouldBeIndex(floatingAboveEl, collectionBelowEl) { if (!isCenterOfAInsideB(floatingAboveEl, collectionBelowEl)) { return null; } var children = collectionBelowEl.children; // the container is empty, floating element should be the first if (children.length === 0) { return { index: 0, isProximityBased: true }; } var 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 (var i = 0; i < children.length; i++) { if (isCenterOfAInsideB(floatingAboveEl, children[i])) { var cachedShadowRect = dzToShadowIndexToRect.has(collectionBelowEl) && dzToShadowIndexToRect.get(collectionBelowEl).get(i); if (cachedShadowRect) { if (!isPointInsideRect(findCenterOfElement(floatingAboveEl), 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 var minDistanceSoFar = Number.MAX_VALUE; var 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 (var _i = 0; _i < children.length; _i++) { var distance = calcDistanceBetweenCenters(floatingAboveEl, 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) { var originalLen = children.length; // before we append the phantom var template = children[originalLen - 1]; var phantom = template.cloneNode(false); // shallow clone is enough for size phantom.style.visibility = "hidden"; phantom.style.pointerEvents = "none"; collectionBelowEl.appendChild(phantom); var phantomDistance = calcDistanceBetweenCenters(floatingAboveEl, phantom); if (phantomDistance < minDistanceSoFar) { indexOfMin = originalLen; // index of phantom slot in original list } collectionBelowEl.removeChild(phantom); } return { index: indexOfMin, isProximityBased: true }; } /** * @param {Object} object * @return {string} */ function toString(object) { return JSON.stringify(object, null, 2); } /** * Finds the depth of the given node in the DOM tree * @param {HTMLElement} node * @return {number} - the depth of the node */ function getDepth(node) { if (!node) { throw new Error("cannot get depth of a falsy node"); } return _getDepth(node, 0); } function _getDepth(node) { var countSoFar = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if (!node.parentElement) { return countSoFar - 1; } return _getDepth(node.parentElement, countSoFar + 1); } /** * A simple util to shallow compare objects quickly, it doesn't validate the arguments so pass objects in * @param {Object} objA * @param {Object} objB * @return {boolean} - true if objA and objB are shallow equal */ function areObjectsShallowEqual(objA, objB) { if (Object.keys(objA).length !== Object.keys(objB).length) { return false; } for (var keyA in objA) { if (!{}.hasOwnProperty.call(objB, keyA) || objB[keyA] !== objA[keyA]) { return false; } } return true; } /** * Shallow compares two arrays * @param arrA * @param arrB * @return {boolean} - whether the arrays are shallow equal */ function areArraysShallowEqualSameOrder(arrA, arrB) { if (arrA.length !== arrB.length) { return false; } for (var i = 0; i < arrA.length; i++) { if (arrA[i] !== arrB[i]) { return false; } } return true; } var INTERVAL_MS = 200; var TOLERANCE_PX = 10; var next; /** * Tracks the dragged elements and performs the side effects when it is dragged over a drop zone (basically dispatching custom-events scrolling) * @param {Set<HTMLElement>} dropZones * @param {HTMLElement} draggedEl * @param {number} [intervalMs = INTERVAL_MS] * @param {MultiScroller} multiScroller */ function observe(draggedEl, dropZones) { var intervalMs = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : INTERVAL_MS; var multiScroller = arguments.length > 3 ? arguments[3] : undefined; // initialization var lastDropZoneFound; var lastIndexFound; var lastIsDraggedInADropZone = false; var lastCentrePositionOfDragged; // We are sorting to make sure that in case of nested zones of the same type the one "on top" is considered first var dropZonesFromDeepToShallow = Array.from(dropZones).sort(function (dz1, dz2) { return getDepth(dz2) - getDepth(dz1); }); /** * The main function in this module. Tracks where everything is/ should be a take the actions */ function andNow() { var currentCenterOfDragged = findCenterOfElement(draggedEl); var scrolled = multiScroller.multiScrollIfNeeded(); // we only want to make a new decision after the element was moved a bit to prevent flickering if (!scrolled && lastCentrePositionOfDragged && Math.abs(lastCentrePositionOfDragged.x - currentCenterOfDragged.x) < TOLERANCE_PX && Math.abs(lastCentrePositionOfDragged.y - currentCenterOfDragged.y) < TOLERANCE_PX) { next = window.setTimeout(andNow, intervalMs); return; } if (isElementOffDocument(draggedEl)) { printDebug(function () { return "off document"; }); dispatchDraggedLeftDocument(draggedEl); return; } lastCentrePositionOfDragged = currentCenterOfDragged; // this is a simple algorithm, potential improvement: first look at lastDropZoneFound var isDraggedInADropZone = false; var _iterator = _createForOfIteratorHelper(dropZonesFromDeepToShallow), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var dz = _step.value; if (scrolled) resetIndexesCache(); var indexObj = findWouldBeIndex(draggedEl, dz); if (indexObj === null) { // it is not inside continue; } var index = indexObj.index; isDraggedInADropZone = true; // the element is over a container if (dz !== lastDropZoneFound) { lastDropZoneFound && dispatchDraggedElementLeftContainerForAnother(lastDropZoneFound, draggedEl, dz); dispatchDraggedElementEnteredContainer(dz, indexObj, draggedEl); lastDropZoneFound = dz; } else if (index !== lastIndexFound) { dispatchDraggedElementIsOverIndex(dz, indexObj, draggedEl); lastIndexFound = index; } // we handle looping with the 'continue' statement above break; } // the first time the dragged element is not in any dropzone we need to notify the last dropzone it was in } catch (err) { _iterator.e(err); } finally { _iterator.f(); } if (!isDraggedInADropZone && lastIsDraggedInADropZone && lastDropZoneFound) { dispatchDraggedElementLeftContainerForNone(lastDropZoneFound, draggedEl); lastDropZoneFound = undefined; lastIndexFound = undefined; lastIsDraggedInADropZone = false; } else { lastIsDraggedInADropZone = true; } next = window.setTimeout(andNow, intervalMs); } andNow(); } // assumption - we can only observe one dragged element at a time, this could be changed in the future function unobserve() { printDebug(function () { return "unobserving"; }); clearTimeout(next); resetIndexesCache(); } var SCROLL_ZONE_PX = 30; /** * Will make a scroller that can scroll any element given to it in any direction * @returns {{scrollIfNeeded: function(Point, HTMLElement): boolean, resetScrolling: function(void):void}} */ function makeScroller() { var scrollingInfo; function resetScrolling() { scrollingInfo = { directionObj: undefined, stepPx: 0 }; } resetScrolling(); // directionObj {x: 0|1|-1, y:0|1|-1} - 1 means down in y and right in x function scrollContainer(containerEl) { var _scrollingInfo = scrollingInfo, directionObj = _scrollingInfo.directionObj, stepPx = _scrollingInfo.stepPx; if (directionObj) { containerEl.scrollBy(directionObj.x * stepPx, directionObj.y * stepPx); window.requestAnimationFrame(function () { return scrollContainer(containerEl); }); } } function calcScrollStepPx(distancePx) { return SCROLL_ZONE_PX - distancePx; } /** * @param {Point} pointer - the pointer will be used to decide in which direction to scroll * @param {HTMLElement} elementToScroll - the scroll container * If the pointer is next to the sides of the element to scroll, will trigger scrolling * Can be called repeatedly with updated pointer and elementToScroll values without issues * @return {boolean} - true if scrolling was needed */ function scrollIfNeeded(pointer, elementToScroll) { if (!elementToScroll) { return false; } var distances = calcInnerDistancesBetweenPointAndSidesOfElement(pointer, elementToScroll); var isAlreadyScrolling = !!scrollingInfo.directionObj; if (distances === null) { if (isAlreadyScrolling) resetScrolling(); return false; } var scrollingVertically = false, scrollingHorizontally = false; // vertical if (elementToScroll.scrollHeight > elementToScroll.clientHeight) { if (distances.bottom < SCROLL_ZONE_PX) { scrollingVertically = true; scrollingInfo.directionObj = { x: 0, y: 1 }; scrollingInfo.stepPx = calcScrollStepPx(distances.bottom); } else if (distances.top < SCROLL_ZONE_PX) { scrollingVertically = true; scrollingInfo.directionObj = { x: 0, y: -1 }; scrollingInfo.stepPx = calcScrollStepPx(distances.top); } if (!isAlreadyScrolling && scrollingVertically) { scrollContainer(elementToScroll); return true; } } // horizontal if (elementToScroll.scrollWidth > elementToScroll.clientWidth) { if (distances.right < SCROLL_ZONE_PX) { scrollingHorizontally = true; scrollingInfo.directionObj = { x: 1, y: 0 }; scrollingInfo.stepPx = calcScrollStepPx(distances.right); } else if (distances.left < SCROLL_ZONE_PX) { scrollingHorizontally = true; scrollingInfo.directionObj = { x: -1, y: 0 }; scrollingInfo.stepPx = calcScrollStepPx(distances.left); } if (!isAlreadyScrolling && scrollingHorizontally) { scrollContainer(elementToScroll); return true; } } resetScrolling(); return false; } return { scrollIfNeeded: scrollIfNeeded, resetScrolling: resetScrolling }; } /** * If the point is inside the element returns its distances from the sides, otherwise returns null * @param {Point} point * @param {HTMLElement} el * @return {null|{top: number, left: number, bottom: number, right: number}} */ function calcInnerDistancesBetweenPointAndSidesOfElement(point, el) { // Even if the scrolling element is small it acts as a scroller for the viewport var rect = el === document.scrollingElement ? { top: 0, bottom: window.innerHeight, left: 0, right: window.innerWidth } : el.getBoundingClientRect(); if (!isPointInsideRect(point, rect)) { return null; } return { top: point.y - rect.top, bottom: rect.bottom - point.y, left: point.x - rect.left, right: rect.right - point.x }; } /** @typedef {Object} MultiScroller @property {function():boolean} multiScrollIfNeeded - call this on every "tick" to scroll containers if needed, returns true if anything was scrolled /** * Creates a scroller than can scroll any of the provided containers or any of their scrollable parents (including the document's scrolling element) * @param {HTMLElement[]} baseElementsForScrolling * @param {function():Point} getPointerPosition * @return {MultiScroller} */ function createMultiScroller() { var baseElementsForScrolling = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var getPointerPosition = arguments.length > 1 ? arguments[1] : undefined; printDebug(function () { return "creating multi-scroller"; }); var scrollingContainersSet = findRelevantScrollContainers(baseElementsForScrolling); var scrollingContainersDeepToShallow = Array.from(scrollingContainersSet).sort(function (dz1, dz2) { return getDepth(dz2) - getDepth(dz1); }); var _makeScroller = makeScroller(), scrollIfNeeded = _makeScroller.scrollIfNeeded, resetScrolling = _makeScroller.resetScrolling; /** * @return {boolean} - was any container scrolled */ function tick() { var mousePosition = getPointerPosition(); if (!mousePosition || !scrollingContainersDeepToShallow) { return false; } var scrollContainersUnderCursor = scrollingContainersDeepToShallow.filter(function (el) { return isPointInsideRect(mousePosition, el.getBoundingClientRect()) || el === document.scrollingElement; }); for (var i = 0; i < scrollContainersUnderCursor.length; i++) { var scrolled = scrollIfNeeded(mousePosition, scrollContainersUnderCursor[i]); if (scrolled) { return true; } } return false; } return { multiScrollIfNeeded: scrollingContainersSet.size > 0 ? tick : function () { return false; }, destroy: function destroy() { return resetScrolling(); } }; } // internal utils function findScrollableParents(element) { if (!element) { return []; } var scrollableContainers = []; var parent = element; while (parent) { var _window$getComputedSt = window.getComputedStyle(parent), overflow = _window$getComputedSt.overflow; if (overflow.split(" ").some(function (o) { return o.includes("auto") || o.includes("scroll"); })) { scrollableContainers.push(parent); } parent = parent.parentElement; } return scrollableContainers; } function findRelevantScrollContainers(dropZones) { var scrollingContainers = new Set(); var _iterator = _createForOfIteratorHelper(dropZones), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var dz = _step.value; findScrollableParents(dz).forEach(function (container) { return scrollingContainers.add(container); }); } // The scrolling element might have overflow visible and still be scrollable } catch (err) { _iterator.e(err); } finally { _iterator.f(); } if (document.scrollingElement.scrollHeight > document.scrollingElement.clientHeight || document.scrollingElement.scrollWidth > document.scrollingElement.clientHeight) { scrollingContainers.add(document.scrollingElement); } return scrollingContainers; } /** * Fixes svelte issue when cloning node containing (or being) <select> which will loose it's value. * Since svelte manages select value internally. * @see https://github.com/sveltejs/svelte/issues/6717 * @see https://github.com/isaacHagoel/svelte-dnd-action/issues/306 * * @param {HTMLElement} el * @returns */ function svelteNodeClone(el) { var cloned = el.cloneNode(true); var values = []; var elIsSelect = el.tagName === "SELECT"; var selects = elIsSelect ? [el] : _toConsumableArray(el.querySelectorAll("select")); var _iterator = _createForOfIteratorHelper(selects), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _select = _step.value; values.push(_select.value); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } if (selects.length > 0) { var clonedSelects = elIsSelect ? [cloned] : _toConsumableArray(cloned.querySelectorAll("select")); for (var i = 0; i < clonedSelects.length; i++) { var select = clonedSelects[i]; var value = values[i]; var optionEl = select.querySelector("option[value=\"".concat(value, "\"")); if (optionEl) { optionEl.setAttribute("selected", true); } } } var elIsCanvas = el.tagName === "CANVAS"; var canvases = elIsCanvas ? [el] : _toConsumableArray(el.querySelectorAll("canvas")); if (canvases.length > 0) { var clonedCanvases = elIsCanvas ? [cloned] : _toConsumableArray(cloned.querySelectorAll("canvas")); for (var _i = 0; _i < clonedCanvases.length; _i++) { var canvas = canvases[_i]; var clonedCanvas = clonedCanvases[_i]; clonedCanvas.width = canvas.width; clonedCanvas.height = canvas.height; if (canvas.width > 0 && canvas.height > 0) { clonedCanvas.getContext("2d").drawImage(canvas, 0, 0); } } } return cloned; } /** * @type {{USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT: string}} */ var FEATURE_FLAG_NAMES = Object.freeze({ // This flag exists as a workaround for issue 454 (basically a browser bug) - seems like these rect values take time to update when in grid layout. Setting it to true can cause strange behaviour in the REPL for non-grid zones, see issue 470 USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT: "USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT" }); var featureFlagsMap = _defineProperty({}, FEATURE_FLAG_NAMES.USE_COMPUTED_STYLE_INSTEAD_OF_BOUNDING_RECT, false); /** * @param {FEATURE_FLAG_NAMES} flagName * @param {boolean} flagValue */ function setFeatureFlag(flagName, flagValue) { if (!FEATURE_FLAG_NAMES[flagName]) throw new Error("Can't set non existing feature flag ".concat(flagName, "! Supported flags: ").concat(Object.keys(FEATURE_FLAG_NAMES))); featureFlagsMap[flagName] = !!flagValue; } /** * * @param {FEATURE_FLAG_NAMES} flagName * @return {boolean} */ function getFeatureFlag(flagName) { if (!FEATURE_FLAG_NAMES[flagName]) throw new Error("Can't get non existing feature flag ".concat(flagName, "! Supported flags: ").concat(Object.keys(FEATURE_FLAG_NAMES))); return featureFlagsMap[flagName]; } var 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 "".concat(property, " ").concat(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 */ function createDraggedElementFrom(originalElement, positionCenterOnXY) { var rect = originalElement.getBoundingClientRect(); var draggedEl = svelteNodeClone(originalElement); copyStylesFromTo(originalElement, draggedEl); draggedEl.id = DRAGGED_ELEMENT_ID; draggedEl.style.position = "fixed"; var elTopPx = rect.top; var elLeftPx = rect.left; draggedEl.style.top = "".concat(elTopPx, "px"); draggedEl.style.left = "".concat(elLeftPx, "px"); if (positionCenterOnXY) { var center = findCenter(rect); elTopPx -= center.y - positionCenterOnXY.y; elLeftPx -= center.x - positionCenterOnXY.x; window.setTimeout(function () { draggedEl.style.top = "".concat(elTopPx, "px"); draggedEl.style.left = "".concat(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 = "".concat(rect.height, "px"); draggedEl.style.width = "".concat(rect.width, "px"); draggedEl.style.transition = "".concat(trs("top"), ", ").concat(trs("left"), ", ").concat(trs("background-color"), ", ").concat(trs("opacity"), ", ").concat(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(function () { return draggedEl.style.transition += ", ".concat(trs("width"), ", ").concat(trs("height")); }, 0); draggedEl.style.zIndex = "9999"; draggedEl.style.cursor = "grabbing"; return draggedEl; } /** * styles the dragged element to a 'dropped' state * @param {HTMLElement} draggedEl */ 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 */ function morphDraggedElementToBeLike(draggedEl, copyFromEl, currentMouseX, currentMouseY) { copyStylesFromTo(copyFromEl, draggedEl); var newRect = copyFromEl.getBoundingClientRect(); var draggedElRect = draggedEl.getBoundingClientRect(); var widthChange = newRect.width - draggedElRect.width; var heightChange = newRect.height - draggedElRect.height; if (widthChange || heightChange) { var 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 = "".concat(newRect.height, "px"); draggedEl.style.width = "".concat(newRect.width, "px"); } draggedEl.style.left = "".concat(parseFloat(draggedEl.style.left) - relativeDistanceOfMousePointerFromDraggedSides.left * widthChange, "px"); draggedEl.style.top = "".concat(parseFloat(draggedEl.style.top) - relativeDistanceOfMousePointerFromDraggedSides.top * heightChange, "px"); } } /** * @param {HTMLElement} copyFromEl * @param {HTMLElement} copyToEl */ function copyStylesFromTo(copyFromEl, copyToEl) { var computedStyle = window.getComputedStyle(copyFromEl); Array.from(computedStyle).filter(function (s) { return 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(function (s) { return copyToEl.style.setProperty(s, computedStyle.getPropertyValue(s), computedStyle.getPropertyPriority(s)); }); } /** * makes the element compatible with being draggable * @param {HTMLElement} draggableEl * @param {boolean} dragDisabled */ function styleDraggable(draggableEl, dragDisabled) { draggableEl.draggable = false; draggableEl.ondragstart = function () { return 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 */ function hideElement(dragTarget) { dragTarget.style.display = "none"; dragTarget.style.position = "fixed"; dragTarget.style.zIndex = "-5"; } /** * styles the shadow element * @param {HTMLElement} shadowEl */ function decorateShadowEl(shadowEl) { shadowEl.style.visibility = "hidden"; shadowEl.setAttribute(SHADOW_ELEMENT_ATTRIBUTE_NAME, "true"); } /** * undo the styles the shadow element * @param {HTMLElement} shadowEl */ 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 */ function styleActiveDropZones(dropZones) { var getStyles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; var getClasses = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { return []; }; dropZones.forEach(function (dz) { var styles = getStyles(dz); Object.keys(styles).forEach(function (style) { dz.style[style] = styles[style]; }); getClasses(dz).forEach(function (c) { return 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 */ function styleInactiveDropZones(dropZones) { var getStyles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; var getClasses = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { return []; }; dropZones.forEach(function (dz) { var styles = getStyles(dz); Object.keys(styles).forEach(function (style) { dz.style[style] = ""; }); getClasses(dz).forEach(function (c) { return 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 */ function preventShrinking(el) { var originalMinHeight = el.style.minHeight; el.style.minHeight = window.getComputedStyle(el).getPropertyValue("height"); var originalMinWidth = el.style.minWidth; el.style.minWidth = window.getComputedStyle(el).getPropertyValue("width"); return function undo() { el.style.minHeight = originalMinHeight; el.style.minWidth = originalMinWidth; }; } var DEFAULT_DROP_ZONE_TYPE$1 = "--any--"; var MIN_OBSERVATION_INTERVAL_MS = 100; var DISABLED_OBSERVATION_INTERVAL_MS = 20; var MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3; var DEFAULT_TOUCH_DELAY_MS = 80; var DEFAULT_DROP_TARGET_STYLE$1 = { outline: "rgba(255, 255, 102, 0.7) solid 2px" }; var ORIGINAL_DRAGGED_ITEM_MARKER_ATTRIBUTE = "data-is-dnd-original-dragged-item"; var originalDragTarget; var draggedEl; var draggedElData; var draggedElType; var originDropZone; var originIndex; var shadowElData; var shadowElDropZone; var dragStartMousePosition; var currentMousePosition; var isWorkingOnPreviousDrag = false; var finalizingPreviousDrag = false; var unlockOriginDzMinDimensions; var isDraggedOutsideOfAnyDz = false; var scheduledForRemovalAfterDrop = []; var multiScroller; var touchDragHoldTimer; var touchHoldElapsed = false; // a map from type to a set of drop-zones var typeToDropZones$1 = 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 var dzToConfig$1 = 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) var elToMouseDownListener = new WeakMap(); /* drop-zones registration management */ function registerDropZone$1(dropZoneEl, type) { printDebug(function () { return "registering drop-zone if absent"; }); if (!typeToDropZones$1.has(type)) { typeToDropZones$1.set(type, new Set()); } if (!typeToDropZones$1.get(type).has(dropZoneEl)) { typeToDropZones$1.get(type).add(dropZoneEl); incrementActiveDropZoneCount(); } } function unregisterDropZone$1(dropZoneEl, type) { typeToDropZones$1.get(type)["delete"](dropZoneEl); decrementActiveDropZoneCount(); if (typeToDropZones$1.get(type).size === 0) { typeToDropZones$1["delete"](type); } } /* functions to manage observing the dragged element and trigger custom drag-events */ function watchDraggedElement() { printDebug(function () { return "watching dragged element"; }); var dropZones = typeToDropZones$1.get(draggedElType); var _iterator = _createForOfIteratorHelper(dropZones), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var dz = _step.value; dz.addEventListener(DRAGGED_ENTERED_EVENT_NAME, handleDraggedEntered); dz.addEventListener(DRAGGED_LEFT_EVENT_NAME, handleDraggedLeft); dz.addEventListener(DRAGGED_OVER_INDEX_EVENT_NAME, handleDraggedIsOverIndex); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } window.addEventListener(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, handleDrop$1); // 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 var setIntervalMs = Math.max.apply(Math, _toConsumableArray(Array.from(dropZones.keys()).map(function (dz) { return dzToConfig$1.get(dz).dropAnimationDurationMs; }))); var observationIntervalMs = setIntervalMs === 0 ? DISABLED_OBSERVATION_INTERVAL_MS : Math.max(setIntervalMs, MIN_OBSERVATION_INTERVAL_MS); // if setIntervalMs is 0 it goes to 20, otherwise it is max between it and min observation. multiScroller = createMultiScroller(dropZones, function () { return currentMousePosition; }); observe(draggedEl, dropZones, observationIntervalMs * 1.07, multiScroller); } function unWatchDraggedElement() { printDebug(function () { return "unwatching dragged element"; }); var dropZones = typeToDropZones$1.get(draggedElType); var _iterator2 = _createForOfIteratorHelper(dropZones), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var dz = _step2.value