vanduo-framework 1.1.8-docs-update
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 +216 -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,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Modals Component
|
|
3
|
+
* JavaScript functionality for modal dialogs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Modals Component
|
|
11
|
+
*/
|
|
12
|
+
const Modals = {
|
|
13
|
+
modals: new Map(),
|
|
14
|
+
openModals: [],
|
|
15
|
+
zIndexCounter: 1050,
|
|
16
|
+
|
|
17
|
+
// Store trigger cleanup functions
|
|
18
|
+
_triggerCleanups: [],
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize modals
|
|
22
|
+
*/
|
|
23
|
+
init: function () {
|
|
24
|
+
const modals = document.querySelectorAll('.vd-modal');
|
|
25
|
+
|
|
26
|
+
modals.forEach(modal => {
|
|
27
|
+
if (this.modals.has(modal)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.initModal(modal);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Handle data-modal triggers
|
|
34
|
+
const triggers = document.querySelectorAll('[data-modal]');
|
|
35
|
+
triggers.forEach(trigger => {
|
|
36
|
+
if (trigger.dataset.modalTriggerInitialized) return;
|
|
37
|
+
trigger.dataset.modalTriggerInitialized = 'true';
|
|
38
|
+
|
|
39
|
+
const triggerClickHandler = (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
const modalId = trigger.dataset.modal;
|
|
42
|
+
const modal = document.querySelector(modalId);
|
|
43
|
+
if (modal) {
|
|
44
|
+
this.open(modal);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
trigger.addEventListener('click', triggerClickHandler);
|
|
48
|
+
this._triggerCleanups.push(() => trigger.removeEventListener('click', triggerClickHandler));
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Initialize a single modal
|
|
54
|
+
* @param {HTMLElement} modal - Modal element
|
|
55
|
+
*/
|
|
56
|
+
initModal: function (modal) {
|
|
57
|
+
const backdrop = this.createBackdrop(modal);
|
|
58
|
+
const closeButtons = modal.querySelectorAll('.vd-modal-close, [data-dismiss="modal"]');
|
|
59
|
+
const dialog = modal.querySelector('.vd-modal-dialog');
|
|
60
|
+
|
|
61
|
+
if (!dialog) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cleanupFunctions = [];
|
|
66
|
+
|
|
67
|
+
// Set ARIA attributes
|
|
68
|
+
modal.setAttribute('role', 'dialog');
|
|
69
|
+
modal.setAttribute('aria-modal', 'true');
|
|
70
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
71
|
+
|
|
72
|
+
// Generate ID if not exists
|
|
73
|
+
if (!modal.id) {
|
|
74
|
+
modal.id = 'modal-' + Math.random().toString(36).substr(2, 9);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Set aria-labelledby
|
|
78
|
+
const title = modal.querySelector('.vd-modal-title');
|
|
79
|
+
if (title && !title.id) {
|
|
80
|
+
title.id = modal.id + '-title';
|
|
81
|
+
modal.setAttribute('aria-labelledby', title.id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Close button handlers
|
|
85
|
+
closeButtons.forEach(button => {
|
|
86
|
+
const closeHandler = () => {
|
|
87
|
+
this.close(modal);
|
|
88
|
+
};
|
|
89
|
+
button.addEventListener('click', closeHandler);
|
|
90
|
+
cleanupFunctions.push(() => button.removeEventListener('click', closeHandler));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Backdrop click handler
|
|
94
|
+
const backdropClickHandler = (e) => {
|
|
95
|
+
if (e.target === backdrop && modal.dataset.backdrop !== 'static') {
|
|
96
|
+
this.close(modal);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
backdrop.addEventListener('click', backdropClickHandler);
|
|
100
|
+
cleanupFunctions.push(() => backdrop.removeEventListener('click', backdropClickHandler));
|
|
101
|
+
|
|
102
|
+
// ESC key handler
|
|
103
|
+
const escKeyHandler = (e) => {
|
|
104
|
+
if (e.key === 'Escape' && this.openModals.length > 0) {
|
|
105
|
+
const topModal = this.openModals[this.openModals.length - 1];
|
|
106
|
+
if (topModal === modal && topModal.dataset.keyboard !== 'false') {
|
|
107
|
+
this.close(topModal);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
document.addEventListener('keydown', escKeyHandler);
|
|
112
|
+
cleanupFunctions.push(() => document.removeEventListener('keydown', escKeyHandler));
|
|
113
|
+
|
|
114
|
+
this.modals.set(modal, { backdrop, dialog, trapHandler: null, cleanup: cleanupFunctions });
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create backdrop element
|
|
119
|
+
* @param {HTMLElement} modal - Modal element
|
|
120
|
+
* @returns {HTMLElement} Backdrop element
|
|
121
|
+
*/
|
|
122
|
+
createBackdrop: function (modal) {
|
|
123
|
+
let backdrop = modal.querySelector('.vd-modal-backdrop');
|
|
124
|
+
|
|
125
|
+
if (!backdrop) {
|
|
126
|
+
backdrop = document.createElement('div');
|
|
127
|
+
backdrop.className = 'vd-modal-backdrop';
|
|
128
|
+
document.body.appendChild(backdrop);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return backdrop;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Open modal
|
|
136
|
+
* @param {HTMLElement|string} modal - Modal element or selector
|
|
137
|
+
*/
|
|
138
|
+
open: function (modal) {
|
|
139
|
+
const el = typeof modal === 'string' ? document.querySelector(modal) : modal;
|
|
140
|
+
|
|
141
|
+
if (!el) {
|
|
142
|
+
console.warn('[Vanduo Modals] Modal element not found:', modal);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!this.modals.has(el)) {
|
|
147
|
+
console.warn('[Vanduo Modals] Modal not initialized:', el);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const modalData = this.modals.get(el);
|
|
152
|
+
const { backdrop, dialog: _dialog } = modalData;
|
|
153
|
+
|
|
154
|
+
// Increment z-index for stacking
|
|
155
|
+
this.zIndexCounter += 10;
|
|
156
|
+
el.style.zIndex = this.zIndexCounter;
|
|
157
|
+
backdrop.style.zIndex = this.zIndexCounter - 1;
|
|
158
|
+
|
|
159
|
+
// Add to open modals stack
|
|
160
|
+
this.openModals.push(el);
|
|
161
|
+
|
|
162
|
+
// Show backdrop
|
|
163
|
+
backdrop.classList.add('is-visible');
|
|
164
|
+
|
|
165
|
+
// Show modal
|
|
166
|
+
el.classList.add('is-open');
|
|
167
|
+
el.setAttribute('aria-hidden', 'false');
|
|
168
|
+
|
|
169
|
+
// Lock body scroll
|
|
170
|
+
if (this.openModals.length === 1) {
|
|
171
|
+
document.body.classList.add('body-modal-open');
|
|
172
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
173
|
+
if (scrollbarWidth > 0) {
|
|
174
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Focus trap (store handler for cleanup)
|
|
179
|
+
const trapHandler = this.trapFocus(el);
|
|
180
|
+
modalData.trapHandler = trapHandler;
|
|
181
|
+
|
|
182
|
+
// Auto-focus first focusable element
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
const firstFocusable = this.getFocusableElements(el)[0];
|
|
185
|
+
if (firstFocusable) {
|
|
186
|
+
firstFocusable.focus();
|
|
187
|
+
}
|
|
188
|
+
}, 100);
|
|
189
|
+
|
|
190
|
+
// Dispatch event
|
|
191
|
+
el.dispatchEvent(new CustomEvent('modal:open', { bubbles: true }));
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Close modal
|
|
196
|
+
* @param {HTMLElement|string} modal - Modal element or selector
|
|
197
|
+
*/
|
|
198
|
+
close: function (modal) {
|
|
199
|
+
const el = typeof modal === 'string' ? document.querySelector(modal) : modal;
|
|
200
|
+
|
|
201
|
+
if (!el) {
|
|
202
|
+
console.warn('[Vanduo Modals] Modal element not found:', modal);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!this.modals.has(el)) {
|
|
207
|
+
console.warn('[Vanduo Modals] Modal not initialized:', el);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const modalData = this.modals.get(el);
|
|
212
|
+
const { backdrop, trapHandler } = modalData;
|
|
213
|
+
|
|
214
|
+
// Remove focus trap event listener to prevent memory leak
|
|
215
|
+
if (trapHandler) {
|
|
216
|
+
el.removeEventListener('keydown', trapHandler);
|
|
217
|
+
modalData.trapHandler = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Remove from open modals stack
|
|
221
|
+
const index = this.openModals.indexOf(el);
|
|
222
|
+
if (index > -1) {
|
|
223
|
+
this.openModals.splice(index, 1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Hide modal
|
|
227
|
+
el.classList.remove('is-open');
|
|
228
|
+
el.setAttribute('aria-hidden', 'true');
|
|
229
|
+
|
|
230
|
+
// Hide backdrop if no other modals open
|
|
231
|
+
if (this.openModals.length === 0) {
|
|
232
|
+
backdrop.classList.remove('is-visible');
|
|
233
|
+
document.body.classList.remove('body-modal-open');
|
|
234
|
+
document.body.style.paddingRight = '';
|
|
235
|
+
// Reset z-index counter to prevent indefinite growth
|
|
236
|
+
this.zIndexCounter = 1050;
|
|
237
|
+
} else {
|
|
238
|
+
// Show backdrop for top modal
|
|
239
|
+
const topModal = this.openModals[this.openModals.length - 1];
|
|
240
|
+
const topBackdrop = this.modals.get(topModal).backdrop;
|
|
241
|
+
topBackdrop.classList.add('is-visible');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Return focus to trigger
|
|
245
|
+
const trigger = document.querySelector(`[data-modal="#${el.id}"]`);
|
|
246
|
+
if (trigger) {
|
|
247
|
+
trigger.focus();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Dispatch event
|
|
251
|
+
el.dispatchEvent(new CustomEvent('modal:close', { bubbles: true }));
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Trap focus within modal
|
|
256
|
+
* @param {HTMLElement} modal - Modal element
|
|
257
|
+
* @returns {Function} The trap handler function for cleanup
|
|
258
|
+
*/
|
|
259
|
+
trapFocus: function (modal) {
|
|
260
|
+
const self = this;
|
|
261
|
+
|
|
262
|
+
const trapHandler = function (e) {
|
|
263
|
+
if (e.key !== 'Tab') {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const focusableElements = self.getFocusableElements(modal);
|
|
268
|
+
const firstElement = focusableElements[0];
|
|
269
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
270
|
+
|
|
271
|
+
if (e.shiftKey) {
|
|
272
|
+
// Shift + Tab
|
|
273
|
+
if (document.activeElement === firstElement) {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
lastElement.focus();
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// Tab
|
|
279
|
+
if (document.activeElement === lastElement) {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
firstElement.focus();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
modal.addEventListener('keydown', trapHandler);
|
|
287
|
+
return trapHandler;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get focusable elements within modal
|
|
292
|
+
* @param {HTMLElement} modal - Modal element
|
|
293
|
+
* @returns {Array<HTMLElement>} Focusable elements
|
|
294
|
+
*/
|
|
295
|
+
getFocusableElements: function (modal) {
|
|
296
|
+
const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
297
|
+
return Array.from(modal.querySelectorAll(selector)).filter(el => {
|
|
298
|
+
return !el.hasAttribute('disabled') &&
|
|
299
|
+
el.offsetWidth > 0 &&
|
|
300
|
+
el.offsetHeight > 0;
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Toggle modal
|
|
306
|
+
* @param {HTMLElement|string} modal - Modal element or selector
|
|
307
|
+
*/
|
|
308
|
+
toggle: function (modal) {
|
|
309
|
+
const el = typeof modal === 'string' ? document.querySelector(modal) : modal;
|
|
310
|
+
if (el) {
|
|
311
|
+
if (el.classList.contains('is-open')) {
|
|
312
|
+
this.close(el);
|
|
313
|
+
} else {
|
|
314
|
+
this.open(el);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Destroy a modal instance and clean up event listeners
|
|
321
|
+
* @param {HTMLElement} modal - Modal element
|
|
322
|
+
*/
|
|
323
|
+
destroy: function (modal) {
|
|
324
|
+
const modalData = this.modals.get(modal);
|
|
325
|
+
if (!modalData) return;
|
|
326
|
+
|
|
327
|
+
// Close if open
|
|
328
|
+
if (modal.classList.contains('is-open')) {
|
|
329
|
+
this.close(modal);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Run all cleanup functions
|
|
333
|
+
if (modalData.cleanup) {
|
|
334
|
+
modalData.cleanup.forEach(fn => fn());
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove created backdrop
|
|
338
|
+
if (modalData.backdrop && modalData.backdrop.parentNode) {
|
|
339
|
+
modalData.backdrop.parentNode.removeChild(modalData.backdrop);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.modals.delete(modal);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Destroy all modal instances
|
|
347
|
+
*/
|
|
348
|
+
destroyAll: function () {
|
|
349
|
+
this.modals.forEach((data, modal) => {
|
|
350
|
+
this.destroy(modal);
|
|
351
|
+
});
|
|
352
|
+
// Clean up trigger listeners
|
|
353
|
+
this._triggerCleanups.forEach(fn => fn());
|
|
354
|
+
this._triggerCleanups = [];
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Register with Vanduo framework if available
|
|
359
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
360
|
+
window.Vanduo.register('modals', Modals);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Expose globally
|
|
364
|
+
window.VanduoModals = Modals;
|
|
365
|
+
|
|
366
|
+
})();
|
|
367
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Navbar Component
|
|
3
|
+
* JavaScript functionality for navbar mobile menu
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Navbar Component
|
|
11
|
+
*/
|
|
12
|
+
const Navbar = {
|
|
13
|
+
// Store initialized navbars and their cleanup functions
|
|
14
|
+
instances: new Map(),
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the breakpoint value from CSS variable or use fallback
|
|
18
|
+
* @returns {number} Breakpoint in pixels
|
|
19
|
+
*/
|
|
20
|
+
getBreakpoint: function () {
|
|
21
|
+
const root = getComputedStyle(document.documentElement);
|
|
22
|
+
const breakpointValue = root.getPropertyValue('--breakpoint-lg').trim();
|
|
23
|
+
|
|
24
|
+
// Parse the value (could be "992px" or just "992")
|
|
25
|
+
const parsed = parseInt(breakpointValue, 10);
|
|
26
|
+
return isNaN(parsed) ? 992 : parsed;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize navbar component
|
|
31
|
+
*/
|
|
32
|
+
init: function () {
|
|
33
|
+
const navbars = document.querySelectorAll('.vd-navbar');
|
|
34
|
+
|
|
35
|
+
navbars.forEach(navbar => {
|
|
36
|
+
// Skip if already initialized
|
|
37
|
+
if (this.instances.has(navbar)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.initNavbar(navbar);
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initialize a single navbar
|
|
46
|
+
* @param {HTMLElement} navbar - Navbar element
|
|
47
|
+
*/
|
|
48
|
+
initNavbar: function (navbar) {
|
|
49
|
+
const toggle = navbar.querySelector('.vd-navbar-toggle, .vd-navbar-burger');
|
|
50
|
+
const menu = navbar.querySelector('.vd-navbar-menu');
|
|
51
|
+
const overlay = navbar.querySelector('.vd-navbar-overlay') || this.createOverlay(navbar);
|
|
52
|
+
|
|
53
|
+
if (!toggle || !menu) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Store cleanup functions for this navbar instance
|
|
58
|
+
const cleanupFunctions = [];
|
|
59
|
+
|
|
60
|
+
// Toggle menu on button click
|
|
61
|
+
const toggleClickHandler = (e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
this.toggleMenu(navbar, toggle, menu, overlay);
|
|
65
|
+
};
|
|
66
|
+
toggle.addEventListener('click', toggleClickHandler);
|
|
67
|
+
cleanupFunctions.push(() => toggle.removeEventListener('click', toggleClickHandler));
|
|
68
|
+
|
|
69
|
+
// Close menu on overlay click
|
|
70
|
+
if (overlay) {
|
|
71
|
+
const overlayClickHandler = () => {
|
|
72
|
+
this.closeMenu(navbar, toggle, menu, overlay);
|
|
73
|
+
};
|
|
74
|
+
overlay.addEventListener('click', overlayClickHandler);
|
|
75
|
+
cleanupFunctions.push(() => overlay.removeEventListener('click', overlayClickHandler));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Close menu on escape key
|
|
79
|
+
const keydownHandler = (e) => {
|
|
80
|
+
if (e.key === 'Escape' && menu.classList.contains('is-open')) {
|
|
81
|
+
this.closeMenu(navbar, toggle, menu, overlay);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
document.addEventListener('keydown', keydownHandler);
|
|
85
|
+
cleanupFunctions.push(() => document.removeEventListener('keydown', keydownHandler));
|
|
86
|
+
|
|
87
|
+
// Close menu on window resize (if resizing to desktop)
|
|
88
|
+
let resizeTimer;
|
|
89
|
+
const resizeHandler = () => {
|
|
90
|
+
clearTimeout(resizeTimer);
|
|
91
|
+
resizeTimer = setTimeout(() => {
|
|
92
|
+
const breakpoint = this.getBreakpoint();
|
|
93
|
+
if (window.innerWidth >= breakpoint && menu.classList.contains('is-open')) {
|
|
94
|
+
this.closeMenu(navbar, toggle, menu, overlay);
|
|
95
|
+
}
|
|
96
|
+
}, 250);
|
|
97
|
+
};
|
|
98
|
+
window.addEventListener('resize', resizeHandler);
|
|
99
|
+
cleanupFunctions.push(() => {
|
|
100
|
+
clearTimeout(resizeTimer);
|
|
101
|
+
window.removeEventListener('resize', resizeHandler);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Close menu when clicking outside
|
|
105
|
+
const documentClickHandler = (e) => {
|
|
106
|
+
if (menu.classList.contains('is-open') &&
|
|
107
|
+
!navbar.contains(e.target) &&
|
|
108
|
+
!menu.contains(e.target)) {
|
|
109
|
+
this.closeMenu(navbar, toggle, menu, overlay);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener('click', documentClickHandler);
|
|
113
|
+
cleanupFunctions.push(() => document.removeEventListener('click', documentClickHandler));
|
|
114
|
+
|
|
115
|
+
// Handle dropdown toggles in mobile menu
|
|
116
|
+
const dropdownToggles = menu.querySelectorAll('.vd-navbar-dropdown > .vd-nav-link, .vd-navbar-dropdown > .nav-link');
|
|
117
|
+
dropdownToggles.forEach(dropdownToggle => {
|
|
118
|
+
const dropdownClickHandler = (e) => {
|
|
119
|
+
const breakpoint = this.getBreakpoint();
|
|
120
|
+
if (window.innerWidth < breakpoint) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
const dropdown = dropdownToggle.parentElement;
|
|
123
|
+
const dropdownMenu = dropdown.querySelector('.vd-navbar-dropdown-menu');
|
|
124
|
+
|
|
125
|
+
if (dropdownMenu) {
|
|
126
|
+
dropdownMenu.classList.toggle('is-open');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
dropdownToggle.addEventListener('click', dropdownClickHandler);
|
|
131
|
+
cleanupFunctions.push(() => dropdownToggle.removeEventListener('click', dropdownClickHandler));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Store instance with cleanup functions
|
|
135
|
+
this.instances.set(navbar, {
|
|
136
|
+
toggle,
|
|
137
|
+
menu,
|
|
138
|
+
overlay,
|
|
139
|
+
cleanup: cleanupFunctions
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Destroy a navbar instance and clean up event listeners
|
|
145
|
+
* @param {HTMLElement} navbar - Navbar element
|
|
146
|
+
*/
|
|
147
|
+
destroy: function (navbar) {
|
|
148
|
+
const instance = this.instances.get(navbar);
|
|
149
|
+
if (!instance) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Run all cleanup functions
|
|
154
|
+
instance.cleanup.forEach(fn => fn());
|
|
155
|
+
|
|
156
|
+
// Remove created overlay if it exists
|
|
157
|
+
if (instance.overlay && instance.overlay.parentNode) {
|
|
158
|
+
instance.overlay.parentNode.removeChild(instance.overlay);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Remove from instances map
|
|
162
|
+
this.instances.delete(navbar);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Destroy all navbar instances
|
|
167
|
+
*/
|
|
168
|
+
destroyAll: function () {
|
|
169
|
+
this.instances.forEach((instance, navbar) => {
|
|
170
|
+
this.destroy(navbar);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Toggle mobile menu
|
|
176
|
+
* @param {HTMLElement} navbar - Navbar element
|
|
177
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
178
|
+
* @param {HTMLElement} menu - Menu element
|
|
179
|
+
* @param {HTMLElement} overlay - Overlay element
|
|
180
|
+
*/
|
|
181
|
+
toggleMenu: function (navbar, toggle, menu, overlay) {
|
|
182
|
+
const isOpen = menu.classList.contains('is-open');
|
|
183
|
+
|
|
184
|
+
if (isOpen) {
|
|
185
|
+
this.closeMenu(navbar, toggle, menu, overlay);
|
|
186
|
+
} else {
|
|
187
|
+
this.openMenu(navbar, toggle, menu, overlay);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Open mobile menu
|
|
193
|
+
* @param {HTMLElement} navbar - Navbar element
|
|
194
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
195
|
+
* @param {HTMLElement} menu - Menu element
|
|
196
|
+
* @param {HTMLElement} overlay - Overlay element
|
|
197
|
+
*/
|
|
198
|
+
openMenu: function (navbar, toggle, menu, overlay) {
|
|
199
|
+
menu.classList.add('is-open');
|
|
200
|
+
toggle.classList.add('is-active');
|
|
201
|
+
|
|
202
|
+
if (overlay) {
|
|
203
|
+
overlay.classList.add('is-active');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Prevent body scroll when menu is open
|
|
207
|
+
document.body.style.overflow = 'hidden';
|
|
208
|
+
|
|
209
|
+
// Set ARIA attributes
|
|
210
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
211
|
+
menu.setAttribute('aria-hidden', 'false');
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Close mobile menu
|
|
216
|
+
* @param {HTMLElement} navbar - Navbar element
|
|
217
|
+
* @param {HTMLElement} toggle - Toggle button
|
|
218
|
+
* @param {HTMLElement} menu - Menu element
|
|
219
|
+
* @param {HTMLElement} overlay - Overlay element
|
|
220
|
+
*/
|
|
221
|
+
closeMenu: function (navbar, toggle, menu, overlay) {
|
|
222
|
+
menu.classList.remove('is-open');
|
|
223
|
+
toggle.classList.remove('is-active');
|
|
224
|
+
|
|
225
|
+
if (overlay) {
|
|
226
|
+
overlay.classList.remove('is-active');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Restore body scroll
|
|
230
|
+
document.body.style.overflow = '';
|
|
231
|
+
|
|
232
|
+
// Close all dropdown menus
|
|
233
|
+
const dropdownMenus = menu.querySelectorAll('.vd-navbar-dropdown-menu.is-open');
|
|
234
|
+
dropdownMenus.forEach(dropdownMenu => {
|
|
235
|
+
dropdownMenu.classList.remove('is-open');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Set ARIA attributes
|
|
239
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
240
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Create overlay element if it doesn't exist
|
|
245
|
+
* @param {HTMLElement} navbar - Navbar element
|
|
246
|
+
* @returns {HTMLElement} Overlay element
|
|
247
|
+
*/
|
|
248
|
+
createOverlay: function (_navbar) {
|
|
249
|
+
const overlay = document.createElement('div');
|
|
250
|
+
overlay.className = 'vd-navbar-overlay';
|
|
251
|
+
document.body.appendChild(overlay);
|
|
252
|
+
return overlay;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Register with Vanduo framework if available
|
|
257
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
258
|
+
window.Vanduo.register('navbar', Navbar);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Expose globally
|
|
262
|
+
window.VanduoNavbar = Navbar;
|
|
263
|
+
|
|
264
|
+
})();
|