@bagelink/vue 1.15.433 → 1.15.440

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.
Files changed (67) hide show
  1. package/dist/components/DragOver.vue.d.ts.map +1 -1
  2. package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
  3. package/dist/components/draggable/Draggable.vue.d.ts.map +1 -1
  4. package/dist/components/draggable/index.d.ts +1 -0
  5. package/dist/components/draggable/index.d.ts.map +1 -1
  6. package/dist/components/draggable/reorder.d.ts +5 -0
  7. package/dist/components/draggable/reorder.d.ts.map +1 -0
  8. package/dist/components/draggable/useDraggable.d.ts +5 -1
  9. package/dist/components/draggable/useDraggable.d.ts.map +1 -1
  10. package/dist/components/draggable/vDraggable.d.ts.map +1 -1
  11. package/dist/components/file-drop/useFileDropzone.d.ts +23 -0
  12. package/dist/components/file-drop/useFileDropzone.d.ts.map +1 -0
  13. package/dist/components/file-drop/useFileDropzone.test.d.ts +2 -0
  14. package/dist/components/file-drop/useFileDropzone.test.d.ts.map +1 -0
  15. package/dist/components/form/inputs/ArrayInput.vue.d.ts.map +1 -1
  16. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
  17. package/dist/components/index.d.ts +4 -1
  18. package/dist/components/index.d.ts.map +1 -1
  19. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  20. package/dist/components/layout/Panel.vue.d.ts.map +1 -1
  21. package/dist/components/layout/Resizable.vue.d.ts.map +1 -1
  22. package/dist/components/layout/appLayoutContext.d.ts +1 -1
  23. package/dist/components/layout/appLayoutContext.d.ts.map +1 -1
  24. package/dist/composables/index.d.ts +6 -0
  25. package/dist/composables/index.d.ts.map +1 -1
  26. package/dist/composables/resizeGeometry.d.ts +20 -0
  27. package/dist/composables/resizeGeometry.d.ts.map +1 -0
  28. package/dist/composables/useBoundedRectDrag.d.ts +26 -0
  29. package/dist/composables/useBoundedRectDrag.d.ts.map +1 -0
  30. package/dist/composables/usePointerSession.d.ts +17 -0
  31. package/dist/composables/usePointerSession.d.ts.map +1 -0
  32. package/dist/composables/useResizableLayout.d.ts +1 -2
  33. package/dist/composables/useResizableLayout.d.ts.map +1 -1
  34. package/dist/composables/useResizeObserver.d.ts +5 -4
  35. package/dist/composables/useResizeObserver.d.ts.map +1 -1
  36. package/dist/core.css +1 -1
  37. package/dist/directives/vResize.d.ts +2 -1
  38. package/dist/directives/vResize.d.ts.map +1 -1
  39. package/dist/index.cjs +42 -51
  40. package/dist/index.css +1 -1
  41. package/dist/index.mjs +10269 -9989
  42. package/dist/style.css +1 -1
  43. package/package.json +1 -1
  44. package/src/components/DragOver.vue +6 -41
  45. package/src/components/dataTable/DataTable.vue +11 -7
  46. package/src/components/draggable/Draggable.vue +5 -6
  47. package/src/components/draggable/index.ts +1 -0
  48. package/src/components/draggable/reorder.ts +17 -0
  49. package/src/components/draggable/useDraggable.ts +73 -71
  50. package/src/components/draggable/vDraggable.ts +18 -8
  51. package/src/components/file-drop/useFileDropzone.test.ts +108 -0
  52. package/src/components/file-drop/useFileDropzone.ts +83 -0
  53. package/src/components/form/inputs/ArrayInput.vue +3 -5
  54. package/src/components/form/inputs/Upload/UploadInput.vue +11 -21
  55. package/src/components/index.ts +4 -1
  56. package/src/components/layout/AppLayout.design.md +13 -15
  57. package/src/components/layout/AppLayout.vue +57 -38
  58. package/src/components/layout/Panel.vue +19 -1
  59. package/src/components/layout/Resizable.vue +2 -5
  60. package/src/components/layout/appLayoutContext.ts +2 -1
  61. package/src/composables/index.ts +6 -0
  62. package/src/composables/resizeGeometry.ts +31 -0
  63. package/src/composables/useBoundedRectDrag.ts +69 -0
  64. package/src/composables/usePointerSession.ts +70 -0
  65. package/src/composables/useResizableLayout.ts +66 -55
  66. package/src/composables/useResizeObserver.ts +32 -27
  67. package/src/directives/vResize.ts +86 -165
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.433",
4
+ "version": "1.15.440",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { Icon } from '@bagelink/vue'
3
- import { ref } from 'vue'
3
+ import { useFileDropzone } from './file-drop/useFileDropzone'
4
4
 
5
5
  const props = defineProps<{
6
6
  multiple?: boolean
@@ -13,32 +13,10 @@ const emit = defineEmits<{
13
13
  (e: 'addFiles', files: FileList): void
14
14
  }>()
15
15
 
16
- const isDragging = ref(false)
17
- const dragCounter = ref(0)
18
-
19
- function handleDragEnter(e: DragEvent) {
20
- if (props.disabled) { return }
21
- e.preventDefault()
22
- e.stopPropagation()
23
- dragCounter.value++
24
- isDragging.value = true
25
- }
26
-
27
- function handleDragOver(e: DragEvent) {
28
- if (props.disabled) { return }
29
- e.preventDefault()
30
- e.stopPropagation()
31
- }
32
-
33
- function handleDragLeave(e: DragEvent) {
34
- if (props.disabled) { return }
35
- e.preventDefault()
36
- e.stopPropagation()
37
- dragCounter.value--
38
- if (dragCounter.value === 0) {
39
- isDragging.value = false
40
- }
41
- }
16
+ const { isDragging, handlers } = useFileDropzone({
17
+ disabled: () => props.disabled,
18
+ onDrop: files => emit('addFiles', files),
19
+ })
42
20
 
43
21
  function handleClick(e: MouseEvent) {
44
22
  if (!props.browse || props.disabled) {
@@ -59,16 +37,6 @@ function handleClick(e: MouseEvent) {
59
37
  input.click()
60
38
  }
61
39
 
62
- function handleDrop(e: DragEvent) {
63
- if (props.disabled) { return }
64
- e.preventDefault()
65
- e.stopPropagation()
66
- dragCounter.value = 0
67
- isDragging.value = false
68
-
69
- if (!e.dataTransfer) { return }
70
- emit('addFiles', e.dataTransfer.files)
71
- }
72
40
  </script>
73
41
 
74
42
  <template>
@@ -78,10 +46,7 @@ function handleDrop(e: DragEvent) {
78
46
  'is-dragging': isDragging,
79
47
  'is-disabled': disabled,
80
48
  }"
81
- @dragenter="handleDragEnter"
82
- @dragover="handleDragOver"
83
- @dragleave="handleDragLeave"
84
- @drop="handleDrop"
49
+ v-on="handlers"
85
50
  @click="handleClick"
86
51
  >
87
52
  <slot />
@@ -94,14 +94,18 @@ const dragHandle = computed(() => {
94
94
  const tbodyRef = ref<HTMLTableSectionElement>()
95
95
 
96
96
  const { initDraggableContainer } = useDraggable({
97
- handle: dragHandle.value,
97
+ get handle() { return dragHandle.value },
98
+ get disabled() { return !isSortable.value },
98
99
  mode: 'line',
99
- items: props.data,
100
+ get items() { return list.value.map(entry => entry.data) },
100
101
  onEnd: (event: DraggableEvent) => {
101
102
  if (event.oldIndex === event.newIndex) return
102
- const item = computedData.value[event.oldIndex]
103
- const cleanItem = cleanTransformedData(item)
104
- emit('reorder', { oldIndex: event.oldIndex, newIndex: event.newIndex, item: cleanItem })
103
+ const source = list.value[event.oldIndex]
104
+ const target = list.value[event.newIndex]
105
+ if (!source || !target) return
106
+ const cleanItem = cleanTransformedData(source.data)
107
+ // Virtual indices include the synthetic header at index zero.
108
+ emit('reorder', { oldIndex: source.index - 1, newIndex: target.index - 1, item: cleanItem })
105
109
  }
106
110
  })
107
111
 
@@ -112,7 +116,7 @@ function reinitDrag() {
112
116
  })
113
117
  }
114
118
 
115
- watch(() => props.data, reinitDrag, { deep: true })
119
+ watch(list, reinitDrag, { flush: 'post' })
116
120
  watch(isSortable, (val) => { if (val) nextTick(reinitDrag) })
117
121
  onMounted(reinitDrag)
118
122
 
@@ -225,7 +229,7 @@ const headerFontSize = computed(() => {
225
229
  </div>
226
230
  </td>
227
231
  </tr>
228
- <tr v-if="props.onLastItemVisible !== undefined" ref="lastItemEl" style="height: 1px;" />
232
+ <tr v-if="props.onLastItemVisible !== undefined" ref="lastItemEl" data-draggable="false" style="height: 1px;" />
229
233
  </tbody>
230
234
  </table>
231
235
  </div>
@@ -2,6 +2,7 @@
2
2
  import type { DraggableEvent } from './useDraggable'
3
3
  import { ref, watch, onMounted, nextTick } from 'vue'
4
4
  import { useDraggable } from './useDraggable'
5
+ import { reorderItems } from './reorder'
5
6
 
6
7
  const props = withDefaults(defineProps<{
7
8
  modelValue?: any[]
@@ -22,7 +23,7 @@ const emit = defineEmits<{
22
23
 
23
24
  const dragContainer = ref<HTMLElement>()
24
25
 
25
- const { initDraggableContainer } = useDraggable({
26
+ const { initDraggableContainer } = useDraggable(() => ({
26
27
  group: props.group,
27
28
  handle: props.handle,
28
29
  mode: props.mode,
@@ -30,9 +31,7 @@ const { initDraggableContainer } = useDraggable({
30
31
  items: props.modelValue,
31
32
  onStart: (event) => { emit('start', event) },
32
33
  onEnd: (event: DraggableEvent) => {
33
- const newList = [...props.modelValue!]
34
- const [removed] = newList.splice(event.oldIndex, 1)
35
- newList.splice(event.newIndex, 0, removed)
34
+ const newList = reorderItems(props.modelValue ?? [], event.oldIndex, event.newIndex)
36
35
  emit('update:modelValue', newList)
37
36
 
38
37
  emit('end', event)
@@ -42,13 +41,13 @@ const { initDraggableContainer } = useDraggable({
42
41
  }
43
42
  })
44
43
  }
45
- })
44
+ }))
46
45
 
47
46
  watch(() => props.modelValue, () => {
48
47
  if (dragContainer.value) {
49
48
  initDraggableContainer(dragContainer.value)
50
49
  }
51
- }, { deep: true })
50
+ }, { deep: true, flush: 'post' })
52
51
 
53
52
  onMounted(() => {
54
53
  if (dragContainer.value) {
@@ -2,3 +2,4 @@ export { default as Draggable } from './Draggable.vue'
2
2
  export { useDraggable } from './useDraggable'
3
3
  export type { DraggableEvent, DraggableOptions } from './useDraggable'
4
4
  export { vDraggable } from './vDraggable'
5
+ export { insertionIndexToFinalIndex, reorderItems } from './reorder'
@@ -0,0 +1,17 @@
1
+ /** Convert a boundary in the original list to an index after removing the source. */
2
+ export function insertionIndexToFinalIndex(oldIndex: number, insertionIndex: number, length: number): number {
3
+ if (!Number.isInteger(oldIndex) || oldIndex < 0 || oldIndex >= length
4
+ || !Number.isInteger(insertionIndex) || insertionIndex < 0 || insertionIndex > length) return -1
5
+ return insertionIndex > oldIndex ? insertionIndex - 1 : insertionIndex
6
+ }
7
+
8
+ /** Return a reordered copy; both indices refer to item positions, not boundaries. */
9
+ export function reorderItems<T>(items: readonly T[], oldIndex: number, newIndex: number): T[] {
10
+ const result = [...items]
11
+ if (!Number.isInteger(oldIndex) || !Number.isInteger(newIndex)
12
+ || oldIndex < 0 || newIndex < 0 || oldIndex >= result.length || newIndex >= result.length
13
+ || oldIndex === newIndex) return result
14
+ const [item] = result.splice(oldIndex, 1)
15
+ result.splice(newIndex, 0, item!)
16
+ return result
17
+ }
@@ -1,7 +1,9 @@
1
- import { ref, onUnmounted } from 'vue'
1
+ import { ref, computed, toValue, getCurrentInstance, onUnmounted, type MaybeRefOrGetter } from 'vue'
2
+ import { insertionIndexToFinalIndex } from './reorder'
2
3
 
3
4
  export interface DraggableEvent extends MouseEvent {
4
5
  oldIndex: number
6
+ /** Final array index after removing the source item, in both line and ghost modes. */
5
7
  newIndex: number
6
8
  item?: any
7
9
  }
@@ -28,7 +30,7 @@ export interface DraggableElement extends HTMLElement {
28
30
  __drag_data?: any
29
31
  }
30
32
 
31
- export function useDraggable(options: DraggableOptions = {}) {
33
+ export function useDraggable(options: MaybeRefOrGetter<DraggableOptions> = {}) {
32
34
  const isDragging = ref(false)
33
35
  const dragElement = ref<DraggableElement | null>(null)
34
36
  const ghostElement = ref<HTMLElement | null>(null)
@@ -55,14 +57,14 @@ export function useDraggable(options: DraggableOptions = {}) {
55
57
  }
56
58
  }
57
59
 
58
- const defaultOptions: DraggableOptions = {
60
+ const defaultOptions = computed(() => ({
59
61
  animation: 150,
60
62
  mode: 'line',
61
63
  ghostClass: 'draggable-ghost',
62
64
  dragClass: 'draggable-dragging',
63
65
  dropIndicatorClass: 'draggable-drop-indicator',
64
- ...options
65
- }
66
+ ...toValue(options)
67
+ }))
66
68
 
67
69
  let startX = 0
68
70
  let startY = 0
@@ -87,11 +89,11 @@ export function useDraggable(options: DraggableOptions = {}) {
87
89
  element.style.margin = '0'
88
90
  element.style.padding = '0'
89
91
  element.style.border = '0'
90
- element.showPopover()
92
+ element.showPopover?.()
91
93
  }
92
94
 
93
95
  function removeFromTopLayer(element: HTMLElement) {
94
- if (element.matches(':popover-open')) element.hidePopover()
96
+ if (element.hidePopover && element.matches(':popover-open')) element.hidePopover()
95
97
  element.parentNode?.removeChild(element)
96
98
  }
97
99
 
@@ -110,15 +112,15 @@ export function useDraggable(options: DraggableOptions = {}) {
110
112
  ghost.style.zIndex = '9999'
111
113
  ghost.style.cursor = 'grabbing'
112
114
  ghost.style.willChange = 'transform'
113
- ghost.classList.add(defaultOptions.ghostClass!)
115
+ ghost.classList.add(defaultOptions.value.ghostClass!)
114
116
  appendToTopLayer(ghost)
115
117
  return ghost
116
118
  }
117
119
 
118
- function createDropIndicator(el: DraggableElement) {
120
+ function createDropIndicator() {
119
121
  if (!isBrowser) { return null }
120
122
  const line = document.createElement('div')
121
- line.classList.add(defaultOptions.dropIndicatorClass!)
123
+ line.classList.add(defaultOptions.value.dropIndicatorClass!)
122
124
  line.style.position = 'fixed'
123
125
  line.style.pointerEvents = 'none'
124
126
  line.style.zIndex = '9999'
@@ -179,7 +181,7 @@ export function useDraggable(options: DraggableOptions = {}) {
179
181
  }
180
182
 
181
183
  // function resetDragStyles(dragEl: DraggableElement) {
182
- // if (defaultOptions.mode === 'ghost') {
184
+ // if (defaultOptions.value.mode === 'ghost') {
183
185
  // dragEl.style.opacity = ''
184
186
  // dragEl.style.transform = ''
185
187
  // } else {
@@ -258,26 +260,13 @@ export function useDraggable(options: DraggableOptions = {}) {
258
260
  document.removeEventListener('keydown', onKeyDown)
259
261
  document.removeEventListener('touchmove', onTouchMove)
260
262
  document.removeEventListener('touchend', onTouchEnd)
261
- document.removeEventListener('touchcancel', onTouchEnd)
263
+ document.removeEventListener('touchcancel', cleanup)
262
264
  window.removeEventListener('blur', cleanup)
263
265
 
264
- // Remove elements with animation
265
- if (ghostElement.value) {
266
- ghostElement.value.style.opacity = '0'
267
- ghostElement.value.style.transform += ' scale(0.8)'
268
- setTimeout(() => {
269
- if (ghostElement.value) removeFromTopLayer(ghostElement.value)
270
- ghostElement.value = null
271
- }, 150)
272
- }
273
-
274
- if (dropIndicator.value) {
275
- dropIndicator.value.style.opacity = '0'
276
- setTimeout(() => {
277
- if (dropIndicator.value) removeFromTopLayer(dropIndicator.value)
278
- dropIndicator.value = null
279
- }, 150)
280
- }
266
+ if (ghostElement.value) removeFromTopLayer(ghostElement.value)
267
+ if (dropIndicator.value) removeFromTopLayer(dropIndicator.value)
268
+ ghostElement.value = null
269
+ dropIndicator.value = null
281
270
 
282
271
  // Reset all elements
283
272
  if (dragElement.value) {
@@ -287,7 +276,7 @@ export function useDraggable(options: DraggableOptions = {}) {
287
276
  el.style.transform = ''
288
277
  el.style.opacity = ''
289
278
  })
290
- dragElement.value.classList.remove(defaultOptions.dragClass!)
279
+ dragElement.value.classList.remove(defaultOptions.value.dragClass!)
291
280
  dragElement.value = null
292
281
  }
293
282
 
@@ -317,16 +306,16 @@ export function useDraggable(options: DraggableOptions = {}) {
317
306
  }
318
307
 
319
308
  function onDragStart(e: MouseEvent) {
320
- if (!isBrowser || defaultOptions.disabled) { return }
309
+ if (!isBrowser || isDragging.value || defaultOptions.value.disabled) { return }
321
310
  if (e.button !== 0) { return } // Only left mouse button
322
311
  if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) { return }
323
312
 
324
313
  const dragEl = findDraggableParent(e.target as Element)
325
- if (!dragEl || dragEl.dataset.draggable === 'false') { return }
314
+ if (!dragEl || dragEl.dataset.draggable === 'false' || e.currentTarget !== dragEl) { return }
326
315
 
327
316
  // If handle is specified, check if clicked element matches
328
- if (defaultOptions.handle) {
329
- const handleEl = (e.target as Element).closest(defaultOptions.handle)
317
+ if (defaultOptions.value.handle) {
318
+ const handleEl = (e.target as Element).closest(defaultOptions.value.handle)
330
319
  if (!handleEl || !dragEl.contains(handleEl)) { return }
331
320
  }
332
321
 
@@ -349,17 +338,17 @@ export function useDraggable(options: DraggableOptions = {}) {
349
338
  }
350
339
 
351
340
  // Only create drop indicator in line mode
352
- if (defaultOptions.mode === 'line') {
353
- dropIndicator.value = createDropIndicator(dragEl)
341
+ if (defaultOptions.value.mode === 'line') {
342
+ dropIndicator.value = createDropIndicator()
354
343
  }
355
344
 
356
345
  // Apply mode-specific styles and get shift amount
357
- dragEl.classList.add(defaultOptions.dragClass!)
358
- shiftAmount = defaultOptions.mode === 'ghost'
346
+ dragEl.classList.add(defaultOptions.value.dragClass!)
347
+ shiftAmount = defaultOptions.value.mode === 'ghost'
359
348
  ? handleGhostMode(dragEl)
360
349
  : handleLineMode(dragEl)
361
350
 
362
- defaultOptions.onStart?.(e)
351
+ defaultOptions.value.onStart?.(e)
363
352
 
364
353
  // Prevent text selection during drag
365
354
  if (isBrowser) {
@@ -430,7 +419,8 @@ export function useDraggable(options: DraggableOptions = {}) {
430
419
  const newDropTarget = findDraggableParent(elementAtPoint)
431
420
 
432
421
  // Validate drop target
433
- if (!newDropTarget || newDropTarget.__drag_group !== dragElement.value.__drag_group) {
422
+ if (!newDropTarget || !draggedList.includes(newDropTarget)) {
423
+ dropTarget.value = null
434
424
  if (dropIndicator.value) {
435
425
  dropIndicator.value.style.display = 'none'
436
426
  }
@@ -445,32 +435,33 @@ export function useDraggable(options: DraggableOptions = {}) {
445
435
  dropTarget.value.__intended_index = dropIndex
446
436
 
447
437
  // Update line indicator
448
- if (dropIndicator.value && defaultOptions.mode === 'line') {
438
+ if (dropIndicator.value && defaultOptions.value.mode === 'line') {
449
439
  const targetIndex = draggedList.indexOf(newDropTarget)
450
440
  const isAfter = dropIndex > targetIndex
451
441
  updateDropIndicator(newDropTarget, isAfter)
452
442
  }
453
443
 
454
444
  // Update element positions
445
+ const finalIndex = insertionIndexToFinalIndex(dragIndex, dropIndex, draggedList.length)
455
446
  const shift = shiftAmount
456
447
  draggedList.forEach((el) => {
457
448
  // Use transform3d for better performance
458
- el.style.transition = `transform ${defaultOptions.animation}ms cubic-bezier(0.2, 0, 0, 1)`
449
+ el.style.transition = `transform ${defaultOptions.value.animation}ms cubic-bezier(0.2, 0, 0, 1)`
459
450
  el.style.transform = 'translate3d(0, 0, 0)'
460
451
 
461
452
  if (el === dragElement.value) {
462
- if (defaultOptions.mode === 'ghost') {
463
- const offset = (dropIndex - dragIndex) * shift
453
+ if (defaultOptions.value.mode === 'ghost') {
454
+ const offset = (finalIndex - dragIndex) * shift
464
455
  el.style.transform = `translate3d(0, ${offset}px, 0)`
465
456
  }
466
457
  } else {
467
458
  const currentIndex = draggedList.indexOf(el)
468
- if (dragIndex < dropIndex) {
469
- if (currentIndex > dragIndex && currentIndex <= dropIndex) {
459
+ if (dragIndex < finalIndex) {
460
+ if (currentIndex > dragIndex && currentIndex <= finalIndex) {
470
461
  el.style.transform = `translate3d(0, -${shift}px, 0)`
471
462
  }
472
- } else if (dragIndex > dropIndex) {
473
- if (currentIndex >= dropIndex && currentIndex < dragIndex) {
463
+ } else if (dragIndex > finalIndex) {
464
+ if (currentIndex >= finalIndex && currentIndex < dragIndex) {
474
465
  el.style.transform = `translate3d(0, ${shift}px, 0)`
475
466
  }
476
467
  }
@@ -482,6 +473,11 @@ export function useDraggable(options: DraggableOptions = {}) {
482
473
  }
483
474
 
484
475
  function onDragEnd(e: MouseEvent) {
476
+ if (defaultOptions.value.disabled) { cleanup(); return }
477
+ if (isDragging.value) {
478
+ lastUpdateTime = 0
479
+ onDragMove(e)
480
+ }
485
481
  if (!isDragging.value || !dragElement.value || !dropTarget.value) {
486
482
  cleanup()
487
483
  return
@@ -495,9 +491,9 @@ export function useDraggable(options: DraggableOptions = {}) {
495
491
 
496
492
  const draggedList = list.filter(el => el.dataset.draggable !== 'false')
497
493
  const actualDragIndex = draggedList.indexOf(dragElement.value)
498
- const actualDropIndex = dropTarget.value.__intended_index!
494
+ const actualDropIndex = insertionIndexToFinalIndex(actualDragIndex, dropTarget.value.__intended_index!, draggedList.length)
499
495
 
500
- if (actualDragIndex !== -1 && actualDropIndex !== -1) {
496
+ if (actualDropIndex !== -1 && actualDragIndex !== actualDropIndex) {
501
497
  list.forEach((el) => {
502
498
  el.style.transition = 'none'
503
499
  el.style.transform = ''
@@ -508,16 +504,18 @@ export function useDraggable(options: DraggableOptions = {}) {
508
504
  const dragEvent = new MouseEvent('mouseup', e) as DraggableEvent
509
505
  dragEvent.oldIndex = actualDragIndex
510
506
  dragEvent.newIndex = actualDropIndex
511
- dragEvent.item = dragElement.value.__drag_data
507
+ dragEvent.item = defaultOptions.value.items?.[actualDragIndex] ?? dragElement.value.__drag_data
512
508
 
513
- defaultOptions.onEnd?.(dragEvent)
509
+ cleanup()
510
+ defaultOptions.value.onEnd?.(dragEvent)
511
+ return
514
512
  }
515
513
 
516
514
  cleanup()
517
515
  }
518
516
 
519
517
  function onTouchStart(e: TouchEvent) {
520
- if (!isBrowser || defaultOptions.disabled || !e.target) { return }
518
+ if (!isBrowser || defaultOptions.value.disabled || !e.target) { return }
521
519
 
522
520
  const touch = e.touches[0]
523
521
  if (!touch) { return }
@@ -525,9 +523,9 @@ export function useDraggable(options: DraggableOptions = {}) {
525
523
  const target = e.target as Element
526
524
  const draggableParent = findDraggableParent(target)
527
525
 
528
- if (!draggableParent) { return }
526
+ if (!draggableParent || e.currentTarget !== draggableParent || isDragging.value) { return }
529
527
 
530
- const { handle } = defaultOptions
528
+ const { handle } = defaultOptions.value
531
529
  if (handle && !target.closest(handle)) { return }
532
530
 
533
531
  e.preventDefault() // Prevent scrolling
@@ -548,17 +546,17 @@ export function useDraggable(options: DraggableOptions = {}) {
548
546
  }
549
547
 
550
548
  // Only create drop indicator in line mode
551
- if (defaultOptions.mode === 'line') {
552
- dropIndicator.value = createDropIndicator(draggableParent)
549
+ if (defaultOptions.value.mode === 'line') {
550
+ dropIndicator.value = createDropIndicator()
553
551
  }
554
552
 
555
553
  // Apply mode-specific styles and get shift amount
556
- dragElement.value.classList.add(defaultOptions.dragClass!)
557
- shiftAmount = defaultOptions.mode === 'ghost'
554
+ dragElement.value.classList.add(defaultOptions.value.dragClass!)
555
+ shiftAmount = defaultOptions.value.mode === 'ghost'
558
556
  ? handleGhostMode(dragElement.value)
559
557
  : handleLineMode(dragElement.value)
560
558
 
561
- defaultOptions.onStart?.(e as unknown as MouseEvent)
559
+ defaultOptions.value.onStart?.(e as unknown as MouseEvent)
562
560
 
563
561
  if (isBrowser) {
564
562
  // Prevent text selection during drag
@@ -569,7 +567,8 @@ export function useDraggable(options: DraggableOptions = {}) {
569
567
  window.addEventListener('blur', cleanup)
570
568
  document.addEventListener('touchmove', onTouchMove, { passive: false })
571
569
  document.addEventListener('touchend', onTouchEnd)
572
- document.addEventListener('touchcancel', onTouchEnd)
570
+ document.addEventListener('touchcancel', cleanup)
571
+ document.addEventListener('keydown', onKeyDown)
573
572
  }
574
573
  }
575
574
 
@@ -601,7 +600,7 @@ export function useDraggable(options: DraggableOptions = {}) {
601
600
  onDragEnd(mouseEvent)
602
601
  document.removeEventListener('touchmove', onTouchMove)
603
602
  document.removeEventListener('touchend', onTouchEnd)
604
- document.removeEventListener('touchcancel', onTouchEnd)
603
+ document.removeEventListener('touchcancel', cleanup)
605
604
  }
606
605
 
607
606
  function initDraggableContainer(container: HTMLElement) {
@@ -624,17 +623,15 @@ export function useDraggable(options: DraggableOptions = {}) {
624
623
  })
625
624
 
626
625
  let currentIndex = 0
627
- elements.forEach((el, index) => {
626
+ elements.forEach((el) => {
628
627
  if (el.dataset.draggable !== 'false') {
629
628
  el.style.userSelect = 'none'
630
629
  el.style.webkitUserSelect = 'none'
631
- el.__drag_group = options.group
630
+ el.__drag_group = defaultOptions.value.group
632
631
  el.__drag_index = currentIndex++
633
632
  el.__drag_list = elements.filter(e => e.dataset.draggable !== 'false')
634
633
 
635
- if (options.items) {
636
- el.__drag_data = options.items[index]
637
- }
634
+ el.__drag_data = defaultOptions.value.items?.[el.__drag_index]
638
635
 
639
636
  el.addEventListener('mousedown', onDragStart)
640
637
  el.addEventListener('touchstart', onTouchStart)
@@ -642,6 +639,7 @@ export function useDraggable(options: DraggableOptions = {}) {
642
639
  })
643
640
 
644
641
  cleanupFn = () => {
642
+ cleanup()
645
643
  elements.forEach((el) => {
646
644
  el.removeEventListener('mousedown', onDragStart)
647
645
  el.removeEventListener('touchstart', onTouchStart)
@@ -653,14 +651,18 @@ export function useDraggable(options: DraggableOptions = {}) {
653
651
  }
654
652
  }
655
653
 
656
- // Automatically clean up when component is unmounted
657
- onUnmounted(() => {
654
+ function destroy() {
658
655
  cleanupFn?.()
659
- })
656
+ cleanupFn = undefined
657
+ }
658
+
659
+ if (getCurrentInstance()) onUnmounted(destroy)
660
660
 
661
661
  return {
662
662
  isDragging,
663
663
  dragElement,
664
- initDraggableContainer
664
+ initDraggableContainer,
665
+ destroy,
666
+ cancel: cleanup
665
667
  }
666
668
  }
@@ -1,17 +1,27 @@
1
- import type { Directive } from 'vue'
1
+ import { shallowRef, type Directive } from 'vue'
2
2
  import type { DraggableOptions } from './useDraggable'
3
3
  import { useDraggable } from './useDraggable'
4
4
 
5
+ const instances = new WeakMap<HTMLElement, {
6
+ options: ReturnType<typeof shallowRef<DraggableOptions>>
7
+ draggable: ReturnType<typeof useDraggable>
8
+ }>()
9
+
5
10
  export const vDraggable: Directive<HTMLElement, DraggableOptions> = {
6
11
  mounted(el, binding) {
7
- const { initDraggableContainer } = useDraggable(binding.value || {})
8
- initDraggableContainer(el)
12
+ const options = shallowRef(binding.value || {})
13
+ const draggable = useDraggable(options)
14
+ instances.set(el, { options, draggable })
15
+ draggable.initDraggableContainer(el)
9
16
  },
10
17
  updated(el, binding) {
11
- // Only reinitialize if options changed
12
- if (binding.value !== binding.oldValue) {
13
- const { initDraggableContainer } = useDraggable(binding.value || {})
14
- initDraggableContainer(el)
15
- }
18
+ const instance = instances.get(el)
19
+ if (!instance) return
20
+ instance.options.value = binding.value || {}
21
+ instance.draggable.initDraggableContainer(el)
22
+ },
23
+ beforeUnmount(el) {
24
+ instances.get(el)?.draggable.destroy()
25
+ instances.delete(el)
16
26
  }
17
27
  }