@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.
Files changed (197) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +216 -0
  3. package/css/components/alerts.css +224 -0
  4. package/css/components/avatar.css +275 -0
  5. package/css/components/badges.css +230 -0
  6. package/css/components/breadcrumbs.css +146 -0
  7. package/css/components/button-group.css +82 -0
  8. package/css/components/buttons.css +530 -0
  9. package/css/components/cards.css +304 -0
  10. package/css/components/chips.css +259 -0
  11. package/css/components/code-snippet.css +555 -0
  12. package/css/components/collapsible.css +267 -0
  13. package/css/components/collections.css +253 -0
  14. package/css/components/doc-search.css +464 -0
  15. package/css/components/doc-tabs.css +38 -0
  16. package/css/components/draggable.css +317 -0
  17. package/css/components/dropdown.css +266 -0
  18. package/css/components/footer.css +375 -0
  19. package/css/components/forms.css +1774 -0
  20. package/css/components/image-box.css +279 -0
  21. package/css/components/modals.css +285 -0
  22. package/css/components/navbar.css +530 -0
  23. package/css/components/pagination.css +186 -0
  24. package/css/components/preloader.css +340 -0
  25. package/css/components/progress.css +107 -0
  26. package/css/components/sidenav.css +301 -0
  27. package/css/components/skeleton.css +241 -0
  28. package/css/components/spinner.css +144 -0
  29. package/css/components/tabs.css +327 -0
  30. package/css/components/theme-customizer.css +835 -0
  31. package/css/components/toast.css +357 -0
  32. package/css/components/tooltips.css +270 -0
  33. package/css/core/colors.css +1017 -0
  34. package/css/core/fonts.css +266 -0
  35. package/css/core/grid.css +1699 -0
  36. package/css/core/helpers.css +2202 -0
  37. package/css/core/reset.css +128 -0
  38. package/css/core/tokens.css +213 -0
  39. package/css/core/typography.css +405 -0
  40. package/css/core/vd-aliases.css +47 -0
  41. package/css/effects/parallax.css +113 -0
  42. package/css/icons/icons-all.css +23 -0
  43. package/css/icons/icons.css +25 -0
  44. package/css/utilities/media.css +167 -0
  45. package/css/utilities/print.css +111 -0
  46. package/css/utilities/shadow.css +243 -0
  47. package/css/utilities/table.css +381 -0
  48. package/css/utilities/transforms.css +71 -0
  49. package/css/utilities/transitions.css +87 -0
  50. package/css/vanduo.css +80 -0
  51. package/dist/build-info.json +6 -0
  52. package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  53. package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  54. package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  55. package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  56. package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  57. package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  58. package/dist/fonts/inter/inter-bold.woff2 +0 -0
  59. package/dist/fonts/inter/inter-medium.woff2 +0 -0
  60. package/dist/fonts/inter/inter-regular.woff2 +0 -0
  61. package/dist/fonts/inter/inter-semibold.woff2 +0 -0
  62. package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  63. package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  64. package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
  65. package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
  66. package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
  67. package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
  68. package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
  69. package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
  70. package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
  71. package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
  72. package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  73. package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  74. package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  75. package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  76. package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  77. package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  78. package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  79. package/dist/icons/phosphor/LICENSE +21 -0
  80. package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  81. package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  82. package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  83. package/dist/icons/phosphor/bold/style.css +4627 -0
  84. package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  85. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  86. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  87. package/dist/icons/phosphor/duotone/style.css +12115 -0
  88. package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  89. package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  90. package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  91. package/dist/icons/phosphor/fill/style.css +4627 -0
  92. package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  93. package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
  94. package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  95. package/dist/icons/phosphor/light/style.css +4627 -0
  96. package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
  97. package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
  98. package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
  99. package/dist/icons/phosphor/regular/style.css +4627 -0
  100. package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  101. package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  102. package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  103. package/dist/icons/phosphor/thin/style.css +4627 -0
  104. package/dist/vanduo.cjs.js +6178 -0
  105. package/dist/vanduo.cjs.js.map +7 -0
  106. package/dist/vanduo.cjs.min.js +48 -0
  107. package/dist/vanduo.cjs.min.js.map +7 -0
  108. package/dist/vanduo.css +60950 -0
  109. package/dist/vanduo.css.map +1 -0
  110. package/dist/vanduo.esm.js +6157 -0
  111. package/dist/vanduo.esm.js.map +7 -0
  112. package/dist/vanduo.esm.min.js +48 -0
  113. package/dist/vanduo.esm.min.js.map +7 -0
  114. package/dist/vanduo.js +6154 -0
  115. package/dist/vanduo.js.map +7 -0
  116. package/dist/vanduo.min.css +2 -0
  117. package/dist/vanduo.min.css.map +1 -0
  118. package/dist/vanduo.min.js +48 -0
  119. package/dist/vanduo.min.js.map +7 -0
  120. package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  121. package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  122. package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  123. package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  124. package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  125. package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  126. package/fonts/inter/inter-bold.woff2 +0 -0
  127. package/fonts/inter/inter-medium.woff2 +0 -0
  128. package/fonts/inter/inter-regular.woff2 +0 -0
  129. package/fonts/inter/inter-semibold.woff2 +0 -0
  130. package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  131. package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  132. package/fonts/open-sans/open-sans-bold.woff2 +0 -0
  133. package/fonts/open-sans/open-sans-medium.woff2 +0 -0
  134. package/fonts/open-sans/open-sans-regular.woff2 +0 -0
  135. package/fonts/rubik/rubik-bold.woff2 +0 -0
  136. package/fonts/rubik/rubik-medium.woff2 +0 -0
  137. package/fonts/rubik/rubik-regular.woff2 +0 -0
  138. package/fonts/source-sans/source-sans-bold.woff2 +0 -0
  139. package/fonts/source-sans/source-sans-regular.woff2 +0 -0
  140. package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  141. package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  142. package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  143. package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  144. package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  145. package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  146. package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  147. package/icons/phosphor/LICENSE +21 -0
  148. package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  149. package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  150. package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  151. package/icons/phosphor/bold/style.css +4627 -0
  152. package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  153. package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  154. package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  155. package/icons/phosphor/duotone/style.css +12115 -0
  156. package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  157. package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  158. package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  159. package/icons/phosphor/fill/style.css +4627 -0
  160. package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  161. package/icons/phosphor/light/Phosphor-Light.woff +0 -0
  162. package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  163. package/icons/phosphor/light/style.css +4627 -0
  164. package/icons/phosphor/regular/Phosphor.ttf +0 -0
  165. package/icons/phosphor/regular/Phosphor.woff +0 -0
  166. package/icons/phosphor/regular/Phosphor.woff2 +0 -0
  167. package/icons/phosphor/regular/style.css +4627 -0
  168. package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  169. package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  170. package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  171. package/icons/phosphor/thin/style.css +4627 -0
  172. package/js/components/code-snippet.js +641 -0
  173. package/js/components/collapsible.js +226 -0
  174. package/js/components/doc-search.js +953 -0
  175. package/js/components/draggable.js +728 -0
  176. package/js/components/dropdown.js +362 -0
  177. package/js/components/font-switcher.js +253 -0
  178. package/js/components/grid.js +279 -0
  179. package/js/components/image-box.js +372 -0
  180. package/js/components/lazy-load.js +353 -0
  181. package/js/components/modals.js +367 -0
  182. package/js/components/navbar.js +264 -0
  183. package/js/components/pagination.js +286 -0
  184. package/js/components/parallax.js +216 -0
  185. package/js/components/preloader.js +183 -0
  186. package/js/components/select.js +444 -0
  187. package/js/components/sidenav.js +303 -0
  188. package/js/components/tabs.js +303 -0
  189. package/js/components/theme-customizer.js +800 -0
  190. package/js/components/theme-switcher.js +183 -0
  191. package/js/components/toast.js +343 -0
  192. package/js/components/tooltips.js +306 -0
  193. package/js/index.js +53 -0
  194. package/js/utils/helpers.js +318 -0
  195. package/js/utils/lifecycle.js +135 -0
  196. package/js/vanduo.js +120 -0
  197. 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
+ })();