@vanduo-oss/framework 1.2.3

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