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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Grid Layout Component
|
|
3
|
+
* Toggle between standard 12-column and Fibonacci grid modes
|
|
4
|
+
* via data-layout-mode attribute and toggle buttons
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
var supportsHas = (function () {
|
|
11
|
+
try {
|
|
12
|
+
return CSS.supports('selector(:has(*))');
|
|
13
|
+
} catch (_e) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Grid Layout Component
|
|
20
|
+
*/
|
|
21
|
+
var GridLayout = {
|
|
22
|
+
instances: new Map(),
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize all grid layout containers
|
|
26
|
+
*/
|
|
27
|
+
init: function () {
|
|
28
|
+
var containers = document.querySelectorAll('[data-layout-mode]');
|
|
29
|
+
|
|
30
|
+
containers.forEach(function (container) {
|
|
31
|
+
if (this.instances.has(container)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.initContainer(container);
|
|
35
|
+
}.bind(this));
|
|
36
|
+
|
|
37
|
+
this.initToggleButtons();
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize a single grid container
|
|
42
|
+
* @param {HTMLElement} container - Element with data-layout-mode
|
|
43
|
+
*/
|
|
44
|
+
initContainer: function (container) {
|
|
45
|
+
var mode = container.getAttribute('data-layout-mode') || 'standard';
|
|
46
|
+
var cleanupFunctions = [];
|
|
47
|
+
|
|
48
|
+
this.applyMode(container, mode);
|
|
49
|
+
|
|
50
|
+
container.setAttribute('role', 'region');
|
|
51
|
+
container.setAttribute('aria-label', 'Grid layout: ' + mode + ' mode');
|
|
52
|
+
|
|
53
|
+
this.instances.set(container, {
|
|
54
|
+
cleanup: cleanupFunctions,
|
|
55
|
+
mode: mode
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize toggle buttons that target grid containers
|
|
61
|
+
*/
|
|
62
|
+
initToggleButtons: function () {
|
|
63
|
+
var toggleButtons = document.querySelectorAll('[data-grid-toggle]');
|
|
64
|
+
|
|
65
|
+
toggleButtons.forEach(function (button) {
|
|
66
|
+
if (button.getAttribute('data-grid-initialized') === 'true') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var clickHandler = function (e) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
var targetSelector = button.getAttribute('data-grid-toggle');
|
|
73
|
+
var target;
|
|
74
|
+
|
|
75
|
+
if (targetSelector) {
|
|
76
|
+
target = document.querySelector(targetSelector);
|
|
77
|
+
} else {
|
|
78
|
+
target = button.closest('[data-layout-mode]');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (target) {
|
|
82
|
+
this.toggle(target);
|
|
83
|
+
}
|
|
84
|
+
}.bind(this);
|
|
85
|
+
|
|
86
|
+
button.addEventListener('click', clickHandler);
|
|
87
|
+
button.setAttribute('data-grid-initialized', 'true');
|
|
88
|
+
button.setAttribute('aria-pressed', 'false');
|
|
89
|
+
|
|
90
|
+
button._gridCleanup = function () {
|
|
91
|
+
button.removeEventListener('click', clickHandler);
|
|
92
|
+
button.removeAttribute('data-grid-initialized');
|
|
93
|
+
button.removeAttribute('aria-pressed');
|
|
94
|
+
};
|
|
95
|
+
}.bind(this));
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Apply Fibonacci grid-template-columns inline for browsers without :has()
|
|
100
|
+
* @param {HTMLElement} container - Grid container
|
|
101
|
+
*/
|
|
102
|
+
applyFibFallback: function (container) {
|
|
103
|
+
if (supportsHas) return;
|
|
104
|
+
|
|
105
|
+
var rows = container.querySelectorAll('.vd-row, .row');
|
|
106
|
+
rows.forEach(function (row) {
|
|
107
|
+
var cols = row.querySelectorAll(':scope > [class*="vd-col-"], :scope > [class*="col-"]');
|
|
108
|
+
var count = cols.length;
|
|
109
|
+
|
|
110
|
+
if (count === 1) {
|
|
111
|
+
row.style.gridTemplateColumns = '1fr';
|
|
112
|
+
} else if (count === 2) {
|
|
113
|
+
row.style.gridTemplateColumns = '1fr 1.618fr';
|
|
114
|
+
} else if (count === 3) {
|
|
115
|
+
row.style.gridTemplateColumns = '2fr 3fr 5fr';
|
|
116
|
+
} else if (count === 4) {
|
|
117
|
+
row.style.gridTemplateColumns = '1fr 2fr 3fr 5fr';
|
|
118
|
+
} else {
|
|
119
|
+
row.style.gridTemplateColumns = 'repeat(' + count + ', 1fr)';
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove inline grid-template-columns fallback
|
|
126
|
+
* @param {HTMLElement} container - Grid container
|
|
127
|
+
*/
|
|
128
|
+
removeFibFallback: function (container) {
|
|
129
|
+
var rows = container.querySelectorAll('.vd-row, .row');
|
|
130
|
+
rows.forEach(function (row) {
|
|
131
|
+
row.style.gridTemplateColumns = '';
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Apply a layout mode to a container
|
|
137
|
+
* @param {HTMLElement} container - Target container
|
|
138
|
+
* @param {string} mode - 'fibonacci' or 'standard'
|
|
139
|
+
*/
|
|
140
|
+
applyMode: function (container, mode) {
|
|
141
|
+
container.classList.remove('vd-grid-standard', 'vd-grid-fibonacci');
|
|
142
|
+
|
|
143
|
+
if (mode === 'fibonacci') {
|
|
144
|
+
container.classList.add('vd-grid-fibonacci');
|
|
145
|
+
this.applyFibFallback(container);
|
|
146
|
+
} else {
|
|
147
|
+
container.classList.add('vd-grid-standard');
|
|
148
|
+
this.removeFibFallback(container);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
container.setAttribute('data-layout-mode', mode);
|
|
152
|
+
container.setAttribute('aria-label', 'Grid layout: ' + mode + ' mode');
|
|
153
|
+
|
|
154
|
+
// Update associated toggle button states
|
|
155
|
+
var toggleButtons = document.querySelectorAll('[data-grid-toggle]');
|
|
156
|
+
toggleButtons.forEach(function (btn) {
|
|
157
|
+
var targetSelector = btn.getAttribute('data-grid-toggle');
|
|
158
|
+
if (targetSelector && container.matches(targetSelector)) {
|
|
159
|
+
var isActive = (mode === 'fibonacci');
|
|
160
|
+
if (isActive) {
|
|
161
|
+
btn.classList.add('is-active');
|
|
162
|
+
} else {
|
|
163
|
+
btn.classList.remove('is-active');
|
|
164
|
+
}
|
|
165
|
+
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Store mode in instance
|
|
170
|
+
var instance = this.instances.get(container);
|
|
171
|
+
if (instance) {
|
|
172
|
+
instance.mode = mode;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Dispatch custom event
|
|
176
|
+
var event;
|
|
177
|
+
try {
|
|
178
|
+
event = new CustomEvent('grid:modechange', {
|
|
179
|
+
bubbles: true,
|
|
180
|
+
detail: {
|
|
181
|
+
container: container,
|
|
182
|
+
mode: mode
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
} catch (_e) {
|
|
186
|
+
event = document.createEvent('CustomEvent');
|
|
187
|
+
event.initCustomEvent('grid:modechange', true, true, {
|
|
188
|
+
container: container,
|
|
189
|
+
mode: mode
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
container.dispatchEvent(event);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Toggle between standard and fibonacci modes
|
|
197
|
+
* @param {HTMLElement|string} container - Container element or selector
|
|
198
|
+
*/
|
|
199
|
+
toggle: function (container) {
|
|
200
|
+
if (typeof container === 'string') {
|
|
201
|
+
container = document.querySelector(container);
|
|
202
|
+
}
|
|
203
|
+
if (!container) return;
|
|
204
|
+
|
|
205
|
+
var currentMode = container.getAttribute('data-layout-mode') || 'standard';
|
|
206
|
+
var newMode = (currentMode === 'fibonacci') ? 'standard' : 'fibonacci';
|
|
207
|
+
this.applyMode(container, newMode);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set a specific mode
|
|
212
|
+
* @param {HTMLElement|string} container - Container element or selector
|
|
213
|
+
* @param {string} mode - 'fibonacci' or 'standard'
|
|
214
|
+
*/
|
|
215
|
+
setMode: function (container, mode) {
|
|
216
|
+
if (typeof container === 'string') {
|
|
217
|
+
container = document.querySelector(container);
|
|
218
|
+
}
|
|
219
|
+
if (!container) return;
|
|
220
|
+
if (mode !== 'fibonacci' && mode !== 'standard') return;
|
|
221
|
+
|
|
222
|
+
this.applyMode(container, mode);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get the current mode of a container
|
|
227
|
+
* @param {HTMLElement|string} container - Container element or selector
|
|
228
|
+
* @returns {string|null} Current mode or null
|
|
229
|
+
*/
|
|
230
|
+
getMode: function (container) {
|
|
231
|
+
if (typeof container === 'string') {
|
|
232
|
+
container = document.querySelector(container);
|
|
233
|
+
}
|
|
234
|
+
if (!container) return null;
|
|
235
|
+
return container.getAttribute('data-layout-mode') || 'standard';
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Destroy a single grid layout instance
|
|
240
|
+
* @param {HTMLElement} container - Grid container
|
|
241
|
+
*/
|
|
242
|
+
destroy: function (container) {
|
|
243
|
+
var instance = this.instances.get(container);
|
|
244
|
+
if (!instance) return;
|
|
245
|
+
|
|
246
|
+
instance.cleanup.forEach(function (fn) { fn(); });
|
|
247
|
+
container.classList.remove('vd-grid-standard', 'vd-grid-fibonacci');
|
|
248
|
+
container.removeAttribute('aria-label');
|
|
249
|
+
this.removeFibFallback(container);
|
|
250
|
+
this.instances.delete(container);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Destroy all grid layout instances and clean up toggle buttons
|
|
255
|
+
*/
|
|
256
|
+
destroyAll: function () {
|
|
257
|
+
this.instances.forEach(function (instance, container) {
|
|
258
|
+
this.destroy(container);
|
|
259
|
+
}.bind(this));
|
|
260
|
+
|
|
261
|
+
var toggleButtons = document.querySelectorAll('[data-grid-initialized="true"]');
|
|
262
|
+
toggleButtons.forEach(function (button) {
|
|
263
|
+
if (button._gridCleanup) {
|
|
264
|
+
button._gridCleanup();
|
|
265
|
+
delete button._gridCleanup;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Register with Vanduo framework
|
|
272
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
273
|
+
window.Vanduo.register('gridLayout', GridLayout);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Expose globally
|
|
277
|
+
window.VanduoGridLayout = GridLayout;
|
|
278
|
+
|
|
279
|
+
})();
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Image Box Component
|
|
3
|
+
* Lightbox-style image enlargement with smooth transitions
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Click to enlarge images with data-image-box attribute
|
|
7
|
+
* - Smooth scale and opacity transitions
|
|
8
|
+
* - Dismiss via click, ESC key, or scroll
|
|
9
|
+
* - Magnifying glass cursor on hover
|
|
10
|
+
* - Accessible with ARIA attributes
|
|
11
|
+
* - Reduced motion support
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Image Box Component
|
|
19
|
+
*/
|
|
20
|
+
const ImageBox = {
|
|
21
|
+
backdrop: null,
|
|
22
|
+
container: null,
|
|
23
|
+
img: null,
|
|
24
|
+
closeBtn: null,
|
|
25
|
+
caption: null,
|
|
26
|
+
currentTrigger: null,
|
|
27
|
+
scrollThreshold: 50,
|
|
28
|
+
initialScrollY: 0,
|
|
29
|
+
isOpen: false,
|
|
30
|
+
|
|
31
|
+
// Store cleanup functions for event listeners
|
|
32
|
+
_cleanupFunctions: [],
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize Image Box component
|
|
36
|
+
*/
|
|
37
|
+
init: function () {
|
|
38
|
+
this.createBackdrop();
|
|
39
|
+
this.bindTriggers();
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create backdrop elements
|
|
44
|
+
*/
|
|
45
|
+
createBackdrop: function () {
|
|
46
|
+
// Prevent duplicate backdrop creation
|
|
47
|
+
if (this.backdrop || document.querySelector('.vd-image-box-backdrop')) {
|
|
48
|
+
// If backdrop already exists in DOM, reuse it
|
|
49
|
+
if (!this.backdrop) {
|
|
50
|
+
this.backdrop = document.querySelector('.vd-image-box-backdrop');
|
|
51
|
+
this.container = this.backdrop.querySelector('.vd-image-box-container');
|
|
52
|
+
this.img = this.backdrop.querySelector('.vd-image-box-img');
|
|
53
|
+
this.closeBtn = this.backdrop.querySelector('.vd-image-box-close');
|
|
54
|
+
this.caption = this.backdrop.querySelector('.vd-image-box-caption');
|
|
55
|
+
this.bindBackdropEvents();
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create backdrop
|
|
61
|
+
this.backdrop = document.createElement('div');
|
|
62
|
+
this.backdrop.className = 'vd-image-box-backdrop';
|
|
63
|
+
this.backdrop.setAttribute('role', 'dialog');
|
|
64
|
+
this.backdrop.setAttribute('aria-modal', 'true');
|
|
65
|
+
this.backdrop.setAttribute('aria-label', 'Image viewer');
|
|
66
|
+
this.backdrop.setAttribute('tabindex', '-1');
|
|
67
|
+
|
|
68
|
+
// Create container
|
|
69
|
+
this.container = document.createElement('div');
|
|
70
|
+
this.container.className = 'vd-image-box-container';
|
|
71
|
+
|
|
72
|
+
// Create image
|
|
73
|
+
this.img = document.createElement('img');
|
|
74
|
+
this.img.className = 'vd-image-box-img';
|
|
75
|
+
this.img.alt = '';
|
|
76
|
+
|
|
77
|
+
// Create close button
|
|
78
|
+
this.closeBtn = document.createElement('button');
|
|
79
|
+
this.closeBtn.className = 'vd-image-box-close';
|
|
80
|
+
this.closeBtn.setAttribute('aria-label', 'Close image viewer');
|
|
81
|
+
this.closeBtn.innerHTML = '×';
|
|
82
|
+
|
|
83
|
+
// Create caption element
|
|
84
|
+
this.caption = document.createElement('div');
|
|
85
|
+
this.caption.className = 'vd-image-box-caption';
|
|
86
|
+
|
|
87
|
+
// Assemble
|
|
88
|
+
this.container.appendChild(this.img);
|
|
89
|
+
this.backdrop.appendChild(this.closeBtn);
|
|
90
|
+
this.backdrop.appendChild(this.container);
|
|
91
|
+
this.backdrop.appendChild(this.caption);
|
|
92
|
+
document.body.appendChild(this.backdrop);
|
|
93
|
+
|
|
94
|
+
// Bind backdrop events
|
|
95
|
+
this.bindBackdropEvents();
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Bind events to backdrop elements
|
|
100
|
+
*/
|
|
101
|
+
bindBackdropEvents: function () {
|
|
102
|
+
const self = this;
|
|
103
|
+
|
|
104
|
+
// Close on backdrop click (but not when clicking the image)
|
|
105
|
+
const backdropClickHandler = function (e) {
|
|
106
|
+
if (e.target === self.backdrop || e.target === self.container) {
|
|
107
|
+
self.close();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
this.backdrop.addEventListener('click', backdropClickHandler);
|
|
111
|
+
this._cleanupFunctions.push(() => this.backdrop.removeEventListener('click', backdropClickHandler));
|
|
112
|
+
|
|
113
|
+
// Close on image click
|
|
114
|
+
const imgClickHandler = function () {
|
|
115
|
+
self.close();
|
|
116
|
+
};
|
|
117
|
+
this.img.addEventListener('click', imgClickHandler);
|
|
118
|
+
this._cleanupFunctions.push(() => this.img.removeEventListener('click', imgClickHandler));
|
|
119
|
+
|
|
120
|
+
// Close on close button click
|
|
121
|
+
const closeBtnHandler = function () {
|
|
122
|
+
self.close();
|
|
123
|
+
};
|
|
124
|
+
this.closeBtn.addEventListener('click', closeBtnHandler);
|
|
125
|
+
this._cleanupFunctions.push(() => this.closeBtn.removeEventListener('click', closeBtnHandler));
|
|
126
|
+
|
|
127
|
+
// ESC key handler
|
|
128
|
+
const escHandler = function (e) {
|
|
129
|
+
if (e.key === 'Escape' && self.isOpen) {
|
|
130
|
+
self.close();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
document.addEventListener('keydown', escHandler);
|
|
134
|
+
this._cleanupFunctions.push(() => document.removeEventListener('keydown', escHandler));
|
|
135
|
+
|
|
136
|
+
// Scroll handler for dismissal
|
|
137
|
+
const scrollHandler = function () {
|
|
138
|
+
if (!self.isOpen) return;
|
|
139
|
+
|
|
140
|
+
const currentScrollY = window.scrollY;
|
|
141
|
+
const scrollDelta = Math.abs(currentScrollY - self.initialScrollY);
|
|
142
|
+
|
|
143
|
+
if (scrollDelta > self.scrollThreshold) {
|
|
144
|
+
self.close();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
window.addEventListener('scroll', scrollHandler, { passive: true });
|
|
148
|
+
this._cleanupFunctions.push(() => window.removeEventListener('scroll', scrollHandler));
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Bind triggers to all images with data-image-box attribute
|
|
153
|
+
*/
|
|
154
|
+
bindTriggers: function () {
|
|
155
|
+
const self = this;
|
|
156
|
+
const triggers = document.querySelectorAll('[data-image-box]');
|
|
157
|
+
|
|
158
|
+
triggers.forEach(function (trigger) {
|
|
159
|
+
// Skip if already initialized
|
|
160
|
+
if (trigger.dataset.imageBoxInitialized) return;
|
|
161
|
+
trigger.dataset.imageBoxInitialized = 'true';
|
|
162
|
+
|
|
163
|
+
// Add trigger class
|
|
164
|
+
trigger.classList.add('vd-image-box-trigger');
|
|
165
|
+
|
|
166
|
+
// Handle broken images
|
|
167
|
+
if (trigger.tagName === 'IMG') {
|
|
168
|
+
// Check if already in error state
|
|
169
|
+
if (trigger.complete && trigger.naturalWidth === 0) {
|
|
170
|
+
trigger.classList.add('is-broken');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Listen for error events
|
|
174
|
+
const errorHandler = function () {
|
|
175
|
+
trigger.classList.add('is-broken');
|
|
176
|
+
};
|
|
177
|
+
trigger.addEventListener('error', errorHandler);
|
|
178
|
+
|
|
179
|
+
// Listen for successful load
|
|
180
|
+
const loadHandler = function () {
|
|
181
|
+
trigger.classList.remove('is-broken');
|
|
182
|
+
};
|
|
183
|
+
trigger.addEventListener('load', loadHandler);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Bind click event
|
|
187
|
+
const clickHandler = function (e) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
self.open(trigger);
|
|
190
|
+
};
|
|
191
|
+
trigger.addEventListener('click', clickHandler);
|
|
192
|
+
|
|
193
|
+
// Store cleanup
|
|
194
|
+
trigger._imageBoxCleanup = () => trigger.removeEventListener('click', clickHandler);
|
|
195
|
+
|
|
196
|
+
// Keyboard accessibility for non-button triggers
|
|
197
|
+
if (trigger.tagName !== 'BUTTON' && trigger.tagName !== 'A') {
|
|
198
|
+
trigger.setAttribute('role', 'button');
|
|
199
|
+
trigger.setAttribute('tabindex', '0');
|
|
200
|
+
trigger.setAttribute('aria-label', 'View enlarged image');
|
|
201
|
+
|
|
202
|
+
const keyHandler = function (e) {
|
|
203
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
self.open(trigger);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
trigger.addEventListener('keydown', keyHandler);
|
|
209
|
+
|
|
210
|
+
const originalCleanup = trigger._imageBoxCleanup;
|
|
211
|
+
trigger._imageBoxCleanup = () => {
|
|
212
|
+
originalCleanup();
|
|
213
|
+
trigger.removeEventListener('keydown', keyHandler);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Open image box
|
|
221
|
+
* @param {HTMLElement} trigger - The trigger element
|
|
222
|
+
*/
|
|
223
|
+
open: function (trigger) {
|
|
224
|
+
if (this.isOpen) return;
|
|
225
|
+
|
|
226
|
+
this.currentTrigger = trigger;
|
|
227
|
+
this.isOpen = true;
|
|
228
|
+
this.initialScrollY = window.scrollY;
|
|
229
|
+
|
|
230
|
+
// Get image source - support dual images (thumbnail + full-size)
|
|
231
|
+
// data-image-box-full-src takes precedence for the lightbox
|
|
232
|
+
const imgSrc = trigger.dataset.imageBoxFullSrc ||
|
|
233
|
+
trigger.dataset.imageBoxSrc ||
|
|
234
|
+
trigger.src ||
|
|
235
|
+
trigger.href;
|
|
236
|
+
|
|
237
|
+
if (!imgSrc) {
|
|
238
|
+
console.warn('[Vanduo ImageBox] No image source found for trigger:', trigger);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Get caption
|
|
243
|
+
const captionText = trigger.dataset.imageBoxCaption || trigger.alt || '';
|
|
244
|
+
|
|
245
|
+
// Set image source
|
|
246
|
+
this.img.src = imgSrc;
|
|
247
|
+
this.img.alt = trigger.alt || '';
|
|
248
|
+
|
|
249
|
+
// Set caption
|
|
250
|
+
if (captionText) {
|
|
251
|
+
this.caption.textContent = captionText;
|
|
252
|
+
this.caption.style.display = 'block';
|
|
253
|
+
} else {
|
|
254
|
+
this.caption.style.display = 'none';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Calculate scrollbar width and lock body scroll
|
|
258
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
259
|
+
document.body.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
|
|
260
|
+
document.body.classList.add('body-image-box-open');
|
|
261
|
+
|
|
262
|
+
// Show backdrop
|
|
263
|
+
this.backdrop.classList.add('is-visible');
|
|
264
|
+
|
|
265
|
+
// Focus management
|
|
266
|
+
this.backdrop.focus();
|
|
267
|
+
|
|
268
|
+
// Dispatch event
|
|
269
|
+
trigger.dispatchEvent(new CustomEvent('imageBox:open', {
|
|
270
|
+
bubbles: true,
|
|
271
|
+
detail: { src: imgSrc }
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
// Handle image load
|
|
275
|
+
if (!this.img.complete) {
|
|
276
|
+
this.img.style.opacity = '0';
|
|
277
|
+
this.img.onload = () => {
|
|
278
|
+
this.img.style.opacity = '';
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Close image box
|
|
285
|
+
*/
|
|
286
|
+
close: function () {
|
|
287
|
+
if (!this.isOpen) return;
|
|
288
|
+
|
|
289
|
+
this.isOpen = false;
|
|
290
|
+
|
|
291
|
+
// Hide backdrop
|
|
292
|
+
this.backdrop.classList.remove('is-visible');
|
|
293
|
+
|
|
294
|
+
// Unlock body scroll
|
|
295
|
+
document.body.classList.remove('body-image-box-open');
|
|
296
|
+
document.body.style.removeProperty('--scrollbar-width');
|
|
297
|
+
|
|
298
|
+
// Return focus to trigger
|
|
299
|
+
if (this.currentTrigger) {
|
|
300
|
+
this.currentTrigger.focus();
|
|
301
|
+
this.currentTrigger.dispatchEvent(new CustomEvent('imageBox:close', { bubbles: true }));
|
|
302
|
+
this.currentTrigger = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Clear image after transition
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
if (!this.isOpen) {
|
|
308
|
+
this.img.src = '';
|
|
309
|
+
this.img.alt = '';
|
|
310
|
+
}
|
|
311
|
+
}, 300);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Reinitialize - useful after dynamic DOM changes
|
|
316
|
+
*/
|
|
317
|
+
reinit: function () {
|
|
318
|
+
this.bindTriggers();
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Destroy component and clean up
|
|
323
|
+
*/
|
|
324
|
+
destroy: function () {
|
|
325
|
+
// Close if open
|
|
326
|
+
if (this.isOpen) {
|
|
327
|
+
this.close();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Remove backdrop
|
|
331
|
+
if (this.backdrop && this.backdrop.parentNode) {
|
|
332
|
+
this.backdrop.parentNode.removeChild(this.backdrop);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Run cleanup functions
|
|
336
|
+
this._cleanupFunctions.forEach(fn => fn());
|
|
337
|
+
this._cleanupFunctions = [];
|
|
338
|
+
|
|
339
|
+
// Remove trigger bindings
|
|
340
|
+
const triggers = document.querySelectorAll('[data-image-box-initialized]');
|
|
341
|
+
triggers.forEach(trigger => {
|
|
342
|
+
trigger.classList.remove('vd-image-box-trigger');
|
|
343
|
+
if (trigger._imageBoxCleanup) {
|
|
344
|
+
trigger._imageBoxCleanup();
|
|
345
|
+
delete trigger._imageBoxCleanup;
|
|
346
|
+
}
|
|
347
|
+
delete trigger.dataset.imageBoxInitialized;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.backdrop = null;
|
|
351
|
+
this.container = null;
|
|
352
|
+
this.img = null;
|
|
353
|
+
this.closeBtn = null;
|
|
354
|
+
this.caption = null;
|
|
355
|
+
this.currentTrigger = null;
|
|
356
|
+
this.isOpen = false;
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
destroyAll: function () {
|
|
360
|
+
this.destroy();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Register with Vanduo framework if available
|
|
365
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
366
|
+
window.Vanduo.register('imageBox', ImageBox);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Expose globally
|
|
370
|
+
window.VanduoImageBox = ImageBox;
|
|
371
|
+
|
|
372
|
+
})();
|