@vanduo-oss/framework 1.2.3 → 1.2.5
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 +47 -82
- package/css/utilities/table.css +7 -2
- package/dist/build-info.json +4 -4
- package/dist/vanduo.cjs.js +240 -31
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +5 -10
- package/dist/vanduo.cjs.min.js.map +3 -3
- package/dist/vanduo.css +2 -2
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +240 -31
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +5 -10
- package/dist/vanduo.esm.min.js.map +3 -3
- package/dist/vanduo.js +240 -31
- package/dist/vanduo.js.map +3 -3
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +5 -10
- package/dist/vanduo.min.js.map +3 -3
- package/js/components/theme-customizer.js +2 -30
- package/js/vanduo.js +4 -3
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Vanduo Framework v1.2.
|
|
1
|
+
# Vanduo Framework v1.2.5
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="vanduo-banner.svg" alt="Vanduo Framework Banner" width="100%">
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
A lightweight, pure HTML/CSS/JS framework for designing beautiful interfaces. Zero runtime dependencies, no mandatory build tools, just clean and simple code.
|
|
23
23
|
|
|
24
|
-
[**Browse Full Documentation
|
|
24
|
+
[**Browse Full Documentation →**](https://vanduo.dev/#docs)
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
@@ -35,110 +35,75 @@ A lightweight, pure HTML/CSS/JS framework for designing beautiful interfaces. Ze
|
|
|
35
35
|
- 🎛️ **Theme Customizer** - Real-time color, radius, font, and mode customization
|
|
36
36
|
- 🔍 **SEO-Ready** - Comprehensive meta tags, structured data, and sitemap
|
|
37
37
|
|
|
38
|
-
### The Vanduo Way
|
|
39
|
-
Stop wrapping everything in bloated container DOMs. Build beautiful, accessible UI components with clean, predictable utility classes:
|
|
40
|
-
|
|
41
|
-
```html
|
|
42
|
-
<!-- Raw HTML -->
|
|
43
|
-
<button>Click Me</button>
|
|
44
|
-
|
|
45
|
-
<!-- With Vanduo Framework -->
|
|
46
|
-
<button class="vd-btn vd-btn-primary vd-radius-full">
|
|
47
|
-
<i class="ph ph-sparkle"></i> Click Me
|
|
48
|
-
</button>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
38
|
---
|
|
52
39
|
|
|
53
40
|
## Quick Start
|
|
54
41
|
|
|
55
|
-
### Option 1:
|
|
42
|
+
### Option 1: CDN (Recommended)
|
|
56
43
|
|
|
57
|
-
|
|
44
|
+
The quickest way to get started — no install, no build step. Add two lines to any HTML file:
|
|
58
45
|
|
|
59
|
-
```
|
|
60
|
-
|
|
46
|
+
```html
|
|
47
|
+
<!-- Vanduo CSS -->
|
|
48
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@main/dist/vanduo.min.css">
|
|
49
|
+
|
|
50
|
+
<!-- Vanduo JS -->
|
|
51
|
+
<script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@main/dist/vanduo.min.js"></script>
|
|
52
|
+
<script>Vanduo.init();</script>
|
|
61
53
|
```
|
|
62
54
|
|
|
63
|
-
|
|
55
|
+
**Pin to a specific version** for production:
|
|
56
|
+
```html
|
|
57
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.2.5/dist/vanduo.min.css">
|
|
58
|
+
<script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@v1.2.5/dist/vanduo.min.js"></script>
|
|
59
|
+
<script>Vanduo.init();</script>
|
|
60
|
+
```
|
|
64
61
|
|
|
65
|
-
### Option 2:
|
|
62
|
+
### Option 2: Download
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
[**Download the dist/ folder**](https://github.com/vanduo-oss/framework/tree/main/dist) and include locally — no internet connection required at runtime:
|
|
68
65
|
|
|
69
66
|
```html
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
<meta charset="UTF-8">
|
|
74
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
75
|
-
<title>My Website</title>
|
|
76
|
-
<!-- Vanduo CSS via CDN -->
|
|
77
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@main/dist/vanduo.min.css">
|
|
78
|
-
</head>
|
|
79
|
-
<body>
|
|
80
|
-
<!-- Your content here -->
|
|
81
|
-
|
|
82
|
-
<!-- Vanduo JS via CDN -->
|
|
83
|
-
<script src="https://cdn.jsdelivr.net/gh/vanduo-oss/framework@main/dist/vanduo.min.js"></script>
|
|
84
|
-
<script>Vanduo.init();</script>
|
|
85
|
-
</body>
|
|
86
|
-
</html>
|
|
67
|
+
<link rel="stylesheet" href="dist/vanduo.min.css">
|
|
68
|
+
<script src="dist/vanduo.min.js"></script>
|
|
69
|
+
<script>Vanduo.init();</script>
|
|
87
70
|
```
|
|
88
71
|
|
|
89
|
-
|
|
72
|
+
The `dist/` folder is **self-contained** (CSS, JS, Fonts, Icons).
|
|
73
|
+
|
|
74
|
+
### Option 3: Source Files
|
|
75
|
+
|
|
76
|
+
For development or when you need more control, use the unminified source:
|
|
77
|
+
|
|
90
78
|
```html
|
|
91
|
-
<link rel="stylesheet" href="
|
|
92
|
-
<script src="
|
|
79
|
+
<link rel="stylesheet" href="css/vanduo.css">
|
|
80
|
+
<script src="js/vanduo.js"></script>
|
|
93
81
|
<script>Vanduo.init();</script>
|
|
94
82
|
```
|
|
95
83
|
|
|
84
|
+
### Option 4: With a Bundler (Vite)
|
|
96
85
|
|
|
97
|
-
|
|
86
|
+
> **Requires a build tool.** The imports below use bare module specifiers (`@vanduo-oss/framework`) which browsers cannot resolve on their own. For static HTML files, use the CDN or Download options above.
|
|
98
87
|
|
|
99
|
-
|
|
88
|
+
Scaffold a Vite project and install Vanduo:
|
|
100
89
|
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<meta charset="UTF-8">
|
|
106
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
107
|
-
<title>My Website</title>
|
|
108
|
-
<link rel="stylesheet" href="dist/vanduo.min.css">
|
|
109
|
-
</head>
|
|
110
|
-
<body>
|
|
111
|
-
<!-- Your content here -->
|
|
112
|
-
|
|
113
|
-
<script src="dist/vanduo.min.js"></script>
|
|
114
|
-
<script>Vanduo.init();</script>
|
|
115
|
-
</body>
|
|
116
|
-
</html>
|
|
90
|
+
```bash
|
|
91
|
+
pnpm create vite my-app --template vanilla
|
|
92
|
+
cd my-app
|
|
93
|
+
pnpm add @vanduo-oss/framework
|
|
117
94
|
```
|
|
118
95
|
|
|
119
|
-
|
|
96
|
+
Import in your entry file (e.g. `main.js`):
|
|
120
97
|
|
|
121
|
-
|
|
98
|
+
```js
|
|
99
|
+
import '@vanduo-oss/framework/css';
|
|
100
|
+
import { Vanduo } from '@vanduo-oss/framework';
|
|
101
|
+
Vanduo.init();
|
|
102
|
+
```
|
|
122
103
|
|
|
123
|
-
|
|
104
|
+
**Why pnpm?** pnpm enforces a strict lockfile and creates an isolated `node_modules` structure. Vanduo's `.npmrc` security policies work best with pnpm out of the box.
|
|
124
105
|
|
|
125
|
-
|
|
126
|
-
<!DOCTYPE html>
|
|
127
|
-
<html lang="en">
|
|
128
|
-
<head>
|
|
129
|
-
<meta charset="UTF-8">
|
|
130
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
131
|
-
<title>My Website</title>
|
|
132
|
-
<link rel="stylesheet" href="css/vanduo.css">
|
|
133
|
-
</head>
|
|
134
|
-
<body>
|
|
135
|
-
<!-- Your content here -->
|
|
136
|
-
|
|
137
|
-
<script src="js/vanduo.js"></script>
|
|
138
|
-
<script>Vanduo.init();</script>
|
|
139
|
-
</body>
|
|
140
|
-
</html>
|
|
141
|
-
```
|
|
106
|
+
*(Note: `npm install @vanduo-oss/framework` and `yarn add @vanduo-oss/framework` will also work, but they do not enforce the same strict lockfile and isolated `node_modules` security guarantees.)*
|
|
142
107
|
|
|
143
108
|
---
|
|
144
109
|
|
|
@@ -153,7 +118,7 @@ This project includes an [`llms.txt`](llms.txt) file — a structured markdown s
|
|
|
153
118
|
Use the hardened upload script to attach only approved bundle artifacts from `dist/`:
|
|
154
119
|
|
|
155
120
|
```bash
|
|
156
|
-
pnpm run release:assets -- v1.2.
|
|
121
|
+
pnpm run release:assets -- v1.2.5
|
|
157
122
|
```
|
|
158
123
|
|
|
159
124
|
Notes:
|
|
@@ -166,7 +131,7 @@ Notes:
|
|
|
166
131
|
|
|
167
132
|
Comprehensive documentation for all components, utilities, and customization options is available at vanduo.dev.
|
|
168
133
|
|
|
169
|
-
[**View Documentation**](
|
|
134
|
+
[**View Documentation**](https://vanduo.dev/#docs)
|
|
170
135
|
|
|
171
136
|
### Key Capabilities
|
|
172
137
|
|
package/css/utilities/table.css
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
.table th,
|
|
38
38
|
.table td {
|
|
39
39
|
padding: var(--table-padding-y) var(--table-padding-x);
|
|
40
|
-
vertical-align:
|
|
40
|
+
vertical-align: middle;
|
|
41
41
|
border-top: 1px solid var(--table-border-color);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -217,6 +217,7 @@ tfoot {
|
|
|
217
217
|
|
|
218
218
|
/* Responsive Breakpoints */
|
|
219
219
|
@media (max-width: 575.98px) {
|
|
220
|
+
|
|
220
221
|
.vd-table-responsive-sm,
|
|
221
222
|
.table-responsive-sm {
|
|
222
223
|
display: block;
|
|
@@ -233,6 +234,7 @@ tfoot {
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
@media (max-width: 767.98px) {
|
|
237
|
+
|
|
236
238
|
.vd-table-responsive-md,
|
|
237
239
|
.table-responsive-md {
|
|
238
240
|
display: block;
|
|
@@ -249,6 +251,7 @@ tfoot {
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
@media (max-width: 991.98px) {
|
|
254
|
+
|
|
252
255
|
.vd-table-responsive-lg,
|
|
253
256
|
.table-responsive-lg {
|
|
254
257
|
display: block;
|
|
@@ -265,6 +268,7 @@ tfoot {
|
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
@media (max-width: 1199.98px) {
|
|
271
|
+
|
|
268
272
|
.vd-table-responsive-xl,
|
|
269
273
|
.table-responsive-xl {
|
|
270
274
|
display: block;
|
|
@@ -345,6 +349,7 @@ tfoot {
|
|
|
345
349
|
|
|
346
350
|
/* Dark mode support for system preference */
|
|
347
351
|
@media (prefers-color-scheme: dark) {
|
|
352
|
+
|
|
348
353
|
:root:not([data-theme]) .vd-table,
|
|
349
354
|
:root:not([data-theme]) .table {
|
|
350
355
|
--table-bg: var(--bg-primary);
|
|
@@ -378,4 +383,4 @@ tfoot {
|
|
|
378
383
|
:root:not([data-theme]) thead {
|
|
379
384
|
background-color: var(--bg-secondary);
|
|
380
385
|
}
|
|
381
|
-
}
|
|
386
|
+
}
|
package/dist/build-info.json
CHANGED
package/dist/vanduo.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.2.
|
|
1
|
+
/*! Vanduo v1.2.5 | Built: 2026-03-08T10:52:48.224Z | git:f37c545 | development */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -132,8 +132,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
132
132
|
// js/vanduo.js
|
|
133
133
|
(function() {
|
|
134
134
|
"use strict";
|
|
135
|
+
const VANDUO_VERSION = true ? "1.2.5" : "0.0.0-dev";
|
|
135
136
|
const Vanduo2 = {
|
|
136
|
-
version:
|
|
137
|
+
version: VANDUO_VERSION,
|
|
137
138
|
components: {},
|
|
138
139
|
/**
|
|
139
140
|
* Initialize framework
|
|
@@ -168,7 +169,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
});
|
|
171
|
-
console.log("Vanduo Framework
|
|
172
|
+
console.log("Vanduo Framework v" + this.version + " initialized");
|
|
172
173
|
},
|
|
173
174
|
/**
|
|
174
175
|
* Register a component
|
|
@@ -4036,12 +4037,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4036
4037
|
this.updateUI();
|
|
4037
4038
|
});
|
|
4038
4039
|
}
|
|
4039
|
-
this.elements.panel.querySelectorAll("[data-mode]").forEach((btn) => {
|
|
4040
|
-
this.addListener(btn, "click", () => {
|
|
4041
|
-
this.applyTheme(btn.dataset.mode);
|
|
4042
|
-
this.updateUI();
|
|
4043
|
-
});
|
|
4044
|
-
});
|
|
4045
4040
|
const resetBtn = this.elements.panel.querySelector(".customizer-reset");
|
|
4046
4041
|
if (resetBtn) {
|
|
4047
4042
|
this.addListener(resetBtn, "click", () => {
|
|
@@ -4092,15 +4087,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4092
4087
|
for (const [key, value] of Object.entries(this.FONT_OPTIONS)) {
|
|
4093
4088
|
fontOptions += `<option value="${esc(key)}"${key === this.state.font ? " selected" : ""}>${esc(value.name)}</option>`;
|
|
4094
4089
|
}
|
|
4095
|
-
const modeIcons = {
|
|
4096
|
-
"system": "ph-desktop",
|
|
4097
|
-
"dark": "ph-moon",
|
|
4098
|
-
"light": "ph-sun"
|
|
4099
|
-
};
|
|
4100
|
-
let modeButtons = "";
|
|
4101
|
-
this.THEME_MODES.forEach((mode) => {
|
|
4102
|
-
modeButtons += `<button class="tc-mode-btn${mode === this.state.theme ? " is-active" : ""}" data-mode="${mode}"><i class="ph ${modeIcons[mode]}"></i><span>${mode.charAt(0).toUpperCase() + mode.slice(1)}</span></button>`;
|
|
4103
|
-
});
|
|
4104
4090
|
return `
|
|
4105
4091
|
<div class="tc-header">
|
|
4106
4092
|
<h3 class="tc-title">Customize Theme</h3>
|
|
@@ -4109,12 +4095,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4109
4095
|
</button>
|
|
4110
4096
|
</div>
|
|
4111
4097
|
<div class="tc-body">
|
|
4112
|
-
|
|
4113
|
-
<label class="tc-label">Color Mode</label>
|
|
4114
|
-
<div class="tc-mode-group">
|
|
4115
|
-
${modeButtons}
|
|
4116
|
-
</div>
|
|
4117
|
-
</div>
|
|
4098
|
+
|
|
4118
4099
|
<div class="tc-section">
|
|
4119
4100
|
<label class="tc-label">Primary Color</label>
|
|
4120
4101
|
<div class="tc-color-grid">
|
|
@@ -4250,9 +4231,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4250
4231
|
if (fontSelect) {
|
|
4251
4232
|
fontSelect.value = this.state.font;
|
|
4252
4233
|
}
|
|
4253
|
-
this.elements.panel.querySelectorAll("[data-mode]").forEach((btn) => {
|
|
4254
|
-
btn.classList.toggle("is-active", btn.dataset.mode === this.state.theme);
|
|
4255
|
-
});
|
|
4256
4234
|
},
|
|
4257
4235
|
/**
|
|
4258
4236
|
* Reset all preferences to defaults
|
|
@@ -5688,12 +5666,12 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5688
5666
|
const touchMoveHandler = (e) => {
|
|
5689
5667
|
this.handleTouchMove(e, element);
|
|
5690
5668
|
};
|
|
5691
|
-
element.addEventListener("touchmove", touchMoveHandler);
|
|
5669
|
+
element.addEventListener("touchmove", touchMoveHandler, { passive: false });
|
|
5692
5670
|
cleanupFunctions.push(() => element.removeEventListener("touchmove", touchMoveHandler));
|
|
5693
5671
|
const touchEndHandler = (e) => {
|
|
5694
5672
|
this.handleTouchEnd(e, element);
|
|
5695
5673
|
};
|
|
5696
|
-
element.addEventListener("touchend", touchEndHandler);
|
|
5674
|
+
element.addEventListener("touchend", touchEndHandler, { passive: false });
|
|
5697
5675
|
cleanupFunctions.push(() => element.removeEventListener("touchend", touchEndHandler));
|
|
5698
5676
|
const touchCancelHandler = (e) => {
|
|
5699
5677
|
this.handleTouchEnd(e, element);
|
|
@@ -5899,7 +5877,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5899
5877
|
const deltaX = touch.clientX - this.touchState.startX;
|
|
5900
5878
|
const deltaY = touch.clientY - this.touchState.startY;
|
|
5901
5879
|
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
|
5902
|
-
e.preventDefault();
|
|
5880
|
+
if (e.cancelable) e.preventDefault();
|
|
5903
5881
|
if (!this.touchState.isDragging) {
|
|
5904
5882
|
this.touchState.isDragging = true;
|
|
5905
5883
|
element.classList.add("is-dragging");
|
|
@@ -5944,7 +5922,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5944
5922
|
*/
|
|
5945
5923
|
handleTouchEnd: function(e, element) {
|
|
5946
5924
|
if (this.touchState && this.touchState.isDragging) {
|
|
5947
|
-
e.preventDefault();
|
|
5925
|
+
if (e.cancelable) e.preventDefault();
|
|
5948
5926
|
element.classList.remove("is-dragging");
|
|
5949
5927
|
element.classList.add("is-dropped");
|
|
5950
5928
|
element.setAttribute("aria-grabbed", "false");
|
|
@@ -6172,6 +6150,237 @@ module.exports = __toCommonJS(index_exports);
|
|
|
6172
6150
|
window.VanduoDraggable = Draggable;
|
|
6173
6151
|
})();
|
|
6174
6152
|
|
|
6153
|
+
// js/components/lazy-load.js
|
|
6154
|
+
(function() {
|
|
6155
|
+
"use strict";
|
|
6156
|
+
const _observerMap = /* @__PURE__ */ new Map();
|
|
6157
|
+
function _isSafeUrl(url) {
|
|
6158
|
+
try {
|
|
6159
|
+
const resolved = new URL(url, window.location.href);
|
|
6160
|
+
return resolved.origin === window.location.origin;
|
|
6161
|
+
} catch (_) {
|
|
6162
|
+
return false;
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
function _safeInjectHtml(containerEl, html) {
|
|
6166
|
+
const parser = new DOMParser();
|
|
6167
|
+
const doc = parser.parseFromString(html.trim(), "text/html");
|
|
6168
|
+
const DANGEROUS_TAGS = ["SCRIPT", "IFRAME", "OBJECT", "EMBED", "FORM", "BASE", "LINK", "META", "STYLE"];
|
|
6169
|
+
for (const tag of DANGEROUS_TAGS) {
|
|
6170
|
+
const els = doc.querySelectorAll(tag);
|
|
6171
|
+
for (let i = els.length - 1; i >= 0; i--) {
|
|
6172
|
+
els[i].parentNode.removeChild(els[i]);
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
function _sanitizeNode(node) {
|
|
6176
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
6177
|
+
const attrs = node.attributes;
|
|
6178
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
6179
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
6180
|
+
const attrValue = attrs[i].value.toLowerCase();
|
|
6181
|
+
const trimmedValue = attrValue.trim();
|
|
6182
|
+
if (attrName.startsWith("on") || trimmedValue.startsWith("javascript:") || trimmedValue.startsWith("data:") || trimmedValue.startsWith("vbscript:")) {
|
|
6183
|
+
node.removeAttribute(attrs[i].name);
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
const children = node.childNodes;
|
|
6187
|
+
for (let i = 0; i < children.length; i++) {
|
|
6188
|
+
_sanitizeNode(children[i]);
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
_sanitizeNode(doc.body);
|
|
6193
|
+
const nodes = Array.from(doc.body.childNodes);
|
|
6194
|
+
while (containerEl.firstChild) {
|
|
6195
|
+
containerEl.removeChild(containerEl.firstChild);
|
|
6196
|
+
}
|
|
6197
|
+
nodes.forEach(function(node) {
|
|
6198
|
+
containerEl.appendChild(document.adoptNode(node));
|
|
6199
|
+
});
|
|
6200
|
+
}
|
|
6201
|
+
function _skeletonHtml() {
|
|
6202
|
+
return '<div class="vd-skeleton-card" style="position:relative;min-height:200px;padding:2rem;overflow:hidden;"><div class="vd-skeleton vd-skeleton-heading-lg" style="margin-bottom:1.5rem;"></div><div class="vd-skeleton vd-skeleton-paragraph"><div class="vd-skeleton vd-skeleton-text"></div><div class="vd-skeleton vd-skeleton-text"></div><div class="vd-skeleton vd-skeleton-text"></div></div><div class="vd-dynamic-loader" style="position:absolute;inset:0;"><div class="vd-dynamic-loader-grid"><div class="vd-spinner vd-spinner-sm vd-spinner-success" style="animation-delay:0s;"></div><div class="vd-spinner vd-spinner-sm vd-spinner-warning" style="animation-delay:-0.15s;"></div><div class="vd-spinner vd-spinner-sm vd-spinner-error" style="animation-delay:-0.3s;"></div><div class="vd-spinner vd-spinner-sm vd-spinner-info" style="animation-delay:-0.45s;"></div></div><span class="vd-dynamic-loader-text">Loading\u2026</span></div></div>';
|
|
6203
|
+
}
|
|
6204
|
+
function _spinnerHtml() {
|
|
6205
|
+
return '<div class="vd-dynamic-loader" style="min-height:180px;display:flex;align-items:center;justify-content:center;"><div class="vd-dynamic-loader-grid"><div class="vd-spinner vd-spinner-sm vd-spinner-success" style="animation-delay:0s;"></div><div class="vd-spinner vd-spinner-sm vd-spinner-warning" style="animation-delay:-0.15s;"></div><div class="vd-spinner vd-spinner-sm vd-spinner-error" style="animation-delay:-0.3s;"></div><div class="vd-spinner vd-spinner-sm vd-spinner-info" style="animation-delay:-0.45s;"></div></div><span class="vd-dynamic-loader-text">Loading\u2026</span></div>';
|
|
6206
|
+
}
|
|
6207
|
+
function _resolvePlaceholder(placeholder) {
|
|
6208
|
+
if (!placeholder || placeholder === "skeleton") return _skeletonHtml();
|
|
6209
|
+
if (placeholder === "spinner") return _spinnerHtml();
|
|
6210
|
+
return placeholder;
|
|
6211
|
+
}
|
|
6212
|
+
function _dispatch(el, eventName, detail) {
|
|
6213
|
+
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: detail || {} }));
|
|
6214
|
+
}
|
|
6215
|
+
const VanduoLazyLoad = {
|
|
6216
|
+
/* ─────────────────────────────────────────────────
|
|
6217
|
+
* LOW-LEVEL API
|
|
6218
|
+
* ───────────────────────────────────────────────── */
|
|
6219
|
+
/**
|
|
6220
|
+
* Observe an element. `callback` is invoked once when the element
|
|
6221
|
+
* enters the viewport, then the element is automatically unobserved.
|
|
6222
|
+
*
|
|
6223
|
+
* @param {Element} element
|
|
6224
|
+
* @param {function(Element): void} callback
|
|
6225
|
+
* @param {{ threshold?: number, rootMargin?: string }} [options]
|
|
6226
|
+
*/
|
|
6227
|
+
observe: function(element, callback, options) {
|
|
6228
|
+
if (!(element instanceof Element)) {
|
|
6229
|
+
console.warn("[VanduoLazyLoad] observe() requires a DOM Element.");
|
|
6230
|
+
return;
|
|
6231
|
+
}
|
|
6232
|
+
if (typeof callback !== "function") {
|
|
6233
|
+
console.warn("[VanduoLazyLoad] observe() requires a callback function.");
|
|
6234
|
+
return;
|
|
6235
|
+
}
|
|
6236
|
+
if (_observerMap.has(element)) return;
|
|
6237
|
+
const threshold = options && options.threshold != null ? options.threshold : 0;
|
|
6238
|
+
const rootMargin = options && options.rootMargin ? options.rootMargin : "0px";
|
|
6239
|
+
const observer = new IntersectionObserver(function(entries, obs) {
|
|
6240
|
+
entries.forEach(function(entry) {
|
|
6241
|
+
if (entry.isIntersecting) {
|
|
6242
|
+
obs.unobserve(entry.target);
|
|
6243
|
+
_observerMap.delete(entry.target);
|
|
6244
|
+
try {
|
|
6245
|
+
callback(entry.target);
|
|
6246
|
+
} catch (e) {
|
|
6247
|
+
console.error("[VanduoLazyLoad] Callback threw:", e);
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6250
|
+
});
|
|
6251
|
+
}, { threshold, rootMargin });
|
|
6252
|
+
_observerMap.set(element, observer);
|
|
6253
|
+
observer.observe(element);
|
|
6254
|
+
},
|
|
6255
|
+
/**
|
|
6256
|
+
* Stop observing an element that was previously passed to observe().
|
|
6257
|
+
* @param {Element} element
|
|
6258
|
+
*/
|
|
6259
|
+
unobserve: function(element) {
|
|
6260
|
+
const observer = _observerMap.get(element);
|
|
6261
|
+
if (observer) {
|
|
6262
|
+
observer.unobserve(element);
|
|
6263
|
+
_observerMap.delete(element);
|
|
6264
|
+
}
|
|
6265
|
+
},
|
|
6266
|
+
/**
|
|
6267
|
+
* Stop observing ALL currently observed elements.
|
|
6268
|
+
*/
|
|
6269
|
+
unobserveAll: function() {
|
|
6270
|
+
_observerMap.forEach(function(observer, element) {
|
|
6271
|
+
observer.unobserve(element);
|
|
6272
|
+
});
|
|
6273
|
+
_observerMap.clear();
|
|
6274
|
+
},
|
|
6275
|
+
/* ─────────────────────────────────────────────────
|
|
6276
|
+
* HIGH-LEVEL API
|
|
6277
|
+
* ───────────────────────────────────────────────── */
|
|
6278
|
+
/**
|
|
6279
|
+
* Fetch an HTML partial and inject it into `containerEl` when the
|
|
6280
|
+
* container enters the viewport. A placeholder is shown immediately.
|
|
6281
|
+
*
|
|
6282
|
+
* @param {string} url URL of the HTML partial to fetch
|
|
6283
|
+
* @param {Element} containerEl Target element whose content will be replaced
|
|
6284
|
+
* @param {{
|
|
6285
|
+
* placeholder?: 'skeleton'|'spinner'|string,
|
|
6286
|
+
* threshold?: number,
|
|
6287
|
+
* rootMargin?: string,
|
|
6288
|
+
* onLoaded?: function(Element): void,
|
|
6289
|
+
* onError?: function(Error): void
|
|
6290
|
+
* }} [options]
|
|
6291
|
+
*/
|
|
6292
|
+
loadSection: function(url, containerEl, options) {
|
|
6293
|
+
if (typeof url !== "string" || !url) {
|
|
6294
|
+
console.warn("[VanduoLazyLoad] loadSection() requires a non-empty URL string.");
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
if (!(containerEl instanceof Element)) {
|
|
6298
|
+
console.warn("[VanduoLazyLoad] loadSection() requires a DOM Element as containerEl.");
|
|
6299
|
+
return;
|
|
6300
|
+
}
|
|
6301
|
+
if (!_isSafeUrl(url)) {
|
|
6302
|
+
console.error("[VanduoLazyLoad] loadSection() blocked cross-origin URL:", url);
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
6305
|
+
const opts = options || {};
|
|
6306
|
+
const placeholderHtml = _resolvePlaceholder(opts.placeholder);
|
|
6307
|
+
_safeInjectHtml(containerEl, placeholderHtml);
|
|
6308
|
+
_dispatch(containerEl, "lazysection:loading", { url });
|
|
6309
|
+
this.observe(containerEl, function() {
|
|
6310
|
+
const controller = new window.AbortController();
|
|
6311
|
+
const timeoutId = setTimeout(function() {
|
|
6312
|
+
controller.abort();
|
|
6313
|
+
}, 1e4);
|
|
6314
|
+
window.fetch(url, { signal: controller.signal }).then(function(res) {
|
|
6315
|
+
clearTimeout(timeoutId);
|
|
6316
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
6317
|
+
return res.text();
|
|
6318
|
+
}).then(function(html) {
|
|
6319
|
+
_safeInjectHtml(containerEl, html);
|
|
6320
|
+
_dispatch(containerEl, "lazysection:loaded", { url });
|
|
6321
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
6322
|
+
window.Vanduo.init();
|
|
6323
|
+
}
|
|
6324
|
+
if (typeof opts.onLoaded === "function") {
|
|
6325
|
+
opts.onLoaded(containerEl);
|
|
6326
|
+
}
|
|
6327
|
+
}).catch(function(err) {
|
|
6328
|
+
const alertEl = document.createElement("div");
|
|
6329
|
+
alertEl.className = "vd-alert vd-alert-error";
|
|
6330
|
+
alertEl.setAttribute("role", "alert");
|
|
6331
|
+
const msgEl = document.createElement("span");
|
|
6332
|
+
msgEl.textContent = "Failed to load content. ";
|
|
6333
|
+
const detailEl = document.createElement("small");
|
|
6334
|
+
detailEl.style.opacity = "0.7";
|
|
6335
|
+
detailEl.textContent = err.message;
|
|
6336
|
+
alertEl.appendChild(msgEl);
|
|
6337
|
+
alertEl.appendChild(detailEl);
|
|
6338
|
+
while (containerEl.firstChild) {
|
|
6339
|
+
containerEl.removeChild(containerEl.firstChild);
|
|
6340
|
+
}
|
|
6341
|
+
containerEl.appendChild(alertEl);
|
|
6342
|
+
_dispatch(containerEl, "lazysection:error", { url, error: err });
|
|
6343
|
+
console.error("[VanduoLazyLoad] loadSection failed:", err);
|
|
6344
|
+
if (typeof opts.onError === "function") {
|
|
6345
|
+
opts.onError(err);
|
|
6346
|
+
}
|
|
6347
|
+
});
|
|
6348
|
+
}, { threshold: opts.threshold, rootMargin: opts.rootMargin });
|
|
6349
|
+
},
|
|
6350
|
+
/* ─────────────────────────────────────────────────
|
|
6351
|
+
* ATTRIBUTE-DRIVEN INIT
|
|
6352
|
+
* ───────────────────────────────────────────────── */
|
|
6353
|
+
/**
|
|
6354
|
+
* Scan the DOM for [data-vd-lazy] elements and wire them up.
|
|
6355
|
+
* Safe to call multiple times — already-observed elements are skipped.
|
|
6356
|
+
*/
|
|
6357
|
+
init: function() {
|
|
6358
|
+
const self = this;
|
|
6359
|
+
const elements = document.querySelectorAll("[data-vd-lazy]");
|
|
6360
|
+
elements.forEach(function(el) {
|
|
6361
|
+
if (_observerMap.has(el) || el.dataset.vdLazyState === "loading" || el.dataset.vdLazyState === "loaded") return;
|
|
6362
|
+
const url = el.getAttribute("data-vd-lazy");
|
|
6363
|
+
if (!url) return;
|
|
6364
|
+
el.dataset.vdLazyState = "loading";
|
|
6365
|
+
const placeholder = el.getAttribute("data-vd-lazy-placeholder") || "skeleton";
|
|
6366
|
+
self.loadSection(url, el, {
|
|
6367
|
+
placeholder,
|
|
6368
|
+
onLoaded: function() {
|
|
6369
|
+
el.dataset.vdLazyState = "loaded";
|
|
6370
|
+
},
|
|
6371
|
+
onError: function() {
|
|
6372
|
+
el.dataset.vdLazyState = "error";
|
|
6373
|
+
}
|
|
6374
|
+
});
|
|
6375
|
+
});
|
|
6376
|
+
}
|
|
6377
|
+
};
|
|
6378
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
6379
|
+
window.Vanduo.register("LazyLoad", VanduoLazyLoad);
|
|
6380
|
+
}
|
|
6381
|
+
window.VanduoLazyLoad = VanduoLazyLoad;
|
|
6382
|
+
})();
|
|
6383
|
+
|
|
6175
6384
|
// js/index.js
|
|
6176
6385
|
var Vanduo = window.Vanduo;
|
|
6177
6386
|
var index_default = Vanduo;
|