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.
- package/LICENSE +35 -0
- package/README.md +205 -0
- package/css/components/alerts.css +224 -0
- package/css/components/avatar.css +275 -0
- package/css/components/badges.css +230 -0
- package/css/components/breadcrumbs.css +146 -0
- package/css/components/button-group.css +82 -0
- package/css/components/buttons.css +530 -0
- package/css/components/cards.css +304 -0
- package/css/components/chips.css +259 -0
- package/css/components/code-snippet.css +555 -0
- package/css/components/collapsible.css +267 -0
- package/css/components/collections.css +253 -0
- package/css/components/doc-search.css +464 -0
- package/css/components/doc-tabs.css +38 -0
- package/css/components/draggable.css +317 -0
- package/css/components/dropdown.css +266 -0
- package/css/components/footer.css +375 -0
- package/css/components/forms.css +1774 -0
- package/css/components/image-box.css +279 -0
- package/css/components/modals.css +285 -0
- package/css/components/navbar.css +530 -0
- package/css/components/pagination.css +186 -0
- package/css/components/preloader.css +340 -0
- package/css/components/progress.css +107 -0
- package/css/components/sidenav.css +301 -0
- package/css/components/skeleton.css +241 -0
- package/css/components/spinner.css +144 -0
- package/css/components/tabs.css +327 -0
- package/css/components/theme-customizer.css +835 -0
- package/css/components/toast.css +357 -0
- package/css/components/tooltips.css +270 -0
- package/css/core/colors.css +1017 -0
- package/css/core/fonts.css +266 -0
- package/css/core/grid.css +1699 -0
- package/css/core/helpers.css +2202 -0
- package/css/core/reset.css +128 -0
- package/css/core/tokens.css +213 -0
- package/css/core/typography.css +405 -0
- package/css/core/vd-aliases.css +47 -0
- package/css/effects/parallax.css +113 -0
- package/css/icons/icons-all.css +23 -0
- package/css/icons/icons.css +25 -0
- package/css/utilities/media.css +167 -0
- package/css/utilities/print.css +111 -0
- package/css/utilities/shadow.css +243 -0
- package/css/utilities/table.css +381 -0
- package/css/utilities/transforms.css +71 -0
- package/css/utilities/transitions.css +87 -0
- package/css/vanduo.css +80 -0
- package/dist/build-info.json +6 -0
- package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-bold.woff2 +0 -0
- package/dist/fonts/inter/inter-medium.woff2 +0 -0
- package/dist/fonts/inter/inter-regular.woff2 +0 -0
- package/dist/fonts/inter/inter-semibold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
- package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
- package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/dist/icons/phosphor/LICENSE +21 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/dist/icons/phosphor/bold/style.css +4627 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/dist/icons/phosphor/duotone/style.css +12115 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/dist/icons/phosphor/fill/style.css +4627 -0
- package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/dist/icons/phosphor/light/style.css +4627 -0
- package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
- package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/dist/icons/phosphor/regular/style.css +4627 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/dist/icons/phosphor/thin/style.css +4627 -0
- package/dist/vanduo.cjs.js +5569 -0
- package/dist/vanduo.cjs.js.map +7 -0
- package/dist/vanduo.cjs.min.js +48 -0
- package/dist/vanduo.cjs.min.js.map +7 -0
- package/dist/vanduo.css +60666 -0
- package/dist/vanduo.css.map +1 -0
- package/dist/vanduo.esm.js +5548 -0
- package/dist/vanduo.esm.js.map +7 -0
- package/dist/vanduo.esm.min.js +48 -0
- package/dist/vanduo.esm.min.js.map +7 -0
- package/dist/vanduo.js +5545 -0
- package/dist/vanduo.js.map +7 -0
- package/dist/vanduo.min.css +2 -0
- package/dist/vanduo.min.css.map +1 -0
- package/dist/vanduo.min.js +48 -0
- package/dist/vanduo.min.js.map +7 -0
- package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
- package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
- package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
- package/fonts/inter/inter-bold.woff2 +0 -0
- package/fonts/inter/inter-medium.woff2 +0 -0
- package/fonts/inter/inter-regular.woff2 +0 -0
- package/fonts/inter/inter-semibold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
- package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
- package/fonts/open-sans/open-sans-bold.woff2 +0 -0
- package/fonts/open-sans/open-sans-medium.woff2 +0 -0
- package/fonts/open-sans/open-sans-regular.woff2 +0 -0
- package/fonts/rubik/rubik-bold.woff2 +0 -0
- package/fonts/rubik/rubik-medium.woff2 +0 -0
- package/fonts/rubik/rubik-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-bold.woff2 +0 -0
- package/fonts/source-sans/source-sans-regular.woff2 +0 -0
- package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
- package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
- package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
- package/icons/phosphor/LICENSE +21 -0
- package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
- package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
- package/icons/phosphor/bold/style.css +4627 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
- package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
- package/icons/phosphor/duotone/style.css +12115 -0
- package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
- package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
- package/icons/phosphor/fill/style.css +4627 -0
- package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff +0 -0
- package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
- package/icons/phosphor/light/style.css +4627 -0
- package/icons/phosphor/regular/Phosphor.ttf +0 -0
- package/icons/phosphor/regular/Phosphor.woff +0 -0
- package/icons/phosphor/regular/Phosphor.woff2 +0 -0
- package/icons/phosphor/regular/style.css +4627 -0
- package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
- package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
- package/icons/phosphor/thin/style.css +4627 -0
- package/js/components/code-snippet.js +639 -0
- package/js/components/collapsible.js +226 -0
- package/js/components/doc-search.js +936 -0
- package/js/components/draggable.js +725 -0
- package/js/components/dropdown.js +362 -0
- package/js/components/font-switcher.js +253 -0
- package/js/components/grid.js +279 -0
- package/js/components/image-box.js +372 -0
- package/js/components/modals.js +367 -0
- package/js/components/navbar.js +264 -0
- package/js/components/pagination.js +286 -0
- package/js/components/parallax.js +216 -0
- package/js/components/preloader.js +183 -0
- package/js/components/select.js +444 -0
- package/js/components/sidenav.js +303 -0
- package/js/components/tabs.js +303 -0
- package/js/components/theme-customizer.js +784 -0
- package/js/components/theme-switcher.js +183 -0
- package/js/components/toast.js +343 -0
- package/js/components/tooltips.js +306 -0
- package/js/index.js +52 -0
- package/js/utils/helpers.js +306 -0
- package/js/utils/lifecycle.js +135 -0
- package/js/vanduo.js +120 -0
- 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
|
+
})();
|