@vanduo-oss/framework 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/css/components/affix.css +53 -0
- package/css/components/bubble.css +165 -0
- package/css/components/datepicker.css +216 -0
- package/css/components/fab.css +225 -0
- package/css/components/flow.css +265 -0
- package/css/components/rating.css +112 -0
- package/css/components/ripple.css +63 -0
- package/css/components/sidenav.css +70 -0
- package/css/components/spotlight.css +119 -0
- package/css/components/stepper.css +176 -0
- package/css/components/suggest.css +119 -0
- package/css/components/timeline.css +201 -0
- package/css/components/timepicker.css +80 -0
- package/css/components/transfer.css +165 -0
- package/css/components/tree.css +173 -0
- package/css/components/waypoint.css +59 -0
- package/css/utilities/color-utilities.css +352 -0
- package/css/vanduo.css +20 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +2152 -4
- package/dist/vanduo.cjs.js.map +4 -4
- package/dist/vanduo.cjs.min.js +5 -5
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +3253 -271
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +2152 -4
- package/dist/vanduo.esm.js.map +4 -4
- package/dist/vanduo.esm.min.js +5 -5
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +2152 -4
- package/dist/vanduo.js.map +4 -4
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +5 -5
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/affix.js +129 -0
- package/js/components/bubble.js +203 -0
- package/js/components/datepicker.js +287 -0
- package/js/components/flow.js +264 -0
- package/js/components/rating.js +160 -0
- package/js/components/ripple.js +74 -0
- package/js/components/sidenav.js +9 -2
- package/js/components/spotlight.js +295 -0
- package/js/components/stepper.js +97 -0
- package/js/components/suggest.js +219 -0
- package/js/components/timepicker.js +142 -0
- package/js/components/transfer.js +206 -0
- package/js/components/tree.js +191 -0
- package/js/components/validate.js +185 -0
- package/js/components/waypoint.js +120 -0
- package/js/index.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Flow (Carousel/Slider) Component
|
|
3
|
+
* Touch-enabled carousel with slide/fade transitions, autoplay, indicators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Flow = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const carousels = document.querySelectorAll('.vd-flow, .vd-carousel');
|
|
14
|
+
carousels.forEach(el => {
|
|
15
|
+
if (this.instances.has(el)) return;
|
|
16
|
+
this.initInstance(el);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (el) {
|
|
21
|
+
const track = el.querySelector('.vd-flow-track');
|
|
22
|
+
if (!track) return;
|
|
23
|
+
|
|
24
|
+
const slides = Array.from(track.querySelectorAll('.vd-flow-slide'));
|
|
25
|
+
if (slides.length === 0) return;
|
|
26
|
+
|
|
27
|
+
const isFade = el.classList.contains('vd-flow-fade');
|
|
28
|
+
const autoplay = el.hasAttribute('data-vd-autoplay');
|
|
29
|
+
const interval = parseInt(el.getAttribute('data-vd-interval'), 10) || 5000;
|
|
30
|
+
const loop = el.getAttribute('data-vd-loop') !== 'false';
|
|
31
|
+
|
|
32
|
+
const state = {
|
|
33
|
+
current: 0,
|
|
34
|
+
total: slides.length,
|
|
35
|
+
autoplayTimer: null,
|
|
36
|
+
isFade: isFade,
|
|
37
|
+
loop: loop,
|
|
38
|
+
isDragging: false,
|
|
39
|
+
startX: 0,
|
|
40
|
+
currentX: 0,
|
|
41
|
+
threshold: 50
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const cleanup = [];
|
|
45
|
+
|
|
46
|
+
// Set initial active slide
|
|
47
|
+
slides.forEach((slide, i) => {
|
|
48
|
+
slide.setAttribute('role', 'group');
|
|
49
|
+
slide.setAttribute('aria-roledescription', 'slide');
|
|
50
|
+
slide.setAttribute('aria-label', 'Slide ' + (i + 1) + ' of ' + slides.length);
|
|
51
|
+
if (i === 0) slide.classList.add('is-active');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
el.setAttribute('role', 'region');
|
|
55
|
+
el.setAttribute('aria-roledescription', 'carousel');
|
|
56
|
+
if (!el.getAttribute('aria-label')) {
|
|
57
|
+
el.setAttribute('aria-label', 'Carousel');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Live region for announcements
|
|
61
|
+
const liveRegion = document.createElement('div');
|
|
62
|
+
liveRegion.setAttribute('aria-live', 'polite');
|
|
63
|
+
liveRegion.setAttribute('aria-atomic', 'true');
|
|
64
|
+
liveRegion.className = 'sr-only';
|
|
65
|
+
liveRegion.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);';
|
|
66
|
+
el.appendChild(liveRegion);
|
|
67
|
+
|
|
68
|
+
const goTo = (index, announce) => {
|
|
69
|
+
if (announce === undefined) announce = true;
|
|
70
|
+
let target = index;
|
|
71
|
+
if (state.loop) {
|
|
72
|
+
target = ((index % state.total) + state.total) % state.total;
|
|
73
|
+
} else {
|
|
74
|
+
target = Math.max(0, Math.min(index, state.total - 1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const prev = state.current;
|
|
78
|
+
state.current = target;
|
|
79
|
+
|
|
80
|
+
if (state.isFade) {
|
|
81
|
+
slides.forEach((s, i) => {
|
|
82
|
+
s.classList.toggle('is-active', i === target);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
track.style.transform = 'translateX(-' + (target * 100) + '%)';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update indicators
|
|
89
|
+
const indicators = el.querySelectorAll('.vd-flow-indicator');
|
|
90
|
+
indicators.forEach((ind, i) => {
|
|
91
|
+
ind.classList.toggle('is-active', i === target);
|
|
92
|
+
ind.setAttribute('aria-selected', i === target ? 'true' : 'false');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Update slide ARIA
|
|
96
|
+
slides.forEach((s, i) => {
|
|
97
|
+
s.setAttribute('aria-hidden', i !== target ? 'true' : 'false');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (announce) {
|
|
101
|
+
liveRegion.textContent = 'Slide ' + (target + 1) + ' of ' + state.total;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
el.dispatchEvent(new CustomEvent('flow:change', {
|
|
105
|
+
detail: { current: target, previous: prev, total: state.total }
|
|
106
|
+
}));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const next = () => goTo(state.current + 1);
|
|
110
|
+
const prev = () => goTo(state.current - 1);
|
|
111
|
+
|
|
112
|
+
// Controls
|
|
113
|
+
const prevBtn = el.querySelector('.vd-flow-prev');
|
|
114
|
+
const nextBtn = el.querySelector('.vd-flow-next');
|
|
115
|
+
|
|
116
|
+
if (prevBtn) {
|
|
117
|
+
const h = () => prev();
|
|
118
|
+
prevBtn.addEventListener('click', h);
|
|
119
|
+
cleanup.push(() => prevBtn.removeEventListener('click', h));
|
|
120
|
+
}
|
|
121
|
+
if (nextBtn) {
|
|
122
|
+
const h = () => next();
|
|
123
|
+
nextBtn.addEventListener('click', h);
|
|
124
|
+
cleanup.push(() => nextBtn.removeEventListener('click', h));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Indicators
|
|
128
|
+
const indicators = el.querySelectorAll('.vd-flow-indicator');
|
|
129
|
+
indicators.forEach((ind, i) => {
|
|
130
|
+
ind.setAttribute('role', 'tab');
|
|
131
|
+
ind.setAttribute('aria-selected', i === 0 ? 'true' : 'false');
|
|
132
|
+
ind.setAttribute('aria-label', 'Go to slide ' + (i + 1));
|
|
133
|
+
const h = () => goTo(i);
|
|
134
|
+
ind.addEventListener('click', h);
|
|
135
|
+
cleanup.push(() => ind.removeEventListener('click', h));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Keyboard navigation
|
|
139
|
+
const keyHandler = (e) => {
|
|
140
|
+
if (e.key === 'ArrowLeft') { prev(); e.preventDefault(); }
|
|
141
|
+
if (e.key === 'ArrowRight') { next(); e.preventDefault(); }
|
|
142
|
+
};
|
|
143
|
+
el.setAttribute('tabindex', '0');
|
|
144
|
+
el.addEventListener('keydown', keyHandler);
|
|
145
|
+
cleanup.push(() => el.removeEventListener('keydown', keyHandler));
|
|
146
|
+
|
|
147
|
+
// Touch / pointer support
|
|
148
|
+
const pointerDown = (e) => {
|
|
149
|
+
state.isDragging = true;
|
|
150
|
+
state.startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
151
|
+
state.currentX = state.startX;
|
|
152
|
+
el.classList.add('is-dragging');
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const pointerMove = (e) => {
|
|
156
|
+
if (!state.isDragging) return;
|
|
157
|
+
state.currentX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const pointerUp = () => {
|
|
161
|
+
if (!state.isDragging) return;
|
|
162
|
+
state.isDragging = false;
|
|
163
|
+
el.classList.remove('is-dragging');
|
|
164
|
+
const diff = state.startX - state.currentX;
|
|
165
|
+
if (Math.abs(diff) > state.threshold) {
|
|
166
|
+
if (diff > 0) next();
|
|
167
|
+
else prev();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
el.addEventListener('mousedown', pointerDown);
|
|
172
|
+
el.addEventListener('mousemove', pointerMove);
|
|
173
|
+
el.addEventListener('mouseup', pointerUp);
|
|
174
|
+
el.addEventListener('mouseleave', pointerUp);
|
|
175
|
+
el.addEventListener('touchstart', pointerDown, { passive: true });
|
|
176
|
+
el.addEventListener('touchmove', pointerMove, { passive: true });
|
|
177
|
+
el.addEventListener('touchend', pointerUp);
|
|
178
|
+
|
|
179
|
+
cleanup.push(
|
|
180
|
+
() => el.removeEventListener('mousedown', pointerDown),
|
|
181
|
+
() => el.removeEventListener('mousemove', pointerMove),
|
|
182
|
+
() => el.removeEventListener('mouseup', pointerUp),
|
|
183
|
+
() => el.removeEventListener('mouseleave', pointerUp),
|
|
184
|
+
() => el.removeEventListener('touchstart', pointerDown),
|
|
185
|
+
() => el.removeEventListener('touchmove', pointerMove),
|
|
186
|
+
() => el.removeEventListener('touchend', pointerUp)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Autoplay
|
|
190
|
+
const startAutoplay = () => {
|
|
191
|
+
stopAutoplay();
|
|
192
|
+
state.autoplayTimer = setInterval(next, interval);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const stopAutoplay = () => {
|
|
196
|
+
if (state.autoplayTimer) {
|
|
197
|
+
clearInterval(state.autoplayTimer);
|
|
198
|
+
state.autoplayTimer = null;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (autoplay) {
|
|
203
|
+
startAutoplay();
|
|
204
|
+
const pauseHandler = () => stopAutoplay();
|
|
205
|
+
const resumeHandler = () => startAutoplay();
|
|
206
|
+
el.addEventListener('mouseenter', pauseHandler);
|
|
207
|
+
el.addEventListener('mouseleave', resumeHandler);
|
|
208
|
+
el.addEventListener('focusin', pauseHandler);
|
|
209
|
+
el.addEventListener('focusout', resumeHandler);
|
|
210
|
+
cleanup.push(
|
|
211
|
+
() => el.removeEventListener('mouseenter', pauseHandler),
|
|
212
|
+
() => el.removeEventListener('mouseleave', resumeHandler),
|
|
213
|
+
() => el.removeEventListener('focusin', pauseHandler),
|
|
214
|
+
() => el.removeEventListener('focusout', resumeHandler),
|
|
215
|
+
() => stopAutoplay()
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Initial ARIA state
|
|
220
|
+
goTo(0, false);
|
|
221
|
+
|
|
222
|
+
this.instances.set(el, {
|
|
223
|
+
cleanup: cleanup,
|
|
224
|
+
goTo: goTo,
|
|
225
|
+
next: next,
|
|
226
|
+
prev: prev,
|
|
227
|
+
getState: () => ({ ...state })
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
goTo: function (el, index) {
|
|
232
|
+
const instance = this.instances.get(el);
|
|
233
|
+
if (instance) instance.goTo(index);
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
next: function (el) {
|
|
237
|
+
const instance = this.instances.get(el);
|
|
238
|
+
if (instance) instance.next();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
prev: function (el) {
|
|
242
|
+
const instance = this.instances.get(el);
|
|
243
|
+
if (instance) instance.prev();
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
destroy: function (el) {
|
|
247
|
+
const instance = this.instances.get(el);
|
|
248
|
+
if (!instance) return;
|
|
249
|
+
instance.cleanup.forEach(fn => fn());
|
|
250
|
+
this.instances.delete(el);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
destroyAll: function () {
|
|
254
|
+
this.instances.forEach((_, el) => this.destroy(el));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
259
|
+
window.Vanduo.register('flow', Flow);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
window.VanduoFlow = Flow;
|
|
263
|
+
|
|
264
|
+
})();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Rating Component
|
|
3
|
+
* Star-based rating input with hover preview and read-only mode
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Rating = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const ratings = document.querySelectorAll('[data-vd-rating]');
|
|
14
|
+
ratings.forEach(el => {
|
|
15
|
+
if (this.instances.has(el)) return;
|
|
16
|
+
this.initInstance(el);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (el) {
|
|
21
|
+
const cleanup = [];
|
|
22
|
+
const max = parseInt(el.getAttribute('data-vd-rating-max') || '5', 10);
|
|
23
|
+
const initialValue = parseFloat(el.getAttribute('data-vd-rating-value') || '0');
|
|
24
|
+
const readonly = el.classList.contains('vd-rating-readonly') || el.hasAttribute('data-vd-rating-readonly');
|
|
25
|
+
let currentValue = initialValue;
|
|
26
|
+
|
|
27
|
+
el.classList.add('vd-rating');
|
|
28
|
+
el.setAttribute('role', 'radiogroup');
|
|
29
|
+
el.setAttribute('aria-label', el.getAttribute('aria-label') || 'Rating');
|
|
30
|
+
|
|
31
|
+
// Clear existing stars
|
|
32
|
+
el.innerHTML = '';
|
|
33
|
+
|
|
34
|
+
// Create stars
|
|
35
|
+
const stars = [];
|
|
36
|
+
for (let i = 1; i <= max; i++) {
|
|
37
|
+
const star = document.createElement('button');
|
|
38
|
+
star.type = 'button';
|
|
39
|
+
star.className = 'vd-rating-star';
|
|
40
|
+
star.setAttribute('role', 'radio');
|
|
41
|
+
star.setAttribute('aria-label', i + ' star' + (i > 1 ? 's' : ''));
|
|
42
|
+
star.setAttribute('aria-checked', i <= currentValue ? 'true' : 'false');
|
|
43
|
+
if (readonly) star.tabIndex = -1;
|
|
44
|
+
stars.push(star);
|
|
45
|
+
el.appendChild(star);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Value display
|
|
49
|
+
const valueDisplay = document.createElement('span');
|
|
50
|
+
valueDisplay.className = 'vd-rating-value';
|
|
51
|
+
valueDisplay.textContent = currentValue > 0 ? currentValue.toString() : '';
|
|
52
|
+
el.appendChild(valueDisplay);
|
|
53
|
+
|
|
54
|
+
const updateStars = (value) => {
|
|
55
|
+
stars.forEach((star, i) => {
|
|
56
|
+
star.classList.remove('is-active', 'is-half');
|
|
57
|
+
const starNum = i + 1;
|
|
58
|
+
if (starNum <= Math.floor(value)) {
|
|
59
|
+
star.classList.add('is-active');
|
|
60
|
+
} else if (starNum - 0.5 <= value) {
|
|
61
|
+
star.classList.add('is-half');
|
|
62
|
+
}
|
|
63
|
+
star.setAttribute('aria-checked', starNum <= value ? 'true' : 'false');
|
|
64
|
+
});
|
|
65
|
+
valueDisplay.textContent = value > 0 ? value.toString() : '';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
updateStars(currentValue);
|
|
69
|
+
|
|
70
|
+
if (!readonly) {
|
|
71
|
+
stars.forEach((star, i) => {
|
|
72
|
+
const enterHandler = () => {
|
|
73
|
+
stars.forEach((s, j) => {
|
|
74
|
+
s.classList.toggle('is-hovered', j <= i);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const leaveHandler = () => {
|
|
78
|
+
stars.forEach(s => s.classList.remove('is-hovered'));
|
|
79
|
+
};
|
|
80
|
+
const clickHandler = () => {
|
|
81
|
+
currentValue = i + 1;
|
|
82
|
+
el.setAttribute('data-vd-rating-value', currentValue);
|
|
83
|
+
updateStars(currentValue);
|
|
84
|
+
el.dispatchEvent(new CustomEvent('rating:change', {
|
|
85
|
+
detail: { value: currentValue, max },
|
|
86
|
+
bubbles: true
|
|
87
|
+
}));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
star.addEventListener('mouseenter', enterHandler);
|
|
91
|
+
star.addEventListener('mouseleave', leaveHandler);
|
|
92
|
+
star.addEventListener('click', clickHandler);
|
|
93
|
+
|
|
94
|
+
cleanup.push(
|
|
95
|
+
() => star.removeEventListener('mouseenter', enterHandler),
|
|
96
|
+
() => star.removeEventListener('mouseleave', leaveHandler),
|
|
97
|
+
() => star.removeEventListener('click', clickHandler)
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Keyboard
|
|
102
|
+
const keyHandler = (e) => {
|
|
103
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
if (currentValue < max) {
|
|
106
|
+
currentValue++;
|
|
107
|
+
updateStars(currentValue);
|
|
108
|
+
stars[currentValue - 1].focus();
|
|
109
|
+
el.dispatchEvent(new CustomEvent('rating:change', { detail: { value: currentValue, max }, bubbles: true }));
|
|
110
|
+
}
|
|
111
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
if (currentValue > 1) {
|
|
114
|
+
currentValue--;
|
|
115
|
+
updateStars(currentValue);
|
|
116
|
+
stars[currentValue - 1].focus();
|
|
117
|
+
el.dispatchEvent(new CustomEvent('rating:change', { detail: { value: currentValue, max }, bubbles: true }));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
el.addEventListener('keydown', keyHandler);
|
|
122
|
+
cleanup.push(() => el.removeEventListener('keydown', keyHandler));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.instances.set(el, {
|
|
126
|
+
cleanup,
|
|
127
|
+
getValue: () => currentValue,
|
|
128
|
+
setValue: (v) => { currentValue = v; updateStars(v); }
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
getValue: function (el) {
|
|
133
|
+
const inst = this.instances.get(el);
|
|
134
|
+
return inst ? inst.getValue() : 0;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
setValue: function (el, value) {
|
|
138
|
+
const inst = this.instances.get(el);
|
|
139
|
+
if (inst) inst.setValue(value);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
destroy: function (el) {
|
|
143
|
+
const inst = this.instances.get(el);
|
|
144
|
+
if (!inst) return;
|
|
145
|
+
inst.cleanup.forEach(fn => fn());
|
|
146
|
+
this.instances.delete(el);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
destroyAll: function () {
|
|
150
|
+
this.instances.forEach((_, el) => this.destroy(el));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
155
|
+
window.Vanduo.register('rating', Rating);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
window.VanduoRating = Rating;
|
|
159
|
+
|
|
160
|
+
})();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Ripple (Waves Effect) Component
|
|
3
|
+
* Adds expanding circle animation on click at pointer position
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Ripple = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const elements = document.querySelectorAll('.vd-ripple, [data-vd-ripple]');
|
|
14
|
+
elements.forEach(el => {
|
|
15
|
+
if (this.instances.has(el)) return;
|
|
16
|
+
this.initInstance(el);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (el) {
|
|
21
|
+
const cleanup = [];
|
|
22
|
+
|
|
23
|
+
const createWave = (e) => {
|
|
24
|
+
const rect = el.getBoundingClientRect();
|
|
25
|
+
const size = Math.max(rect.width, rect.height);
|
|
26
|
+
const x = (e.clientX || (e.touches && e.touches[0].clientX) || rect.left + rect.width / 2) - rect.left - size / 2;
|
|
27
|
+
const y = (e.clientY || (e.touches && e.touches[0].clientY) || rect.top + rect.height / 2) - rect.top - size / 2;
|
|
28
|
+
|
|
29
|
+
const wave = document.createElement('span');
|
|
30
|
+
wave.className = 'vd-ripple-wave';
|
|
31
|
+
wave.style.width = size + 'px';
|
|
32
|
+
wave.style.height = size + 'px';
|
|
33
|
+
wave.style.left = x + 'px';
|
|
34
|
+
wave.style.top = y + 'px';
|
|
35
|
+
|
|
36
|
+
el.appendChild(wave);
|
|
37
|
+
|
|
38
|
+
wave.addEventListener('animationend', () => {
|
|
39
|
+
if (wave.parentNode) wave.parentNode.removeChild(wave);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
el.addEventListener('mousedown', createWave);
|
|
44
|
+
el.addEventListener('touchstart', createWave, { passive: true });
|
|
45
|
+
|
|
46
|
+
cleanup.push(
|
|
47
|
+
() => el.removeEventListener('mousedown', createWave),
|
|
48
|
+
() => el.removeEventListener('touchstart', createWave)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
this.instances.set(el, { cleanup });
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
destroy: function (el) {
|
|
55
|
+
const instance = this.instances.get(el);
|
|
56
|
+
if (!instance) return;
|
|
57
|
+
instance.cleanup.forEach(fn => fn());
|
|
58
|
+
// Remove any lingering wave elements
|
|
59
|
+
el.querySelectorAll('.vd-ripple-wave').forEach(w => w.remove());
|
|
60
|
+
this.instances.delete(el);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
destroyAll: function () {
|
|
64
|
+
this.instances.forEach((_, el) => this.destroy(el));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
69
|
+
window.Vanduo.register('ripple', Ripple);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
window.VanduoRipple = Ripple;
|
|
73
|
+
|
|
74
|
+
})();
|
package/js/components/sidenav.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* Initialize sidenav components
|
|
33
33
|
*/
|
|
34
34
|
init: function() {
|
|
35
|
-
const sidenavs = document.querySelectorAll('.vd-sidenav');
|
|
35
|
+
const sidenavs = document.querySelectorAll('.vd-sidenav, .vd-offcanvas');
|
|
36
36
|
|
|
37
37
|
sidenavs.forEach(sidenav => {
|
|
38
38
|
if (this.sidenavs.has(sidenav)) {
|
|
@@ -73,8 +73,15 @@
|
|
|
73
73
|
* @param {HTMLElement} sidenav - Sidenav element
|
|
74
74
|
*/
|
|
75
75
|
initSidenav: function(sidenav) {
|
|
76
|
+
// Apply data-vd-position direction class if specified
|
|
77
|
+
const position = sidenav.getAttribute('data-vd-position');
|
|
78
|
+
if (position) {
|
|
79
|
+
const prefix = sidenav.classList.contains('vd-offcanvas') ? 'vd-offcanvas' : 'vd-sidenav';
|
|
80
|
+
sidenav.classList.add(prefix + '-' + position);
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
const overlay = this.createOverlay(sidenav);
|
|
77
|
-
const closeButton = sidenav.querySelector('.vd-sidenav-close');
|
|
84
|
+
const closeButton = sidenav.querySelector('.vd-sidenav-close, .vd-offcanvas-close');
|
|
78
85
|
const cleanupFunctions = [];
|
|
79
86
|
|
|
80
87
|
// Set ARIA attributes
|