@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/dist/vanduo.esm.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
|
|
|
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.5" : "0.0.0-dev";
|
|
110
111
|
const Vanduo2 = {
|
|
111
|
-
version:
|
|
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
|
|
147
|
+
console.log("Vanduo Framework v" + this.version + " initialized");
|
|
147
148
|
},
|
|
148
149
|
/**
|
|
149
150
|
* Register a component
|
|
@@ -4011,12 +4012,6 @@
|
|
|
4011
4012
|
this.updateUI();
|
|
4012
4013
|
});
|
|
4013
4014
|
}
|
|
4014
|
-
this.elements.panel.querySelectorAll("[data-mode]").forEach((btn) => {
|
|
4015
|
-
this.addListener(btn, "click", () => {
|
|
4016
|
-
this.applyTheme(btn.dataset.mode);
|
|
4017
|
-
this.updateUI();
|
|
4018
|
-
});
|
|
4019
|
-
});
|
|
4020
4015
|
const resetBtn = this.elements.panel.querySelector(".customizer-reset");
|
|
4021
4016
|
if (resetBtn) {
|
|
4022
4017
|
this.addListener(resetBtn, "click", () => {
|
|
@@ -4067,15 +4062,6 @@
|
|
|
4067
4062
|
for (const [key, value] of Object.entries(this.FONT_OPTIONS)) {
|
|
4068
4063
|
fontOptions += `<option value="${esc(key)}"${key === this.state.font ? " selected" : ""}>${esc(value.name)}</option>`;
|
|
4069
4064
|
}
|
|
4070
|
-
const modeIcons = {
|
|
4071
|
-
"system": "ph-desktop",
|
|
4072
|
-
"dark": "ph-moon",
|
|
4073
|
-
"light": "ph-sun"
|
|
4074
|
-
};
|
|
4075
|
-
let modeButtons = "";
|
|
4076
|
-
this.THEME_MODES.forEach((mode) => {
|
|
4077
|
-
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>`;
|
|
4078
|
-
});
|
|
4079
4065
|
return `
|
|
4080
4066
|
<div class="tc-header">
|
|
4081
4067
|
<h3 class="tc-title">Customize Theme</h3>
|
|
@@ -4084,12 +4070,7 @@
|
|
|
4084
4070
|
</button>
|
|
4085
4071
|
</div>
|
|
4086
4072
|
<div class="tc-body">
|
|
4087
|
-
|
|
4088
|
-
<label class="tc-label">Color Mode</label>
|
|
4089
|
-
<div class="tc-mode-group">
|
|
4090
|
-
${modeButtons}
|
|
4091
|
-
</div>
|
|
4092
|
-
</div>
|
|
4073
|
+
|
|
4093
4074
|
<div class="tc-section">
|
|
4094
4075
|
<label class="tc-label">Primary Color</label>
|
|
4095
4076
|
<div class="tc-color-grid">
|
|
@@ -4225,9 +4206,6 @@
|
|
|
4225
4206
|
if (fontSelect) {
|
|
4226
4207
|
fontSelect.value = this.state.font;
|
|
4227
4208
|
}
|
|
4228
|
-
this.elements.panel.querySelectorAll("[data-mode]").forEach((btn) => {
|
|
4229
|
-
btn.classList.toggle("is-active", btn.dataset.mode === this.state.theme);
|
|
4230
|
-
});
|
|
4231
4209
|
},
|
|
4232
4210
|
/**
|
|
4233
4211
|
* Reset all preferences to defaults
|
|
@@ -5663,12 +5641,12 @@
|
|
|
5663
5641
|
const touchMoveHandler = (e) => {
|
|
5664
5642
|
this.handleTouchMove(e, element);
|
|
5665
5643
|
};
|
|
5666
|
-
element.addEventListener("touchmove", touchMoveHandler);
|
|
5644
|
+
element.addEventListener("touchmove", touchMoveHandler, { passive: false });
|
|
5667
5645
|
cleanupFunctions.push(() => element.removeEventListener("touchmove", touchMoveHandler));
|
|
5668
5646
|
const touchEndHandler = (e) => {
|
|
5669
5647
|
this.handleTouchEnd(e, element);
|
|
5670
5648
|
};
|
|
5671
|
-
element.addEventListener("touchend", touchEndHandler);
|
|
5649
|
+
element.addEventListener("touchend", touchEndHandler, { passive: false });
|
|
5672
5650
|
cleanupFunctions.push(() => element.removeEventListener("touchend", touchEndHandler));
|
|
5673
5651
|
const touchCancelHandler = (e) => {
|
|
5674
5652
|
this.handleTouchEnd(e, element);
|
|
@@ -5874,7 +5852,7 @@
|
|
|
5874
5852
|
const deltaX = touch.clientX - this.touchState.startX;
|
|
5875
5853
|
const deltaY = touch.clientY - this.touchState.startY;
|
|
5876
5854
|
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
|
5877
|
-
e.preventDefault();
|
|
5855
|
+
if (e.cancelable) e.preventDefault();
|
|
5878
5856
|
if (!this.touchState.isDragging) {
|
|
5879
5857
|
this.touchState.isDragging = true;
|
|
5880
5858
|
element.classList.add("is-dragging");
|
|
@@ -5919,7 +5897,7 @@
|
|
|
5919
5897
|
*/
|
|
5920
5898
|
handleTouchEnd: function(e, element) {
|
|
5921
5899
|
if (this.touchState && this.touchState.isDragging) {
|
|
5922
|
-
e.preventDefault();
|
|
5900
|
+
if (e.cancelable) e.preventDefault();
|
|
5923
5901
|
element.classList.remove("is-dragging");
|
|
5924
5902
|
element.classList.add("is-dropped");
|
|
5925
5903
|
element.setAttribute("aria-grabbed", "false");
|
|
@@ -6147,6 +6125,237 @@
|
|
|
6147
6125
|
window.VanduoDraggable = Draggable;
|
|
6148
6126
|
})();
|
|
6149
6127
|
|
|
6128
|
+
// js/components/lazy-load.js
|
|
6129
|
+
(function() {
|
|
6130
|
+
"use strict";
|
|
6131
|
+
const _observerMap = /* @__PURE__ */ new Map();
|
|
6132
|
+
function _isSafeUrl(url) {
|
|
6133
|
+
try {
|
|
6134
|
+
const resolved = new URL(url, window.location.href);
|
|
6135
|
+
return resolved.origin === window.location.origin;
|
|
6136
|
+
} catch (_) {
|
|
6137
|
+
return false;
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
function _safeInjectHtml(containerEl, html) {
|
|
6141
|
+
const parser = new DOMParser();
|
|
6142
|
+
const doc = parser.parseFromString(html.trim(), "text/html");
|
|
6143
|
+
const DANGEROUS_TAGS = ["SCRIPT", "IFRAME", "OBJECT", "EMBED", "FORM", "BASE", "LINK", "META", "STYLE"];
|
|
6144
|
+
for (const tag of DANGEROUS_TAGS) {
|
|
6145
|
+
const els = doc.querySelectorAll(tag);
|
|
6146
|
+
for (let i = els.length - 1; i >= 0; i--) {
|
|
6147
|
+
els[i].parentNode.removeChild(els[i]);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
function _sanitizeNode(node) {
|
|
6151
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
6152
|
+
const attrs = node.attributes;
|
|
6153
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
6154
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
6155
|
+
const attrValue = attrs[i].value.toLowerCase();
|
|
6156
|
+
const trimmedValue = attrValue.trim();
|
|
6157
|
+
if (attrName.startsWith("on") || trimmedValue.startsWith("javascript:") || trimmedValue.startsWith("data:") || trimmedValue.startsWith("vbscript:")) {
|
|
6158
|
+
node.removeAttribute(attrs[i].name);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
const children = node.childNodes;
|
|
6162
|
+
for (let i = 0; i < children.length; i++) {
|
|
6163
|
+
_sanitizeNode(children[i]);
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
_sanitizeNode(doc.body);
|
|
6168
|
+
const nodes = Array.from(doc.body.childNodes);
|
|
6169
|
+
while (containerEl.firstChild) {
|
|
6170
|
+
containerEl.removeChild(containerEl.firstChild);
|
|
6171
|
+
}
|
|
6172
|
+
nodes.forEach(function(node) {
|
|
6173
|
+
containerEl.appendChild(document.adoptNode(node));
|
|
6174
|
+
});
|
|
6175
|
+
}
|
|
6176
|
+
function _skeletonHtml() {
|
|
6177
|
+
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>';
|
|
6178
|
+
}
|
|
6179
|
+
function _spinnerHtml() {
|
|
6180
|
+
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>';
|
|
6181
|
+
}
|
|
6182
|
+
function _resolvePlaceholder(placeholder) {
|
|
6183
|
+
if (!placeholder || placeholder === "skeleton") return _skeletonHtml();
|
|
6184
|
+
if (placeholder === "spinner") return _spinnerHtml();
|
|
6185
|
+
return placeholder;
|
|
6186
|
+
}
|
|
6187
|
+
function _dispatch(el, eventName, detail) {
|
|
6188
|
+
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: detail || {} }));
|
|
6189
|
+
}
|
|
6190
|
+
const VanduoLazyLoad = {
|
|
6191
|
+
/* ─────────────────────────────────────────────────
|
|
6192
|
+
* LOW-LEVEL API
|
|
6193
|
+
* ───────────────────────────────────────────────── */
|
|
6194
|
+
/**
|
|
6195
|
+
* Observe an element. `callback` is invoked once when the element
|
|
6196
|
+
* enters the viewport, then the element is automatically unobserved.
|
|
6197
|
+
*
|
|
6198
|
+
* @param {Element} element
|
|
6199
|
+
* @param {function(Element): void} callback
|
|
6200
|
+
* @param {{ threshold?: number, rootMargin?: string }} [options]
|
|
6201
|
+
*/
|
|
6202
|
+
observe: function(element, callback, options) {
|
|
6203
|
+
if (!(element instanceof Element)) {
|
|
6204
|
+
console.warn("[VanduoLazyLoad] observe() requires a DOM Element.");
|
|
6205
|
+
return;
|
|
6206
|
+
}
|
|
6207
|
+
if (typeof callback !== "function") {
|
|
6208
|
+
console.warn("[VanduoLazyLoad] observe() requires a callback function.");
|
|
6209
|
+
return;
|
|
6210
|
+
}
|
|
6211
|
+
if (_observerMap.has(element)) return;
|
|
6212
|
+
const threshold = options && options.threshold != null ? options.threshold : 0;
|
|
6213
|
+
const rootMargin = options && options.rootMargin ? options.rootMargin : "0px";
|
|
6214
|
+
const observer = new IntersectionObserver(function(entries, obs) {
|
|
6215
|
+
entries.forEach(function(entry) {
|
|
6216
|
+
if (entry.isIntersecting) {
|
|
6217
|
+
obs.unobserve(entry.target);
|
|
6218
|
+
_observerMap.delete(entry.target);
|
|
6219
|
+
try {
|
|
6220
|
+
callback(entry.target);
|
|
6221
|
+
} catch (e) {
|
|
6222
|
+
console.error("[VanduoLazyLoad] Callback threw:", e);
|
|
6223
|
+
}
|
|
6224
|
+
}
|
|
6225
|
+
});
|
|
6226
|
+
}, { threshold, rootMargin });
|
|
6227
|
+
_observerMap.set(element, observer);
|
|
6228
|
+
observer.observe(element);
|
|
6229
|
+
},
|
|
6230
|
+
/**
|
|
6231
|
+
* Stop observing an element that was previously passed to observe().
|
|
6232
|
+
* @param {Element} element
|
|
6233
|
+
*/
|
|
6234
|
+
unobserve: function(element) {
|
|
6235
|
+
const observer = _observerMap.get(element);
|
|
6236
|
+
if (observer) {
|
|
6237
|
+
observer.unobserve(element);
|
|
6238
|
+
_observerMap.delete(element);
|
|
6239
|
+
}
|
|
6240
|
+
},
|
|
6241
|
+
/**
|
|
6242
|
+
* Stop observing ALL currently observed elements.
|
|
6243
|
+
*/
|
|
6244
|
+
unobserveAll: function() {
|
|
6245
|
+
_observerMap.forEach(function(observer, element) {
|
|
6246
|
+
observer.unobserve(element);
|
|
6247
|
+
});
|
|
6248
|
+
_observerMap.clear();
|
|
6249
|
+
},
|
|
6250
|
+
/* ─────────────────────────────────────────────────
|
|
6251
|
+
* HIGH-LEVEL API
|
|
6252
|
+
* ───────────────────────────────────────────────── */
|
|
6253
|
+
/**
|
|
6254
|
+
* Fetch an HTML partial and inject it into `containerEl` when the
|
|
6255
|
+
* container enters the viewport. A placeholder is shown immediately.
|
|
6256
|
+
*
|
|
6257
|
+
* @param {string} url URL of the HTML partial to fetch
|
|
6258
|
+
* @param {Element} containerEl Target element whose content will be replaced
|
|
6259
|
+
* @param {{
|
|
6260
|
+
* placeholder?: 'skeleton'|'spinner'|string,
|
|
6261
|
+
* threshold?: number,
|
|
6262
|
+
* rootMargin?: string,
|
|
6263
|
+
* onLoaded?: function(Element): void,
|
|
6264
|
+
* onError?: function(Error): void
|
|
6265
|
+
* }} [options]
|
|
6266
|
+
*/
|
|
6267
|
+
loadSection: function(url, containerEl, options) {
|
|
6268
|
+
if (typeof url !== "string" || !url) {
|
|
6269
|
+
console.warn("[VanduoLazyLoad] loadSection() requires a non-empty URL string.");
|
|
6270
|
+
return;
|
|
6271
|
+
}
|
|
6272
|
+
if (!(containerEl instanceof Element)) {
|
|
6273
|
+
console.warn("[VanduoLazyLoad] loadSection() requires a DOM Element as containerEl.");
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
6276
|
+
if (!_isSafeUrl(url)) {
|
|
6277
|
+
console.error("[VanduoLazyLoad] loadSection() blocked cross-origin URL:", url);
|
|
6278
|
+
return;
|
|
6279
|
+
}
|
|
6280
|
+
const opts = options || {};
|
|
6281
|
+
const placeholderHtml = _resolvePlaceholder(opts.placeholder);
|
|
6282
|
+
_safeInjectHtml(containerEl, placeholderHtml);
|
|
6283
|
+
_dispatch(containerEl, "lazysection:loading", { url });
|
|
6284
|
+
this.observe(containerEl, function() {
|
|
6285
|
+
const controller = new window.AbortController();
|
|
6286
|
+
const timeoutId = setTimeout(function() {
|
|
6287
|
+
controller.abort();
|
|
6288
|
+
}, 1e4);
|
|
6289
|
+
window.fetch(url, { signal: controller.signal }).then(function(res) {
|
|
6290
|
+
clearTimeout(timeoutId);
|
|
6291
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
6292
|
+
return res.text();
|
|
6293
|
+
}).then(function(html) {
|
|
6294
|
+
_safeInjectHtml(containerEl, html);
|
|
6295
|
+
_dispatch(containerEl, "lazysection:loaded", { url });
|
|
6296
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
6297
|
+
window.Vanduo.init();
|
|
6298
|
+
}
|
|
6299
|
+
if (typeof opts.onLoaded === "function") {
|
|
6300
|
+
opts.onLoaded(containerEl);
|
|
6301
|
+
}
|
|
6302
|
+
}).catch(function(err) {
|
|
6303
|
+
const alertEl = document.createElement("div");
|
|
6304
|
+
alertEl.className = "vd-alert vd-alert-error";
|
|
6305
|
+
alertEl.setAttribute("role", "alert");
|
|
6306
|
+
const msgEl = document.createElement("span");
|
|
6307
|
+
msgEl.textContent = "Failed to load content. ";
|
|
6308
|
+
const detailEl = document.createElement("small");
|
|
6309
|
+
detailEl.style.opacity = "0.7";
|
|
6310
|
+
detailEl.textContent = err.message;
|
|
6311
|
+
alertEl.appendChild(msgEl);
|
|
6312
|
+
alertEl.appendChild(detailEl);
|
|
6313
|
+
while (containerEl.firstChild) {
|
|
6314
|
+
containerEl.removeChild(containerEl.firstChild);
|
|
6315
|
+
}
|
|
6316
|
+
containerEl.appendChild(alertEl);
|
|
6317
|
+
_dispatch(containerEl, "lazysection:error", { url, error: err });
|
|
6318
|
+
console.error("[VanduoLazyLoad] loadSection failed:", err);
|
|
6319
|
+
if (typeof opts.onError === "function") {
|
|
6320
|
+
opts.onError(err);
|
|
6321
|
+
}
|
|
6322
|
+
});
|
|
6323
|
+
}, { threshold: opts.threshold, rootMargin: opts.rootMargin });
|
|
6324
|
+
},
|
|
6325
|
+
/* ─────────────────────────────────────────────────
|
|
6326
|
+
* ATTRIBUTE-DRIVEN INIT
|
|
6327
|
+
* ───────────────────────────────────────────────── */
|
|
6328
|
+
/**
|
|
6329
|
+
* Scan the DOM for [data-vd-lazy] elements and wire them up.
|
|
6330
|
+
* Safe to call multiple times — already-observed elements are skipped.
|
|
6331
|
+
*/
|
|
6332
|
+
init: function() {
|
|
6333
|
+
const self = this;
|
|
6334
|
+
const elements = document.querySelectorAll("[data-vd-lazy]");
|
|
6335
|
+
elements.forEach(function(el) {
|
|
6336
|
+
if (_observerMap.has(el) || el.dataset.vdLazyState === "loading" || el.dataset.vdLazyState === "loaded") return;
|
|
6337
|
+
const url = el.getAttribute("data-vd-lazy");
|
|
6338
|
+
if (!url) return;
|
|
6339
|
+
el.dataset.vdLazyState = "loading";
|
|
6340
|
+
const placeholder = el.getAttribute("data-vd-lazy-placeholder") || "skeleton";
|
|
6341
|
+
self.loadSection(url, el, {
|
|
6342
|
+
placeholder,
|
|
6343
|
+
onLoaded: function() {
|
|
6344
|
+
el.dataset.vdLazyState = "loaded";
|
|
6345
|
+
},
|
|
6346
|
+
onError: function() {
|
|
6347
|
+
el.dataset.vdLazyState = "error";
|
|
6348
|
+
}
|
|
6349
|
+
});
|
|
6350
|
+
});
|
|
6351
|
+
}
|
|
6352
|
+
};
|
|
6353
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
6354
|
+
window.Vanduo.register("LazyLoad", VanduoLazyLoad);
|
|
6355
|
+
}
|
|
6356
|
+
window.VanduoLazyLoad = VanduoLazyLoad;
|
|
6357
|
+
})();
|
|
6358
|
+
|
|
6150
6359
|
// js/index.js
|
|
6151
6360
|
var Vanduo = window.Vanduo;
|
|
6152
6361
|
var index_default = Vanduo;
|