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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Sidenav Component
|
|
3
|
+
* JavaScript functionality for side navigation drawer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sidenav Component
|
|
11
|
+
*/
|
|
12
|
+
const Sidenav = {
|
|
13
|
+
sidenavs: new Map(),
|
|
14
|
+
breakpoint: 992, // Desktop breakpoint
|
|
15
|
+
|
|
16
|
+
// Global cleanup functions (toggles, resize)
|
|
17
|
+
_globalCleanups: [],
|
|
18
|
+
|
|
19
|
+
isFixedVariant: function(sidenav) {
|
|
20
|
+
return sidenav.classList.contains('vd-sidenav-fixed') || sidenav.classList.contains('sidenav-fixed');
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
isPushVariant: function(sidenav) {
|
|
24
|
+
return sidenav.classList.contains('vd-sidenav-push') || sidenav.classList.contains('sidenav-push');
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
isRightVariant: function(sidenav) {
|
|
28
|
+
return sidenav.classList.contains('vd-sidenav-right') || sidenav.classList.contains('sidenav-right');
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize sidenav components
|
|
33
|
+
*/
|
|
34
|
+
init: function() {
|
|
35
|
+
const sidenavs = document.querySelectorAll('.vd-sidenav');
|
|
36
|
+
|
|
37
|
+
sidenavs.forEach(sidenav => {
|
|
38
|
+
if (this.sidenavs.has(sidenav)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.initSidenav(sidenav);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Handle toggle buttons
|
|
45
|
+
const toggles = document.querySelectorAll('[data-sidenav-toggle]');
|
|
46
|
+
toggles.forEach(toggle => {
|
|
47
|
+
if (toggle.dataset.sidenavToggleInitialized) return;
|
|
48
|
+
toggle.dataset.sidenavToggleInitialized = 'true';
|
|
49
|
+
|
|
50
|
+
const toggleClickHandler = (e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
const targetId = toggle.dataset.sidenavToggle;
|
|
53
|
+
const sidenav = document.querySelector(targetId);
|
|
54
|
+
if (sidenav) {
|
|
55
|
+
this.toggle(sidenav);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
toggle.addEventListener('click', toggleClickHandler);
|
|
59
|
+
this._globalCleanups.push(() => toggle.removeEventListener('click', toggleClickHandler));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Handle responsive behavior
|
|
63
|
+
this.handleResize();
|
|
64
|
+
const resizeHandler = () => {
|
|
65
|
+
this.handleResize();
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener('resize', resizeHandler);
|
|
68
|
+
this._globalCleanups.push(() => window.removeEventListener('resize', resizeHandler));
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize a single sidenav
|
|
73
|
+
* @param {HTMLElement} sidenav - Sidenav element
|
|
74
|
+
*/
|
|
75
|
+
initSidenav: function(sidenav) {
|
|
76
|
+
const overlay = this.createOverlay(sidenav);
|
|
77
|
+
const closeButton = sidenav.querySelector('.vd-sidenav-close');
|
|
78
|
+
const cleanupFunctions = [];
|
|
79
|
+
|
|
80
|
+
// Set ARIA attributes
|
|
81
|
+
sidenav.setAttribute('role', 'navigation');
|
|
82
|
+
sidenav.setAttribute('aria-hidden', 'true');
|
|
83
|
+
|
|
84
|
+
// Close button handler
|
|
85
|
+
if (closeButton) {
|
|
86
|
+
const closeHandler = () => {
|
|
87
|
+
this.close(sidenav);
|
|
88
|
+
};
|
|
89
|
+
closeButton.addEventListener('click', closeHandler);
|
|
90
|
+
cleanupFunctions.push(() => closeButton.removeEventListener('click', closeHandler));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Overlay click handler
|
|
94
|
+
const overlayClickHandler = () => {
|
|
95
|
+
if (sidenav.dataset.backdrop !== 'static') {
|
|
96
|
+
this.close(sidenav);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
overlay.addEventListener('click', overlayClickHandler);
|
|
100
|
+
cleanupFunctions.push(() => overlay.removeEventListener('click', overlayClickHandler));
|
|
101
|
+
|
|
102
|
+
// ESC key handler
|
|
103
|
+
const escKeyHandler = (e) => {
|
|
104
|
+
if (e.key === 'Escape' && sidenav.classList.contains('is-open')) {
|
|
105
|
+
if (sidenav.dataset.keyboard !== 'false') {
|
|
106
|
+
this.close(sidenav);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
document.addEventListener('keydown', escKeyHandler);
|
|
111
|
+
cleanupFunctions.push(() => document.removeEventListener('keydown', escKeyHandler));
|
|
112
|
+
|
|
113
|
+
this.sidenavs.set(sidenav, { overlay, cleanup: cleanupFunctions });
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create overlay element
|
|
118
|
+
* @param {HTMLElement} sidenav - Sidenav element
|
|
119
|
+
* @returns {HTMLElement} Overlay element
|
|
120
|
+
*/
|
|
121
|
+
createOverlay: function(sidenav) {
|
|
122
|
+
let overlay = sidenav.querySelector('.vd-sidenav-overlay');
|
|
123
|
+
|
|
124
|
+
if (!overlay) {
|
|
125
|
+
overlay = document.createElement('div');
|
|
126
|
+
overlay.className = 'vd-sidenav-overlay';
|
|
127
|
+
document.body.appendChild(overlay);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return overlay;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Open sidenav
|
|
135
|
+
* @param {HTMLElement|string} sidenav - Sidenav element or selector
|
|
136
|
+
*/
|
|
137
|
+
open: function(sidenav) {
|
|
138
|
+
const el = typeof sidenav === 'string' ? document.querySelector(sidenav) : sidenav;
|
|
139
|
+
|
|
140
|
+
if (!el || !this.sidenavs.has(el)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { overlay } = this.sidenavs.get(el);
|
|
145
|
+
|
|
146
|
+
// Show overlay (if not fixed)
|
|
147
|
+
if (!this.isFixedVariant(el)) {
|
|
148
|
+
overlay.classList.add('is-visible');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Open sidenav
|
|
152
|
+
el.classList.add('is-open');
|
|
153
|
+
el.setAttribute('aria-hidden', 'false');
|
|
154
|
+
|
|
155
|
+
// Lock body scroll
|
|
156
|
+
document.body.classList.add('body-sidenav-open');
|
|
157
|
+
|
|
158
|
+
// Handle push variant
|
|
159
|
+
if (this.isPushVariant(el)) {
|
|
160
|
+
this.handlePushVariant(el, true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Dispatch event
|
|
164
|
+
el.dispatchEvent(new CustomEvent('sidenav:open', { bubbles: true }));
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Close sidenav
|
|
169
|
+
* @param {HTMLElement|string} sidenav - Sidenav element or selector
|
|
170
|
+
*/
|
|
171
|
+
close: function(sidenav) {
|
|
172
|
+
const el = typeof sidenav === 'string' ? document.querySelector(sidenav) : sidenav;
|
|
173
|
+
|
|
174
|
+
if (!el || !this.sidenavs.has(el)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { overlay } = this.sidenavs.get(el);
|
|
179
|
+
|
|
180
|
+
// Hide overlay
|
|
181
|
+
overlay.classList.remove('is-visible');
|
|
182
|
+
|
|
183
|
+
// Close sidenav
|
|
184
|
+
el.classList.remove('is-open');
|
|
185
|
+
el.setAttribute('aria-hidden', 'true');
|
|
186
|
+
|
|
187
|
+
// Unlock body scroll
|
|
188
|
+
document.body.classList.remove('body-sidenav-open');
|
|
189
|
+
|
|
190
|
+
// Handle push variant
|
|
191
|
+
if (this.isPushVariant(el)) {
|
|
192
|
+
this.handlePushVariant(el, false);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Dispatch event
|
|
196
|
+
el.dispatchEvent(new CustomEvent('sidenav:close', { bubbles: true }));
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Toggle sidenav
|
|
201
|
+
* @param {HTMLElement|string} sidenav - Sidenav element or selector
|
|
202
|
+
*/
|
|
203
|
+
toggle: function(sidenav) {
|
|
204
|
+
const el = typeof sidenav === 'string' ? document.querySelector(sidenav) : sidenav;
|
|
205
|
+
if (el) {
|
|
206
|
+
if (el.classList.contains('is-open')) {
|
|
207
|
+
this.close(el);
|
|
208
|
+
} else {
|
|
209
|
+
this.open(el);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Handle push variant
|
|
216
|
+
* @param {HTMLElement} sidenav - Sidenav element
|
|
217
|
+
* @param {boolean} isOpen - Whether sidenav is open
|
|
218
|
+
*/
|
|
219
|
+
handlePushVariant: function(sidenav, isOpen) {
|
|
220
|
+
// Find the main content wrapper
|
|
221
|
+
const content = document.querySelector('main, .main-content, .content, [role="main"]') || document.body;
|
|
222
|
+
|
|
223
|
+
if (isOpen) {
|
|
224
|
+
if (window.innerWidth >= this.breakpoint) {
|
|
225
|
+
if (this.isRightVariant(sidenav)) {
|
|
226
|
+
content.style.marginRight = sidenav.offsetWidth + 'px';
|
|
227
|
+
} else {
|
|
228
|
+
content.style.marginLeft = sidenav.offsetWidth + 'px';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
content.style.marginLeft = '';
|
|
233
|
+
content.style.marginRight = '';
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Handle window resize
|
|
239
|
+
*/
|
|
240
|
+
handleResize: function() {
|
|
241
|
+
this.sidenavs.forEach(({ overlay }, sidenav) => {
|
|
242
|
+
// Close overlay sidenavs on resize to desktop if they're open
|
|
243
|
+
if (window.innerWidth >= this.breakpoint) {
|
|
244
|
+
if (this.isFixedVariant(sidenav) && !sidenav.classList.contains('is-open')) {
|
|
245
|
+
// Fixed sidenavs should be visible on desktop
|
|
246
|
+
sidenav.classList.add('is-open');
|
|
247
|
+
sidenav.setAttribute('aria-hidden', 'false');
|
|
248
|
+
overlay.classList.remove('is-visible');
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// On mobile, fixed sidenavs become overlay
|
|
252
|
+
if (this.isFixedVariant(sidenav) && sidenav.classList.contains('is-open')) {
|
|
253
|
+
this.close(sidenav);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Destroy a sidenav instance and clean up event listeners
|
|
261
|
+
* @param {HTMLElement} sidenav - Sidenav element
|
|
262
|
+
*/
|
|
263
|
+
destroy: function(sidenav) {
|
|
264
|
+
const data = this.sidenavs.get(sidenav);
|
|
265
|
+
if (!data) return;
|
|
266
|
+
|
|
267
|
+
// Close if open
|
|
268
|
+
if (sidenav.classList.contains('is-open')) {
|
|
269
|
+
this.close(sidenav);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
data.cleanup.forEach(fn => fn());
|
|
273
|
+
|
|
274
|
+
// Remove created overlay
|
|
275
|
+
if (data.overlay && data.overlay.parentNode) {
|
|
276
|
+
data.overlay.parentNode.removeChild(data.overlay);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.sidenavs.delete(sidenav);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Destroy all sidenav instances
|
|
284
|
+
*/
|
|
285
|
+
destroyAll: function() {
|
|
286
|
+
this.sidenavs.forEach((data, sidenav) => {
|
|
287
|
+
this.destroy(sidenav);
|
|
288
|
+
});
|
|
289
|
+
this._globalCleanups.forEach(fn => fn());
|
|
290
|
+
this._globalCleanups = [];
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Register with Vanduo framework if available
|
|
295
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
296
|
+
window.Vanduo.register('sidenav', Sidenav);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Expose globally
|
|
300
|
+
window.VanduoSidenav = Sidenav;
|
|
301
|
+
|
|
302
|
+
})();
|
|
303
|
+
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Tabs Component
|
|
3
|
+
* Tabbed content navigation with keyboard support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tabs Component
|
|
11
|
+
*/
|
|
12
|
+
const Tabs = {
|
|
13
|
+
// Store initialized tab containers and their cleanup functions
|
|
14
|
+
instances: new Map(),
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize all tab components
|
|
18
|
+
*/
|
|
19
|
+
init: function() {
|
|
20
|
+
const tabContainers = document.querySelectorAll('.vd-tabs, [data-tabs]');
|
|
21
|
+
|
|
22
|
+
tabContainers.forEach(container => {
|
|
23
|
+
if (this.instances.has(container)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.initTabs(container);
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize a single tab container
|
|
32
|
+
* @param {HTMLElement} container - Tabs container element
|
|
33
|
+
*/
|
|
34
|
+
initTabs: function(container) {
|
|
35
|
+
const tabList = container.querySelector('.vd-tab-list, [role="tablist"]');
|
|
36
|
+
const tabLinks = container.querySelectorAll('.vd-tab-link, [data-tab]');
|
|
37
|
+
const tabPanes = container.querySelectorAll('.vd-tab-pane, [data-tab-pane]');
|
|
38
|
+
|
|
39
|
+
if (!tabList || tabLinks.length === 0) return;
|
|
40
|
+
|
|
41
|
+
const cleanupFunctions = [];
|
|
42
|
+
|
|
43
|
+
// Set up ARIA attributes
|
|
44
|
+
tabList.setAttribute('role', 'tablist');
|
|
45
|
+
|
|
46
|
+
tabLinks.forEach((link, index) => {
|
|
47
|
+
const tabId = link.dataset.tab || link.getAttribute('href')?.replace('#', '') || `tab-${index}`;
|
|
48
|
+
const pane = this.findPane(container, tabId, tabPanes);
|
|
49
|
+
|
|
50
|
+
// Set up tab attributes
|
|
51
|
+
link.setAttribute('role', 'tab');
|
|
52
|
+
link.setAttribute('aria-selected', link.classList.contains('is-active') ? 'true' : 'false');
|
|
53
|
+
link.setAttribute('tabindex', link.classList.contains('is-active') ? '0' : '-1');
|
|
54
|
+
|
|
55
|
+
if (!link.id) {
|
|
56
|
+
link.id = `tab-btn-${tabId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Set up pane attributes
|
|
60
|
+
if (pane) {
|
|
61
|
+
pane.setAttribute('role', 'tabpanel');
|
|
62
|
+
pane.setAttribute('aria-labelledby', link.id);
|
|
63
|
+
if (!pane.id) {
|
|
64
|
+
pane.id = `tab-pane-${tabId}`;
|
|
65
|
+
}
|
|
66
|
+
link.setAttribute('aria-controls', pane.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Click handler
|
|
70
|
+
const clickHandler = (e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
if (!link.classList.contains('disabled') && !link.disabled) {
|
|
73
|
+
this.activateTab(container, link, tabLinks, tabPanes);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
link.addEventListener('click', clickHandler);
|
|
77
|
+
cleanupFunctions.push(() => link.removeEventListener('click', clickHandler));
|
|
78
|
+
|
|
79
|
+
// Keyboard navigation
|
|
80
|
+
const keydownHandler = (e) => {
|
|
81
|
+
this.handleKeydown(e, container, link, tabLinks, tabPanes);
|
|
82
|
+
};
|
|
83
|
+
link.addEventListener('keydown', keydownHandler);
|
|
84
|
+
cleanupFunctions.push(() => link.removeEventListener('keydown', keydownHandler));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Ensure one tab is active
|
|
88
|
+
const activeTab = container.querySelector('.vd-tab-link.is-active, [data-tab].is-active');
|
|
89
|
+
if (!activeTab && tabLinks.length > 0) {
|
|
90
|
+
this.activateTab(container, tabLinks[0], tabLinks, tabPanes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.instances.set(container, { cleanup: cleanupFunctions });
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find the pane associated with a tab
|
|
98
|
+
* @param {HTMLElement} container - Tabs container
|
|
99
|
+
* @param {string} tabId - Tab identifier
|
|
100
|
+
* @param {NodeList} tabPanes - All tab panes
|
|
101
|
+
* @returns {HTMLElement|null} The matching pane
|
|
102
|
+
*/
|
|
103
|
+
findPane: function(container, tabId, tabPanes) {
|
|
104
|
+
// Try data attribute first
|
|
105
|
+
let pane = container.querySelector(`[data-tab-pane="${tabId}"]`);
|
|
106
|
+
|
|
107
|
+
// Try ID
|
|
108
|
+
if (!pane) {
|
|
109
|
+
pane = container.querySelector(`#${tabId}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Try matching by index
|
|
113
|
+
if (!pane) {
|
|
114
|
+
const tabLinks = container.querySelectorAll('.vd-tab-link, [data-tab]');
|
|
115
|
+
tabLinks.forEach((link, index) => {
|
|
116
|
+
const linkTabId = link.dataset.tab || link.getAttribute('href')?.replace('#', '');
|
|
117
|
+
if (linkTabId === tabId && tabPanes[index]) {
|
|
118
|
+
pane = tabPanes[index];
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return pane;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Activate a tab
|
|
128
|
+
* @param {HTMLElement} container - Tabs container
|
|
129
|
+
* @param {HTMLElement} tab - Tab to activate
|
|
130
|
+
* @param {NodeList} allTabs - All tab links
|
|
131
|
+
* @param {NodeList} allPanes - All tab panes
|
|
132
|
+
*/
|
|
133
|
+
activateTab: function(container, tab, allTabs, allPanes) {
|
|
134
|
+
const tabId = tab.dataset.tab || tab.getAttribute('href')?.replace('#', '') || tab.id;
|
|
135
|
+
|
|
136
|
+
// Deactivate all tabs
|
|
137
|
+
allTabs.forEach(t => {
|
|
138
|
+
t.classList.remove('is-active');
|
|
139
|
+
t.setAttribute('aria-selected', 'false');
|
|
140
|
+
t.setAttribute('tabindex', '-1');
|
|
141
|
+
|
|
142
|
+
// Also handle parent li if exists
|
|
143
|
+
if (t.parentElement && t.parentElement.classList.contains('tab-item')) {
|
|
144
|
+
t.parentElement.classList.remove('is-active');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Hide all panes
|
|
149
|
+
allPanes.forEach(p => {
|
|
150
|
+
p.classList.remove('is-active');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Activate selected tab
|
|
154
|
+
tab.classList.add('is-active');
|
|
155
|
+
tab.setAttribute('aria-selected', 'true');
|
|
156
|
+
tab.setAttribute('tabindex', '0');
|
|
157
|
+
|
|
158
|
+
// Also handle parent li if exists
|
|
159
|
+
if (tab.parentElement && tab.parentElement.classList.contains('tab-item')) {
|
|
160
|
+
tab.parentElement.classList.add('is-active');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Show corresponding pane
|
|
164
|
+
const pane = this.findPane(container, tabId, allPanes);
|
|
165
|
+
if (pane) {
|
|
166
|
+
pane.classList.add('is-active');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Dispatch custom event
|
|
170
|
+
const event = new CustomEvent('tab:change', {
|
|
171
|
+
bubbles: true,
|
|
172
|
+
detail: {
|
|
173
|
+
tab: tab,
|
|
174
|
+
pane: pane,
|
|
175
|
+
tabId: tabId
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
container.dispatchEvent(event);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Handle keyboard navigation
|
|
183
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
184
|
+
* @param {HTMLElement} container - Tabs container
|
|
185
|
+
* @param {HTMLElement} currentTab - Currently focused tab
|
|
186
|
+
* @param {NodeList} allTabs - All tab links
|
|
187
|
+
* @param {NodeList} allPanes - All tab panes
|
|
188
|
+
*/
|
|
189
|
+
handleKeydown: function(e, container, currentTab, allTabs, allPanes) {
|
|
190
|
+
const isVertical = container.classList.contains('tabs-vertical');
|
|
191
|
+
const tabs = Array.from(allTabs).filter(t => !t.classList.contains('disabled') && !t.disabled);
|
|
192
|
+
const currentIndex = tabs.indexOf(currentTab);
|
|
193
|
+
|
|
194
|
+
let newIndex = currentIndex;
|
|
195
|
+
|
|
196
|
+
switch (e.key) {
|
|
197
|
+
case 'ArrowLeft':
|
|
198
|
+
if (!isVertical) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'ArrowRight':
|
|
205
|
+
if (!isVertical) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
case 'ArrowUp':
|
|
212
|
+
if (isVertical) {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'ArrowDown':
|
|
219
|
+
if (isVertical) {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'Home':
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
newIndex = 0;
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case 'End':
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
newIndex = tabs.length - 1;
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case 'Enter':
|
|
236
|
+
case ' ':
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
this.activateTab(container, currentTab, allTabs, allPanes);
|
|
239
|
+
return;
|
|
240
|
+
|
|
241
|
+
default:
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Focus and activate new tab
|
|
246
|
+
if (newIndex !== currentIndex) {
|
|
247
|
+
tabs[newIndex].focus();
|
|
248
|
+
this.activateTab(container, tabs[newIndex], allTabs, allPanes);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Programmatically show a tab
|
|
254
|
+
* @param {string|HTMLElement} tab - Tab identifier or element
|
|
255
|
+
*/
|
|
256
|
+
show: function(tab) {
|
|
257
|
+
let tabElement;
|
|
258
|
+
|
|
259
|
+
if (typeof tab === 'string') {
|
|
260
|
+
tabElement = document.querySelector(`[data-tab="${tab}"], [href="#${tab}"]`);
|
|
261
|
+
} else {
|
|
262
|
+
tabElement = tab;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!tabElement) return;
|
|
266
|
+
|
|
267
|
+
const container = tabElement.closest('.vd-tabs, [data-tabs]');
|
|
268
|
+
if (!container) return;
|
|
269
|
+
|
|
270
|
+
const allTabs = container.querySelectorAll('.vd-tab-link, [data-tab]');
|
|
271
|
+
const allPanes = container.querySelectorAll('.vd-tab-pane, [data-tab-pane]');
|
|
272
|
+
|
|
273
|
+
this.activateTab(container, tabElement, allTabs, allPanes);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Destroy a tabs instance and clean up event listeners
|
|
278
|
+
* @param {HTMLElement} container - Tabs container
|
|
279
|
+
*/
|
|
280
|
+
destroy: function(container) {
|
|
281
|
+
const instance = this.instances.get(container);
|
|
282
|
+
if (!instance) return;
|
|
283
|
+
|
|
284
|
+
instance.cleanup.forEach(fn => fn());
|
|
285
|
+
this.instances.delete(container);
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Destroy all tabs instances
|
|
290
|
+
*/
|
|
291
|
+
destroyAll: function() {
|
|
292
|
+
this.instances.forEach((instance, container) => {
|
|
293
|
+
this.destroy(container);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Register with Vanduo framework if available
|
|
299
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
300
|
+
window.Vanduo.register('tabs', Tabs);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
})();
|