svelte-dnd-action 0.9.61 → 0.9.63

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
@@ -1331,6 +1331,7 @@
1331
1331
  var MIN_OBSERVATION_INTERVAL_MS = 100;
1332
1332
  var DISABLED_OBSERVATION_INTERVAL_MS = 20;
1333
1333
  var MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3;
1334
+ var DEFAULT_TOUCH_DELAY_MS = 80;
1334
1335
  var DEFAULT_DROP_TARGET_STYLE$1 = {
1335
1336
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
1336
1337
  };
@@ -1351,6 +1352,8 @@
1351
1352
  var isDraggedOutsideOfAnyDz = false;
1352
1353
  var scheduledForRemovalAfterDrop = [];
1353
1354
  var multiScroller;
1355
+ var touchDragHoldTimer;
1356
+ var touchHoldElapsed = false;
1354
1357
 
1355
1358
  // a map from type to a set of drop-zones
1356
1359
  var typeToDropZones$1 = new Map();
@@ -1673,19 +1676,12 @@
1673
1676
  }
1674
1677
  /* cleanup */
1675
1678
  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 = [];
1679
+ // Remove the temporary elements that were kept in the DOM during the drag
1680
+ if (draggedEl && draggedEl.remove) {
1681
+ draggedEl.remove();
1682
+ }
1683
+ if (originalDragTarget && originalDragTarget.remove) {
1684
+ originalDragTarget.remove();
1689
1685
  }
1690
1686
  draggedEl = undefined;
1691
1687
  originalDragTarget = undefined;
@@ -1701,6 +1697,23 @@
1701
1697
  finalizingPreviousDrag = false;
1702
1698
  unlockOriginDzMinDimensions = undefined;
1703
1699
  isDraggedOutsideOfAnyDz = false;
1700
+ if (touchDragHoldTimer) {
1701
+ clearTimeout(touchDragHoldTimer);
1702
+ }
1703
+ touchDragHoldTimer = undefined;
1704
+ touchHoldElapsed = false;
1705
+ if (scheduledForRemovalAfterDrop.length) {
1706
+ printDebug(function () {
1707
+ return ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop];
1708
+ });
1709
+ scheduledForRemovalAfterDrop.forEach(function (_ref) {
1710
+ var dz = _ref.dz,
1711
+ destroy = _ref.destroy;
1712
+ destroy();
1713
+ dz.remove();
1714
+ });
1715
+ scheduledForRemovalAfterDrop = [];
1716
+ }
1704
1717
  }
1705
1718
  function dndzone$2(node, options) {
1706
1719
  var initialized = false;
@@ -1715,7 +1728,8 @@
1715
1728
  dropTargetClasses: [],
1716
1729
  transformDraggedElement: function transformDraggedElement() {},
1717
1730
  centreDraggedOnCursor: false,
1718
- dropAnimationDisabled: false
1731
+ dropAnimationDisabled: false,
1732
+ delayTouchStartMs: 0
1719
1733
  };
1720
1734
  printDebug(function () {
1721
1735
  return ["dndzone good to go options: ".concat(toString(options), ", config: ").concat(toString(config)), {
@@ -1743,6 +1757,11 @@
1743
1757
  window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart);
1744
1758
  window.removeEventListener("mouseup", handleFalseAlarm);
1745
1759
  window.removeEventListener("touchend", handleFalseAlarm);
1760
+ if (touchDragHoldTimer) {
1761
+ clearTimeout(touchDragHoldTimer);
1762
+ touchDragHoldTimer = undefined;
1763
+ touchHoldElapsed = false;
1764
+ }
1746
1765
  }
1747
1766
  function handleFalseAlarm(e) {
1748
1767
  removeMaybeListeners();
@@ -1761,8 +1780,29 @@
1761
1780
  }
1762
1781
  }
1763
1782
  function handleMouseMoveMaybeDragStart(e) {
1783
+ var isTouch = !!e.touches;
1784
+ var c = isTouch ? e.touches[0] : e;
1785
+ // If touch drag delay is configured and not elapsed yet, allow scrolling until either
1786
+ // the delay elapses (timer will call handleDragStart) or the user moves significantly,
1787
+ // in which case we cancel the potential drag and let the interaction be a scroll.
1788
+ if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) {
1789
+ currentMousePosition = {
1790
+ x: c.clientX,
1791
+ y: c.clientY
1792
+ };
1793
+ 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) {
1794
+ // User started scrolling, cancel drag attempt.
1795
+ if (touchDragHoldTimer) {
1796
+ clearTimeout(touchDragHoldTimer);
1797
+ touchDragHoldTimer = undefined;
1798
+ }
1799
+ handleFalseAlarm(e);
1800
+ }
1801
+ return; // Do not preventDefault so scrolling works.
1802
+ }
1803
+
1804
+ // legacy / post-delay path – block scrolling and maybe start drag
1764
1805
  e.preventDefault();
1765
- var c = e.touches ? e.touches[0] : e;
1766
1806
  currentMousePosition = {
1767
1807
  x: c.clientX,
1768
1808
  y: c.clientY
@@ -1793,15 +1833,29 @@
1793
1833
  });
1794
1834
  return;
1795
1835
  }
1796
- e.preventDefault();
1836
+ var isTouchStart = !!e.touches;
1837
+ var useDelay = isTouchStart && config.delayTouchStartMs > 0;
1838
+ if (!useDelay) {
1839
+ e.preventDefault();
1840
+ }
1797
1841
  e.stopPropagation();
1798
- var c = e.touches ? e.touches[0] : e;
1842
+ var c = isTouchStart ? e.touches[0] : e;
1799
1843
  dragStartMousePosition = {
1800
1844
  x: c.clientX,
1801
1845
  y: c.clientY
1802
1846
  };
1803
1847
  currentMousePosition = _objectSpread2({}, dragStartMousePosition);
1804
1848
  originalDragTarget = e.currentTarget;
1849
+ if (useDelay) {
1850
+ touchHoldElapsed = false;
1851
+ touchDragHoldTimer = window.setTimeout(function () {
1852
+ // If the finger is still down and no false-alarm happened
1853
+ if (!originalDragTarget) return;
1854
+ touchHoldElapsed = true;
1855
+ removeMaybeListeners();
1856
+ handleDragStart();
1857
+ }, config.delayTouchStartMs);
1858
+ }
1805
1859
  addMaybeListeners();
1806
1860
  }
1807
1861
  function handleDragStart() {
@@ -1899,8 +1953,17 @@
1899
1953
  _ref2$centreDraggedOn = _ref2.centreDraggedOnCursor,
1900
1954
  centreDraggedOnCursor = _ref2$centreDraggedOn === void 0 ? false : _ref2$centreDraggedOn,
1901
1955
  _ref2$dropAnimationDi = _ref2.dropAnimationDisabled,
1902
- dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi;
1956
+ dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi,
1957
+ _ref2$delayTouchStart = _ref2.delayTouchStart,
1958
+ delayTouchStartOpt = _ref2$delayTouchStart === void 0 ? false : _ref2$delayTouchStart;
1903
1959
  config.dropAnimationDurationMs = dropAnimationDurationMs;
1960
+ var effectiveDelayMs = 0;
1961
+ if (delayTouchStartOpt === true) {
1962
+ effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS;
1963
+ } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) {
1964
+ effectiveDelayMs = delayTouchStartOpt;
1965
+ }
1966
+ config.delayTouchStartMs = effectiveDelayMs;
1904
1967
  if (config.type && newType !== config.type) {
1905
1968
  unregisterDropZone$1(node, config.type);
1906
1969
  }
@@ -2502,7 +2565,7 @@
2502
2565
  return handles;
2503
2566
  }
2504
2567
 
2505
- var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "dropAnimationDisabled"];
2568
+ var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "delayTouchStart", "dropAnimationDisabled"];
2506
2569
 
2507
2570
  /**
2508
2571
  * A custom action to turn any container to a dnd zone and all of its direct children to draggables
@@ -2521,6 +2584,8 @@
2521
2584
  * @property {number} [zoneItemTabIndex] - set the tabindex of the list container items when not dragging
2522
2585
  * @property {object} [dropTargetStyle]
2523
2586
  * @property {string[]} [dropTargetClasses]
2587
+ * @property {boolean|number} [delayTouchStart] - On touch devices, wait this long before converting the gesture to a drag.
2588
+ * `true` uses the built-in default (80 ms).
2524
2589
  * @property {boolean} [dropAnimationDisabled] - cancels the drop animation to place
2525
2590
  * @property {function} [transformDraggedElement]
2526
2591
  * @param {HTMLElement} node - the element to enhance
@@ -2574,6 +2639,7 @@
2574
2639
  options.transformDraggedElement;
2575
2640
  options.autoAriaDisabled;
2576
2641
  options.centreDraggedOnCursor;
2642
+ var delayTouchStart = options.delayTouchStart;
2577
2643
  options.dropAnimationDisabled;
2578
2644
  var rest = _objectWithoutProperties(options, _excluded);
2579
2645
  /*eslint-enable*/
@@ -2598,6 +2664,13 @@
2598
2664
  if (zoneItemTabIndex && !isInt(zoneItemTabIndex)) {
2599
2665
  throw new Error("zoneItemTabIndex should be a number but instead it is a ".concat(_typeof(zoneItemTabIndex), ", ").concat(toString(zoneItemTabIndex)));
2600
2666
  }
2667
+ if (delayTouchStart !== undefined && delayTouchStart !== false) {
2668
+ var validBoolean = delayTouchStart === true;
2669
+ var validNumber = typeof delayTouchStart === "number" && isFinite(delayTouchStart) && delayTouchStart >= 0;
2670
+ if (!validBoolean && !validNumber) {
2671
+ 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)));
2672
+ }
2673
+ }
2601
2674
  }
2602
2675
  function isInt(value) {
2603
2676
  return !isNaN(value) && function (x) {
@@ -2629,10 +2702,10 @@
2629
2702
  }
2630
2703
 
2631
2704
  var isItemsDragDisabled = createStore(true);
2705
+ var userDragDisabled = createStore(false);
2632
2706
  function getAddedOptions() {
2633
- var isItemsDragDisabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
2634
2707
  return {
2635
- dragDisabled: isItemsDragDisabled,
2708
+ dragDisabled: userDragDisabled.get() || isItemsDragDisabled.get(),
2636
2709
  zoneItemTabIndex: -1
2637
2710
  };
2638
2711
  }
@@ -2647,12 +2720,22 @@
2647
2720
  * @return {{update: (newOptions: Object) => {}, destroy: () => {}}}
2648
2721
  */
2649
2722
  function dragHandleZone(node, options) {
2723
+ var _options$dragDisabled;
2724
+ // Initialise stores from initial options
2725
+ userDragDisabled.set((_options$dragDisabled = options === null || options === void 0 ? void 0 : options.dragDisabled) !== null && _options$dragDisabled !== void 0 ? _options$dragDisabled : false);
2650
2726
  var currentOptions = options;
2651
2727
  var zone = dndzone(node, _objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions()));
2652
- function isItemDisabledCB(isItemsDragDisabled) {
2653
- zone.update(_objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions(isItemsDragDisabled)));
2728
+ function updateZone() {
2729
+ zone.update(_objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions()));
2654
2730
  }
2655
- isItemsDragDisabled.subscribe(isItemDisabledCB);
2731
+
2732
+ // Subscribe to internal store so finishing a drag updates the zone
2733
+ isItemsDragDisabled.subscribe(updateZone);
2734
+
2735
+ // We don't need to subscribe to userDragDisabled here because updates to
2736
+ // it always come through the `update` lifecycle and will call `updateZone`
2737
+ // anyway.
2738
+
2656
2739
  function consider(e) {
2657
2740
  var _e$detail$info = e.detail.info,
2658
2741
  source = _e$detail$info.source,
@@ -2673,13 +2756,16 @@
2673
2756
  node.addEventListener("finalize", finalize);
2674
2757
  return {
2675
2758
  update: function update(newOptions) {
2759
+ var _currentOptions$dragD, _currentOptions;
2676
2760
  currentOptions = newOptions;
2677
- zone.update(_objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions(isItemsDragDisabled.get())));
2761
+ // keep store in sync with external prop
2762
+ userDragDisabled.set((_currentOptions$dragD = (_currentOptions = currentOptions) === null || _currentOptions === void 0 ? void 0 : _currentOptions.dragDisabled) !== null && _currentOptions$dragD !== void 0 ? _currentOptions$dragD : false);
2763
+ updateZone();
2678
2764
  },
2679
2765
  destroy: function destroy() {
2680
2766
  node.removeEventListener("consider", consider);
2681
2767
  node.removeEventListener("finalize", finalize);
2682
- isItemsDragDisabled.unsubscribe(isItemDisabledCB);
2768
+ isItemsDragDisabled.unsubscribe(updateZone);
2683
2769
  }
2684
2770
  };
2685
2771
  }
@@ -2708,10 +2794,21 @@
2708
2794
  window.removeEventListener("mouseup", resetStartDrag);
2709
2795
  window.removeEventListener("touchend", resetStartDrag);
2710
2796
  }
2711
- isItemsDragDisabled.subscribe(function (disabled) {
2712
- handle.tabIndex = disabled ? 0 : -1;
2713
- handle.style.cursor = disabled ? "grab" : "grabbing";
2714
- });
2797
+ var recomputeHandleState = function recomputeHandleState() {
2798
+ var userDisabled = userDragDisabled.get();
2799
+ var internalDisabled = isItemsDragDisabled.get();
2800
+ if (userDisabled) {
2801
+ handle.tabIndex = -1;
2802
+ handle.style.cursor = ""; // default cursor
2803
+ } else {
2804
+ handle.tabIndex = internalDisabled ? 0 : -1;
2805
+ handle.style.cursor = internalDisabled ? "grab" : "grabbing";
2806
+ }
2807
+ };
2808
+
2809
+ // Subscribe to both stores
2810
+ userDragDisabled.subscribe(recomputeHandleState);
2811
+ isItemsDragDisabled.subscribe(recomputeHandleState);
2715
2812
  handle.addEventListener("mousedown", startDrag);
2716
2813
  handle.addEventListener("touchstart", startDrag);
2717
2814
  handle.addEventListener("keydown", handleKeyDown);
@@ -2721,6 +2818,8 @@
2721
2818
  handle.removeEventListener("mousedown", startDrag);
2722
2819
  handle.removeEventListener("touchstart", startDrag);
2723
2820
  handle.removeEventListener("keydown", handleKeyDown);
2821
+ userDragDisabled.unsubscribe(recomputeHandleState);
2822
+ isItemsDragDisabled.unsubscribe(recomputeHandleState);
2724
2823
  }
2725
2824
  };
2726
2825
  }
package/dist/index.mjs CHANGED
@@ -1325,6 +1325,7 @@ var DEFAULT_DROP_ZONE_TYPE$1 = "--any--";
1325
1325
  var MIN_OBSERVATION_INTERVAL_MS = 100;
1326
1326
  var DISABLED_OBSERVATION_INTERVAL_MS = 20;
1327
1327
  var MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3;
1328
+ var DEFAULT_TOUCH_DELAY_MS = 80;
1328
1329
  var DEFAULT_DROP_TARGET_STYLE$1 = {
1329
1330
  outline: "rgba(255, 255, 102, 0.7) solid 2px"
1330
1331
  };
@@ -1345,6 +1346,8 @@ var unlockOriginDzMinDimensions;
1345
1346
  var isDraggedOutsideOfAnyDz = false;
1346
1347
  var scheduledForRemovalAfterDrop = [];
1347
1348
  var multiScroller;
1349
+ var touchDragHoldTimer;
1350
+ var touchHoldElapsed = false;
1348
1351
 
1349
1352
  // a map from type to a set of drop-zones
1350
1353
  var typeToDropZones$1 = new Map();
@@ -1667,19 +1670,12 @@ function scheduleDZForRemovalAfterDrop(dz, destroy) {
1667
1670
  }
1668
1671
  /* cleanup */
1669
1672
  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 = [];
1673
+ // Remove the temporary elements that were kept in the DOM during the drag
1674
+ if (draggedEl && draggedEl.remove) {
1675
+ draggedEl.remove();
1676
+ }
1677
+ if (originalDragTarget && originalDragTarget.remove) {
1678
+ originalDragTarget.remove();
1683
1679
  }
1684
1680
  draggedEl = undefined;
1685
1681
  originalDragTarget = undefined;
@@ -1695,6 +1691,23 @@ function cleanupPostDrop() {
1695
1691
  finalizingPreviousDrag = false;
1696
1692
  unlockOriginDzMinDimensions = undefined;
1697
1693
  isDraggedOutsideOfAnyDz = false;
1694
+ if (touchDragHoldTimer) {
1695
+ clearTimeout(touchDragHoldTimer);
1696
+ }
1697
+ touchDragHoldTimer = undefined;
1698
+ touchHoldElapsed = false;
1699
+ if (scheduledForRemovalAfterDrop.length) {
1700
+ printDebug(function () {
1701
+ return ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop];
1702
+ });
1703
+ scheduledForRemovalAfterDrop.forEach(function (_ref) {
1704
+ var dz = _ref.dz,
1705
+ destroy = _ref.destroy;
1706
+ destroy();
1707
+ dz.remove();
1708
+ });
1709
+ scheduledForRemovalAfterDrop = [];
1710
+ }
1698
1711
  }
1699
1712
  function dndzone$2(node, options) {
1700
1713
  var initialized = false;
@@ -1709,7 +1722,8 @@ function dndzone$2(node, options) {
1709
1722
  dropTargetClasses: [],
1710
1723
  transformDraggedElement: function transformDraggedElement() {},
1711
1724
  centreDraggedOnCursor: false,
1712
- dropAnimationDisabled: false
1725
+ dropAnimationDisabled: false,
1726
+ delayTouchStartMs: 0
1713
1727
  };
1714
1728
  printDebug(function () {
1715
1729
  return ["dndzone good to go options: ".concat(toString(options), ", config: ").concat(toString(config)), {
@@ -1737,6 +1751,11 @@ function dndzone$2(node, options) {
1737
1751
  window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart);
1738
1752
  window.removeEventListener("mouseup", handleFalseAlarm);
1739
1753
  window.removeEventListener("touchend", handleFalseAlarm);
1754
+ if (touchDragHoldTimer) {
1755
+ clearTimeout(touchDragHoldTimer);
1756
+ touchDragHoldTimer = undefined;
1757
+ touchHoldElapsed = false;
1758
+ }
1740
1759
  }
1741
1760
  function handleFalseAlarm(e) {
1742
1761
  removeMaybeListeners();
@@ -1755,8 +1774,29 @@ function dndzone$2(node, options) {
1755
1774
  }
1756
1775
  }
1757
1776
  function handleMouseMoveMaybeDragStart(e) {
1777
+ var isTouch = !!e.touches;
1778
+ var c = isTouch ? e.touches[0] : e;
1779
+ // If touch drag delay is configured and not elapsed yet, allow scrolling until either
1780
+ // the delay elapses (timer will call handleDragStart) or the user moves significantly,
1781
+ // in which case we cancel the potential drag and let the interaction be a scroll.
1782
+ if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) {
1783
+ currentMousePosition = {
1784
+ x: c.clientX,
1785
+ y: c.clientY
1786
+ };
1787
+ 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) {
1788
+ // User started scrolling, cancel drag attempt.
1789
+ if (touchDragHoldTimer) {
1790
+ clearTimeout(touchDragHoldTimer);
1791
+ touchDragHoldTimer = undefined;
1792
+ }
1793
+ handleFalseAlarm(e);
1794
+ }
1795
+ return; // Do not preventDefault so scrolling works.
1796
+ }
1797
+
1798
+ // legacy / post-delay path – block scrolling and maybe start drag
1758
1799
  e.preventDefault();
1759
- var c = e.touches ? e.touches[0] : e;
1760
1800
  currentMousePosition = {
1761
1801
  x: c.clientX,
1762
1802
  y: c.clientY
@@ -1787,15 +1827,29 @@ function dndzone$2(node, options) {
1787
1827
  });
1788
1828
  return;
1789
1829
  }
1790
- e.preventDefault();
1830
+ var isTouchStart = !!e.touches;
1831
+ var useDelay = isTouchStart && config.delayTouchStartMs > 0;
1832
+ if (!useDelay) {
1833
+ e.preventDefault();
1834
+ }
1791
1835
  e.stopPropagation();
1792
- var c = e.touches ? e.touches[0] : e;
1836
+ var c = isTouchStart ? e.touches[0] : e;
1793
1837
  dragStartMousePosition = {
1794
1838
  x: c.clientX,
1795
1839
  y: c.clientY
1796
1840
  };
1797
1841
  currentMousePosition = _objectSpread2({}, dragStartMousePosition);
1798
1842
  originalDragTarget = e.currentTarget;
1843
+ if (useDelay) {
1844
+ touchHoldElapsed = false;
1845
+ touchDragHoldTimer = window.setTimeout(function () {
1846
+ // If the finger is still down and no false-alarm happened
1847
+ if (!originalDragTarget) return;
1848
+ touchHoldElapsed = true;
1849
+ removeMaybeListeners();
1850
+ handleDragStart();
1851
+ }, config.delayTouchStartMs);
1852
+ }
1799
1853
  addMaybeListeners();
1800
1854
  }
1801
1855
  function handleDragStart() {
@@ -1893,8 +1947,17 @@ function dndzone$2(node, options) {
1893
1947
  _ref2$centreDraggedOn = _ref2.centreDraggedOnCursor,
1894
1948
  centreDraggedOnCursor = _ref2$centreDraggedOn === void 0 ? false : _ref2$centreDraggedOn,
1895
1949
  _ref2$dropAnimationDi = _ref2.dropAnimationDisabled,
1896
- dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi;
1950
+ dropAnimationDisabled = _ref2$dropAnimationDi === void 0 ? false : _ref2$dropAnimationDi,
1951
+ _ref2$delayTouchStart = _ref2.delayTouchStart,
1952
+ delayTouchStartOpt = _ref2$delayTouchStart === void 0 ? false : _ref2$delayTouchStart;
1897
1953
  config.dropAnimationDurationMs = dropAnimationDurationMs;
1954
+ var effectiveDelayMs = 0;
1955
+ if (delayTouchStartOpt === true) {
1956
+ effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS;
1957
+ } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) {
1958
+ effectiveDelayMs = delayTouchStartOpt;
1959
+ }
1960
+ config.delayTouchStartMs = effectiveDelayMs;
1898
1961
  if (config.type && newType !== config.type) {
1899
1962
  unregisterDropZone$1(node, config.type);
1900
1963
  }
@@ -2496,7 +2559,7 @@ function dndzone$1(node, options) {
2496
2559
  return handles;
2497
2560
  }
2498
2561
 
2499
- var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "dropAnimationDisabled"];
2562
+ var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabled", "dropFromOthersDisabled", "zoneTabIndex", "zoneItemTabIndex", "dropTargetStyle", "dropTargetClasses", "transformDraggedElement", "autoAriaDisabled", "centreDraggedOnCursor", "delayTouchStart", "dropAnimationDisabled"];
2500
2563
 
2501
2564
  /**
2502
2565
  * A custom action to turn any container to a dnd zone and all of its direct children to draggables
@@ -2515,6 +2578,8 @@ var _excluded = ["items", "flipDurationMs", "type", "dragDisabled", "morphDisabl
2515
2578
  * @property {number} [zoneItemTabIndex] - set the tabindex of the list container items when not dragging
2516
2579
  * @property {object} [dropTargetStyle]
2517
2580
  * @property {string[]} [dropTargetClasses]
2581
+ * @property {boolean|number} [delayTouchStart] - On touch devices, wait this long before converting the gesture to a drag.
2582
+ * `true` uses the built-in default (80 ms).
2518
2583
  * @property {boolean} [dropAnimationDisabled] - cancels the drop animation to place
2519
2584
  * @property {function} [transformDraggedElement]
2520
2585
  * @param {HTMLElement} node - the element to enhance
@@ -2568,6 +2633,7 @@ function validateOptions(options) {
2568
2633
  options.transformDraggedElement;
2569
2634
  options.autoAriaDisabled;
2570
2635
  options.centreDraggedOnCursor;
2636
+ var delayTouchStart = options.delayTouchStart;
2571
2637
  options.dropAnimationDisabled;
2572
2638
  var rest = _objectWithoutProperties(options, _excluded);
2573
2639
  /*eslint-enable*/
@@ -2592,6 +2658,13 @@ function validateOptions(options) {
2592
2658
  if (zoneItemTabIndex && !isInt(zoneItemTabIndex)) {
2593
2659
  throw new Error("zoneItemTabIndex should be a number but instead it is a ".concat(_typeof(zoneItemTabIndex), ", ").concat(toString(zoneItemTabIndex)));
2594
2660
  }
2661
+ if (delayTouchStart !== undefined && delayTouchStart !== false) {
2662
+ var validBoolean = delayTouchStart === true;
2663
+ var validNumber = typeof delayTouchStart === "number" && isFinite(delayTouchStart) && delayTouchStart >= 0;
2664
+ if (!validBoolean && !validNumber) {
2665
+ 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)));
2666
+ }
2667
+ }
2595
2668
  }
2596
2669
  function isInt(value) {
2597
2670
  return !isNaN(value) && function (x) {
@@ -2623,10 +2696,10 @@ function createStore(initialValue) {
2623
2696
  }
2624
2697
 
2625
2698
  var isItemsDragDisabled = createStore(true);
2699
+ var userDragDisabled = createStore(false);
2626
2700
  function getAddedOptions() {
2627
- var isItemsDragDisabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
2628
2701
  return {
2629
- dragDisabled: isItemsDragDisabled,
2702
+ dragDisabled: userDragDisabled.get() || isItemsDragDisabled.get(),
2630
2703
  zoneItemTabIndex: -1
2631
2704
  };
2632
2705
  }
@@ -2641,12 +2714,22 @@ function getAddedOptions() {
2641
2714
  * @return {{update: (newOptions: Object) => {}, destroy: () => {}}}
2642
2715
  */
2643
2716
  function dragHandleZone(node, options) {
2717
+ var _options$dragDisabled;
2718
+ // Initialise stores from initial options
2719
+ userDragDisabled.set((_options$dragDisabled = options === null || options === void 0 ? void 0 : options.dragDisabled) !== null && _options$dragDisabled !== void 0 ? _options$dragDisabled : false);
2644
2720
  var currentOptions = options;
2645
2721
  var zone = dndzone(node, _objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions()));
2646
- function isItemDisabledCB(isItemsDragDisabled) {
2647
- zone.update(_objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions(isItemsDragDisabled)));
2722
+ function updateZone() {
2723
+ zone.update(_objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions()));
2648
2724
  }
2649
- isItemsDragDisabled.subscribe(isItemDisabledCB);
2725
+
2726
+ // Subscribe to internal store so finishing a drag updates the zone
2727
+ isItemsDragDisabled.subscribe(updateZone);
2728
+
2729
+ // We don't need to subscribe to userDragDisabled here because updates to
2730
+ // it always come through the `update` lifecycle and will call `updateZone`
2731
+ // anyway.
2732
+
2650
2733
  function consider(e) {
2651
2734
  var _e$detail$info = e.detail.info,
2652
2735
  source = _e$detail$info.source,
@@ -2667,13 +2750,16 @@ function dragHandleZone(node, options) {
2667
2750
  node.addEventListener("finalize", finalize);
2668
2751
  return {
2669
2752
  update: function update(newOptions) {
2753
+ var _currentOptions$dragD, _currentOptions;
2670
2754
  currentOptions = newOptions;
2671
- zone.update(_objectSpread2(_objectSpread2({}, currentOptions), getAddedOptions(isItemsDragDisabled.get())));
2755
+ // keep store in sync with external prop
2756
+ userDragDisabled.set((_currentOptions$dragD = (_currentOptions = currentOptions) === null || _currentOptions === void 0 ? void 0 : _currentOptions.dragDisabled) !== null && _currentOptions$dragD !== void 0 ? _currentOptions$dragD : false);
2757
+ updateZone();
2672
2758
  },
2673
2759
  destroy: function destroy() {
2674
2760
  node.removeEventListener("consider", consider);
2675
2761
  node.removeEventListener("finalize", finalize);
2676
- isItemsDragDisabled.unsubscribe(isItemDisabledCB);
2762
+ isItemsDragDisabled.unsubscribe(updateZone);
2677
2763
  }
2678
2764
  };
2679
2765
  }
@@ -2702,10 +2788,21 @@ function dragHandle(handle) {
2702
2788
  window.removeEventListener("mouseup", resetStartDrag);
2703
2789
  window.removeEventListener("touchend", resetStartDrag);
2704
2790
  }
2705
- isItemsDragDisabled.subscribe(function (disabled) {
2706
- handle.tabIndex = disabled ? 0 : -1;
2707
- handle.style.cursor = disabled ? "grab" : "grabbing";
2708
- });
2791
+ var recomputeHandleState = function recomputeHandleState() {
2792
+ var userDisabled = userDragDisabled.get();
2793
+ var internalDisabled = isItemsDragDisabled.get();
2794
+ if (userDisabled) {
2795
+ handle.tabIndex = -1;
2796
+ handle.style.cursor = ""; // default cursor
2797
+ } else {
2798
+ handle.tabIndex = internalDisabled ? 0 : -1;
2799
+ handle.style.cursor = internalDisabled ? "grab" : "grabbing";
2800
+ }
2801
+ };
2802
+
2803
+ // Subscribe to both stores
2804
+ userDragDisabled.subscribe(recomputeHandleState);
2805
+ isItemsDragDisabled.subscribe(recomputeHandleState);
2709
2806
  handle.addEventListener("mousedown", startDrag);
2710
2807
  handle.addEventListener("touchstart", startDrag);
2711
2808
  handle.addEventListener("keydown", handleKeyDown);
@@ -2715,6 +2812,8 @@ function dragHandle(handle) {
2715
2812
  handle.removeEventListener("mousedown", startDrag);
2716
2813
  handle.removeEventListener("touchstart", startDrag);
2717
2814
  handle.removeEventListener("keydown", handleKeyDown);
2815
+ userDragDisabled.unsubscribe(recomputeHandleState);
2816
+ isItemsDragDisabled.unsubscribe(recomputeHandleState);
2718
2817
  }
2719
2818
  };
2720
2819
  }
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.61",
4
+ "version": "0.9.63",
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) {
@@ -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();
@@ -310,16 +313,14 @@ function scheduleDZForRemovalAfterDrop(dz, destroy) {
310
313
  }
311
314
  /* cleanup */
312
315
  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 = [];
316
+ // Remove the temporary elements that were kept in the DOM during the drag
317
+ if (draggedEl && draggedEl.remove) {
318
+ draggedEl.remove();
322
319
  }
320
+ if (originalDragTarget && originalDragTarget.remove) {
321
+ originalDragTarget.remove();
322
+ }
323
+
323
324
  draggedEl = undefined;
324
325
  originalDragTarget = undefined;
325
326
  draggedElData = undefined;
@@ -334,6 +335,19 @@ function cleanupPostDrop() {
334
335
  finalizingPreviousDrag = false;
335
336
  unlockOriginDzMinDimensions = undefined;
336
337
  isDraggedOutsideOfAnyDz = false;
338
+ if (touchDragHoldTimer) {
339
+ clearTimeout(touchDragHoldTimer);
340
+ }
341
+ touchDragHoldTimer = undefined;
342
+ touchHoldElapsed = false;
343
+ if (scheduledForRemovalAfterDrop.length) {
344
+ printDebug(() => ["will destroy zones that were removed during drag", scheduledForRemovalAfterDrop]);
345
+ scheduledForRemovalAfterDrop.forEach(({dz, destroy}) => {
346
+ destroy();
347
+ dz.remove();
348
+ });
349
+ scheduledForRemovalAfterDrop = [];
350
+ }
337
351
  }
338
352
 
339
353
  export function dndzone(node, options) {
@@ -349,7 +363,8 @@ export function dndzone(node, options) {
349
363
  dropTargetClasses: [],
350
364
  transformDraggedElement: () => {},
351
365
  centreDraggedOnCursor: false,
352
- dropAnimationDisabled: false
366
+ dropAnimationDisabled: false,
367
+ delayTouchStartMs: 0
353
368
  };
354
369
  printDebug(() => [`dndzone good to go options: ${toString(options)}, config: ${toString(config)}`, {node}]);
355
370
  let elToIdx = new Map();
@@ -365,6 +380,11 @@ export function dndzone(node, options) {
365
380
  window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart);
366
381
  window.removeEventListener("mouseup", handleFalseAlarm);
367
382
  window.removeEventListener("touchend", handleFalseAlarm);
383
+ if (touchDragHoldTimer) {
384
+ clearTimeout(touchDragHoldTimer);
385
+ touchDragHoldTimer = undefined;
386
+ touchHoldElapsed = false;
387
+ }
368
388
  }
369
389
  function handleFalseAlarm(e) {
370
390
  removeMaybeListeners();
@@ -384,8 +404,30 @@ export function dndzone(node, options) {
384
404
  }
385
405
 
386
406
  function handleMouseMoveMaybeDragStart(e) {
407
+ const isTouch = !!e.touches;
408
+ const c = isTouch ? e.touches[0] : e;
409
+ // If touch drag delay is configured and not elapsed yet, allow scrolling until either
410
+ // the delay elapses (timer will call handleDragStart) or the user moves significantly,
411
+ // in which case we cancel the potential drag and let the interaction be a scroll.
412
+ if (isTouch && config.delayTouchStartMs > 0 && !touchHoldElapsed) {
413
+ currentMousePosition = {x: c.clientX, y: c.clientY};
414
+ if (
415
+ Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX ||
416
+ Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX
417
+ ) {
418
+ // User started scrolling, cancel drag attempt.
419
+ if (touchDragHoldTimer) {
420
+ clearTimeout(touchDragHoldTimer);
421
+ touchDragHoldTimer = undefined;
422
+ }
423
+ handleFalseAlarm(e);
424
+ }
425
+ return; // Do not preventDefault so scrolling works.
426
+ }
427
+
428
+ // legacy / post-delay path – block scrolling and maybe start drag
387
429
  e.preventDefault();
388
- const c = e.touches ? e.touches[0] : e;
430
+
389
431
  currentMousePosition = {x: c.clientX, y: c.clientY};
390
432
  if (
391
433
  Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX ||
@@ -410,12 +452,30 @@ export function dndzone(node, options) {
410
452
  printDebug(() => "cannot start a new drag before finalizing previous one");
411
453
  return;
412
454
  }
413
- e.preventDefault();
455
+ const isTouchStart = !!e.touches;
456
+ const useDelay = isTouchStart && config.delayTouchStartMs > 0;
457
+
458
+ if (!useDelay) {
459
+ e.preventDefault();
460
+ }
414
461
  e.stopPropagation();
415
- const c = e.touches ? e.touches[0] : e;
462
+
463
+ const c = isTouchStart ? e.touches[0] : e;
416
464
  dragStartMousePosition = {x: c.clientX, y: c.clientY};
417
465
  currentMousePosition = {...dragStartMousePosition};
418
466
  originalDragTarget = e.currentTarget;
467
+
468
+ if (useDelay) {
469
+ touchHoldElapsed = false;
470
+ touchDragHoldTimer = window.setTimeout(() => {
471
+ // If the finger is still down and no false-alarm happened
472
+ if (!originalDragTarget) return;
473
+ touchHoldElapsed = true;
474
+ removeMaybeListeners();
475
+ handleDragStart();
476
+ }, config.delayTouchStartMs);
477
+ }
478
+
419
479
  addMaybeListeners();
420
480
  }
421
481
 
@@ -487,9 +547,19 @@ export function dndzone(node, options) {
487
547
  dropTargetClasses = [],
488
548
  transformDraggedElement = () => {},
489
549
  centreDraggedOnCursor = false,
490
- dropAnimationDisabled = false
550
+ dropAnimationDisabled = false,
551
+ delayTouchStart: delayTouchStartOpt = false
491
552
  }) {
492
553
  config.dropAnimationDurationMs = dropAnimationDurationMs;
554
+
555
+ let effectiveDelayMs = 0;
556
+ if (delayTouchStartOpt === true) {
557
+ effectiveDelayMs = DEFAULT_TOUCH_DELAY_MS;
558
+ } else if (typeof delayTouchStartOpt === "number" && isFinite(delayTouchStartOpt) && delayTouchStartOpt >= 0) {
559
+ effectiveDelayMs = delayTouchStartOpt;
560
+ }
561
+ config.delayTouchStartMs = effectiveDelayMs;
562
+
493
563
  if (config.type && newType !== config.type) {
494
564
  unregisterDropZone(node, config.type);
495
565
  }
@@ -3,10 +3,11 @@ import {dndzone} from "../action";
3
3
  import {createStore} from "./simpleStore";
4
4
 
5
5
  const isItemsDragDisabled = createStore(true);
6
+ const userDragDisabled = createStore(false);
6
7
 
7
- function getAddedOptions(isItemsDragDisabled = true) {
8
+ function getAddedOptions() {
8
9
  return {
9
- dragDisabled: isItemsDragDisabled,
10
+ dragDisabled: userDragDisabled.get() || isItemsDragDisabled.get(),
10
11
  zoneItemTabIndex: -1
11
12
  };
12
13
  }
@@ -21,18 +22,30 @@ function getAddedOptions(isItemsDragDisabled = true) {
21
22
  * @return {{update: (newOptions: Object) => {}, destroy: () => {}}}
22
23
  */
23
24
  export function dragHandleZone(node, options) {
25
+ // Initialise stores from initial options
26
+ userDragDisabled.set(options?.dragDisabled ?? false);
27
+
24
28
  let currentOptions = options;
29
+
25
30
  const zone = dndzone(node, {
26
31
  ...currentOptions,
27
32
  ...getAddedOptions()
28
33
  });
29
- function isItemDisabledCB(isItemsDragDisabled) {
34
+
35
+ function updateZone() {
30
36
  zone.update({
31
37
  ...currentOptions,
32
- ...getAddedOptions(isItemsDragDisabled)
38
+ ...getAddedOptions()
33
39
  });
34
40
  }
35
- isItemsDragDisabled.subscribe(isItemDisabledCB);
41
+
42
+ // Subscribe to internal store so finishing a drag updates the zone
43
+ isItemsDragDisabled.subscribe(updateZone);
44
+
45
+ // We don't need to subscribe to userDragDisabled here because updates to
46
+ // it always come through the `update` lifecycle and will call `updateZone`
47
+ // anyway.
48
+
36
49
  function consider(e) {
37
50
  const {
38
51
  info: {source, trigger}
@@ -59,15 +72,14 @@ export function dragHandleZone(node, options) {
59
72
  return {
60
73
  update: newOptions => {
61
74
  currentOptions = newOptions;
62
- zone.update({
63
- ...currentOptions,
64
- ...getAddedOptions(isItemsDragDisabled.get())
65
- });
75
+ // keep store in sync with external prop
76
+ userDragDisabled.set(currentOptions?.dragDisabled ?? false);
77
+ updateZone();
66
78
  },
67
79
  destroy: () => {
68
80
  node.removeEventListener("consider", consider);
69
81
  node.removeEventListener("finalize", finalize);
70
- isItemsDragDisabled.unsubscribe(isItemDisabledCB);
82
+ isItemsDragDisabled.unsubscribe(updateZone);
71
83
  }
72
84
  };
73
85
  }
@@ -100,10 +112,22 @@ export function dragHandle(handle) {
100
112
  window.removeEventListener("touchend", resetStartDrag);
101
113
  }
102
114
 
103
- isItemsDragDisabled.subscribe(disabled => {
104
- handle.tabIndex = disabled ? 0 : -1;
105
- handle.style.cursor = disabled ? "grab" : "grabbing";
106
- });
115
+ const recomputeHandleState = () => {
116
+ const userDisabled = userDragDisabled.get();
117
+ const internalDisabled = isItemsDragDisabled.get();
118
+
119
+ if (userDisabled) {
120
+ handle.tabIndex = -1;
121
+ handle.style.cursor = ""; // default cursor
122
+ } else {
123
+ handle.tabIndex = internalDisabled ? 0 : -1;
124
+ handle.style.cursor = internalDisabled ? "grab" : "grabbing";
125
+ }
126
+ };
127
+
128
+ // Subscribe to both stores
129
+ userDragDisabled.subscribe(recomputeHandleState);
130
+ isItemsDragDisabled.subscribe(recomputeHandleState);
107
131
 
108
132
  handle.addEventListener("mousedown", startDrag);
109
133
  handle.addEventListener("touchstart", startDrag);
@@ -114,6 +138,8 @@ export function dragHandle(handle) {
114
138
  handle.removeEventListener("mousedown", startDrag);
115
139
  handle.removeEventListener("touchstart", startDrag);
116
140
  handle.removeEventListener("keydown", handleKeyDown);
141
+ userDragDisabled.unsubscribe(recomputeHandleState);
142
+ isItemsDragDisabled.unsubscribe(recomputeHandleState);
117
143
  }
118
144
  };
119
145
  }