@marsio/vue-draggable 1.0.8 → 1.0.10

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
@@ -6,6 +6,8 @@ Draggable and DraggableCore are **Vue3** components designed to make elements dr
6
6
  <img src="https://user-images.githubusercontent.com/6365230/95649276-f3a02480-0b06-11eb-8504-e0614a780ba4.gif" />
7
7
  </p>
8
8
 
9
+ [**[Demo](https://marsio.top/vue-draggable/) | [Changelog](/CHANGELOG.md) | [View Example](/example/example.js) | [MCP Server](./mcp/README.md)**]
10
+
9
11
  ## ✨ Features
10
12
  - **Compatibility**: Compatible with server-rendered apps, PC, and mobile devices.
11
13
  - **Drag Handlers**: Offers customizable drag handlers (`startFn`, `dragFn`, `stopFn`) that allow developers to hook into drag start, during drag, and drag stop events, providing flexibility in handling these events.
@@ -15,7 +17,62 @@ Draggable and DraggableCore are **Vue3** components designed to make elements dr
15
17
  - **Position Offset**: Supports an offset for the draggable position (`positionOffset`), enabling adjustments to the element's position without altering its actual position properties.
16
18
  - **Grid Snapping**: Allows the draggable element to snap to a grid (`grid` prop), facilitating alignment and precise placement during dragging.
17
19
  - **Accessibility and Interaction**: Includes props for disabling the draggable functionality (`disabled`), allowing any mouse button to initiate dragging (`allowAnyClick`), and enabling a hack to prevent text selection during drag (`enableUserSelectHack`), enhancing usability and accessibility.
20
+ ## 🤖 AI IDE Integration (MCP Server)
21
+
22
+ **Supercharge your AI-powered development workflow!** Vue-Draggable now includes an official **Model Context Protocol (MCP) Server** that provides your AI assistant with comprehensive, real-time access to all component APIs, props, and examples.
23
+
24
+ ### ✨ What is MCP?
25
+
26
+ The Model Context Protocol enables AI assistants like Claude Desktop, Cursor, and other AI IDEs to access live, accurate documentation and code examples directly from the source. No more outdated docs or hallucinated APIs!
27
+
28
+ ### 🚀 Key Benefits
29
+
30
+ - **🎯 Accurate API Usage**: AI gets real-time access to all props, types, and default values
31
+ - **📚 Contextual Examples**: Intelligent code suggestions based on actual component usage
32
+ - **🔄 Always Up-to-Date**: Documentation syncs automatically with code changes
33
+ - **⚡ Enhanced Productivity**: Write draggable components faster with AI assistance
34
+
35
+ ### 📋 Quick Setup
36
+
37
+ #### For Cursor IDE:
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "vue-draggable": {
42
+ "command": "npx",
43
+ "args": ["@marsio/vue-draggable-mcp"]
44
+ }
45
+ }
46
+ }
47
+ ```
18
48
 
49
+ #### For Claude Desktop:
50
+ Add to your Claude Desktop configuration:
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "vue-draggable": {
55
+ "command": "npx",
56
+ "args": ["@marsio/vue-draggable-mcp"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### 🛠️ Available Tools
63
+
64
+ - **`get_vue_draggable_docs`**: Complete documentation in Markdown format
65
+ - **`list_vue_draggable_props`**: Structured props information with types and defaults
66
+ - **`get_vue_draggable_type`**: Specific type definitions and interfaces
67
+
68
+ ### 💡 Example AI Prompts
69
+
70
+ After setup, try asking your AI assistant:
71
+ - *"Create a draggable card component with grid snapping"*
72
+ - *"Show me all available props for Draggable component"*
73
+ - *"How do I constrain dragging to horizontal axis only?"*
74
+
75
+ [Learn more about MCP setup →](./mcp/README.md)
19
76
  ## 📦 Quick Start
20
77
 
21
78
  To quickly start using `@marsio/vue-draggable`, follow the steps below:
@@ -96,6 +153,7 @@ A simple component for making elements draggable.
96
153
  #### Technical Documentation
97
154
 
98
155
  - [Installing](#installing)
156
+ - [AI IDE Integration (MCP)](#🤖-ai-ide-integration-mcp-server)
99
157
  - [Exports](#exports)
100
158
  - [Draggable](#draggable)
101
159
  - [Draggable Usage](#draggable-usage)
@@ -202,8 +260,8 @@ type DraggableData = {
202
260
  // If set to `true`, will allow dragging on non left-button clicks.
203
261
  allowAnyClick: boolean,
204
262
 
205
- // Determines which axis the draggable can move. This only affects
206
- // flushing to the DOM. Callbacks will still include all values.
263
+ // Determines which axis the draggable can move.
264
+ // Disabled axis movement is ignored and callbacks will report 0 deltas.
207
265
  // Accepted values:
208
266
  // - `both` allows movement horizontally and vertically (default).
209
267
  // - `x` limits movement to horizontal axis.
@@ -211,6 +269,12 @@ allowAnyClick: boolean,
211
269
  // - 'none' stops all movement.
212
270
  axis: string,
213
271
 
272
+ // If true, lock to the dominant axis after the drag starts (prevents diagonal drift).
273
+ directionLock: boolean,
274
+
275
+ // Distance (px) before directionLock chooses an axis.
276
+ directionLockThreshold: number,
277
+
214
278
  // Specifies movement boundaries. Accepted values:
215
279
  // - `parent` restricts movement within the node's offsetParent
216
280
  // (nearest node with position relative or absolute), or
@@ -225,6 +289,10 @@ bounds: {left?: number, top?: number, right?: number, bottom?: number} | string,
225
289
  // Example: '.body'
226
290
  cancel: string,
227
291
 
292
+ // If true, prevents dragging from starting on common interactive elements
293
+ // (inputs, buttons, links, contenteditable).
294
+ cancelInteractiveElements: boolean,
295
+
228
296
  // Class names for draggable UI.
229
297
  // Default to 'vue-draggable', 'vue-draggable-dragging', and 'vue-draggable-dragged'
230
298
  defaultClassName: string,
@@ -240,6 +308,48 @@ defaultPosition: {x: number, y: number},
240
308
  // If true, will not call any drag handlers.
241
309
  disabled: boolean,
242
310
 
311
+ // If true, do not preventDefault() during touch drags, allowing the page/containers to scroll.
312
+ allowMobileScroll: boolean,
313
+
314
+ // If true, auto-scroll nearest scrollable container (and window) when the pointer is near an edge.
315
+ autoScroll: boolean,
316
+
317
+ // Distance (px) from an edge to start auto-scrolling.
318
+ autoScrollThreshold: number,
319
+
320
+ // Max auto-scroll speed (px per frame).
321
+ autoScrollMaxSpeed: number,
322
+
323
+ // Auto-scroll axis.
324
+ autoScrollAxis: 'both' | 'x' | 'y' | 'none',
325
+
326
+ // If false, never auto-scroll the window.
327
+ autoScrollIncludeWindow: boolean,
328
+
329
+ // Optional: specify which container(s) to auto-scroll.
330
+ // Supports selector string, element, 'window', or an array of those.
331
+ autoScrollContainer: string | HTMLElement | Window | Array<string | HTMLElement | Window> | null,
332
+
333
+ // Minimum distance in pixels before the drag starts.
334
+ // Useful to prevent accidental drags on click/tap.
335
+ dragStartThreshold: number,
336
+
337
+ // Touch-only: delay (ms) before drag can start (long-press activation).
338
+ dragStartDelay: number,
339
+
340
+ // Touch-only: movement tolerance (px) allowed during dragStartDelay before cancelling the drag start.
341
+ dragStartDelayTolerance: number,
342
+
343
+ // If true, suppress the click event fired after a drag.
344
+ enableClickSuppression: boolean,
345
+
346
+ // How long (ms) to suppress the next click after drag stop.
347
+ clickSuppressionDuration: number,
348
+
349
+ // If true, coalesce drag updates to requestAnimationFrame (at most once per frame).
350
+ // This significantly reduces work under high-frequency move events.
351
+ useRafDrag: boolean,
352
+
243
353
  // Specifies the x and y that dragging should snap to.
244
354
  grid: [number, number],
245
355
 
@@ -288,6 +398,24 @@ scale: number
288
398
  Note that sending `class`, `style`, or `transform` as properties will error - set them on the child element
289
399
  directly.
290
400
 
401
+ ## Auto Scroll
402
+
403
+ Enable auto-scroll while dragging near an edge:
404
+
405
+ ```vue
406
+ <Draggable :autoScroll="true" :autoScrollThreshold="40" :autoScrollMaxSpeed="24" />
407
+ ```
408
+
409
+ Scroll only a specific container (and never the window):
410
+
411
+ ```vue
412
+ <Draggable :autoScroll="true" autoScrollContainer=".scroll-pane" :autoScrollIncludeWindow="false" />
413
+ ```
414
+
415
+ ## Pointer Events
416
+
417
+ When supported, `<DraggableCore>` uses Pointer Events (with `setPointerCapture()` for more robust drags outside the element).
418
+ For touch pointers, Pointer Events require `touch-action` to be non-`auto` (e.g. `touch-action: none`); otherwise it falls back to Touch Events.
291
419
 
292
420
  ## Controlled vs. Uncontrolled
293
421
 
@@ -324,7 +452,21 @@ on itself and thus must have callbacks attached to be useful.
324
452
  allowAnyClick: boolean,
325
453
  cancel: string,
326
454
  disabled: boolean,
455
+ allowMobileScroll: boolean,
456
+ autoScroll: boolean,
457
+ autoScrollThreshold: number,
458
+ autoScrollMaxSpeed: number,
459
+ autoScrollAxis: 'both' | 'x' | 'y' | 'none',
460
+ autoScrollIncludeWindow: boolean,
461
+ autoScrollContainer: string | HTMLElement | Window | Array<string | HTMLElement | Window> | null,
462
+ cancelInteractiveElements: boolean,
463
+ enableClickSuppression: boolean,
464
+ clickSuppressionDuration: number,
465
+ dragStartDelay: number,
466
+ dragStartDelayTolerance: number,
327
467
  enableUserSelectHack: boolean,
468
+ dragStartThreshold: number,
469
+ useRafDrag: boolean,
328
470
  offsetParent: HTMLElement,
329
471
  grid: [number, number],
330
472
  handle: string,
@@ -11,15 +11,14 @@ Object.defineProperty(exports, "DraggableCore", {
11
11
  });
12
12
  exports.draggableProps = exports.default = void 0;
13
13
  var _vue = require("vue");
14
- var _get = _interopRequireDefault(require("lodash/get"));
15
14
  var _clsx = _interopRequireDefault(require("clsx"));
16
15
  var _domFns = require("./utils/domFns");
17
16
  var _positionFns = require("./utils/positionFns");
18
17
  var _shims = require("./utils/shims");
19
18
  var _DraggableCore = _interopRequireWildcard(require("./DraggableCore"));
20
19
  var _log = _interopRequireDefault(require("./utils/log"));
21
- function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
22
- function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
20
+ var _noop = _interopRequireDefault(require("./utils/noop"));
21
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
23
22
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
24
23
  /**
25
24
  * Draggable is a Vue component that allows elements to be dragged and dropped.
@@ -56,15 +55,21 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
56
55
  * Note:
57
56
  * This component requires Vue 3 and is designed to work within a Vue 3 application.
58
57
  */
59
- function _isSlot(s) {
60
- return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !(0, _vue.isVNode)(s);
61
- }
58
+
62
59
  const draggableProps = exports.draggableProps = {
63
60
  ..._DraggableCore.draggableCoreProps,
64
61
  axis: {
65
62
  type: String,
66
63
  default: 'both'
67
64
  },
65
+ directionLock: {
66
+ type: Boolean,
67
+ default: false
68
+ },
69
+ directionLockThreshold: {
70
+ type: Number,
71
+ default: 4
72
+ },
68
73
  bounds: {
69
74
  type: [Object, String, Boolean],
70
75
  default: false
@@ -113,31 +118,86 @@ const Draggable = exports.default = (0, _vue.defineComponent)({
113
118
  let {
114
119
  slots
115
120
  } = _ref;
116
- console.log('props', props);
117
- const rootElement = (0, _vue.ref)(null);
118
- if (props.position && !(props.dragFn || props.stopFn)) {
119
- // eslint-disable-next-line no-console
120
- console.warn('A `position` was applied to this <Draggable>, without drag handlers. This will make this ' + 'component effectively undraggable. Please attach `dragFn` or `stopFn` handlers so you can adjust the ' + '`position` of this element.');
121
- }
121
+ const localNodeRef = (0, _vue.ref)(null);
122
122
  const state = (0, _vue.reactive)({
123
123
  // Whether or not we are currently dragging.
124
124
  dragging: false,
125
125
  // Whether or not we have been dragged before.
126
126
  dragged: false,
127
127
  // Current transform x and y.
128
- x: props.position ? props.position.x : props.defaultPosition.x,
129
- y: props.position ? props.position.y : props.defaultPosition.y,
130
- prevPropsPosition: {
131
- ...props.position
132
- },
128
+ x: props.position?.x ?? props.defaultPosition.x ?? 0,
129
+ y: props.position?.y ?? props.defaultPosition.y ?? 0,
133
130
  // Used for compensating for out-of-bounds drags
134
131
  slackX: 0,
135
132
  slackY: 0,
136
133
  // Can only determine if SVG after mounting
137
134
  isElementSVG: false
138
135
  });
136
+ const isElementNode = v => {
137
+ return !!v && typeof v === 'object' && 'nodeType' in v && v.nodeType === 1;
138
+ };
139
+ const isRefLike = v => {
140
+ return !!v && typeof v === 'object' && 'value' in v;
141
+ };
139
142
  const findDOMNode = () => {
140
- return (0, _get.default)(props, 'nodeRef.value') || rootElement.value;
143
+ const nodeRef = props.nodeRef;
144
+ if (isRefLike(nodeRef)) {
145
+ const v = nodeRef.value;
146
+ if (isElementNode(v)) return v;
147
+ } else if (isElementNode(nodeRef)) {
148
+ return nodeRef;
149
+ }
150
+ return localNodeRef.value;
151
+ };
152
+ const boundsContext = {
153
+ props,
154
+ findDOMNode,
155
+ __boundsCache: {
156
+ key: '',
157
+ node: null,
158
+ boundEl: null,
159
+ boundClientWidth: 0,
160
+ boundClientHeight: 0,
161
+ nodeClientWidth: 0,
162
+ nodeClientHeight: 0,
163
+ bounds: null
164
+ }
165
+ };
166
+ let rafId = null;
167
+ let internalX = state.x;
168
+ let internalY = state.y;
169
+ let internalSlackX = state.slackX;
170
+ let internalSlackY = state.slackY;
171
+ let directionLockAxis = null;
172
+ let directionLockFixedX = NaN;
173
+ let directionLockFixedY = NaN;
174
+ let directionLockTotalX = 0;
175
+ let directionLockTotalY = 0;
176
+ const resetDirectionLock = () => {
177
+ directionLockAxis = null;
178
+ directionLockFixedX = NaN;
179
+ directionLockFixedY = NaN;
180
+ directionLockTotalX = 0;
181
+ directionLockTotalY = 0;
182
+ };
183
+ const getDirectionLockThreshold = () => {
184
+ const threshold = typeof props.directionLockThreshold === 'number' ? props.directionLockThreshold : 0;
185
+ if (threshold <= 0) return 0;
186
+ const scale = typeof props.scale === 'number' ? props.scale : 1;
187
+ if (!scale) return threshold;
188
+ return threshold / scale;
189
+ };
190
+ const flushToReactiveState = () => {
191
+ rafId = null;
192
+ if (internalX === state.x && internalY === state.y && internalSlackX === state.slackX && internalSlackY === state.slackY) return;
193
+ state.x = internalX;
194
+ state.y = internalY;
195
+ state.slackX = internalSlackX;
196
+ state.slackY = internalSlackY;
197
+ };
198
+ const scheduleFlush = () => {
199
+ if (rafId != null) return;
200
+ rafId = window.requestAnimationFrame(flushToReactiveState);
141
201
  };
142
202
  (0, _vue.onMounted)(() => {
143
203
  if (typeof window.SVGElement !== 'undefined' && findDOMNode() instanceof window.SVGElement) {
@@ -146,83 +206,190 @@ const Draggable = exports.default = (0, _vue.defineComponent)({
146
206
  });
147
207
  (0, _vue.onUnmounted)(() => {
148
208
  state.dragging = false;
209
+ if (rafId != null) {
210
+ window.cancelAnimationFrame(rafId);
211
+ rafId = null;
212
+ }
149
213
  });
150
214
  const onDragStart = (e, coreData) => {
151
215
  (0, _log.default)('Draggable: onDragStart: %j', coreData);
152
216
 
153
217
  // Short-circuit if user's callback killed it.
154
- const shouldStart = props.startFn?.(e, (0, _positionFns.createDraggableData)({
155
- props,
156
- state
157
- }, coreData));
158
- // Kills start event on core as well, so move handlers are never bound.
159
- if (shouldStart === false) return false;
218
+ const isControlled = Boolean(props.position);
219
+ if (isControlled) {
220
+ internalX = props.position.x;
221
+ internalY = props.position.y;
222
+ } else {
223
+ internalX = state.x;
224
+ internalY = state.y;
225
+ }
226
+ internalSlackX = state.slackX;
227
+ internalSlackY = state.slackY;
228
+ boundsContext.__boundsCache.key = '';
229
+ boundsContext.__boundsCache.node = null;
230
+ boundsContext.__boundsCache.boundEl = null;
231
+ boundsContext.__boundsCache.boundClientWidth = 0;
232
+ boundsContext.__boundsCache.boundClientHeight = 0;
233
+ boundsContext.__boundsCache.nodeClientWidth = 0;
234
+ boundsContext.__boundsCache.nodeClientHeight = 0;
235
+ boundsContext.__boundsCache.bounds = null;
236
+ resetDirectionLock();
237
+ flushToReactiveState();
238
+ if (props.startFn !== _noop.default) {
239
+ const scale = typeof props.scale === 'number' ? props.scale : 1;
240
+ const uiStart = {
241
+ node: coreData.node,
242
+ x: internalX + coreData.deltaX / scale,
243
+ y: internalY + coreData.deltaY / scale,
244
+ deltaX: coreData.deltaX / scale,
245
+ deltaY: coreData.deltaY / scale,
246
+ lastX: internalX,
247
+ lastY: internalY
248
+ };
249
+ const shouldStart = props.startFn?.(e, uiStart);
250
+ // Kills start event on core as well, so move handlers are never bound.
251
+ if (shouldStart === false) return false;
252
+ }
160
253
  state.dragging = true;
161
254
  state.dragged = true;
162
255
  };
163
256
  const onDrag = (e, coreData) => {
164
257
  if (!state.dragging) return false;
165
258
  (0, _log.default)('Draggable: dragFn: %j', coreData);
166
- const uiData = (0, _positionFns.createDraggableData)({
167
- props,
168
- state
169
- }, coreData);
170
- const newState = {
171
- x: uiData.x,
172
- y: uiData.y,
173
- slackX: 0,
174
- slackY: 0
175
- };
259
+ const scale = typeof props.scale === 'number' ? props.scale : 1;
260
+ const rawDeltaX = coreData.deltaX / scale;
261
+ const rawDeltaY = coreData.deltaY / scale;
262
+ let newX = internalX + rawDeltaX;
263
+ let newY = internalY + rawDeltaY;
264
+ let newSlackX = 0;
265
+ let newSlackY = 0;
266
+ let uiDeltaX = rawDeltaX;
267
+ let uiDeltaY = rawDeltaY;
268
+ const allowAxisX = (0, _positionFns.canDragX)({
269
+ props
270
+ });
271
+ const allowAxisY = (0, _positionFns.canDragY)({
272
+ props
273
+ });
274
+ let effectiveAxisX = allowAxisX;
275
+ let effectiveAxisY = allowAxisY;
276
+ if (props.directionLock && allowAxisX && allowAxisY) {
277
+ if (directionLockAxis == null) {
278
+ directionLockTotalX += rawDeltaX;
279
+ directionLockTotalY += rawDeltaY;
280
+ const threshold = getDirectionLockThreshold();
281
+ if (!threshold || Math.hypot(directionLockTotalX, directionLockTotalY) >= threshold) {
282
+ directionLockAxis = Math.abs(directionLockTotalX) >= Math.abs(directionLockTotalY) ? 'x' : 'y';
283
+ directionLockFixedX = internalX;
284
+ directionLockFixedY = internalY;
285
+ }
286
+ }
287
+ if (directionLockAxis === 'x' && Number.isFinite(directionLockFixedY)) {
288
+ newY = directionLockFixedY;
289
+ uiDeltaY = newY - internalY;
290
+ } else if (directionLockAxis === 'y' && Number.isFinite(directionLockFixedX)) {
291
+ newX = directionLockFixedX;
292
+ uiDeltaX = newX - internalX;
293
+ }
294
+ }
295
+ if (directionLockAxis === 'x') effectiveAxisY = false;
296
+ if (directionLockAxis === 'y') effectiveAxisX = false;
297
+ if (!effectiveAxisX) {
298
+ newX = internalX;
299
+ uiDeltaX = 0;
300
+ }
301
+ if (!effectiveAxisY) {
302
+ newY = internalY;
303
+ uiDeltaY = 0;
304
+ }
176
305
 
177
306
  // Keep within bounds.
178
307
  if (props.bounds) {
179
308
  // Save original x and y.
180
- const {
181
- x,
182
- y
183
- } = newState;
309
+ const x = newX;
310
+ const y = newY;
184
311
 
185
312
  // Add slack to the values used to calculate bound position. This will ensure that if
186
313
  // completely removed.
187
- newState.x += state.slackX;
188
- newState.y += state.slackY;
314
+ const slackX = effectiveAxisX ? internalSlackX : 0;
315
+ const slackY = effectiveAxisY ? internalSlackY : 0;
316
+ newX += slackX;
317
+ newY += slackY;
189
318
 
190
319
  // Get bound position. This will ceil/floor the x and y within the boundaries.
191
- const [newStateX, newStateY] = (0, _positionFns.getBoundPosition)({
192
- props,
193
- findDOMNode
194
- }, newState.x, newState.y);
195
- newState.x = newStateX;
196
- newState.y = newStateY;
320
+ const [boundX, boundY] = (0, _positionFns.getBoundPosition)(boundsContext, newX, newY);
321
+ newX = boundX;
322
+ newY = boundY;
197
323
 
198
324
  // Recalculate slack by noting how much was shaved by the boundPosition handler.
199
- newState.slackX = state.slackX + (x - newState.x);
200
- newState.slackY = state.slackY + (y - newState.y);
325
+ newSlackX = slackX + (x - newX);
326
+ newSlackY = slackY + (y - newY);
201
327
 
202
328
  // Update the event we fire to reflect what really happened after bounds took effect.
203
- uiData.x = newState.x;
204
- uiData.y = newState.y;
205
- uiData.deltaX = newState.x - (state.x ?? 0);
206
- uiData.deltaY = newState.y - (state.y ?? 0);
329
+ uiDeltaX = newX - internalX;
330
+ uiDeltaY = newY - internalY;
207
331
  }
332
+ if (!effectiveAxisX) newSlackX = 0;
333
+ if (!effectiveAxisY) newSlackY = 0;
208
334
 
209
335
  // Short-circuit if user's callback killed it.
210
- const shouldUpdate = props.dragFn?.(e, uiData);
211
- if (shouldUpdate === false) return false;
212
- Object.keys(newState).forEach(key => {
213
- state[key] = newState[key];
214
- });
336
+ if (props.dragFn !== _noop.default) {
337
+ const uiData = {
338
+ node: coreData.node,
339
+ x: newX,
340
+ y: newY,
341
+ deltaX: uiDeltaX,
342
+ deltaY: uiDeltaY,
343
+ lastX: internalX,
344
+ lastY: internalY
345
+ };
346
+ const shouldUpdate = props.dragFn?.(e, uiData);
347
+ if (shouldUpdate === false) return false;
348
+ }
349
+ internalX = newX;
350
+ internalY = newY;
351
+ internalSlackX = newSlackX;
352
+ internalSlackY = newSlackY;
353
+ if (props.useRafDrag) {
354
+ if (rafId != null) {
355
+ window.cancelAnimationFrame(rafId);
356
+ rafId = null;
357
+ }
358
+ flushToReactiveState();
359
+ } else {
360
+ scheduleFlush();
361
+ }
215
362
  };
216
363
  const onDragStop = (e, coreData) => {
217
364
  if (!state.dragging) return false;
218
365
 
219
366
  // Short-circuit if user's callback killed it.
220
- const shouldContinue = props.stopFn?.(e, (0, _positionFns.createDraggableData)({
221
- props,
222
- state
223
- }, coreData));
224
- if (shouldContinue === false) return false;
367
+ if (props.stopFn !== _noop.default) {
368
+ const scale = typeof props.scale === 'number' ? props.scale : 1;
369
+ const allowAxisX = (0, _positionFns.canDragX)({
370
+ props
371
+ });
372
+ const allowAxisY = (0, _positionFns.canDragY)({
373
+ props
374
+ });
375
+ const effectiveAxisX = allowAxisX && directionLockAxis !== 'y';
376
+ const effectiveAxisY = allowAxisY && directionLockAxis !== 'x';
377
+ const deltaX = effectiveAxisX ? coreData.deltaX / scale : 0;
378
+ const deltaY = effectiveAxisY ? coreData.deltaY / scale : 0;
379
+ const uiStop = {
380
+ node: coreData.node,
381
+ x: internalX + deltaX,
382
+ y: internalY + deltaY,
383
+ deltaX,
384
+ deltaY,
385
+ lastX: internalX,
386
+ lastY: internalY
387
+ };
388
+ const shouldContinue = props.stopFn?.(e, uiStop);
389
+ if (shouldContinue === false) return false;
390
+ }
225
391
  (0, _log.default)('Draggable: onDragStop: %j', coreData);
392
+ resetDirectionLock();
226
393
  const newState = {
227
394
  dragging: false,
228
395
  slackX: 0,
@@ -240,14 +407,55 @@ const Draggable = exports.default = (0, _vue.defineComponent)({
240
407
  newState.x = x;
241
408
  newState.y = y;
242
409
  }
243
- Object.keys(newState).forEach(key => {
244
- state[key] = newState[key];
245
- });
410
+ state.dragging = newState.dragging;
411
+ internalSlackX = newState.slackX;
412
+ internalSlackY = newState.slackY;
413
+ if (typeof newState.x === 'number') internalX = newState.x;
414
+ if (typeof newState.y === 'number') internalY = newState.y;
415
+ if (rafId != null) {
416
+ window.cancelAnimationFrame(rafId);
417
+ rafId = null;
418
+ }
419
+ flushToReactiveState();
420
+ };
421
+ const getFirstUsableChild = () => {
422
+ const raw = slots.default ? slots.default() : [];
423
+ const stack = Array.isArray(raw) ? [...raw] : [raw];
424
+ while (stack.length) {
425
+ const item = stack.shift();
426
+ if (Array.isArray(item)) {
427
+ for (let i = item.length - 1; i >= 0; i -= 1) stack.unshift(item[i]);
428
+ continue;
429
+ }
430
+ if (!(0, _vue.isVNode)(item)) continue;
431
+
432
+ // Skip comment nodes and whitespace-only text nodes.
433
+ if (item.type === _vue.Comment) continue;
434
+ if (item.type === _vue.Text) {
435
+ const txt = typeof item.children === 'string' ? item.children : '';
436
+ if (!txt || !txt.trim()) continue;
437
+ // Draggable requires an element/component vnode, not bare text.
438
+ continue;
439
+ }
440
+
441
+ // Unwrap fragments to find the first real node.
442
+ if (item.type === _vue.Fragment) {
443
+ const fragChildren = item.children;
444
+ if (Array.isArray(fragChildren)) {
445
+ for (let i = fragChildren.length - 1; i >= 0; i -= 1) stack.unshift(fragChildren[i]);
446
+ }
447
+ continue;
448
+ }
449
+ return item;
450
+ }
451
+ return null;
246
452
  };
247
453
  return () => {
248
454
  /* eslint-disable @typescript-eslint/no-unused-vars */
249
455
  const {
250
456
  axis,
457
+ directionLock,
458
+ directionLockThreshold,
251
459
  bounds,
252
460
  defaultPosition,
253
461
  defaultClassName,
@@ -292,7 +500,7 @@ const Draggable = exports.default = (0, _vue.defineComponent)({
292
500
  [defaultClassNameDragging]: state.dragging,
293
501
  [defaultClassNameDragged]: state.dragged
294
502
  });
295
- const child = slots.default ? slots.default()[0] : null;
503
+ const child = getFirstUsableChild();
296
504
  if (!child) return null;
297
505
  const clonedChildren = (0, _vue.cloneVNode)(child, {
298
506
  class: className,
@@ -305,10 +513,10 @@ const Draggable = exports.default = (0, _vue.defineComponent)({
305
513
  dragFn: onDrag,
306
514
  stopFn: onDragStop
307
515
  };
308
- return (0, _vue.createVNode)(_DraggableCore.default, (0, _vue.mergeProps)({
309
- "ref": rootElement
310
- }, coreProps), _isSlot(clonedChildren) ? clonedChildren : {
311
- default: () => [clonedChildren]
516
+ return (0, _vue.createVNode)(_DraggableCore.default, (0, _vue.mergeProps)(coreProps, {
517
+ "nodeRef": props.nodeRef || localNodeRef
518
+ }), {
519
+ default: () => clonedChildren
312
520
  });
313
521
  };
314
522
  }