@vanduo-oss/framework 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/css/components/navbar.css +5 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +68 -44
- package/dist/vanduo.cjs.js.map +2 -2
- package/dist/vanduo.cjs.min.js +4 -4
- package/dist/vanduo.cjs.min.js.map +3 -3
- package/dist/vanduo.css +5 -1
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +68 -44
- package/dist/vanduo.esm.js.map +2 -2
- package/dist/vanduo.esm.min.js +4 -4
- package/dist/vanduo.esm.min.js.map +3 -3
- package/dist/vanduo.js +68 -44
- package/dist/vanduo.js.map +2 -2
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +4 -4
- package/dist/vanduo.min.js.map +3 -3
- package/js/components/code-snippet.js +5 -4
- package/js/components/dropdown.js +9 -9
- package/js/components/image-box.js +7 -1
- package/js/components/modals.js +18 -10
- package/js/components/navbar.js +3 -3
- package/js/components/select.js +15 -13
- package/js/components/suggest.js +14 -1
- package/js/components/theme-customizer.js +0 -1
- package/js/components/validate.js +14 -3
- package/js/utils/helpers.js +7 -3
- package/package.json +2 -2
|
@@ -282,19 +282,20 @@
|
|
|
282
282
|
const codeElement = activePane.querySelector('code') || activePane;
|
|
283
283
|
const code = codeElement.textContent;
|
|
284
284
|
|
|
285
|
+
let copySuccess;
|
|
285
286
|
try {
|
|
286
287
|
await navigator.clipboard.writeText(code);
|
|
287
|
-
|
|
288
|
+
copySuccess = true;
|
|
288
289
|
} catch (_err) {
|
|
289
290
|
// Fallback for older browsers
|
|
290
|
-
|
|
291
|
-
this.showCopyFeedback(copyBtn, success);
|
|
291
|
+
copySuccess = this.fallbackCopy(code);
|
|
292
292
|
}
|
|
293
|
+
this.showCopyFeedback(copyBtn, copySuccess);
|
|
293
294
|
|
|
294
295
|
// Dispatch event
|
|
295
296
|
const event = new CustomEvent('codesnippet:copy', {
|
|
296
297
|
bubbles: true,
|
|
297
|
-
detail: { snippet, code, success:
|
|
298
|
+
detail: { snippet, code, success: copySuccess }
|
|
298
299
|
});
|
|
299
300
|
snippet.dispatchEvent(event);
|
|
300
301
|
},
|
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
const Dropdown = {
|
|
13
13
|
// Store initialized dropdowns and their cleanup functions
|
|
14
14
|
instances: new Map(),
|
|
15
|
-
// Typeahead state
|
|
16
|
-
_typeaheadBuffer: '',
|
|
17
|
-
_typeaheadTimer: null,
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
17
|
* Initialize dropdown components
|
|
@@ -95,7 +92,7 @@
|
|
|
95
92
|
cleanupFunctions.push(() => item.removeEventListener('keydown', itemKeydownHandler));
|
|
96
93
|
});
|
|
97
94
|
|
|
98
|
-
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions });
|
|
95
|
+
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions, typeaheadBuffer: '', typeaheadTimer: null });
|
|
99
96
|
},
|
|
100
97
|
|
|
101
98
|
/**
|
|
@@ -249,18 +246,21 @@
|
|
|
249
246
|
default:
|
|
250
247
|
// Typeahead: jump to matching item when typing printable characters
|
|
251
248
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
252
|
-
|
|
253
|
-
|
|
249
|
+
// Per-instance typeahead state to avoid cross-instance corruption
|
|
250
|
+
const instance = this.instances.get(dropdown);
|
|
251
|
+
if (!instance) break;
|
|
252
|
+
clearTimeout(instance.typeaheadTimer);
|
|
253
|
+
instance.typeaheadBuffer += e.key.toLowerCase();
|
|
254
254
|
|
|
255
255
|
const match = items.find(item =>
|
|
256
|
-
item.textContent.trim().toLowerCase().startsWith(
|
|
256
|
+
item.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
|
|
257
257
|
);
|
|
258
258
|
if (match) {
|
|
259
259
|
match.focus();
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
instance.typeaheadTimer = setTimeout(() => {
|
|
263
|
+
instance.typeaheadBuffer = '';
|
|
264
264
|
}, 500);
|
|
265
265
|
}
|
|
266
266
|
break;
|
|
@@ -274,9 +274,10 @@
|
|
|
274
274
|
// Handle image load
|
|
275
275
|
if (!this.img.complete) {
|
|
276
276
|
this.img.style.opacity = '0';
|
|
277
|
-
this.
|
|
277
|
+
this._imgLoadHandler = () => {
|
|
278
278
|
this.img.style.opacity = '';
|
|
279
279
|
};
|
|
280
|
+
this.img.addEventListener('load', this._imgLoadHandler, { once: true });
|
|
280
281
|
}
|
|
281
282
|
},
|
|
282
283
|
|
|
@@ -305,6 +306,11 @@
|
|
|
305
306
|
// Clear image after transition
|
|
306
307
|
setTimeout(() => {
|
|
307
308
|
if (!this.isOpen) {
|
|
309
|
+
// Clean up load handler if still pending
|
|
310
|
+
if (this._imgLoadHandler) {
|
|
311
|
+
this.img.removeEventListener('load', this._imgLoadHandler);
|
|
312
|
+
this._imgLoadHandler = null;
|
|
313
|
+
}
|
|
308
314
|
this.img.src = '';
|
|
309
315
|
this.img.alt = '';
|
|
310
316
|
}
|
package/js/components/modals.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
// Store trigger cleanup functions
|
|
18
18
|
_triggerCleanups: [],
|
|
19
|
+
// Shared ESC key handler (installed once)
|
|
20
|
+
_sharedEscHandler: null,
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Initialize modals
|
|
@@ -99,17 +101,18 @@
|
|
|
99
101
|
backdrop.addEventListener('click', backdropClickHandler);
|
|
100
102
|
cleanupFunctions.push(() => backdrop.removeEventListener('click', backdropClickHandler));
|
|
101
103
|
|
|
102
|
-
// ESC key handler
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
// ESC key handler — use a single shared handler instead of one-per-modal
|
|
105
|
+
if (!this._sharedEscHandler) {
|
|
106
|
+
this._sharedEscHandler = (e) => {
|
|
107
|
+
if (e.key === 'Escape' && this.openModals.length > 0) {
|
|
108
|
+
const topModal = this.openModals[this.openModals.length - 1];
|
|
109
|
+
if (topModal.dataset.keyboard !== 'false') {
|
|
110
|
+
this.close(topModal);
|
|
111
|
+
}
|
|
108
112
|
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
cleanupFunctions.push(() => document.removeEventListener('keydown', escKeyHandler));
|
|
113
|
+
};
|
|
114
|
+
document.addEventListener('keydown', this._sharedEscHandler);
|
|
115
|
+
}
|
|
113
116
|
|
|
114
117
|
this.modals.set(modal, { backdrop, dialog, trapHandler: null, cleanup: cleanupFunctions });
|
|
115
118
|
},
|
|
@@ -352,6 +355,11 @@
|
|
|
352
355
|
// Clean up trigger listeners
|
|
353
356
|
this._triggerCleanups.forEach(fn => fn());
|
|
354
357
|
this._triggerCleanups = [];
|
|
358
|
+
// Remove shared ESC handler
|
|
359
|
+
if (this._sharedEscHandler) {
|
|
360
|
+
document.removeEventListener('keydown', this._sharedEscHandler);
|
|
361
|
+
this._sharedEscHandler = null;
|
|
362
|
+
}
|
|
355
363
|
}
|
|
356
364
|
};
|
|
357
365
|
|
package/js/components/navbar.js
CHANGED
|
@@ -203,8 +203,8 @@
|
|
|
203
203
|
overlay.classList.add('is-active');
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
// Prevent body scroll when menu is open
|
|
207
|
-
document.body.
|
|
206
|
+
// Prevent body scroll when menu is open (use class to avoid conflicts with modals)
|
|
207
|
+
document.body.classList.add('body-navbar-open');
|
|
208
208
|
|
|
209
209
|
// Set ARIA attributes
|
|
210
210
|
toggle.setAttribute('aria-expanded', 'true');
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
// Restore body scroll
|
|
230
|
-
document.body.
|
|
230
|
+
document.body.classList.remove('body-navbar-open');
|
|
231
231
|
|
|
232
232
|
// Close all dropdown menus
|
|
233
233
|
const dropdownMenus = menu.querySelectorAll('.vd-navbar-dropdown-menu.is-open');
|
package/js/components/select.js
CHANGED
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
const Select = {
|
|
13
13
|
// Store initialized selects and their cleanup functions
|
|
14
14
|
instances: new Map(),
|
|
15
|
-
// Typeahead state
|
|
16
|
-
_typeaheadBuffer: '',
|
|
17
|
-
_typeaheadTimer: null,
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
17
|
* Initialize select components
|
|
@@ -123,7 +120,7 @@
|
|
|
123
120
|
select.addEventListener('change', changeHandler);
|
|
124
121
|
cleanupFunctions.push(() => select.removeEventListener('change', changeHandler));
|
|
125
122
|
|
|
126
|
-
this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions });
|
|
123
|
+
this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions, typeaheadBuffer: '', typeaheadTimer: null });
|
|
127
124
|
},
|
|
128
125
|
|
|
129
126
|
/**
|
|
@@ -233,7 +230,7 @@
|
|
|
233
230
|
* @param {HTMLElement} dropdown - Dropdown container
|
|
234
231
|
*/
|
|
235
232
|
updateSelectedOptions: function (select, dropdown) {
|
|
236
|
-
const options = dropdown.querySelectorAll('.
|
|
233
|
+
const options = dropdown.querySelectorAll('.custom-select-option');
|
|
237
234
|
const selectedValues = Array.from(select.selectedOptions).map(opt => opt.value);
|
|
238
235
|
|
|
239
236
|
options.forEach(optionEl => {
|
|
@@ -273,7 +270,7 @@
|
|
|
273
270
|
button.setAttribute('aria-expanded', 'true');
|
|
274
271
|
|
|
275
272
|
// Focus first option
|
|
276
|
-
const firstOption = dropdown.querySelector('.
|
|
273
|
+
const firstOption = dropdown.querySelector('.custom-select-option:not(.is-disabled)');
|
|
277
274
|
if (firstOption) {
|
|
278
275
|
firstOption.focus();
|
|
279
276
|
}
|
|
@@ -298,7 +295,7 @@
|
|
|
298
295
|
*/
|
|
299
296
|
handleKeydown: function (e, select, button, dropdown) {
|
|
300
297
|
const isOpen = dropdown.classList.contains('is-open');
|
|
301
|
-
const options = Array.from(dropdown.querySelectorAll('.
|
|
298
|
+
const options = Array.from(dropdown.querySelectorAll('.custom-select-option:not(.is-disabled)'));
|
|
302
299
|
const currentIndex = options.findIndex(opt => opt === document.activeElement);
|
|
303
300
|
|
|
304
301
|
switch (e.key) {
|
|
@@ -357,18 +354,21 @@
|
|
|
357
354
|
default:
|
|
358
355
|
// Typeahead: jump to matching option when typing printable characters
|
|
359
356
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
360
|
-
|
|
361
|
-
|
|
357
|
+
// Per-instance typeahead state to avoid cross-instance corruption
|
|
358
|
+
const instance = this.instances.get(select);
|
|
359
|
+
if (!instance) break;
|
|
360
|
+
clearTimeout(instance.typeaheadTimer);
|
|
361
|
+
instance.typeaheadBuffer += e.key.toLowerCase();
|
|
362
362
|
|
|
363
363
|
const match = options.find(opt =>
|
|
364
|
-
opt.textContent.trim().toLowerCase().startsWith(
|
|
364
|
+
opt.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
|
|
365
365
|
);
|
|
366
366
|
if (match) {
|
|
367
367
|
match.focus();
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
instance.typeaheadTimer = setTimeout(() => {
|
|
371
|
+
instance.typeaheadBuffer = '';
|
|
372
372
|
}, 500);
|
|
373
373
|
}
|
|
374
374
|
break;
|
|
@@ -403,7 +403,9 @@
|
|
|
403
403
|
if (element.id) {
|
|
404
404
|
return element.id;
|
|
405
405
|
}
|
|
406
|
-
|
|
406
|
+
const id = 'select-' + Math.random().toString(36).substr(2, 9);
|
|
407
|
+
element.id = id;
|
|
408
|
+
return id;
|
|
407
409
|
},
|
|
408
410
|
|
|
409
411
|
/**
|
package/js/components/suggest.js
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
(function () {
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Escape HTML entities to prevent XSS when inserting into innerHTML
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
function _escapeHtml(text) {
|
|
15
|
+
const div = document.createElement('div');
|
|
16
|
+
div.textContent = text;
|
|
17
|
+
return div.innerHTML;
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
const Suggest = {
|
|
10
21
|
instances: new Map(),
|
|
11
22
|
|
|
@@ -77,8 +88,10 @@
|
|
|
77
88
|
|
|
78
89
|
const text = typeof item === 'object' ? (item.label || item.text || String(item)) : String(item);
|
|
79
90
|
if (query) {
|
|
91
|
+
// Escape HTML first to prevent XSS, then highlight matches in the safe string
|
|
92
|
+
const escaped = _escapeHtml(text);
|
|
80
93
|
const re = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
|
81
|
-
li.innerHTML =
|
|
94
|
+
li.innerHTML = escaped.replace(re, '<span class="vd-suggest-match">$1</span>');
|
|
82
95
|
} else {
|
|
83
96
|
li.textContent = text;
|
|
84
97
|
}
|
|
@@ -714,7 +714,6 @@
|
|
|
714
714
|
this.applyNeutral(this.DEFAULTS.NEUTRAL);
|
|
715
715
|
this.applyRadius(this.DEFAULTS.RADIUS);
|
|
716
716
|
this.applyFont(this.DEFAULTS.FONT);
|
|
717
|
-
this.applyTheme(this.DEFAULTS.THEME);
|
|
718
717
|
this.updateUI();
|
|
719
718
|
|
|
720
719
|
this.dispatchEvent('reset', { state: { ...this.state } });
|
|
@@ -18,10 +18,21 @@
|
|
|
18
18
|
max: (value, param) => value.length <= parseInt(param, 10),
|
|
19
19
|
minVal: (value, param) => parseFloat(value) >= parseFloat(param),
|
|
20
20
|
maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
|
|
21
|
-
pattern: (value, param) => {
|
|
21
|
+
pattern: (value, param) => {
|
|
22
|
+
try {
|
|
23
|
+
// Cap regex length to prevent ReDoS from excessively complex patterns
|
|
24
|
+
if (param.length > 100) return false;
|
|
25
|
+
return new RegExp(param).test(value);
|
|
26
|
+
} catch (_e) { return false; }
|
|
27
|
+
},
|
|
22
28
|
match: (value, param) => {
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
try {
|
|
30
|
+
const escaped = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(param) : param;
|
|
31
|
+
const other = document.querySelector('[name="' + escaped + '"]');
|
|
32
|
+
return other ? value === other.value : false;
|
|
33
|
+
} catch (_e) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
25
36
|
}
|
|
26
37
|
},
|
|
27
38
|
|
package/js/utils/helpers.js
CHANGED
|
@@ -74,6 +74,7 @@ function safeStorageSet(key, value) {
|
|
|
74
74
|
* @param {string} event - Event type
|
|
75
75
|
* @param {string|Function} handlerOrSelector - Event handler or selector for delegation
|
|
76
76
|
* @param {Function} handler - Event handler (if using delegation)
|
|
77
|
+
* @returns {Function|undefined} The actual bound handler (use this with off() to remove delegation listeners)
|
|
77
78
|
*/
|
|
78
79
|
function on(target, event, handlerOrSelector, handler) {
|
|
79
80
|
const element = typeof target === 'string' ? $(target) : target;
|
|
@@ -83,9 +84,10 @@ function on(target, event, handlerOrSelector, handler) {
|
|
|
83
84
|
if (typeof handlerOrSelector === 'function') {
|
|
84
85
|
// Direct event binding
|
|
85
86
|
element.addEventListener(event, handlerOrSelector);
|
|
87
|
+
return handlerOrSelector;
|
|
86
88
|
} else {
|
|
87
|
-
// Event delegation
|
|
88
|
-
|
|
89
|
+
// Event delegation — return the wrapper so callers can remove it via off()
|
|
90
|
+
const wrapper = function (e) {
|
|
89
91
|
const delegateTarget = e.target.closest(handlerOrSelector);
|
|
90
92
|
if (delegateTarget && element.contains(delegateTarget)) {
|
|
91
93
|
try {
|
|
@@ -94,7 +96,9 @@ function on(target, event, handlerOrSelector, handler) {
|
|
|
94
96
|
console.warn('[Vanduo Helpers] Delegated handler error:', error);
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
|
-
}
|
|
99
|
+
};
|
|
100
|
+
element.addEventListener(event, wrapper);
|
|
101
|
+
return wrapper;
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanduo-oss/framework",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Zero-dependency CSS/JS framework built on Fibonacci/Golden Ratio design system with Open Color integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"eslint": "^10.0.3",
|
|
46
46
|
"husky": "^9.1.7",
|
|
47
47
|
"lightningcss": "^1.32.0",
|
|
48
|
-
"stylelint": "^17.
|
|
48
|
+
"stylelint": "^17.5.0",
|
|
49
49
|
"stylelint-config-standard": "^40.0.0"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|