@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
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Vanduo Framework v1.3.
|
|
1
|
+
# Vanduo Framework v1.3.1
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="vanduo-banner.svg" alt="Vanduo Framework Banner" width="100%">
|
|
@@ -38,14 +38,16 @@ A lightweight, pure HTML/CSS/JS framework with **45+ components** for designing
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
## What's New in v1.3.
|
|
41
|
+
## What's New in v1.3.1
|
|
42
42
|
|
|
43
|
-
v1.3.
|
|
43
|
+
v1.3.1 is a security and correctness release (12 issues fixed, 0 breaking changes):
|
|
44
44
|
|
|
45
|
-
- **
|
|
46
|
-
- **Fixed
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
45
|
+
- **XSS fix in Suggest.** `renderItems()` now escapes user/server data before `innerHTML` highlight injection.
|
|
46
|
+
- **Select component repairs.** Fixed 3 broken `querySelector` selectors (keyboard nav + programmatic updates); `generateId()` now assigns `element.id` so ARIA `aria-labelledby` resolves correctly.
|
|
47
|
+
- **Typeahead isolation.** `_typeaheadBuffer` / `_typeaheadTimer` moved to per-instance state in Dropdown and Select — typing in one instance no longer corrupts another.
|
|
48
|
+
- **Navbar scroll-lock fix.** CSS class `body-navbar-open` replaces inline `overflow:hidden`, preventing conflicts with modal scroll locks.
|
|
49
|
+
- **Validate hardening.** 100-char limit on user regex patterns (ReDoS prevention); `CSS.escape()` applied to `match` rule param (selector injection fix).
|
|
50
|
+
- **Release artifacts and docs are aligned for v1.3.1.** Package metadata, generated bundles, `llms.txt`, and release-facing README examples now point at the current version.
|
|
49
51
|
|
|
50
52
|
The framework still ships **45+ components**, including the v1.2.7 additions below.
|
|
51
53
|
|
|
@@ -88,8 +90,8 @@ The quickest way to get started — no install, no build step. Add two lines to
|
|
|
88
90
|
|
|
89
91
|
**Pin to a specific version** for production:
|
|
90
92
|
```html
|
|
91
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.
|
|
92
|
-
<script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.
|
|
93
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.1/dist/vanduo.min.css">
|
|
94
|
+
<script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.3.1/dist/vanduo.min.js"></script>
|
|
93
95
|
<script>Vanduo.init();</script>
|
|
94
96
|
```
|
|
95
97
|
|
|
@@ -152,7 +154,7 @@ This project includes an [`llms.txt`](llms.txt) file — a structured markdown s
|
|
|
152
154
|
Use the hardened upload script to attach only approved bundle artifacts from `dist/`:
|
|
153
155
|
|
|
154
156
|
```bash
|
|
155
|
-
pnpm run release:assets -- v1.3.
|
|
157
|
+
pnpm run release:assets -- v1.3.1
|
|
156
158
|
```
|
|
157
159
|
|
|
158
160
|
Notes:
|
package/dist/build-info.json
CHANGED
package/dist/vanduo.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.3.
|
|
1
|
+
/*! Vanduo v1.3.1 | Built: 2026-03-20T21:48:40.922Z | git:7e73bb8 | development */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -132,7 +132,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
132
132
|
// js/vanduo.js
|
|
133
133
|
(function() {
|
|
134
134
|
"use strict";
|
|
135
|
-
const VANDUO_VERSION = true ? "1.3.
|
|
135
|
+
const VANDUO_VERSION = true ? "1.3.1" : "0.0.0-dev";
|
|
136
136
|
const Vanduo2 = {
|
|
137
137
|
version: VANDUO_VERSION,
|
|
138
138
|
components: {},
|
|
@@ -443,16 +443,17 @@ module.exports = __toCommonJS(index_exports);
|
|
|
443
443
|
}
|
|
444
444
|
const codeElement = activePane.querySelector("code") || activePane;
|
|
445
445
|
const code = codeElement.textContent;
|
|
446
|
+
let copySuccess;
|
|
446
447
|
try {
|
|
447
448
|
await navigator.clipboard.writeText(code);
|
|
448
|
-
|
|
449
|
+
copySuccess = true;
|
|
449
450
|
} catch (_err) {
|
|
450
|
-
|
|
451
|
-
this.showCopyFeedback(copyBtn, success);
|
|
451
|
+
copySuccess = this.fallbackCopy(code);
|
|
452
452
|
}
|
|
453
|
+
this.showCopyFeedback(copyBtn, copySuccess);
|
|
453
454
|
const event = new CustomEvent("codesnippet:copy", {
|
|
454
455
|
bubbles: true,
|
|
455
|
-
detail: { snippet, code, success:
|
|
456
|
+
detail: { snippet, code, success: copySuccess }
|
|
456
457
|
});
|
|
457
458
|
snippet.dispatchEvent(event);
|
|
458
459
|
},
|
|
@@ -889,9 +890,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
889
890
|
const Dropdown = {
|
|
890
891
|
// Store initialized dropdowns and their cleanup functions
|
|
891
892
|
instances: /* @__PURE__ */ new Map(),
|
|
892
|
-
// Typeahead state
|
|
893
|
-
_typeaheadBuffer: "",
|
|
894
|
-
_typeaheadTimer: null,
|
|
895
893
|
/**
|
|
896
894
|
* Initialize dropdown components
|
|
897
895
|
*/
|
|
@@ -955,7 +953,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
955
953
|
item.addEventListener("keydown", itemKeydownHandler);
|
|
956
954
|
cleanupFunctions.push(() => item.removeEventListener("keydown", itemKeydownHandler));
|
|
957
955
|
});
|
|
958
|
-
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions });
|
|
956
|
+
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions, typeaheadBuffer: "", typeaheadTimer: null });
|
|
959
957
|
},
|
|
960
958
|
/**
|
|
961
959
|
* Toggle dropdown
|
|
@@ -1084,16 +1082,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1084
1082
|
break;
|
|
1085
1083
|
default:
|
|
1086
1084
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
1087
|
-
|
|
1088
|
-
|
|
1085
|
+
const instance = this.instances.get(dropdown);
|
|
1086
|
+
if (!instance) break;
|
|
1087
|
+
clearTimeout(instance.typeaheadTimer);
|
|
1088
|
+
instance.typeaheadBuffer += e.key.toLowerCase();
|
|
1089
1089
|
const match = items.find(
|
|
1090
|
-
(item) => item.textContent.trim().toLowerCase().startsWith(
|
|
1090
|
+
(item) => item.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
|
|
1091
1091
|
);
|
|
1092
1092
|
if (match) {
|
|
1093
1093
|
match.focus();
|
|
1094
1094
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1095
|
+
instance.typeaheadTimer = setTimeout(() => {
|
|
1096
|
+
instance.typeaheadBuffer = "";
|
|
1097
1097
|
}, 500);
|
|
1098
1098
|
}
|
|
1099
1099
|
break;
|
|
@@ -1801,9 +1801,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1801
1801
|
}));
|
|
1802
1802
|
if (!this.img.complete) {
|
|
1803
1803
|
this.img.style.opacity = "0";
|
|
1804
|
-
this.
|
|
1804
|
+
this._imgLoadHandler = () => {
|
|
1805
1805
|
this.img.style.opacity = "";
|
|
1806
1806
|
};
|
|
1807
|
+
this.img.addEventListener("load", this._imgLoadHandler, { once: true });
|
|
1807
1808
|
}
|
|
1808
1809
|
},
|
|
1809
1810
|
/**
|
|
@@ -1822,6 +1823,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1822
1823
|
}
|
|
1823
1824
|
setTimeout(() => {
|
|
1824
1825
|
if (!this.isOpen) {
|
|
1826
|
+
if (this._imgLoadHandler) {
|
|
1827
|
+
this.img.removeEventListener("load", this._imgLoadHandler);
|
|
1828
|
+
this._imgLoadHandler = null;
|
|
1829
|
+
}
|
|
1825
1830
|
this.img.src = "";
|
|
1826
1831
|
this.img.alt = "";
|
|
1827
1832
|
}
|
|
@@ -1881,6 +1886,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1881
1886
|
zIndexCounter: 1050,
|
|
1882
1887
|
// Store trigger cleanup functions
|
|
1883
1888
|
_triggerCleanups: [],
|
|
1889
|
+
// Shared ESC key handler (installed once)
|
|
1890
|
+
_sharedEscHandler: null,
|
|
1884
1891
|
/**
|
|
1885
1892
|
* Initialize modals
|
|
1886
1893
|
*/
|
|
@@ -1945,16 +1952,17 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1945
1952
|
};
|
|
1946
1953
|
backdrop.addEventListener("click", backdropClickHandler);
|
|
1947
1954
|
cleanupFunctions.push(() => backdrop.removeEventListener("click", backdropClickHandler));
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1955
|
+
if (!this._sharedEscHandler) {
|
|
1956
|
+
this._sharedEscHandler = (e) => {
|
|
1957
|
+
if (e.key === "Escape" && this.openModals.length > 0) {
|
|
1958
|
+
const topModal = this.openModals[this.openModals.length - 1];
|
|
1959
|
+
if (topModal.dataset.keyboard !== "false") {
|
|
1960
|
+
this.close(topModal);
|
|
1961
|
+
}
|
|
1953
1962
|
}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
cleanupFunctions.push(() => document.removeEventListener("keydown", escKeyHandler));
|
|
1963
|
+
};
|
|
1964
|
+
document.addEventListener("keydown", this._sharedEscHandler);
|
|
1965
|
+
}
|
|
1958
1966
|
this.modals.set(modal, { backdrop, dialog, trapHandler: null, cleanup: cleanupFunctions });
|
|
1959
1967
|
},
|
|
1960
1968
|
/**
|
|
@@ -2134,6 +2142,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2134
2142
|
});
|
|
2135
2143
|
this._triggerCleanups.forEach((fn) => fn());
|
|
2136
2144
|
this._triggerCleanups = [];
|
|
2145
|
+
if (this._sharedEscHandler) {
|
|
2146
|
+
document.removeEventListener("keydown", this._sharedEscHandler);
|
|
2147
|
+
this._sharedEscHandler = null;
|
|
2148
|
+
}
|
|
2137
2149
|
}
|
|
2138
2150
|
};
|
|
2139
2151
|
if (typeof window.Vanduo !== "undefined") {
|
|
@@ -2299,7 +2311,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2299
2311
|
if (overlay) {
|
|
2300
2312
|
overlay.classList.add("is-active");
|
|
2301
2313
|
}
|
|
2302
|
-
document.body.
|
|
2314
|
+
document.body.classList.add("body-navbar-open");
|
|
2303
2315
|
toggle.setAttribute("aria-expanded", "true");
|
|
2304
2316
|
menu.setAttribute("aria-hidden", "false");
|
|
2305
2317
|
},
|
|
@@ -2316,7 +2328,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2316
2328
|
if (overlay) {
|
|
2317
2329
|
overlay.classList.remove("is-active");
|
|
2318
2330
|
}
|
|
2319
|
-
document.body.
|
|
2331
|
+
document.body.classList.remove("body-navbar-open");
|
|
2320
2332
|
const dropdownMenus = menu.querySelectorAll(".vd-navbar-dropdown-menu.is-open");
|
|
2321
2333
|
dropdownMenus.forEach((dropdownMenu) => {
|
|
2322
2334
|
dropdownMenu.classList.remove("is-open");
|
|
@@ -2875,9 +2887,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2875
2887
|
const Select = {
|
|
2876
2888
|
// Store initialized selects and their cleanup functions
|
|
2877
2889
|
instances: /* @__PURE__ */ new Map(),
|
|
2878
|
-
// Typeahead state
|
|
2879
|
-
_typeaheadBuffer: "",
|
|
2880
|
-
_typeaheadTimer: null,
|
|
2881
2890
|
/**
|
|
2882
2891
|
* Initialize select components
|
|
2883
2892
|
*/
|
|
@@ -2958,7 +2967,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2958
2967
|
};
|
|
2959
2968
|
select.addEventListener("change", changeHandler);
|
|
2960
2969
|
cleanupFunctions.push(() => select.removeEventListener("change", changeHandler));
|
|
2961
|
-
this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions });
|
|
2970
|
+
this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions, typeaheadBuffer: "", typeaheadTimer: null });
|
|
2962
2971
|
},
|
|
2963
2972
|
/**
|
|
2964
2973
|
* Build options in dropdown
|
|
@@ -3052,7 +3061,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3052
3061
|
* @param {HTMLElement} dropdown - Dropdown container
|
|
3053
3062
|
*/
|
|
3054
3063
|
updateSelectedOptions: function(select, dropdown) {
|
|
3055
|
-
const options = dropdown.querySelectorAll(".
|
|
3064
|
+
const options = dropdown.querySelectorAll(".custom-select-option");
|
|
3056
3065
|
const selectedValues = Array.from(select.selectedOptions).map((opt) => opt.value);
|
|
3057
3066
|
options.forEach((optionEl) => {
|
|
3058
3067
|
const value = optionEl.dataset.value;
|
|
@@ -3086,7 +3095,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3086
3095
|
openDropdown: function(button, dropdown) {
|
|
3087
3096
|
dropdown.classList.add("is-open");
|
|
3088
3097
|
button.setAttribute("aria-expanded", "true");
|
|
3089
|
-
const firstOption = dropdown.querySelector(".
|
|
3098
|
+
const firstOption = dropdown.querySelector(".custom-select-option:not(.is-disabled)");
|
|
3090
3099
|
if (firstOption) {
|
|
3091
3100
|
firstOption.focus();
|
|
3092
3101
|
}
|
|
@@ -3109,7 +3118,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3109
3118
|
*/
|
|
3110
3119
|
handleKeydown: function(e, select, button, dropdown) {
|
|
3111
3120
|
const isOpen = dropdown.classList.contains("is-open");
|
|
3112
|
-
const options = Array.from(dropdown.querySelectorAll(".
|
|
3121
|
+
const options = Array.from(dropdown.querySelectorAll(".custom-select-option:not(.is-disabled)"));
|
|
3113
3122
|
const currentIndex = options.findIndex((opt) => opt === document.activeElement);
|
|
3114
3123
|
switch (e.key) {
|
|
3115
3124
|
case "Enter":
|
|
@@ -3160,16 +3169,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3160
3169
|
break;
|
|
3161
3170
|
default:
|
|
3162
3171
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
3163
|
-
|
|
3164
|
-
|
|
3172
|
+
const instance = this.instances.get(select);
|
|
3173
|
+
if (!instance) break;
|
|
3174
|
+
clearTimeout(instance.typeaheadTimer);
|
|
3175
|
+
instance.typeaheadBuffer += e.key.toLowerCase();
|
|
3165
3176
|
const match = options.find(
|
|
3166
|
-
(opt) => opt.textContent.trim().toLowerCase().startsWith(
|
|
3177
|
+
(opt) => opt.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
|
|
3167
3178
|
);
|
|
3168
3179
|
if (match) {
|
|
3169
3180
|
match.focus();
|
|
3170
3181
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3182
|
+
instance.typeaheadTimer = setTimeout(() => {
|
|
3183
|
+
instance.typeaheadBuffer = "";
|
|
3173
3184
|
}, 500);
|
|
3174
3185
|
}
|
|
3175
3186
|
break;
|
|
@@ -3201,7 +3212,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3201
3212
|
if (element.id) {
|
|
3202
3213
|
return element.id;
|
|
3203
3214
|
}
|
|
3204
|
-
|
|
3215
|
+
const id = "select-" + Math.random().toString(36).substr(2, 9);
|
|
3216
|
+
element.id = id;
|
|
3217
|
+
return id;
|
|
3205
3218
|
},
|
|
3206
3219
|
/**
|
|
3207
3220
|
* Destroy a select instance and clean up event listeners
|
|
@@ -4255,7 +4268,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4255
4268
|
this.applyNeutral(this.DEFAULTS.NEUTRAL);
|
|
4256
4269
|
this.applyRadius(this.DEFAULTS.RADIUS);
|
|
4257
4270
|
this.applyFont(this.DEFAULTS.FONT);
|
|
4258
|
-
this.applyTheme(this.DEFAULTS.THEME);
|
|
4259
4271
|
this.updateUI();
|
|
4260
4272
|
this.dispatchEvent("reset", { state: { ...this.state } });
|
|
4261
4273
|
},
|
|
@@ -7040,6 +7052,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7040
7052
|
// js/components/suggest.js
|
|
7041
7053
|
(function() {
|
|
7042
7054
|
"use strict";
|
|
7055
|
+
function _escapeHtml(text) {
|
|
7056
|
+
const div = document.createElement("div");
|
|
7057
|
+
div.textContent = text;
|
|
7058
|
+
return div.innerHTML;
|
|
7059
|
+
}
|
|
7043
7060
|
const Suggest = {
|
|
7044
7061
|
instances: /* @__PURE__ */ new Map(),
|
|
7045
7062
|
init: function() {
|
|
@@ -7099,8 +7116,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7099
7116
|
li.id = listId + "-item-" + i;
|
|
7100
7117
|
const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
|
|
7101
7118
|
if (query) {
|
|
7119
|
+
const escaped = _escapeHtml(text);
|
|
7102
7120
|
const re = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
|
|
7103
|
-
li.innerHTML =
|
|
7121
|
+
li.innerHTML = escaped.replace(re, '<span class="vd-suggest-match">$1</span>');
|
|
7104
7122
|
} else {
|
|
7105
7123
|
li.textContent = text;
|
|
7106
7124
|
}
|
|
@@ -7253,14 +7271,20 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7253
7271
|
maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
|
|
7254
7272
|
pattern: (value, param) => {
|
|
7255
7273
|
try {
|
|
7274
|
+
if (param.length > 100) return false;
|
|
7256
7275
|
return new RegExp(param).test(value);
|
|
7257
7276
|
} catch (_e) {
|
|
7258
7277
|
return false;
|
|
7259
7278
|
}
|
|
7260
7279
|
},
|
|
7261
7280
|
match: (value, param) => {
|
|
7262
|
-
|
|
7263
|
-
|
|
7281
|
+
try {
|
|
7282
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(param) : param;
|
|
7283
|
+
const other = document.querySelector('[name="' + escaped + '"]');
|
|
7284
|
+
return other ? value === other.value : false;
|
|
7285
|
+
} catch (_e) {
|
|
7286
|
+
return false;
|
|
7287
|
+
}
|
|
7264
7288
|
}
|
|
7265
7289
|
},
|
|
7266
7290
|
messages: {
|