vanduo-framework 1.1.8

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 (196) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +205 -0
  3. package/css/components/alerts.css +224 -0
  4. package/css/components/avatar.css +275 -0
  5. package/css/components/badges.css +230 -0
  6. package/css/components/breadcrumbs.css +146 -0
  7. package/css/components/button-group.css +82 -0
  8. package/css/components/buttons.css +530 -0
  9. package/css/components/cards.css +304 -0
  10. package/css/components/chips.css +259 -0
  11. package/css/components/code-snippet.css +555 -0
  12. package/css/components/collapsible.css +267 -0
  13. package/css/components/collections.css +253 -0
  14. package/css/components/doc-search.css +464 -0
  15. package/css/components/doc-tabs.css +38 -0
  16. package/css/components/draggable.css +317 -0
  17. package/css/components/dropdown.css +266 -0
  18. package/css/components/footer.css +375 -0
  19. package/css/components/forms.css +1774 -0
  20. package/css/components/image-box.css +279 -0
  21. package/css/components/modals.css +285 -0
  22. package/css/components/navbar.css +530 -0
  23. package/css/components/pagination.css +186 -0
  24. package/css/components/preloader.css +340 -0
  25. package/css/components/progress.css +107 -0
  26. package/css/components/sidenav.css +301 -0
  27. package/css/components/skeleton.css +241 -0
  28. package/css/components/spinner.css +144 -0
  29. package/css/components/tabs.css +327 -0
  30. package/css/components/theme-customizer.css +835 -0
  31. package/css/components/toast.css +357 -0
  32. package/css/components/tooltips.css +270 -0
  33. package/css/core/colors.css +1017 -0
  34. package/css/core/fonts.css +266 -0
  35. package/css/core/grid.css +1699 -0
  36. package/css/core/helpers.css +2202 -0
  37. package/css/core/reset.css +128 -0
  38. package/css/core/tokens.css +213 -0
  39. package/css/core/typography.css +405 -0
  40. package/css/core/vd-aliases.css +47 -0
  41. package/css/effects/parallax.css +113 -0
  42. package/css/icons/icons-all.css +23 -0
  43. package/css/icons/icons.css +25 -0
  44. package/css/utilities/media.css +167 -0
  45. package/css/utilities/print.css +111 -0
  46. package/css/utilities/shadow.css +243 -0
  47. package/css/utilities/table.css +381 -0
  48. package/css/utilities/transforms.css +71 -0
  49. package/css/utilities/transitions.css +87 -0
  50. package/css/vanduo.css +80 -0
  51. package/dist/build-info.json +6 -0
  52. package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  53. package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  54. package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  55. package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  56. package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  57. package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  58. package/dist/fonts/inter/inter-bold.woff2 +0 -0
  59. package/dist/fonts/inter/inter-medium.woff2 +0 -0
  60. package/dist/fonts/inter/inter-regular.woff2 +0 -0
  61. package/dist/fonts/inter/inter-semibold.woff2 +0 -0
  62. package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  63. package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  64. package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
  65. package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
  66. package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
  67. package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
  68. package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
  69. package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
  70. package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
  71. package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
  72. package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  73. package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  74. package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  75. package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  76. package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  77. package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  78. package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  79. package/dist/icons/phosphor/LICENSE +21 -0
  80. package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  81. package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  82. package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  83. package/dist/icons/phosphor/bold/style.css +4627 -0
  84. package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  85. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  86. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  87. package/dist/icons/phosphor/duotone/style.css +12115 -0
  88. package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  89. package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  90. package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  91. package/dist/icons/phosphor/fill/style.css +4627 -0
  92. package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  93. package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
  94. package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  95. package/dist/icons/phosphor/light/style.css +4627 -0
  96. package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
  97. package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
  98. package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
  99. package/dist/icons/phosphor/regular/style.css +4627 -0
  100. package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  101. package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  102. package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  103. package/dist/icons/phosphor/thin/style.css +4627 -0
  104. package/dist/vanduo.cjs.js +5569 -0
  105. package/dist/vanduo.cjs.js.map +7 -0
  106. package/dist/vanduo.cjs.min.js +48 -0
  107. package/dist/vanduo.cjs.min.js.map +7 -0
  108. package/dist/vanduo.css +60666 -0
  109. package/dist/vanduo.css.map +1 -0
  110. package/dist/vanduo.esm.js +5548 -0
  111. package/dist/vanduo.esm.js.map +7 -0
  112. package/dist/vanduo.esm.min.js +48 -0
  113. package/dist/vanduo.esm.min.js.map +7 -0
  114. package/dist/vanduo.js +5545 -0
  115. package/dist/vanduo.js.map +7 -0
  116. package/dist/vanduo.min.css +2 -0
  117. package/dist/vanduo.min.css.map +1 -0
  118. package/dist/vanduo.min.js +48 -0
  119. package/dist/vanduo.min.js.map +7 -0
  120. package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  121. package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  122. package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  123. package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  124. package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  125. package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  126. package/fonts/inter/inter-bold.woff2 +0 -0
  127. package/fonts/inter/inter-medium.woff2 +0 -0
  128. package/fonts/inter/inter-regular.woff2 +0 -0
  129. package/fonts/inter/inter-semibold.woff2 +0 -0
  130. package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  131. package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  132. package/fonts/open-sans/open-sans-bold.woff2 +0 -0
  133. package/fonts/open-sans/open-sans-medium.woff2 +0 -0
  134. package/fonts/open-sans/open-sans-regular.woff2 +0 -0
  135. package/fonts/rubik/rubik-bold.woff2 +0 -0
  136. package/fonts/rubik/rubik-medium.woff2 +0 -0
  137. package/fonts/rubik/rubik-regular.woff2 +0 -0
  138. package/fonts/source-sans/source-sans-bold.woff2 +0 -0
  139. package/fonts/source-sans/source-sans-regular.woff2 +0 -0
  140. package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  141. package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  142. package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  143. package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  144. package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  145. package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  146. package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  147. package/icons/phosphor/LICENSE +21 -0
  148. package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  149. package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  150. package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  151. package/icons/phosphor/bold/style.css +4627 -0
  152. package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  153. package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  154. package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  155. package/icons/phosphor/duotone/style.css +12115 -0
  156. package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  157. package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  158. package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  159. package/icons/phosphor/fill/style.css +4627 -0
  160. package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  161. package/icons/phosphor/light/Phosphor-Light.woff +0 -0
  162. package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  163. package/icons/phosphor/light/style.css +4627 -0
  164. package/icons/phosphor/regular/Phosphor.ttf +0 -0
  165. package/icons/phosphor/regular/Phosphor.woff +0 -0
  166. package/icons/phosphor/regular/Phosphor.woff2 +0 -0
  167. package/icons/phosphor/regular/style.css +4627 -0
  168. package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  169. package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  170. package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  171. package/icons/phosphor/thin/style.css +4627 -0
  172. package/js/components/code-snippet.js +639 -0
  173. package/js/components/collapsible.js +226 -0
  174. package/js/components/doc-search.js +936 -0
  175. package/js/components/draggable.js +725 -0
  176. package/js/components/dropdown.js +362 -0
  177. package/js/components/font-switcher.js +253 -0
  178. package/js/components/grid.js +279 -0
  179. package/js/components/image-box.js +372 -0
  180. package/js/components/modals.js +367 -0
  181. package/js/components/navbar.js +264 -0
  182. package/js/components/pagination.js +286 -0
  183. package/js/components/parallax.js +216 -0
  184. package/js/components/preloader.js +183 -0
  185. package/js/components/select.js +444 -0
  186. package/js/components/sidenav.js +303 -0
  187. package/js/components/tabs.js +303 -0
  188. package/js/components/theme-customizer.js +784 -0
  189. package/js/components/theme-switcher.js +183 -0
  190. package/js/components/toast.js +343 -0
  191. package/js/components/tooltips.js +306 -0
  192. package/js/index.js +52 -0
  193. package/js/utils/helpers.js +306 -0
  194. package/js/utils/lifecycle.js +135 -0
  195. package/js/vanduo.js +120 -0
  196. package/package.json +78 -0
@@ -0,0 +1,725 @@
1
+ /**
2
+ * Vanduo Framework - Draggable Component
3
+ * JavaScript functionality for draggable elements and drop zones
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Draggable Component
11
+ */
12
+ const Draggable = {
13
+ // Store initialized draggables and their cleanup functions
14
+ instances: new Map(),
15
+ // Store current drag state
16
+ currentDrag: null,
17
+ // Store touch state
18
+ touchState: null,
19
+ // Feedback element
20
+ feedbackElement: null,
21
+
22
+ /**
23
+ * Initialize draggable components
24
+ */
25
+ init: function () {
26
+ const draggables = document.querySelectorAll('.vd-draggable, [data-draggable]');
27
+
28
+ draggables.forEach(element => {
29
+ if (this.instances.has(element)) {
30
+ return;
31
+ }
32
+ this.initDraggable(element);
33
+ });
34
+
35
+ const containers = document.querySelectorAll('.vd-draggable-container, .vd-draggable-container-vertical');
36
+ containers.forEach(container => {
37
+ if (!this.instances.has(container)) {
38
+ this.initContainer(container);
39
+ }
40
+ });
41
+
42
+ const dropZones = document.querySelectorAll('.vd-drop-zone');
43
+ dropZones.forEach(zone => {
44
+ if (!this.instances.has(zone)) {
45
+ this.initDropZone(zone);
46
+ }
47
+ });
48
+
49
+ this.createFeedbackElement();
50
+ },
51
+
52
+ /**
53
+ * Initialize a single draggable element
54
+ * @param {HTMLElement} element - Draggable element
55
+ */
56
+ initDraggable: function (element) {
57
+ const cleanupFunctions = [];
58
+
59
+ // Make element draggable if not already
60
+ if (!element.hasAttribute('draggable')) {
61
+ element.setAttribute('draggable', 'true');
62
+ }
63
+
64
+ // Accessibility: add ARIA attributes
65
+ if (!element.hasAttribute('tabindex')) {
66
+ element.setAttribute('tabindex', '0');
67
+ }
68
+ element.setAttribute('role', 'option');
69
+ element.setAttribute('aria-roledescription', 'draggable item');
70
+ element.setAttribute('aria-grabbed', 'false');
71
+
72
+ // Handle drag start
73
+ const dragStartHandler = (e) => {
74
+ this.handleDragStart(e, element);
75
+ };
76
+ element.addEventListener('dragstart', dragStartHandler);
77
+ cleanupFunctions.push(() => element.removeEventListener('dragstart', dragStartHandler));
78
+
79
+ // Handle drag
80
+ const dragHandler = (e) => {
81
+ this.handleDrag(e, element);
82
+ };
83
+ element.addEventListener('drag', dragHandler);
84
+ cleanupFunctions.push(() => element.removeEventListener('drag', dragHandler));
85
+
86
+ // Handle drag end
87
+ const dragEndHandler = (e) => {
88
+ this.handleDragEnd(e, element);
89
+ };
90
+ element.addEventListener('dragend', dragEndHandler);
91
+ cleanupFunctions.push(() => element.removeEventListener('dragend', dragEndHandler));
92
+
93
+ // Handle touch start (for mobile)
94
+ const touchStartHandler = (e) => {
95
+ this.handleTouchStart(e, element);
96
+ };
97
+ element.addEventListener('touchstart', touchStartHandler);
98
+ cleanupFunctions.push(() => element.removeEventListener('touchstart', touchStartHandler));
99
+
100
+ // Handle touch move (for mobile)
101
+ const touchMoveHandler = (e) => {
102
+ this.handleTouchMove(e, element);
103
+ };
104
+ element.addEventListener('touchmove', touchMoveHandler);
105
+ cleanupFunctions.push(() => element.removeEventListener('touchmove', touchMoveHandler));
106
+
107
+ // Handle touch end (for mobile)
108
+ const touchEndHandler = (e) => {
109
+ this.handleTouchEnd(e, element);
110
+ };
111
+ element.addEventListener('touchend', touchEndHandler);
112
+ cleanupFunctions.push(() => element.removeEventListener('touchend', touchEndHandler));
113
+
114
+ // Handle touch cancel (for mobile)
115
+ const touchCancelHandler = (e) => {
116
+ this.handleTouchEnd(e, element);
117
+ };
118
+ element.addEventListener('touchcancel', touchCancelHandler);
119
+ cleanupFunctions.push(() => element.removeEventListener('touchcancel', touchCancelHandler));
120
+
121
+ // Keyboard navigation
122
+ const keydownHandler = (e) => {
123
+ this.handleKeydown(e, element);
124
+ };
125
+ element.addEventListener('keydown', keydownHandler);
126
+ cleanupFunctions.push(() => element.removeEventListener('keydown', keydownHandler));
127
+
128
+ this.instances.set(element, { cleanup: cleanupFunctions });
129
+ },
130
+
131
+ /**
132
+ * Initialize a draggable container
133
+ * @param {HTMLElement} container - Draggable container
134
+ */
135
+ initContainer: function (container) {
136
+ // Accessibility: add ARIA role to container
137
+ container.setAttribute('role', 'listbox');
138
+ container.setAttribute('aria-label', container.getAttribute('aria-label') || 'Draggable items');
139
+
140
+ const items = container.querySelectorAll('.vd-draggable-item');
141
+ items.forEach(item => {
142
+ if (!this.instances.has(item)) {
143
+ this.initDraggable(item);
144
+ }
145
+ });
146
+
147
+ const cleanupFunctions = [];
148
+
149
+ // Handle drag enter
150
+ const dragEnterHandler = (e) => {
151
+ e.preventDefault();
152
+ e.dataTransfer.dropEffect = 'move';
153
+ };
154
+
155
+ // Handle drag over for auto-sorting
156
+ const dragOverHandler = (e) => {
157
+ e.preventDefault(); // Necessary to allow drop
158
+ e.dataTransfer.dropEffect = 'move';
159
+
160
+ if (!this.currentDrag) return;
161
+ const draggingEl = this.currentDrag.element;
162
+
163
+ // Only reorder if dragging an item that belongs to this container (or if we support cross-container drag, but keep simple)
164
+ if (!container.contains(draggingEl)) return;
165
+
166
+ // Prevent jumps when the dragging finalizes with 0,0 coordinates near the end
167
+ if (e.clientX === 0 && e.clientY === 0) return;
168
+
169
+ this.handleReorder(container, draggingEl, e.clientX, e.clientY);
170
+ };
171
+
172
+ // Handle drop
173
+ const dropHandler = (e) => {
174
+ e.preventDefault(); // crucial to prevent the browser's default handling and snapping back
175
+ };
176
+
177
+ container.addEventListener('dragenter', dragEnterHandler);
178
+ container.addEventListener('dragover', dragOverHandler);
179
+ container.addEventListener('drop', dropHandler);
180
+
181
+ cleanupFunctions.push(() => {
182
+ container.removeEventListener('dragenter', dragEnterHandler);
183
+ container.removeEventListener('dragover', dragOverHandler);
184
+ container.removeEventListener('drop', dropHandler);
185
+ });
186
+
187
+ this.instances.set(container, { cleanup: cleanupFunctions });
188
+ },
189
+
190
+ /**
191
+ * Initialize a drop zone
192
+ * @param {HTMLElement} zone - Drop zone element
193
+ */
194
+ initDropZone: function (zone) {
195
+ const cleanupFunctions = [];
196
+
197
+ // Accessibility: add ARIA role to drop zone
198
+ zone.setAttribute('role', 'region');
199
+ zone.setAttribute('aria-dropeffect', 'move');
200
+ if (!zone.hasAttribute('aria-label')) {
201
+ zone.setAttribute('aria-label', 'Drop zone');
202
+ }
203
+
204
+ // Handle drag over
205
+ const dragOverHandler = (e) => {
206
+ e.preventDefault();
207
+ this.handleDragOver(e, zone);
208
+ };
209
+ zone.addEventListener('dragover', dragOverHandler);
210
+ cleanupFunctions.push(() => zone.removeEventListener('dragover', dragOverHandler));
211
+
212
+ // Handle drag enter
213
+ const dragEnterHandler = (e) => {
214
+ e.preventDefault();
215
+ this.handleDragEnter(e, zone);
216
+ };
217
+ zone.addEventListener('dragenter', dragEnterHandler);
218
+ cleanupFunctions.push(() => zone.removeEventListener('dragenter', dragEnterHandler));
219
+
220
+ // Handle drag leave
221
+ const dragLeaveHandler = (e) => {
222
+ this.handleDragLeave(e, zone);
223
+ };
224
+ zone.addEventListener('dragleave', dragLeaveHandler);
225
+ cleanupFunctions.push(() => zone.removeEventListener('dragleave', dragLeaveHandler));
226
+
227
+ // Handle drop
228
+ const dropHandler = (e) => {
229
+ e.preventDefault();
230
+ this.handleDrop(e, zone);
231
+ };
232
+ zone.addEventListener('drop', dropHandler);
233
+ cleanupFunctions.push(() => zone.removeEventListener('drop', dropHandler));
234
+
235
+ this.instances.set(zone, { cleanup: cleanupFunctions });
236
+ },
237
+
238
+ /**
239
+ * Create feedback element for drag operations
240
+ */
241
+ createFeedbackElement: function () {
242
+ if (!this.feedbackElement) {
243
+ // Reuse existing element if present
244
+ const existing = document.querySelector('.vd-drag-feedback');
245
+ if (existing) {
246
+ this.feedbackElement = existing;
247
+ return;
248
+ }
249
+
250
+ this.feedbackElement = document.createElement('div');
251
+ this.feedbackElement.className = 'vd-drag-feedback hidden';
252
+ this.feedbackElement.setAttribute('role', 'presentation');
253
+ document.body.appendChild(this.feedbackElement);
254
+ }
255
+ },
256
+
257
+ /**
258
+ * Handle drag start event
259
+ * @param {DragEvent} e - Drag event
260
+ * @param {HTMLElement} element - Draggable element
261
+ */
262
+ handleDragStart: function (e, element) {
263
+ // Add dragging class
264
+ element.classList.add('is-dragging');
265
+
266
+ // Accessibility: update aria-grabbed
267
+ element.setAttribute('aria-grabbed', 'true');
268
+
269
+ // Store drag state
270
+ this.currentDrag = {
271
+ element: element,
272
+ initialPosition: { x: e.clientX, y: e.clientY },
273
+ initialBounds: element.getBoundingClientRect(),
274
+ data: this.getData(element)
275
+ };
276
+
277
+ // Set drag data
278
+ e.dataTransfer.effectAllowed = 'move';
279
+ e.dataTransfer.setData('text/plain', this.currentDrag.data);
280
+
281
+ // We no longer suppress the native ghost image or manually update feedback
282
+ // for mouse drags, relying on the browser's native rendering instead.
283
+
284
+ // Dispatch event
285
+ element.dispatchEvent(new CustomEvent('draggable:start', {
286
+ bubbles: true,
287
+ detail: {
288
+ element: element,
289
+ data: this.currentDrag.data,
290
+ position: { x: e.clientX, y: e.clientY }
291
+ }
292
+ }));
293
+ },
294
+
295
+ /**
296
+ * Handle drag event
297
+ * @param {DragEvent} e - Drag event
298
+ * @param {HTMLElement} element - Draggable element
299
+ */
300
+ handleDrag: function (e, element) {
301
+ // Guard against null state (race condition on fast interactions)
302
+ if (!this.currentDrag) return;
303
+
304
+ // Dispatch event
305
+ element.dispatchEvent(new CustomEvent('draggable:drag', {
306
+ bubbles: true,
307
+ detail: {
308
+ element: element,
309
+ data: this.currentDrag.data,
310
+ position: { x: e.clientX, y: e.clientY },
311
+ delta: {
312
+ x: e.clientX - this.currentDrag.initialPosition.x,
313
+ y: e.clientY - this.currentDrag.initialPosition.y
314
+ }
315
+ }
316
+ }));
317
+ },
318
+
319
+ /**
320
+ * Handle drag end event
321
+ * @param {DragEvent} e - Drag event
322
+ * @param {HTMLElement} element - Draggable element
323
+ */
324
+ handleDragEnd: function (e, element) {
325
+ // Remove dragging class
326
+ element.classList.remove('is-dragging');
327
+ element.classList.add('is-dropped');
328
+ setTimeout(() => element.classList.remove('is-dropped'), 300);
329
+
330
+ // Accessibility: update aria-grabbed
331
+ element.setAttribute('aria-grabbed', 'false');
332
+
333
+ // Hide feedback
334
+ if (this.feedbackElement) {
335
+ this.feedbackElement.classList.add('hidden');
336
+ }
337
+
338
+ // Guard against null state
339
+ const data = this.currentDrag?.data || this.getData(element);
340
+ const initialPos = this.currentDrag?.initialPosition || { x: 0, y: 0 };
341
+
342
+ // Dispatch event
343
+ element.dispatchEvent(new CustomEvent('draggable:end', {
344
+ bubbles: true,
345
+ detail: {
346
+ element: element,
347
+ data: data,
348
+ position: { x: e.clientX, y: e.clientY },
349
+ delta: {
350
+ x: e.clientX - initialPos.x,
351
+ y: e.clientY - initialPos.y
352
+ }
353
+ }
354
+ }));
355
+
356
+ // Reset drag state
357
+ this.currentDrag = null;
358
+ },
359
+
360
+ /**
361
+ * Handle touch start event (for mobile)
362
+ * @param {TouchEvent} e - Touch event
363
+ * @param {HTMLElement} element - Draggable element
364
+ */
365
+ handleTouchStart: function (e, element) {
366
+ // Don't prevent default here — it blocks scrolling.
367
+ // We only prevent default in touchmove once drag threshold is reached.
368
+ const touch = e.touches[0];
369
+ this.touchState = {
370
+ element: element,
371
+ startX: touch.clientX,
372
+ startY: touch.clientY,
373
+ startTime: Date.now(),
374
+ isDragging: false
375
+ };
376
+ },
377
+
378
+ /**
379
+ * Handle touch move event (for mobile)
380
+ * @param {TouchEvent} e - Touch event
381
+ * @param {HTMLElement} element - Draggable element
382
+ */
383
+ handleTouchMove: function (e, element) {
384
+ if (!this.touchState) return;
385
+
386
+ const touch = e.touches[0];
387
+ const deltaX = touch.clientX - this.touchState.startX;
388
+ const deltaY = touch.clientY - this.touchState.startY;
389
+
390
+ // Only start dragging if moved a minimum distance
391
+ if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
392
+ // Now we know it's a drag, not a scroll — prevent default
393
+ e.preventDefault();
394
+
395
+ if (!this.touchState.isDragging) {
396
+ this.touchState.isDragging = true;
397
+ element.classList.add('is-dragging');
398
+ element.setAttribute('aria-grabbed', 'true');
399
+
400
+ // Store drag state
401
+ this.currentDrag = {
402
+ element: element,
403
+ initialPosition: { x: this.touchState.startX, y: this.touchState.startY },
404
+ initialBounds: element.getBoundingClientRect(),
405
+ data: this.getData(element)
406
+ };
407
+
408
+ // Dispatch event
409
+ element.dispatchEvent(new CustomEvent('draggable:start', {
410
+ bubbles: true,
411
+ detail: {
412
+ element: element,
413
+ data: this.currentDrag.data,
414
+ position: { x: touch.clientX, y: touch.clientY }
415
+ }
416
+ }));
417
+ }
418
+
419
+ // Update feedback
420
+ this.updateFeedback(touch.clientX, touch.clientY);
421
+
422
+ // Dispatch event
423
+ if (this.currentDrag) {
424
+ element.dispatchEvent(new CustomEvent('draggable:drag', {
425
+ bubbles: true,
426
+ detail: {
427
+ element: element,
428
+ data: this.currentDrag.data,
429
+ position: { x: touch.clientX, y: touch.clientY },
430
+ delta: { x: deltaX, y: deltaY }
431
+ }
432
+ }));
433
+
434
+ // Reorder for touch
435
+ const container = element.closest('.vd-draggable-container');
436
+ if (container && container.contains(element)) {
437
+ this.handleReorder(container, element, touch.clientX, touch.clientY);
438
+ }
439
+ }
440
+ }
441
+ },
442
+
443
+ /**
444
+ * Handle touch end event (for mobile)
445
+ * @param {TouchEvent} e - Touch event
446
+ * @param {HTMLElement} element - Draggable element
447
+ */
448
+ handleTouchEnd: function (e, element) {
449
+ if (this.touchState && this.touchState.isDragging) {
450
+ e.preventDefault();
451
+
452
+ element.classList.remove('is-dragging');
453
+ element.classList.add('is-dropped');
454
+ element.setAttribute('aria-grabbed', 'false');
455
+ setTimeout(() => element.classList.remove('is-dropped'), 300);
456
+
457
+ // Hide feedback
458
+ if (this.feedbackElement) {
459
+ this.feedbackElement.classList.add('hidden');
460
+ }
461
+
462
+ // Dispatch event
463
+ const endTouch = e.changedTouches[0];
464
+ const data = this.currentDrag?.data || this.getData(element);
465
+ const startX = this.touchState?.startX || 0;
466
+ const startY = this.touchState?.startY || 0;
467
+
468
+ element.dispatchEvent(new CustomEvent('draggable:end', {
469
+ bubbles: true,
470
+ detail: {
471
+ element: element,
472
+ data: data,
473
+ position: { x: endTouch.clientX, y: endTouch.clientY },
474
+ delta: {
475
+ x: endTouch.clientX - startX,
476
+ y: endTouch.clientY - startY
477
+ }
478
+ }
479
+ }));
480
+ }
481
+
482
+ // Reset states
483
+ this.touchState = null;
484
+ this.currentDrag = null;
485
+ },
486
+
487
+ /**
488
+ * Handle drag over event
489
+ * @param {DragEvent} e - Drag event
490
+ * @param {HTMLElement} _zone - Drop zone element
491
+ */
492
+ handleDragOver: function (e, _zone) {
493
+ e.preventDefault();
494
+ e.dataTransfer.dropEffect = 'move';
495
+ },
496
+
497
+ /**
498
+ * Handle drag enter event
499
+ * @param {DragEvent} e - Drag event
500
+ * @param {HTMLElement} zone - Drop zone element
501
+ */
502
+ handleDragEnter: function (e, zone) {
503
+ e.preventDefault();
504
+ zone.classList.add('is-drag-over');
505
+ },
506
+
507
+ /**
508
+ * Handle drag leave event
509
+ * @param {DragEvent} e - Drag event
510
+ * @param {HTMLElement} zone - Drop zone element
511
+ */
512
+ handleDragLeave: function (e, zone) {
513
+ zone.classList.remove('is-drag-over');
514
+ },
515
+
516
+ /**
517
+ * Handle drop event
518
+ * @param {DragEvent} e - Drag event
519
+ * @param {HTMLElement} zone - Drop zone element
520
+ */
521
+ handleDrop: function (e, zone) {
522
+ e.preventDefault();
523
+ zone.classList.remove('is-drag-over');
524
+
525
+ // Dispatch event
526
+ zone.dispatchEvent(new CustomEvent('draggable:drop', {
527
+ bubbles: true,
528
+ detail: {
529
+ zone: zone,
530
+ element: this.currentDrag?.element,
531
+ data: this.currentDrag?.data,
532
+ position: { x: e.clientX, y: e.clientY }
533
+ }
534
+ }));
535
+ },
536
+
537
+ /**
538
+ * Reorder elements in container based on cursor position
539
+ * @param {HTMLElement} container
540
+ * @param {HTMLElement} element
541
+ * @param {number} clientX
542
+ * @param {number} clientY
543
+ */
544
+ handleReorder: function (container, element, clientX, clientY) {
545
+ const isVertical = container.classList.contains('vd-draggable-container-vertical');
546
+ const siblings = [...container.querySelectorAll('.vd-draggable-item:not(.is-dragging), .vd-draggable:not(.is-dragging)')];
547
+
548
+ const nextSibling = siblings.reduce((closest, child) => {
549
+ const box = child.getBoundingClientRect();
550
+ const offset = isVertical
551
+ ? clientY - box.top - box.height / 2
552
+ : clientX - box.left - box.width / 2;
553
+
554
+ if (offset < 0 && offset > closest.offset) {
555
+ return { offset: offset, element: child };
556
+ } else {
557
+ return closest;
558
+ }
559
+ }, { offset: Number.NEGATIVE_INFINITY }).element;
560
+
561
+ if (nextSibling == null) {
562
+ container.appendChild(element);
563
+ } else {
564
+ container.insertBefore(element, nextSibling);
565
+ }
566
+ },
567
+
568
+ /**
569
+ * Handle keyboard events
570
+ * @param {KeyboardEvent} e - Keyboard event
571
+ * @param {HTMLElement} element - Draggable element
572
+ */
573
+ handleKeydown: function (e, element) {
574
+ switch (e.key) {
575
+ case 'Enter':
576
+ case ' ':
577
+ e.preventDefault();
578
+ // Trigger click or custom action
579
+ element.click();
580
+ break;
581
+ case 'Escape':
582
+ // Cancel drag if in progress
583
+ if (element.classList.contains('is-dragging')) {
584
+ element.classList.remove('is-dragging');
585
+ element.setAttribute('aria-grabbed', 'false');
586
+ if (this.feedbackElement) {
587
+ this.feedbackElement.classList.add('hidden');
588
+ }
589
+ this.currentDrag = null;
590
+ }
591
+ break;
592
+ case 'ArrowUp':
593
+ case 'ArrowLeft': {
594
+ e.preventDefault();
595
+ const prev = element.previousElementSibling;
596
+ if (prev && (prev.classList.contains('vd-draggable') || prev.classList.contains('vd-draggable-item'))) {
597
+ element.parentNode.insertBefore(element, prev);
598
+ element.focus();
599
+ element.dispatchEvent(new CustomEvent('draggable:reorder', {
600
+ bubbles: true,
601
+ detail: { element: element, direction: 'up' }
602
+ }));
603
+ }
604
+ break;
605
+ }
606
+ case 'ArrowDown':
607
+ case 'ArrowRight': {
608
+ e.preventDefault();
609
+ const next = element.nextElementSibling;
610
+ if (next && (next.classList.contains('vd-draggable') || next.classList.contains('vd-draggable-item'))) {
611
+ element.parentNode.insertBefore(next, element);
612
+ element.focus();
613
+ element.dispatchEvent(new CustomEvent('draggable:reorder', {
614
+ bubbles: true,
615
+ detail: { element: element, direction: 'down' }
616
+ }));
617
+ }
618
+ break;
619
+ }
620
+ }
621
+ },
622
+
623
+ /**
624
+ * Get data from draggable element
625
+ * @param {HTMLElement} element - Draggable element
626
+ * @returns {string} Data associated with the element
627
+ */
628
+ getData: function (element) {
629
+ return element.dataset.draggable || element.textContent.trim();
630
+ },
631
+
632
+ /**
633
+ * Update drag feedback element
634
+ * @param {number} x - Current X coordinate
635
+ * @param {number} y - Current Y coordinate
636
+ */
637
+ updateFeedback: function (x, y) {
638
+ if (!this.currentDrag) return;
639
+
640
+ // Show feedback
641
+ this.feedbackElement.classList.remove('hidden');
642
+
643
+ // Update feedback content
644
+ const rect = this.currentDrag.initialBounds;
645
+ this.feedbackElement.innerHTML = '';
646
+ const clone = this.currentDrag.element.cloneNode(true);
647
+ this.feedbackElement.appendChild(clone);
648
+
649
+ // Set styles
650
+ Object.assign(this.feedbackElement.style, {
651
+ left: (x - 20) + 'px',
652
+ top: (y - 20) + 'px',
653
+ width: rect.width + 'px',
654
+ height: rect.height + 'px'
655
+ });
656
+ },
657
+
658
+ /**
659
+ * Make an element draggable programmatically
660
+ * @param {HTMLElement|string} element - Element or selector
661
+ * @param {Object} options - Configuration options
662
+ */
663
+ makeDraggable: function (element, options = {}) {
664
+ const el = typeof element === 'string' ? document.querySelector(element) : element;
665
+
666
+ if (el && !this.instances.has(el)) {
667
+ // Add classes and attributes
668
+ el.classList.add('vd-draggable');
669
+ el.setAttribute('draggable', 'true');
670
+
671
+ // Set options
672
+ if (options.data) {
673
+ el.dataset.draggable = options.data;
674
+ }
675
+
676
+ // Initialize
677
+ this.initDraggable(el);
678
+ }
679
+ },
680
+
681
+ /**
682
+ * Remove draggable functionality from an element
683
+ * @param {HTMLElement|string} element - Element or selector
684
+ */
685
+ removeDraggable: function (element) {
686
+ const el = typeof element === 'string' ? document.querySelector(element) : element;
687
+
688
+ if (el && this.instances.has(el)) {
689
+ // Clean up
690
+ const instance = this.instances.get(el);
691
+ instance.cleanup.forEach(fn => fn());
692
+ this.instances.delete(el);
693
+
694
+ // Remove classes and attributes
695
+ el.classList.remove('vd-draggable');
696
+ el.removeAttribute('draggable');
697
+ el.removeAttribute('data-draggable');
698
+ }
699
+ },
700
+
701
+ /**
702
+ * Destroy a draggable instance and clean up event listeners
703
+ * @param {HTMLElement} element - Draggable element
704
+ */
705
+ destroy: function (element) {
706
+ this.removeDraggable(element);
707
+ },
708
+
709
+ /**
710
+ * Destroy all draggable instances
711
+ */
712
+ destroyAll: function () {
713
+ const instances = Array.from(this.instances.keys());
714
+ instances.forEach(element => this.destroy(element));
715
+ }
716
+ };
717
+
718
+ // Register with Vanduo framework if available
719
+ if (typeof window.Vanduo !== 'undefined') {
720
+ window.Vanduo.register('draggable', Draggable);
721
+ }
722
+
723
+ // Expose globally
724
+ window.VanduoDraggable = Draggable;
725
+ })();