@vanduo-oss/framework 1.2.3 → 1.2.4

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/dist/vanduo.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.2.0 | Built: 2026-02-22T21:30:31.940Z | git:64c88fd | development */
1
+ /*! Vanduo v1.2.4 | Built: 2026-03-04T19:15:26.258Z | git:a524dfc | development */
2
2
  *, :before, :after {
3
3
  box-sizing: border-box;
4
4
  }
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.2.0 | Built: 2026-02-22T21:30:31.940Z | git:64c88fd | development */
1
+ /*! Vanduo v1.2.4 | Built: 2026-03-04T19:15:26.258Z | git:a524dfc | development */
2
2
 
3
3
  // js/utils/lifecycle.js
4
4
  (function() {
@@ -107,8 +107,9 @@
107
107
  // js/vanduo.js
108
108
  (function() {
109
109
  "use strict";
110
+ const VANDUO_VERSION = true ? "1.2.4" : "0.0.0-dev";
110
111
  const Vanduo2 = {
111
- version: "1.2.0",
112
+ version: VANDUO_VERSION,
112
113
  components: {},
113
114
  /**
114
115
  * Initialize framework
@@ -143,7 +144,7 @@
143
144
  }
144
145
  }
145
146
  });
146
- console.log("Vanduo Framework v1.2.0 initialized");
147
+ console.log("Vanduo Framework v" + this.version + " initialized");
147
148
  },
148
149
  /**
149
150
  * Register a component
@@ -5663,12 +5664,12 @@
5663
5664
  const touchMoveHandler = (e) => {
5664
5665
  this.handleTouchMove(e, element);
5665
5666
  };
5666
- element.addEventListener("touchmove", touchMoveHandler);
5667
+ element.addEventListener("touchmove", touchMoveHandler, { passive: false });
5667
5668
  cleanupFunctions.push(() => element.removeEventListener("touchmove", touchMoveHandler));
5668
5669
  const touchEndHandler = (e) => {
5669
5670
  this.handleTouchEnd(e, element);
5670
5671
  };
5671
- element.addEventListener("touchend", touchEndHandler);
5672
+ element.addEventListener("touchend", touchEndHandler, { passive: false });
5672
5673
  cleanupFunctions.push(() => element.removeEventListener("touchend", touchEndHandler));
5673
5674
  const touchCancelHandler = (e) => {
5674
5675
  this.handleTouchEnd(e, element);
@@ -5874,7 +5875,7 @@
5874
5875
  const deltaX = touch.clientX - this.touchState.startX;
5875
5876
  const deltaY = touch.clientY - this.touchState.startY;
5876
5877
  if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
5877
- e.preventDefault();
5878
+ if (e.cancelable) e.preventDefault();
5878
5879
  if (!this.touchState.isDragging) {
5879
5880
  this.touchState.isDragging = true;
5880
5881
  element.classList.add("is-dragging");
@@ -5919,7 +5920,7 @@
5919
5920
  */
5920
5921
  handleTouchEnd: function(e, element) {
5921
5922
  if (this.touchState && this.touchState.isDragging) {
5922
- e.preventDefault();
5923
+ if (e.cancelable) e.preventDefault();
5923
5924
  element.classList.remove("is-dragging");
5924
5925
  element.classList.add("is-dropped");
5925
5926
  element.setAttribute("aria-grabbed", "false");
@@ -6147,6 +6148,237 @@
6147
6148
  window.VanduoDraggable = Draggable;
6148
6149
  })();
6149
6150
 
6151
+ // js/components/lazy-load.js
6152
+ (function() {
6153
+ "use strict";
6154
+ const _observerMap = /* @__PURE__ */ new Map();
6155
+ function _isSafeUrl(url) {
6156
+ try {
6157
+ const resolved = new URL(url, window.location.href);
6158
+ return resolved.origin === window.location.origin;
6159
+ } catch (_) {
6160
+ return false;
6161
+ }
6162
+ }
6163
+ function _safeInjectHtml(containerEl, html) {
6164
+ const parser = new DOMParser();
6165
+ const doc = parser.parseFromString(html.trim(), "text/html");
6166
+ const DANGEROUS_TAGS = ["SCRIPT", "IFRAME", "OBJECT", "EMBED", "FORM", "BASE", "LINK", "META", "STYLE"];
6167
+ for (const tag of DANGEROUS_TAGS) {
6168
+ const els = doc.querySelectorAll(tag);
6169
+ for (let i = els.length - 1; i >= 0; i--) {
6170
+ els[i].parentNode.removeChild(els[i]);
6171
+ }
6172
+ }
6173
+ function _sanitizeNode(node) {
6174
+ if (node.nodeType === Node.ELEMENT_NODE) {
6175
+ const attrs = node.attributes;
6176
+ for (let i = attrs.length - 1; i >= 0; i--) {
6177
+ const attrName = attrs[i].name.toLowerCase();
6178
+ const attrValue = attrs[i].value.toLowerCase();
6179
+ const trimmedValue = attrValue.trim();
6180
+ if (attrName.startsWith("on") || trimmedValue.startsWith("javascript:") || trimmedValue.startsWith("data:") || trimmedValue.startsWith("vbscript:")) {
6181
+ node.removeAttribute(attrs[i].name);
6182
+ }
6183
+ }
6184
+ const children = node.childNodes;
6185
+ for (let i = 0; i < children.length; i++) {
6186
+ _sanitizeNode(children[i]);
6187
+ }
6188
+ }
6189
+ }
6190
+ _sanitizeNode(doc.body);
6191
+ const nodes = Array.from(doc.body.childNodes);
6192
+ while (containerEl.firstChild) {
6193
+ containerEl.removeChild(containerEl.firstChild);
6194
+ }
6195
+ nodes.forEach(function(node) {
6196
+ containerEl.appendChild(document.adoptNode(node));
6197
+ });
6198
+ }
6199
+ function _skeletonHtml() {
6200
+ 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>';
6201
+ }
6202
+ function _spinnerHtml() {
6203
+ 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>';
6204
+ }
6205
+ function _resolvePlaceholder(placeholder) {
6206
+ if (!placeholder || placeholder === "skeleton") return _skeletonHtml();
6207
+ if (placeholder === "spinner") return _spinnerHtml();
6208
+ return placeholder;
6209
+ }
6210
+ function _dispatch(el, eventName, detail) {
6211
+ el.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: detail || {} }));
6212
+ }
6213
+ const VanduoLazyLoad = {
6214
+ /* ─────────────────────────────────────────────────
6215
+ * LOW-LEVEL API
6216
+ * ───────────────────────────────────────────────── */
6217
+ /**
6218
+ * Observe an element. `callback` is invoked once when the element
6219
+ * enters the viewport, then the element is automatically unobserved.
6220
+ *
6221
+ * @param {Element} element
6222
+ * @param {function(Element): void} callback
6223
+ * @param {{ threshold?: number, rootMargin?: string }} [options]
6224
+ */
6225
+ observe: function(element, callback, options) {
6226
+ if (!(element instanceof Element)) {
6227
+ console.warn("[VanduoLazyLoad] observe() requires a DOM Element.");
6228
+ return;
6229
+ }
6230
+ if (typeof callback !== "function") {
6231
+ console.warn("[VanduoLazyLoad] observe() requires a callback function.");
6232
+ return;
6233
+ }
6234
+ if (_observerMap.has(element)) return;
6235
+ const threshold = options && options.threshold != null ? options.threshold : 0;
6236
+ const rootMargin = options && options.rootMargin ? options.rootMargin : "0px";
6237
+ const observer = new IntersectionObserver(function(entries, obs) {
6238
+ entries.forEach(function(entry) {
6239
+ if (entry.isIntersecting) {
6240
+ obs.unobserve(entry.target);
6241
+ _observerMap.delete(entry.target);
6242
+ try {
6243
+ callback(entry.target);
6244
+ } catch (e) {
6245
+ console.error("[VanduoLazyLoad] Callback threw:", e);
6246
+ }
6247
+ }
6248
+ });
6249
+ }, { threshold, rootMargin });
6250
+ _observerMap.set(element, observer);
6251
+ observer.observe(element);
6252
+ },
6253
+ /**
6254
+ * Stop observing an element that was previously passed to observe().
6255
+ * @param {Element} element
6256
+ */
6257
+ unobserve: function(element) {
6258
+ const observer = _observerMap.get(element);
6259
+ if (observer) {
6260
+ observer.unobserve(element);
6261
+ _observerMap.delete(element);
6262
+ }
6263
+ },
6264
+ /**
6265
+ * Stop observing ALL currently observed elements.
6266
+ */
6267
+ unobserveAll: function() {
6268
+ _observerMap.forEach(function(observer, element) {
6269
+ observer.unobserve(element);
6270
+ });
6271
+ _observerMap.clear();
6272
+ },
6273
+ /* ─────────────────────────────────────────────────
6274
+ * HIGH-LEVEL API
6275
+ * ───────────────────────────────────────────────── */
6276
+ /**
6277
+ * Fetch an HTML partial and inject it into `containerEl` when the
6278
+ * container enters the viewport. A placeholder is shown immediately.
6279
+ *
6280
+ * @param {string} url URL of the HTML partial to fetch
6281
+ * @param {Element} containerEl Target element whose content will be replaced
6282
+ * @param {{
6283
+ * placeholder?: 'skeleton'|'spinner'|string,
6284
+ * threshold?: number,
6285
+ * rootMargin?: string,
6286
+ * onLoaded?: function(Element): void,
6287
+ * onError?: function(Error): void
6288
+ * }} [options]
6289
+ */
6290
+ loadSection: function(url, containerEl, options) {
6291
+ if (typeof url !== "string" || !url) {
6292
+ console.warn("[VanduoLazyLoad] loadSection() requires a non-empty URL string.");
6293
+ return;
6294
+ }
6295
+ if (!(containerEl instanceof Element)) {
6296
+ console.warn("[VanduoLazyLoad] loadSection() requires a DOM Element as containerEl.");
6297
+ return;
6298
+ }
6299
+ if (!_isSafeUrl(url)) {
6300
+ console.error("[VanduoLazyLoad] loadSection() blocked cross-origin URL:", url);
6301
+ return;
6302
+ }
6303
+ const opts = options || {};
6304
+ const placeholderHtml = _resolvePlaceholder(opts.placeholder);
6305
+ _safeInjectHtml(containerEl, placeholderHtml);
6306
+ _dispatch(containerEl, "lazysection:loading", { url });
6307
+ this.observe(containerEl, function() {
6308
+ const controller = new window.AbortController();
6309
+ const timeoutId = setTimeout(function() {
6310
+ controller.abort();
6311
+ }, 1e4);
6312
+ window.fetch(url, { signal: controller.signal }).then(function(res) {
6313
+ clearTimeout(timeoutId);
6314
+ if (!res.ok) throw new Error("HTTP " + res.status);
6315
+ return res.text();
6316
+ }).then(function(html) {
6317
+ _safeInjectHtml(containerEl, html);
6318
+ _dispatch(containerEl, "lazysection:loaded", { url });
6319
+ if (typeof window.Vanduo !== "undefined") {
6320
+ window.Vanduo.init();
6321
+ }
6322
+ if (typeof opts.onLoaded === "function") {
6323
+ opts.onLoaded(containerEl);
6324
+ }
6325
+ }).catch(function(err) {
6326
+ const alertEl = document.createElement("div");
6327
+ alertEl.className = "vd-alert vd-alert-error";
6328
+ alertEl.setAttribute("role", "alert");
6329
+ const msgEl = document.createElement("span");
6330
+ msgEl.textContent = "Failed to load content. ";
6331
+ const detailEl = document.createElement("small");
6332
+ detailEl.style.opacity = "0.7";
6333
+ detailEl.textContent = err.message;
6334
+ alertEl.appendChild(msgEl);
6335
+ alertEl.appendChild(detailEl);
6336
+ while (containerEl.firstChild) {
6337
+ containerEl.removeChild(containerEl.firstChild);
6338
+ }
6339
+ containerEl.appendChild(alertEl);
6340
+ _dispatch(containerEl, "lazysection:error", { url, error: err });
6341
+ console.error("[VanduoLazyLoad] loadSection failed:", err);
6342
+ if (typeof opts.onError === "function") {
6343
+ opts.onError(err);
6344
+ }
6345
+ });
6346
+ }, { threshold: opts.threshold, rootMargin: opts.rootMargin });
6347
+ },
6348
+ /* ─────────────────────────────────────────────────
6349
+ * ATTRIBUTE-DRIVEN INIT
6350
+ * ───────────────────────────────────────────────── */
6351
+ /**
6352
+ * Scan the DOM for [data-vd-lazy] elements and wire them up.
6353
+ * Safe to call multiple times — already-observed elements are skipped.
6354
+ */
6355
+ init: function() {
6356
+ const self = this;
6357
+ const elements = document.querySelectorAll("[data-vd-lazy]");
6358
+ elements.forEach(function(el) {
6359
+ if (_observerMap.has(el) || el.dataset.vdLazyState === "loading" || el.dataset.vdLazyState === "loaded") return;
6360
+ const url = el.getAttribute("data-vd-lazy");
6361
+ if (!url) return;
6362
+ el.dataset.vdLazyState = "loading";
6363
+ const placeholder = el.getAttribute("data-vd-lazy-placeholder") || "skeleton";
6364
+ self.loadSection(url, el, {
6365
+ placeholder,
6366
+ onLoaded: function() {
6367
+ el.dataset.vdLazyState = "loaded";
6368
+ },
6369
+ onError: function() {
6370
+ el.dataset.vdLazyState = "error";
6371
+ }
6372
+ });
6373
+ });
6374
+ }
6375
+ };
6376
+ if (typeof window.Vanduo !== "undefined") {
6377
+ window.Vanduo.register("LazyLoad", VanduoLazyLoad);
6378
+ }
6379
+ window.VanduoLazyLoad = VanduoLazyLoad;
6380
+ })();
6381
+
6150
6382
  // js/index.js
6151
6383
  var Vanduo = window.Vanduo;
6152
6384
  var index_default = Vanduo;