svelte-dnd-action 0.9.62 → 0.9.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,10 @@ An options-object with the following attributes:
119
119
  | `autoAriaDisabled` | Boolean | No | `false` | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard).<br /> **Use it only if you intend to implement your own custom instructions, roles and alerts.** In such a case, you might find the exported function `alertToScreenReader(string)` useful. |
120
120
  | `centreDraggedOnCursor` | Boolean | No | `false` | Setting it to true will cause elements from this dnd-zone to position their center on the cursor on drag start, effectively turning the cursor to the focal point that triggers all the dnd events (ex: entering another zone). Useful for dnd-zones with large items that can be dragged over small items. |
121
121
  | `dropAnimationDisabled` | Boolean | No | `false` | Setting it to true will disable the animation of the dropped element to its final place. |
122
+ | `delayTouchStart` | Boolean \| Number | No | `false` | Prevents accidental drags on touch devices that should have been scrolls.
123
+ • `true` – enable with sensible default (80 ms).
124
+ • `number` – custom delay in milliseconds.
125
+ • `false` / omitted – legacy behaviour (drag starts immediately). |
122
126
 
123
127
  ##### Output:
124
128
 
@@ -477,3 +481,15 @@ Starting with version 0.9.42. if you use the hint make sure to include it in the
477
481
  There is still quite a lot to do. If you'd like to contribute please get in touch (raise an issue or comment on an existing one).
478
482
  Ideally, be specific about which area you'd like to help with.
479
483
  Thank you for reading :)
484
+
485
+ If your page is scrollable and you found that quick swipes on touch devices sometimes start a drag instead of scrolling, enable the new `delayTouchStart` option:
486
+
487
+ ```sveltehtml
488
+ <section use:dndzone={{ items, flipDurationMs, delayTouchStart: true }}>
489
+ …
490
+ </section>
491
+ ```
492
+
493
+ Setting it to `true` waits 80 ms before hijacking the gesture; passing a number lets you tune the threshold.
494
+
495
+ 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.
package/dist/index.d.ts CHANGED
@@ -59,6 +59,11 @@ export interface Options<T extends Item = Item> {
59
59
  autoAriaDisabled?: boolean;
60
60
  centreDraggedOnCursor?: boolean;
61
61
  dropAnimationDisabled?: boolean;
62
+ /**
63
+ * Improve touch UX: true => default 80 ms delay, number => custom delay in ms.
64
+ * false/undefined => drag starts instantly (legacy behaviour)
65
+ */
66
+ delayTouchStart?: boolean | number;
62
67
  }
63
68
 
64
69
  export interface DndZoneAttributes<T> {
package/dist/index.js CHANGED
@@ -647,6 +647,32 @@
647
647
  indexOfMin = _i;
648
648
  }
649
649
  }
650
+
651
+ // -------- Phantom slot check --------
652
+ // Regardless of layout (simple vertical list, flex-wrap, grid, floats …) the
653
+ // visually closest drop target can be *after* the current last **real** child.
654
+ // In simple layouts the would be index from the existing children would always be the last index
655
+ // but in more complex layouts (flex-wrap, grid, floats …) it can be any index.
656
+ // The problem is we can't predict where an additional element would be rendered in the general case,
657
+ // We therefore create a temporary, invisible clone of that last element, let
658
+ // the browser position it, measure the distance, and remove it immediately
659
+ // (same task → no paint). This leaves `children` back in its original state
660
+ // before we exit the function, so existing index-caching logic and shadow-
661
+ // element bookkeeping continue to work unchanged.
662
+ if (children.length > 0) {
663
+ var originalLen = children.length; // before we append the phantom
664
+ var template = children[originalLen - 1];
665
+ var phantom = template.cloneNode(false); // shallow clone is enough for size
666
+ phantom.style.visibility = "hidden";
667
+ phantom.style.pointerEvents = "none";
668
+ collectionBelowEl.appendChild(phantom);
669
+ var phantomDistance = calcDistanceBetweenCenters(floatingAboveEl, phantom);
670
+ if (phantomDistance < minDistanceSoFar) {
671
+ indexOfMin = originalLen; // index of phantom slot in original list
672
+ }
673
+
674
+ collectionBelowEl.removeChild(phantom);
675
+ }
650
676
  return {
651
677
  index: indexOfMin,
652
678
  isProximityBased: true
@@ -1331,6 +1357,7 @@
1331
1357
  var MIN_OBSERVATION_INTERVAL_MS = 100;
1332
1358
  var DISABLED_OBSERVATION_INTERVAL_MS = 20;
1333
1359
  var MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3;
1360
+ var DEFAULT_TOUCH_DELAY_MS = 80;
1334
1361
  var DEFAULT_DROP_TARGET_STYLE$1 = {
1335
1362
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
1336
1363
  };
@@ -1351,6 +1378,8 @@
1351
1378
  var isDraggedOutsideOfAnyDz = false;
1352
1379
  var scheduledForRemovalAfterDrop = [];
1353
1380
  var multiScroller;
1381
+ var touchDragHoldTimer;
1382
+ var touchHoldElapsed = false;
1354
1383
 
1355
1384
  // a map from type to a set of drop-zones
1356
1385
  var typeToDropZones$1 = new Map();
@@ -1482,10 +1511,7 @@
1482
1511
  source: SOURCES.POINTER
1483
1512
  });
1484
1513
  }
1485
- var _e$detail$indexObj = e.detail.indexObj,
1486
- index = _e$detail$indexObj.index,
1487
- isProximityBased = _e$detail$indexObj.isProximityBased;
1488
- var shadowElIdx = isProximityBased && index === e.currentTarget.children.length - 1 ? index + 1 : index;
1514
+ var shadowElIdx = e.detail.indexObj.index;
1489
1515
  shadowElDropZone = e.currentTarget;
1490
1516
  items.splice(shadowElIdx, 0, shadowElData);
1491
1517
  dispatchConsiderEvent(e.currentTarget, items, {
@@ -1673,19 +1699,12 @@
1673
1699
  }
1674
1700
  /* cleanup */
1675
1701
  function cleanupPostDrop() {
1676
- draggedEl.remove();
1677
- originalDragTarget.remove();
1678
- if (scheduledForRemovalAfterDrop.length) {
1679
- printDebug(function () {
1680
- return ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop];
1681
- });
1682
- scheduledForRemovalAfterDrop.forEach(function (_ref) {
1683
- var dz = _ref.dz,
1684
- destroy = _ref.destroy;
1685
- destroy();
1686
- dz.remove();
1687
- });
1688
- scheduledForRemovalAfterDrop = [];
1702
+ // Remove the temporary elements that were kept in the DOM during the drag
1703
+ if (draggedEl && draggedEl.remove) {
1704
+ draggedEl.remove();
1705
+ }
1706
+ if (originalDragTarget && originalDragTarget.remove) {
1707
+ originalDragTarget.remove();
1689
1708
  }
1690
1709
  draggedEl = undefined;
1691
1710
  originalDragTarget = undefined;
@@ -1701,6 +1720,23 @@
1701
1720
  finalizingPreviousDrag = false;
1702
1721
  unlockOriginDzMinDimensions = undefined;
1703
1722
  isDraggedOutsideOfAnyDz = false;
1723
+ if (touchDragHoldTimer) {
1724
+ clearTimeout(touchDragHoldTimer);
1725
+ }
1726
+ touchDragHoldTimer = undefined;
1727
+ touchHoldElapsed = false;
1728
+ if (scheduledForRemovalAfterDrop.length) {
1729
+ printDebug(function () {
1730
+ return ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop];
1731
+ });
1732
+ scheduledForRemovalAfterDrop.forEach(function (_ref) {
1733
+ var dz = _ref.dz,
1734
+ destroy = _ref.destroy;
1735
+ destroy();
1736
+ dz.remove();
1737
+ });
1738
+ scheduledForRemovalAfterDrop = [];
1739
+ }
1704
1740
  }
1705
1741
  function dndzone$2(node, options) {
1706
1742
  var initialized = false;
@@ -1715,7 +1751,8 @@
1715
1751
  dropTargetClasses: [],
1716
1752
  transformDraggedElement: function transformDraggedElement() {},
1717
1753
  centreDraggedOnCursor: false,
1718
- dropAnimationDisabled: false
1754
+ dropAnimationDisabled: false,
1755
+ delayTouchStartMs: 0
1719
1756
  };
1720
1757
  printDebug(function () {
1721
1758
  return ["dndzone good to go options: ".concat(toString(options), ", config: ").concat(toString(config)), {
@@ -1743,6 +1780,11 @@
1743
1780
  window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart);
1744
1781
  window.removeEventListener("mouseup", handleFalseAlarm);
1745
1782
  window.removeEventListener("touchend", handleFalseAlarm);
1783
+ if (touchDragHoldTimer) {
1784
+ clearTimeout(touchDragHoldTimer);
1785
+ touchDragHoldTimer = undefined;
1786
+ touchHoldElapsed = false;
1787
+ }
1746
1788
  }
1747
1789
  function handleFalseAlarm(e) {
1748
1790
  removeMaybeListeners();
@@ -1761,8 +1803,29 @@
1761
1803
  }
1762
1804
  }
1763
1805
  function handleMouseMoveMaybeDragStart(e) {
1806
+ var isTouch = !!e.touches;
1807
+ var c = isTouch ? e.touches[0] : e;
1808
+ // If touch drag delay is configured and not elapsed yet, allow scrolling until either
1809
+ // the delay elapses (timer will call handleDragStart) or the user moves significantly,
1810
+ // in which case we cancel the potential drag and let the interaction be a scroll.
1811
+ if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) {
1812
+ currentMousePosition = {
1813
+ x: c.clientX,
1814
+ y: c.clientY
1815
+ };
1816
+ 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) {
1817
+ // User started scrolling, cancel drag attempt.
1818
+ if (touchDragHoldTimer) {
1819
+ clearTimeout(touchDragHoldTimer);
1820
+ touchDragHoldTimer = undefined;
1821
+ }
1822
+ handleFalseAlarm(e);
1823
+ }
1824
+ return; // Do not preventDefault so scrolling works.
1825
+ }
1826
+
1827
+ // legacy / post-delay path – block scrolling and maybe start drag
1764
1828
  e.preventDefault();
1765
- var c = e.touches ? e.touches[0] : e;
1766
1829
  currentMousePosition = {
1767
1830
  x: c.clientX,
1768
1831
  y: c.clientY
@@ -1793,15 +1856,29 @@
1793
1856
  });
1794
1857
  return;
1795
1858
  }
1796
- e.preventDefault();
1859
+ var isTouchStart = !!e.touches;
1860
+ var useDelay = isTouchStart && config.delayTouchStartMs > 0;
1861
+ if (!useDelay) {
1862
+ e.preventDefault();
1863
+ }
1797
1864
  e.stopPropagation();
1798
- var c = e.touches ? e.touches[0] : e;
1865
+ var c = isTouchStart ? e.touches[0] : e;
1799
1866
  dragStartMousePosition = {
1800
1867
  x: c.clientX,
1801
1868
  y: c.clientY
1802
1869
  };
1803
1870
  currentMousePosition = _objectSpread2({}, dragStartMousePosition);
1804
1871
  originalDragTarget = e.currentTarget;
1872
+ if (useDelay) {
1873
+ touchHoldElapsed = false;
1874
+ touchDragHoldTimer = window.setTimeout(function () {
1875
+ // If the finger is still down and no false-alarm happened
1876
+ if (!originalDragTarget) return;
1877
+ touchHoldElapsed = true;
1878
+ removeMaybeListeners();
1879
+ handleDragStart();
1880
+ }, config.delayTouchStartMs);
1881
+ }
1805
1882
  addMaybeListeners();
1806
1883
  }
1807
1884
  function handleDragStart() {
@@ -1899,8 +1976,17 @@
1899
1976
  _ref2$centreDraggedOn = _ref2.centreDraggedOnCursor,
1900
1977
  centreDraggedOnCursor = _ref2$centreDraggedOn === void 0 ? false : _ref2$centreDraggedOn,
1901
1978
  _ref2$dropAnimationDi = _ref2.dropAnimationDisabled,
1902
- dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi;
1979
+ dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi,
1980
+ _ref2$delayTouchStart = _ref2.delayTouchStart,
1981
+ delayTouchStartOpt = _ref2$delayTouchStart === void 0 ? false : _ref2$delayTouchStart;
1903
1982
  config.dropAnimationDurationMs = dropAnimationDurationMs;
1983
+ var effectiveDelayMs = 0;
1984
+ if (delayTouchStartOpt === true) {
1985
+ effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS;
1986
+ } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) {
1987
+ effectiveDelayMs = delayTouchStartOpt;
1988
+ }
1989
+ config.delayTouchStartMs = effectiveDelayMs;
1904
1990
  if (config.type && newType !== config.type) {
1905
1991
  unregisterDropZone$1(node, config.type);
1906
1992
  }
@@ -2502,7 +2588,7 @@
2502
2588
  return handles;
2503
2589
  }
2504
2590
 
2505
- var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "dropAnimationDisabled"];
2591
+ var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "delayTouchStart", "dropAnimationDisabled"];
2506
2592
 
2507
2593
  /**
2508
2594
  * A custom action to turn any container to a dnd zone and all of its direct children to draggables
@@ -2521,6 +2607,8 @@
2521
2607
  * @property {number} [zoneItemTabIndex] - set the tabindex of the list container items when not dragging
2522
2608
  * @property {object} [dropTargetStyle]
2523
2609
  * @property {string[]} [dropTargetClasses]
2610
+ * @property {boolean|number} [delayTouchStart] - On touch devices, wait this long before converting the gesture to a drag.
2611
+ * `true` uses the built-in default (80 ms).
2524
2612
  * @property {boolean} [dropAnimationDisabled] - cancels the drop animation to place
2525
2613
  * @property {function} [transformDraggedElement]
2526
2614
  * @param {HTMLElement} node - the element to enhance
@@ -2574,6 +2662,7 @@
2574
2662
  options.transformDraggedElement;
2575
2663
  options.autoAriaDisabled;
2576
2664
  options.centreDraggedOnCursor;
2665
+ var delayTouchStart = options.delayTouchStart;
2577
2666
  options.dropAnimationDisabled;
2578
2667
  var rest = _objectWithoutProperties(options, _excluded);
2579
2668
  /*eslint-enable*/
@@ -2598,6 +2687,13 @@
2598
2687
  if (zoneItemTabIndex && !isInt(zoneItemTabIndex)) {
2599
2688
  throw new Error("zoneItemTabIndex should be a number but instead it is a ".concat(_typeof(zoneItemTabIndex), ", ").concat(toString(zoneItemTabIndex)));
2600
2689
  }
2690
+ if (delayTouchStart !== undefined && delayTouchStart !== false) {
2691
+ var validBoolean = delayTouchStart === true;
2692
+ var validNumber = typeof delayTouchStart === "number" && isFinite(delayTouchStart) && delayTouchStart >= 0;
2693
+ if (!validBoolean && !validNumber) {
2694
+ throw new Error("delayTouchStart should be a boolean (true/false) or a non-negative number but instead it is a ".concat(_typeof(delayTouchStart), ", ").concat(toString(delayTouchStart)));
2695
+ }
2696
+ }
2601
2697
  }
2602
2698
  function isInt(value) {
2603
2699
  return !isNaN(value) && function (x) {
package/dist/index.mjs CHANGED
@@ -641,6 +641,32 @@ function findWouldBeIndex(floatingAboveEl, collectionBelowEl) {
641
641
  indexOfMin = _i;
642
642
  }
643
643
  }
644
+
645
+ // -------- Phantom slot check --------
646
+ // Regardless of layout (simple vertical list, flex-wrap, grid, floats …) the
647
+ // visually closest drop target can be *after* the current last **real** child.
648
+ // In simple layouts the would be index from the existing children would always be the last index
649
+ // but in more complex layouts (flex-wrap, grid, floats …) it can be any index.
650
+ // The problem is we can't predict where an additional element would be rendered in the general case,
651
+ // We therefore create a temporary, invisible clone of that last element, let
652
+ // the browser position it, measure the distance, and remove it immediately
653
+ // (same task → no paint). This leaves `children` back in its original state
654
+ // before we exit the function, so existing index-caching logic and shadow-
655
+ // element bookkeeping continue to work unchanged.
656
+ if (children.length > 0) {
657
+ var originalLen = children.length; // before we append the phantom
658
+ var template = children[originalLen - 1];
659
+ var phantom = template.cloneNode(false); // shallow clone is enough for size
660
+ phantom.style.visibility = "hidden";
661
+ phantom.style.pointerEvents = "none";
662
+ collectionBelowEl.appendChild(phantom);
663
+ var phantomDistance = calcDistanceBetweenCenters(floatingAboveEl, phantom);
664
+ if (phantomDistance < minDistanceSoFar) {
665
+ indexOfMin = originalLen; // index of phantom slot in original list
666
+ }
667
+
668
+ collectionBelowEl.removeChild(phantom);
669
+ }
644
670
  return {
645
671
  index: indexOfMin,
646
672
  isProximityBased: true
@@ -1325,6 +1351,7 @@ var DEFAULT_DROP_ZONE_TYPE$1 = "--any--";
1325
1351
  var MIN_OBSERVATION_INTERVAL_MS = 100;
1326
1352
  var DISABLED_OBSERVATION_INTERVAL_MS = 20;
1327
1353
  var MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3;
1354
+ var DEFAULT_TOUCH_DELAY_MS = 80;
1328
1355
  var DEFAULT_DROP_TARGET_STYLE$1 = {
1329
1356
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
1330
1357
  };
@@ -1345,6 +1372,8 @@ var unlockOriginDzMinDimensions;
1345
1372
  var isDraggedOutsideOfAnyDz = false;
1346
1373
  var scheduledForRemovalAfterDrop = [];
1347
1374
  var multiScroller;
1375
+ var touchDragHoldTimer;
1376
+ var touchHoldElapsed = false;
1348
1377
 
1349
1378
  // a map from type to a set of drop-zones
1350
1379
  var typeToDropZones$1 = new Map();
@@ -1476,10 +1505,7 @@ function handleDraggedEntered(e) {
1476
1505
  source: SOURCES.POINTER
1477
1506
  });
1478
1507
  }
1479
- var _e$detail$indexObj = e.detail.indexObj,
1480
- index = _e$detail$indexObj.index,
1481
- isProximityBased = _e$detail$indexObj.isProximityBased;
1482
- var shadowElIdx = isProximityBased && index === e.currentTarget.children.length - 1 ? index + 1 : index;
1508
+ var shadowElIdx = e.detail.indexObj.index;
1483
1509
  shadowElDropZone = e.currentTarget;
1484
1510
  items.splice(shadowElIdx, 0, shadowElData);
1485
1511
  dispatchConsiderEvent(e.currentTarget, items, {
@@ -1667,19 +1693,12 @@ function scheduleDZForRemovalAfterDrop(dz, destroy) {
1667
1693
  }
1668
1694
  /* cleanup */
1669
1695
  function cleanupPostDrop() {
1670
- draggedEl.remove();
1671
- originalDragTarget.remove();
1672
- if (scheduledForRemovalAfterDrop.length) {
1673
- printDebug(function () {
1674
- return ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop];
1675
- });
1676
- scheduledForRemovalAfterDrop.forEach(function (_ref) {
1677
- var dz = _ref.dz,
1678
- destroy = _ref.destroy;
1679
- destroy();
1680
- dz.remove();
1681
- });
1682
- scheduledForRemovalAfterDrop = [];
1696
+ // Remove the temporary elements that were kept in the DOM during the drag
1697
+ if (draggedEl && draggedEl.remove) {
1698
+ draggedEl.remove();
1699
+ }
1700
+ if (originalDragTarget && originalDragTarget.remove) {
1701
+ originalDragTarget.remove();
1683
1702
  }
1684
1703
  draggedEl = undefined;
1685
1704
  originalDragTarget = undefined;
@@ -1695,6 +1714,23 @@ function cleanupPostDrop() {
1695
1714
  finalizingPreviousDrag = false;
1696
1715
  unlockOriginDzMinDimensions = undefined;
1697
1716
  isDraggedOutsideOfAnyDz = false;
1717
+ if (touchDragHoldTimer) {
1718
+ clearTimeout(touchDragHoldTimer);
1719
+ }
1720
+ touchDragHoldTimer = undefined;
1721
+ touchHoldElapsed = false;
1722
+ if (scheduledForRemovalAfterDrop.length) {
1723
+ printDebug(function () {
1724
+ return ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop];
1725
+ });
1726
+ scheduledForRemovalAfterDrop.forEach(function (_ref) {
1727
+ var dz = _ref.dz,
1728
+ destroy = _ref.destroy;
1729
+ destroy();
1730
+ dz.remove();
1731
+ });
1732
+ scheduledForRemovalAfterDrop = [];
1733
+ }
1698
1734
  }
1699
1735
  function dndzone$2(node, options) {
1700
1736
  var initialized = false;
@@ -1709,7 +1745,8 @@ function dndzone$2(node, options) {
1709
1745
  dropTargetClasses: [],
1710
1746
  transformDraggedElement: function transformDraggedElement() {},
1711
1747
  centreDraggedOnCursor: false,
1712
- dropAnimationDisabled: false
1748
+ dropAnimationDisabled: false,
1749
+ delayTouchStartMs: 0
1713
1750
  };
1714
1751
  printDebug(function () {
1715
1752
  return ["dndzone good to go options: ".concat(toString(options), ", config: ").concat(toString(config)), {
@@ -1737,6 +1774,11 @@ function dndzone$2(node, options) {
1737
1774
  window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart);
1738
1775
  window.removeEventListener("mouseup", handleFalseAlarm);
1739
1776
  window.removeEventListener("touchend", handleFalseAlarm);
1777
+ if (touchDragHoldTimer) {
1778
+ clearTimeout(touchDragHoldTimer);
1779
+ touchDragHoldTimer = undefined;
1780
+ touchHoldElapsed = false;
1781
+ }
1740
1782
  }
1741
1783
  function handleFalseAlarm(e) {
1742
1784
  removeMaybeListeners();
@@ -1755,8 +1797,29 @@ function dndzone$2(node, options) {
1755
1797
  }
1756
1798
  }
1757
1799
  function handleMouseMoveMaybeDragStart(e) {
1800
+ var isTouch = !!e.touches;
1801
+ var c = isTouch ? e.touches[0] : e;
1802
+ // If touch drag delay is configured and not elapsed yet, allow scrolling until either
1803
+ // the delay elapses (timer will call handleDragStart) or the user moves significantly,
1804
+ // in which case we cancel the potential drag and let the interaction be a scroll.
1805
+ if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) {
1806
+ currentMousePosition = {
1807
+ x: c.clientX,
1808
+ y: c.clientY
1809
+ };
1810
+ 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) {
1811
+ // User started scrolling, cancel drag attempt.
1812
+ if (touchDragHoldTimer) {
1813
+ clearTimeout(touchDragHoldTimer);
1814
+ touchDragHoldTimer = undefined;
1815
+ }
1816
+ handleFalseAlarm(e);
1817
+ }
1818
+ return; // Do not preventDefault so scrolling works.
1819
+ }
1820
+
1821
+ // legacy / post-delay path – block scrolling and maybe start drag
1758
1822
  e.preventDefault();
1759
- var c = e.touches ? e.touches[0] : e;
1760
1823
  currentMousePosition = {
1761
1824
  x: c.clientX,
1762
1825
  y: c.clientY
@@ -1787,15 +1850,29 @@ function dndzone$2(node, options) {
1787
1850
  });
1788
1851
  return;
1789
1852
  }
1790
- e.preventDefault();
1853
+ var isTouchStart = !!e.touches;
1854
+ var useDelay = isTouchStart && config.delayTouchStartMs > 0;
1855
+ if (!useDelay) {
1856
+ e.preventDefault();
1857
+ }
1791
1858
  e.stopPropagation();
1792
- var c = e.touches ? e.touches[0] : e;
1859
+ var c = isTouchStart ? e.touches[0] : e;
1793
1860
  dragStartMousePosition = {
1794
1861
  x: c.clientX,
1795
1862
  y: c.clientY
1796
1863
  };
1797
1864
  currentMousePosition = _objectSpread2({}, dragStartMousePosition);
1798
1865
  originalDragTarget = e.currentTarget;
1866
+ if (useDelay) {
1867
+ touchHoldElapsed = false;
1868
+ touchDragHoldTimer = window.setTimeout(function () {
1869
+ // If the finger is still down and no false-alarm happened
1870
+ if (!originalDragTarget) return;
1871
+ touchHoldElapsed = true;
1872
+ removeMaybeListeners();
1873
+ handleDragStart();
1874
+ }, config.delayTouchStartMs);
1875
+ }
1799
1876
  addMaybeListeners();
1800
1877
  }
1801
1878
  function handleDragStart() {
@@ -1893,8 +1970,17 @@ function dndzone$2(node, options) {
1893
1970
  _ref2$centreDraggedOn = _ref2.centreDraggedOnCursor,
1894
1971
  centreDraggedOnCursor = _ref2$centreDraggedOn === void 0 ? false : _ref2$centreDraggedOn,
1895
1972
  _ref2$dropAnimationDi = _ref2.dropAnimationDisabled,
1896
- dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi;
1973
+ dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi,
1974
+ _ref2$delayTouchStart = _ref2.delayTouchStart,
1975
+ delayTouchStartOpt = _ref2$delayTouchStart === void 0 ? false : _ref2$delayTouchStart;
1897
1976
  config.dropAnimationDurationMs = dropAnimationDurationMs;
1977
+ var effectiveDelayMs = 0;
1978
+ if (delayTouchStartOpt === true) {
1979
+ effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS;
1980
+ } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) {
1981
+ effectiveDelayMs = delayTouchStartOpt;
1982
+ }
1983
+ config.delayTouchStartMs = effectiveDelayMs;
1898
1984
  if (config.type && newType !== config.type) {
1899
1985
  unregisterDropZone$1(node, config.type);
1900
1986
  }
@@ -2496,7 +2582,7 @@ function dndzone$1(node, options) {
2496
2582
  return handles;
2497
2583
  }
2498
2584
 
2499
- var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "dropAnimationDisabled"];
2585
+ var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "delayTouchStart", "dropAnimationDisabled"];
2500
2586
 
2501
2587
  /**
2502
2588
  * A custom action to turn any container to a dnd zone and all of its direct children to draggables
@@ -2515,6 +2601,8 @@ var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabl
2515
2601
  * @property {number} [zoneItemTabIndex] - set the tabindex of the list container items when not dragging
2516
2602
  * @property {object} [dropTargetStyle]
2517
2603
  * @property {string[]} [dropTargetClasses]
2604
+ * @property {boolean|number} [delayTouchStart] - On touch devices, wait this long before converting the gesture to a drag.
2605
+ * `true` uses the built-in default (80 ms).
2518
2606
  * @property {boolean} [dropAnimationDisabled] - cancels the drop animation to place
2519
2607
  * @property {function} [transformDraggedElement]
2520
2608
  * @param {HTMLElement} node - the element to enhance
@@ -2568,6 +2656,7 @@ function validateOptions(options) {
2568
2656
  options.transformDraggedElement;
2569
2657
  options.autoAriaDisabled;
2570
2658
  options.centreDraggedOnCursor;
2659
+ var delayTouchStart = options.delayTouchStart;
2571
2660
  options.dropAnimationDisabled;
2572
2661
  var rest = _objectWithoutProperties(options, _excluded);
2573
2662
  /*eslint-enable*/
@@ -2592,6 +2681,13 @@ function validateOptions(options) {
2592
2681
  if (zoneItemTabIndex && !isInt(zoneItemTabIndex)) {
2593
2682
  throw new Error("zoneItemTabIndex should be a number but instead it is a ".concat(_typeof(zoneItemTabIndex), ", ").concat(toString(zoneItemTabIndex)));
2594
2683
  }
2684
+ if (delayTouchStart !== undefined && delayTouchStart !== false) {
2685
+ var validBoolean = delayTouchStart === true;
2686
+ var validNumber = typeof delayTouchStart === "number" && isFinite(delayTouchStart) && delayTouchStart >= 0;
2687
+ if (!validBoolean && !validNumber) {
2688
+ throw new Error("delayTouchStart should be a boolean (true/false) or a non-negative number but instead it is a ".concat(_typeof(delayTouchStart), ", ").concat(toString(delayTouchStart)));
2689
+ }
2690
+ }
2595
2691
  }
2596
2692
  function isInt(value) {
2597
2693
  return !isNaN(value) && function (x) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-dnd-action",
3
3
  "description": "*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 *",
4
- "version": "0.9.62",
4
+ "version": "0.9.64",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/isaacHagoel/svelte-dnd-action.git"
package/src/action.js CHANGED
@@ -20,6 +20,8 @@ import {toString} from "./helpers/util";
20
20
  * @property {number} [zoneItemTabIndex] - set the tabindex of the list container items when not dragging
21
21
  * @property {object} [dropTargetStyle]
22
22
  * @property {string[]} [dropTargetClasses]
23
+ * @property {boolean|number} [delayTouchStart] - On touch devices, wait this long before converting the gesture to a drag.
24
+ * `true` uses the built-in default (80 ms).
23
25
  * @property {boolean} [dropAnimationDisabled] - cancels the drop animation to place
24
26
  * @property {function} [transformDraggedElement]
25
27
  * @param {HTMLElement} node - the element to enhance
@@ -75,6 +77,7 @@ function validateOptions(options) {
75
77
  transformDraggedElement,
76
78
  autoAriaDisabled,
77
79
  centreDraggedOnCursor,
80
+ delayTouchStart,
78
81
  dropAnimationDisabled,
79
82
  ...rest
80
83
  } = options;
@@ -98,6 +101,17 @@ function validateOptions(options) {
98
101
  if (zoneItemTabIndex && !isInt(zoneItemTabIndex)) {
99
102
  throw new Error(`zoneItemTabIndex should be a number but instead it is a ${typeof zoneItemTabIndex}, ${toString(zoneItemTabIndex)}`);
100
103
  }
104
+ if (delayTouchStart !== undefined && delayTouchStart !== false) {
105
+ const validBoolean = delayTouchStart === true;
106
+ const validNumber = typeof delayTouchStart === "number" && isFinite(delayTouchStart) && delayTouchStart >= 0;
107
+ if (!validBoolean && !validNumber) {
108
+ throw new Error(
109
+ `delayTouchStart should be a boolean (true/false) or a non-negative number but instead it is a ${typeof delayTouchStart}, ${toString(
110
+ delayTouchStart
111
+ )}`
112
+ );
113
+ }
114
+ }
101
115
  }
102
116
 
103
117
  function isInt(value) {
@@ -77,5 +77,33 @@ export function findWouldBeIndex(floatingAboveEl, collectionBelowEl) {
77
77
  indexOfMin = i;
78
78
  }
79
79
  }
80
+
81
+ // -------- Phantom slot check --------
82
+ // Regardless of layout (simple vertical list, flex-wrap, grid, floats …) the
83
+ // visually closest drop target can be *after* the current last **real** child.
84
+ // In simple layouts the would be index from the existing children would always be the last index
85
+ // but in more complex layouts (flex-wrap, grid, floats …) it can be any index.
86
+ // The problem is we can't predict where an additional element would be rendered in the general case,
87
+ // We therefore create a temporary, invisible clone of that last element, let
88
+ // the browser position it, measure the distance, and remove it immediately
89
+ // (same task → no paint). This leaves `children` back in its original state
90
+ // before we exit the function, so existing index-caching logic and shadow-
91
+ // element bookkeeping continue to work unchanged.
92
+ if (children.length > 0) {
93
+ const originalLen = children.length; // before we append the phantom
94
+ const template = children[originalLen - 1];
95
+ const phantom = template.cloneNode(false); // shallow clone is enough for size
96
+ phantom.style.visibility = "hidden";
97
+ phantom.style.pointerEvents = "none";
98
+ collectionBelowEl.appendChild(phantom);
99
+
100
+ const phantomDistance = calcDistanceBetweenCenters(floatingAboveEl, phantom);
101
+ if (phantomDistance < minDistanceSoFar) {
102
+ indexOfMin = originalLen; // index of phantom slot in original list
103
+ }
104
+
105
+ collectionBelowEl.removeChild(phantom);
106
+ }
107
+
80
108
  return {index: indexOfMin, isProximityBased: true};
81
109
  }
@@ -39,6 +39,7 @@ const DEFAULT_DROP_ZONE_TYPE = "--any--";
39
39
  const MIN_OBSERVATION_INTERVAL_MS = 100;
40
40
  const DISABLED_OBSERVATION_INTERVAL_MS = 20;
41
41
  const MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3;
42
+ const DEFAULT_TOUCH_DELAY_MS = 80;
42
43
  const DEFAULT_DROP_TARGET_STYLE = {
43
44
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
44
45
  };
@@ -60,6 +61,8 @@ let unlockOriginDzMinDimensions;
60
61
  let isDraggedOutsideOfAnyDz = false;
61
62
  let scheduledForRemovalAfterDrop = [];
62
63
  let multiScroller;
64
+ let touchDragHoldTimer;
65
+ let touchHoldElapsed = false;
63
66
 
64
67
  // a map from type to a set of drop-zones
65
68
  const typeToDropZones = new Map();
@@ -151,8 +154,7 @@ function handleDraggedEntered(e) {
151
154
  source: SOURCES.POINTER
152
155
  });
153
156
  }
154
- const {index, isProximityBased} = e.detail.indexObj;
155
- const shadowElIdx = isProximityBased && index === e.currentTarget.children.length - 1 ? index + 1 : index;
157
+ const {index: shadowElIdx} = e.detail.indexObj;
156
158
  shadowElDropZone = e.currentTarget;
157
159
  items.splice(shadowElIdx, 0, shadowElData);
158
160
  dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_ENTERED, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER});
@@ -310,16 +312,14 @@ function scheduleDZForRemovalAfterDrop(dz, destroy) {
310
312
  }
311
313
  /* cleanup */
312
314
  function cleanupPostDrop() {
313
- draggedEl.remove();
314
- originalDragTarget.remove();
315
- if (scheduledForRemovalAfterDrop.length) {
316
- printDebug(() => ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop]);
317
- scheduledForRemovalAfterDrop.forEach(({dz, destroy}) => {
318
- destroy();
319
- dz.remove();
320
- });
321
- scheduledForRemovalAfterDrop = [];
315
+ // Remove the temporary elements that were kept in the DOM during the drag
316
+ if (draggedEl && draggedEl.remove) {
317
+ draggedEl.remove();
322
318
  }
319
+ if (originalDragTarget && originalDragTarget.remove) {
320
+ originalDragTarget.remove();
321
+ }
322
+
323
323
  draggedEl = undefined;
324
324
  originalDragTarget = undefined;
325
325
  draggedElData = undefined;
@@ -334,6 +334,19 @@ function cleanupPostDrop() {
334
334
  finalizingPreviousDrag = false;
335
335
  unlockOriginDzMinDimensions = undefined;
336
336
  isDraggedOutsideOfAnyDz = false;
337
+ if (touchDragHoldTimer) {
338
+ clearTimeout(touchDragHoldTimer);
339
+ }
340
+ touchDragHoldTimer = undefined;
341
+ touchHoldElapsed = false;
342
+ if (scheduledForRemovalAfterDrop.length) {
343
+ printDebug(() => ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop]);
344
+ scheduledForRemovalAfterDrop.forEach(({dz, destroy}) => {
345
+ destroy();
346
+ dz.remove();
347
+ });
348
+ scheduledForRemovalAfterDrop = [];
349
+ }
337
350
  }
338
351
 
339
352
  export function dndzone(node, options) {
@@ -349,7 +362,8 @@ export function dndzone(node, options) {
349
362
  dropTargetClasses: [],
350
363
  transformDraggedElement: () => {},
351
364
  centreDraggedOnCursor: false,
352
- dropAnimationDisabled: false
365
+ dropAnimationDisabled: false,
366
+ delayTouchStartMs: 0
353
367
  };
354
368
  printDebug(() => [`dndzone good to go options: ${toString(options)}, config: ${toString(config)}`, {node}]);
355
369
  let elToIdx = new Map();
@@ -365,6 +379,11 @@ export function dndzone(node, options) {
365
379
  window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart);
366
380
  window.removeEventListener("mouseup", handleFalseAlarm);
367
381
  window.removeEventListener("touchend", handleFalseAlarm);
382
+ if (touchDragHoldTimer) {
383
+ clearTimeout(touchDragHoldTimer);
384
+ touchDragHoldTimer = undefined;
385
+ touchHoldElapsed = false;
386
+ }
368
387
  }
369
388
  function handleFalseAlarm(e) {
370
389
  removeMaybeListeners();
@@ -384,8 +403,30 @@ export function dndzone(node, options) {
384
403
  }
385
404
 
386
405
  function handleMouseMoveMaybeDragStart(e) {
406
+ const isTouch = !!e.touches;
407
+ const c = isTouch ? e.touches[0] : e;
408
+ // If touch drag delay is configured and not elapsed yet, allow scrolling until either
409
+ // the delay elapses (timer will call handleDragStart) or the user moves significantly,
410
+ // in which case we cancel the potential drag and let the interaction be a scroll.
411
+ if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) {
412
+ currentMousePosition = {x: c.clientX, y: c.clientY};
413
+ if (
414
+ Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX ||
415
+ Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX
416
+ ) {
417
+ // User started scrolling, cancel drag attempt.
418
+ if (touchDragHoldTimer) {
419
+ clearTimeout(touchDragHoldTimer);
420
+ touchDragHoldTimer = undefined;
421
+ }
422
+ handleFalseAlarm(e);
423
+ }
424
+ return; // Do not preventDefault so scrolling works.
425
+ }
426
+
427
+ // legacy / post-delay path – block scrolling and maybe start drag
387
428
  e.preventDefault();
388
- const c = e.touches ? e.touches[0] : e;
429
+
389
430
  currentMousePosition = {x: c.clientX, y: c.clientY};
390
431
  if (
391
432
  Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX ||
@@ -410,12 +451,30 @@ export function dndzone(node, options) {
410
451
  printDebug(() => "cannot start a new drag before finalizing previous one");
411
452
  return;
412
453
  }
413
- e.preventDefault();
454
+ const isTouchStart = !!e.touches;
455
+ const useDelay = isTouchStart && config.delayTouchStartMs > 0;
456
+
457
+ if (!useDelay) {
458
+ e.preventDefault();
459
+ }
414
460
  e.stopPropagation();
415
- const c = e.touches ? e.touches[0] : e;
461
+
462
+ const c = isTouchStart ? e.touches[0] : e;
416
463
  dragStartMousePosition = {x: c.clientX, y: c.clientY};
417
464
  currentMousePosition = {...dragStartMousePosition};
418
465
  originalDragTarget = e.currentTarget;
466
+
467
+ if (useDelay) {
468
+ touchHoldElapsed = false;
469
+ touchDragHoldTimer = window.setTimeout(() => {
470
+ // If the finger is still down and no false-alarm happened
471
+ if (!originalDragTarget) return;
472
+ touchHoldElapsed = true;
473
+ removeMaybeListeners();
474
+ handleDragStart();
475
+ }, config.delayTouchStartMs);
476
+ }
477
+
419
478
  addMaybeListeners();
420
479
  }
421
480
 
@@ -487,9 +546,19 @@ export function dndzone(node, options) {
487
546
  dropTargetClasses = [],
488
547
  transformDraggedElement = () => {},
489
548
  centreDraggedOnCursor = false,
490
- dropAnimationDisabled = false
549
+ dropAnimationDisabled = false,
550
+ delayTouchStart: delayTouchStartOpt = false
491
551
  }) {
492
552
  config.dropAnimationDurationMs = dropAnimationDurationMs;
553
+
554
+ let effectiveDelayMs = 0;
555
+ if (delayTouchStartOpt === true) {
556
+ effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS;
557
+ } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) {
558
+ effectiveDelayMs = delayTouchStartOpt;
559
+ }
560
+ config.delayTouchStartMs = effectiveDelayMs;
561
+
493
562
  if (config.type && newType !== config.type) {
494
563
  unregisterDropZone(node, config.type);
495
564
  }