@vanduo-oss/framework 1.3.0 → 1.3.2

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.
@@ -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.style.overflow = 'hidden';
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.style.overflow = '';
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');
@@ -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('.vd-custom-select-option');
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('.vd-custom-select-option:not(.is-disabled)');
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('.vd-custom-select-option:not(.is-disabled)'));
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
- clearTimeout(this._typeaheadTimer);
361
- this._typeaheadBuffer += e.key.toLowerCase();
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(this._typeaheadBuffer)
364
+ opt.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
365
365
  );
366
366
  if (match) {
367
367
  match.focus();
368
368
  }
369
369
 
370
- this._typeaheadTimer = setTimeout(() => {
371
- this._typeaheadBuffer = '';
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
- return 'select-' + Math.random().toString(36).substr(2, 9);
406
+ const id = 'select-' + Math.random().toString(36).substr(2, 9);
407
+ element.id = id;
408
+ return id;
407
409
  },
408
410
 
409
411
  /**
@@ -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 = text.replace(re, '<span class="vd-suggest-match">$1</span>');
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) => { try { return new RegExp(param).test(value); } catch (_e) { return false; } },
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
- const other = document.querySelector('[name="' + param + '"]');
24
- return other ? value === other.value : false;
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/index.js CHANGED
@@ -62,6 +62,7 @@ import './components/rating.js';
62
62
  import './components/transfer.js';
63
63
  import './components/tree.js';
64
64
  import './components/spotlight.js';
65
+ import './components/music-player.js';
65
66
 
66
67
  // Re-export for ESM / CJS consumers
67
68
  const Vanduo = window.Vanduo;
@@ -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
- element.addEventListener(event, function (e) {
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.0",
3
+ "version": "1.3.2",
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.4.0",
48
+ "stylelint": "^17.5.0",
49
49
  "stylelint-config-standard": "^40.0.0"
50
50
  },
51
51
  "engines": {