@vanduo-oss/framework 1.2.3
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 +216 -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 +6178 -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 +60950 -0
- package/dist/vanduo.css.map +1 -0
- package/dist/vanduo.esm.js +6157 -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 +6154 -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 +641 -0
- package/js/components/collapsible.js +226 -0
- package/js/components/doc-search.js +953 -0
- package/js/components/draggable.js +728 -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/lazy-load.js +353 -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 +800 -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 +53 -0
- package/js/utils/helpers.js +318 -0
- package/js/utils/lifecycle.js +135 -0
- package/js/vanduo.js +120 -0
- package/package.json +78 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework – LazyLoad Component
|
|
3
|
+
* v1.2.1
|
|
4
|
+
*
|
|
5
|
+
* Provides two levels of API:
|
|
6
|
+
*
|
|
7
|
+
* LOW-LEVEL (generic IntersectionObserver wrapper)
|
|
8
|
+
* VanduoLazyLoad.observe(element, callback, options?)
|
|
9
|
+
* VanduoLazyLoad.unobserve(element)
|
|
10
|
+
* VanduoLazyLoad.unobserveAll()
|
|
11
|
+
*
|
|
12
|
+
* HIGH-LEVEL (HTML-section fetcher, mirrors vanduo-docs SPA behaviour)
|
|
13
|
+
* VanduoLazyLoad.loadSection(url, containerEl, options?)
|
|
14
|
+
* options: { placeholder, threshold, rootMargin, onLoaded, onError }
|
|
15
|
+
* placeholder: 'skeleton' | 'spinner' | HTMLString (default: 'skeleton')
|
|
16
|
+
*
|
|
17
|
+
* ATTRIBUTE-DRIVEN (zero-JS usage, wired by init())
|
|
18
|
+
* <div data-vd-lazy="./path/to/section.html"
|
|
19
|
+
* data-vd-lazy-placeholder="skeleton|spinner">…</div>
|
|
20
|
+
*
|
|
21
|
+
* EVENTS dispatched on the host element:
|
|
22
|
+
* lazysection:loading — fetch started
|
|
23
|
+
* lazysection:loaded — content injected
|
|
24
|
+
* lazysection:error — fetch failed
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
(function () {
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
/* ── Private state ────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
/** @type {Map<Element, IntersectionObserver>} */
|
|
33
|
+
const _observerMap = new Map();
|
|
34
|
+
|
|
35
|
+
/* ── Security helpers ─────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true only if `url` resolves to the same origin as the page
|
|
39
|
+
* (relative paths and same-origin absolute URLs are allowed).
|
|
40
|
+
* Cross-origin URLs are rejected to prevent SSRF-style fetch abuse.
|
|
41
|
+
* @param {string} url
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function _isSafeUrl(url) {
|
|
45
|
+
try {
|
|
46
|
+
// Relative URLs (no origin) are always safe
|
|
47
|
+
const resolved = new URL(url, window.location.href);
|
|
48
|
+
return resolved.origin === window.location.origin;
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Safely inject fetched HTML into a container by parsing it with
|
|
56
|
+
* DOMParser (avoids script execution) and replacing children via
|
|
57
|
+
* standard DOM APIs instead of raw innerHTML assignment.
|
|
58
|
+
* @param {Element} containerEl
|
|
59
|
+
* @param {string} html
|
|
60
|
+
*/
|
|
61
|
+
function _safeInjectHtml(containerEl, html) {
|
|
62
|
+
const parser = new DOMParser();
|
|
63
|
+
const doc = parser.parseFromString(html.trim(), 'text/html'); // CodeQL [js/xss-through-dom] Intentional: parsed nodes are sanitized below before adoption into live DOM
|
|
64
|
+
|
|
65
|
+
const DANGEROUS_TAGS = ['SCRIPT', 'IFRAME', 'OBJECT', 'EMBED', 'FORM', 'BASE', 'LINK', 'META', 'STYLE'];
|
|
66
|
+
for (const tag of DANGEROUS_TAGS) {
|
|
67
|
+
const els = doc.querySelectorAll(tag);
|
|
68
|
+
for (let i = els.length - 1; i >= 0; i--) {
|
|
69
|
+
els[i].parentNode.removeChild(els[i]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Strip dangerous attributes (e.g. onerror, javascript: urls) from all elements
|
|
74
|
+
function _sanitizeNode(node) {
|
|
75
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
76
|
+
const attrs = node.attributes;
|
|
77
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
78
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
79
|
+
const attrValue = attrs[i].value.toLowerCase();
|
|
80
|
+
const trimmedValue = attrValue.trim();
|
|
81
|
+
if (
|
|
82
|
+
attrName.startsWith('on') ||
|
|
83
|
+
trimmedValue.startsWith('javascript:') ||
|
|
84
|
+
trimmedValue.startsWith('data:') ||
|
|
85
|
+
trimmedValue.startsWith('vbscript:')
|
|
86
|
+
) {
|
|
87
|
+
node.removeAttribute(attrs[i].name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const children = node.childNodes;
|
|
91
|
+
for (let i = 0; i < children.length; i++) {
|
|
92
|
+
_sanitizeNode(children[i]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
_sanitizeNode(doc.body);
|
|
97
|
+
|
|
98
|
+
// Collect all top-level body children
|
|
99
|
+
const nodes = Array.from(doc.body.childNodes);
|
|
100
|
+
// Clear container and append parsed nodes
|
|
101
|
+
while (containerEl.firstChild) {
|
|
102
|
+
containerEl.removeChild(containerEl.firstChild);
|
|
103
|
+
}
|
|
104
|
+
nodes.forEach(function (node) {
|
|
105
|
+
containerEl.appendChild(document.adoptNode(node));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ── Placeholder HTML ─────────────────────────────── */
|
|
110
|
+
|
|
111
|
+
function _skeletonHtml() {
|
|
112
|
+
return '<div class="vd-skeleton-card" style="position:relative;min-height:200px;padding:2rem;overflow:hidden;">'
|
|
113
|
+
+ '<div class="vd-skeleton vd-skeleton-heading-lg" style="margin-bottom:1.5rem;"></div>'
|
|
114
|
+
+ '<div class="vd-skeleton vd-skeleton-paragraph">'
|
|
115
|
+
+ '<div class="vd-skeleton vd-skeleton-text"></div>'
|
|
116
|
+
+ '<div class="vd-skeleton vd-skeleton-text"></div>'
|
|
117
|
+
+ '<div class="vd-skeleton vd-skeleton-text"></div></div>'
|
|
118
|
+
+ '<div class="vd-dynamic-loader" style="position:absolute;inset:0;">'
|
|
119
|
+
+ '<div class="vd-dynamic-loader-grid">'
|
|
120
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-success" style="animation-delay:0s;"></div>'
|
|
121
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-warning" style="animation-delay:-0.15s;"></div>'
|
|
122
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-error" style="animation-delay:-0.3s;"></div>'
|
|
123
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-info" style="animation-delay:-0.45s;"></div></div>'
|
|
124
|
+
+ '<span class="vd-dynamic-loader-text">Loading…</span></div></div>';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _spinnerHtml() {
|
|
128
|
+
return '<div class="vd-dynamic-loader" style="min-height:180px;display:flex;align-items:center;justify-content:center;">'
|
|
129
|
+
+ '<div class="vd-dynamic-loader-grid">'
|
|
130
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-success" style="animation-delay:0s;"></div>'
|
|
131
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-warning" style="animation-delay:-0.15s;"></div>'
|
|
132
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-error" style="animation-delay:-0.3s;"></div>'
|
|
133
|
+
+ '<div class="vd-spinner vd-spinner-sm vd-spinner-info" style="animation-delay:-0.45s;"></div></div>'
|
|
134
|
+
+ '<span class="vd-dynamic-loader-text">Loading…</span></div>';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _resolvePlaceholder(placeholder) {
|
|
138
|
+
if (!placeholder || placeholder === 'skeleton') return _skeletonHtml();
|
|
139
|
+
if (placeholder === 'spinner') return _spinnerHtml();
|
|
140
|
+
// Caller-supplied HTML string
|
|
141
|
+
return placeholder;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ── Dispatch helper ──────────────────────────────── */
|
|
145
|
+
|
|
146
|
+
function _dispatch(el, eventName, detail) {
|
|
147
|
+
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: detail || {} }));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ── VanduoLazyLoad ──────────────────────────────── */
|
|
151
|
+
|
|
152
|
+
const VanduoLazyLoad = {
|
|
153
|
+
|
|
154
|
+
/* ─────────────────────────────────────────────────
|
|
155
|
+
* LOW-LEVEL API
|
|
156
|
+
* ───────────────────────────────────────────────── */
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Observe an element. `callback` is invoked once when the element
|
|
160
|
+
* enters the viewport, then the element is automatically unobserved.
|
|
161
|
+
*
|
|
162
|
+
* @param {Element} element
|
|
163
|
+
* @param {function(Element): void} callback
|
|
164
|
+
* @param {{ threshold?: number, rootMargin?: string }} [options]
|
|
165
|
+
*/
|
|
166
|
+
observe: function (element, callback, options) {
|
|
167
|
+
if (!(element instanceof Element)) {
|
|
168
|
+
console.warn('[VanduoLazyLoad] observe() requires a DOM Element.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (typeof callback !== 'function') {
|
|
172
|
+
console.warn('[VanduoLazyLoad] observe() requires a callback function.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Already observed — ignore
|
|
176
|
+
if (_observerMap.has(element)) return;
|
|
177
|
+
|
|
178
|
+
const threshold = (options && options.threshold != null) ? options.threshold : 0;
|
|
179
|
+
const rootMargin = (options && options.rootMargin) ? options.rootMargin : '0px';
|
|
180
|
+
|
|
181
|
+
const observer = new IntersectionObserver(function (entries, obs) {
|
|
182
|
+
entries.forEach(function (entry) {
|
|
183
|
+
if (entry.isIntersecting) {
|
|
184
|
+
obs.unobserve(entry.target);
|
|
185
|
+
_observerMap.delete(entry.target);
|
|
186
|
+
try {
|
|
187
|
+
callback(entry.target);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('[VanduoLazyLoad] Callback threw:', e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}, { threshold: threshold, rootMargin: rootMargin });
|
|
194
|
+
|
|
195
|
+
_observerMap.set(element, observer);
|
|
196
|
+
observer.observe(element);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stop observing an element that was previously passed to observe().
|
|
201
|
+
* @param {Element} element
|
|
202
|
+
*/
|
|
203
|
+
unobserve: function (element) {
|
|
204
|
+
const observer = _observerMap.get(element);
|
|
205
|
+
if (observer) {
|
|
206
|
+
observer.unobserve(element);
|
|
207
|
+
_observerMap.delete(element);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Stop observing ALL currently observed elements.
|
|
213
|
+
*/
|
|
214
|
+
unobserveAll: function () {
|
|
215
|
+
_observerMap.forEach(function (observer, element) {
|
|
216
|
+
observer.unobserve(element);
|
|
217
|
+
});
|
|
218
|
+
_observerMap.clear();
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/* ─────────────────────────────────────────────────
|
|
222
|
+
* HIGH-LEVEL API
|
|
223
|
+
* ───────────────────────────────────────────────── */
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Fetch an HTML partial and inject it into `containerEl` when the
|
|
227
|
+
* container enters the viewport. A placeholder is shown immediately.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} url URL of the HTML partial to fetch
|
|
230
|
+
* @param {Element} containerEl Target element whose content will be replaced
|
|
231
|
+
* @param {{
|
|
232
|
+
* placeholder?: 'skeleton'|'spinner'|string,
|
|
233
|
+
* threshold?: number,
|
|
234
|
+
* rootMargin?: string,
|
|
235
|
+
* onLoaded?: function(Element): void,
|
|
236
|
+
* onError?: function(Error): void
|
|
237
|
+
* }} [options]
|
|
238
|
+
*/
|
|
239
|
+
loadSection: function (url, containerEl, options) {
|
|
240
|
+
if (typeof url !== 'string' || !url) {
|
|
241
|
+
console.warn('[VanduoLazyLoad] loadSection() requires a non-empty URL string.');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!(containerEl instanceof Element)) {
|
|
245
|
+
console.warn('[VanduoLazyLoad] loadSection() requires a DOM Element as containerEl.');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Reject cross-origin URLs to prevent SSRF-style fetch abuse
|
|
249
|
+
if (!_isSafeUrl(url)) {
|
|
250
|
+
console.error('[VanduoLazyLoad] loadSection() blocked cross-origin URL:', url);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const opts = options || {};
|
|
255
|
+
// Placeholders are known-safe static HTML strings built internally
|
|
256
|
+
const placeholderHtml = _resolvePlaceholder(opts.placeholder);
|
|
257
|
+
|
|
258
|
+
// Use _safeInjectHtml instead of innerHTML to prevent XSS from caller-supplied placeholders
|
|
259
|
+
_safeInjectHtml(containerEl, placeholderHtml);
|
|
260
|
+
_dispatch(containerEl, 'lazysection:loading', { url: url });
|
|
261
|
+
|
|
262
|
+
// Fetch when visible
|
|
263
|
+
this.observe(containerEl, function () {
|
|
264
|
+
const controller = new window.AbortController();
|
|
265
|
+
const timeoutId = setTimeout(function () { controller.abort(); }, 10000);
|
|
266
|
+
|
|
267
|
+
window.fetch(url, { signal: controller.signal })
|
|
268
|
+
.then(function (res) {
|
|
269
|
+
clearTimeout(timeoutId);
|
|
270
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
271
|
+
return res.text();
|
|
272
|
+
})
|
|
273
|
+
.then(function (html) {
|
|
274
|
+
// Use DOMParser to parse fetched content safely, avoiding
|
|
275
|
+
// raw innerHTML assignment of externally-sourced strings
|
|
276
|
+
_safeInjectHtml(containerEl, html);
|
|
277
|
+
_dispatch(containerEl, 'lazysection:loaded', { url: url });
|
|
278
|
+
// Re-init Vanduo components inside the new content
|
|
279
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
280
|
+
window.Vanduo.init();
|
|
281
|
+
}
|
|
282
|
+
if (typeof opts.onLoaded === 'function') {
|
|
283
|
+
opts.onLoaded(containerEl);
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
.catch(function (err) {
|
|
287
|
+
// Build error node via DOM APIs — no dynamic HTML strings
|
|
288
|
+
const alertEl = document.createElement('div');
|
|
289
|
+
alertEl.className = 'vd-alert vd-alert-error';
|
|
290
|
+
alertEl.setAttribute('role', 'alert');
|
|
291
|
+
const msgEl = document.createElement('span');
|
|
292
|
+
msgEl.textContent = 'Failed to load content. ';
|
|
293
|
+
const detailEl = document.createElement('small');
|
|
294
|
+
detailEl.style.opacity = '0.7';
|
|
295
|
+
detailEl.textContent = err.message;
|
|
296
|
+
alertEl.appendChild(msgEl);
|
|
297
|
+
alertEl.appendChild(detailEl);
|
|
298
|
+
while (containerEl.firstChild) {
|
|
299
|
+
containerEl.removeChild(containerEl.firstChild);
|
|
300
|
+
}
|
|
301
|
+
containerEl.appendChild(alertEl);
|
|
302
|
+
_dispatch(containerEl, 'lazysection:error', { url: url, error: err });
|
|
303
|
+
console.error('[VanduoLazyLoad] loadSection failed:', err);
|
|
304
|
+
if (typeof opts.onError === 'function') {
|
|
305
|
+
opts.onError(err);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}, { threshold: opts.threshold, rootMargin: opts.rootMargin });
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/* ─────────────────────────────────────────────────
|
|
312
|
+
* ATTRIBUTE-DRIVEN INIT
|
|
313
|
+
* ───────────────────────────────────────────────── */
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Scan the DOM for [data-vd-lazy] elements and wire them up.
|
|
317
|
+
* Safe to call multiple times — already-observed elements are skipped.
|
|
318
|
+
*/
|
|
319
|
+
init: function () {
|
|
320
|
+
const self = this;
|
|
321
|
+
const elements = document.querySelectorAll('[data-vd-lazy]');
|
|
322
|
+
elements.forEach(function (el) {
|
|
323
|
+
// Skip already-observed or already-loaded elements
|
|
324
|
+
if (_observerMap.has(el) || el.dataset.vdLazyState === 'loading' || el.dataset.vdLazyState === 'loaded') return;
|
|
325
|
+
|
|
326
|
+
const url = el.getAttribute('data-vd-lazy');
|
|
327
|
+
if (!url) return;
|
|
328
|
+
|
|
329
|
+
el.dataset.vdLazyState = 'loading';
|
|
330
|
+
const placeholder = el.getAttribute('data-vd-lazy-placeholder') || 'skeleton';
|
|
331
|
+
|
|
332
|
+
self.loadSection(url, el, {
|
|
333
|
+
placeholder: placeholder,
|
|
334
|
+
onLoaded: function () {
|
|
335
|
+
el.dataset.vdLazyState = 'loaded';
|
|
336
|
+
},
|
|
337
|
+
onError: function () {
|
|
338
|
+
el.dataset.vdLazyState = 'error';
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/* ── Register with Vanduo ─────────────────────────── */
|
|
346
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
347
|
+
window.Vanduo.register('LazyLoad', VanduoLazyLoad);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ── Global convenience alias ─────────────────────── */
|
|
351
|
+
window.VanduoLazyLoad = VanduoLazyLoad;
|
|
352
|
+
|
|
353
|
+
})();
|