@vanduo-oss/framework 1.3.0 → 1.3.2
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 +13 -12
- package/css/components/music-player.css +578 -0
- package/css/components/navbar.css +5 -0
- package/css/vanduo.css +1 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +729 -44
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +5 -5
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +512 -1
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +729 -44
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +5 -5
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +729 -44
- 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 -5
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/code-snippet.js +5 -4
- package/js/components/dropdown.js +9 -9
- package/js/components/image-box.js +7 -1
- package/js/components/modals.js +18 -10
- package/js/components/music-player.js +848 -0
- package/js/components/navbar.js +3 -3
- package/js/components/select.js +15 -13
- package/js/components/suggest.js +14 -1
- package/js/components/theme-customizer.js +0 -1
- package/js/components/validate.js +14 -3
- package/js/index.js +1 -0
- package/js/utils/helpers.js +7 -3
- package/package.json +2 -2
package/dist/vanduo.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.3.
|
|
1
|
+
/*! Vanduo v1.3.2 | Built: 2026-04-06T19:04:41.601Z | git:8e08b38 | development */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -132,7 +132,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
132
132
|
// js/vanduo.js
|
|
133
133
|
(function() {
|
|
134
134
|
"use strict";
|
|
135
|
-
const VANDUO_VERSION = true ? "1.3.
|
|
135
|
+
const VANDUO_VERSION = true ? "1.3.2" : "0.0.0-dev";
|
|
136
136
|
const Vanduo2 = {
|
|
137
137
|
version: VANDUO_VERSION,
|
|
138
138
|
components: {},
|
|
@@ -443,16 +443,17 @@ module.exports = __toCommonJS(index_exports);
|
|
|
443
443
|
}
|
|
444
444
|
const codeElement = activePane.querySelector("code") || activePane;
|
|
445
445
|
const code = codeElement.textContent;
|
|
446
|
+
let copySuccess;
|
|
446
447
|
try {
|
|
447
448
|
await navigator.clipboard.writeText(code);
|
|
448
|
-
|
|
449
|
+
copySuccess = true;
|
|
449
450
|
} catch (_err) {
|
|
450
|
-
|
|
451
|
-
this.showCopyFeedback(copyBtn, success);
|
|
451
|
+
copySuccess = this.fallbackCopy(code);
|
|
452
452
|
}
|
|
453
|
+
this.showCopyFeedback(copyBtn, copySuccess);
|
|
453
454
|
const event = new CustomEvent("codesnippet:copy", {
|
|
454
455
|
bubbles: true,
|
|
455
|
-
detail: { snippet, code, success:
|
|
456
|
+
detail: { snippet, code, success: copySuccess }
|
|
456
457
|
});
|
|
457
458
|
snippet.dispatchEvent(event);
|
|
458
459
|
},
|
|
@@ -889,9 +890,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
889
890
|
const Dropdown = {
|
|
890
891
|
// Store initialized dropdowns and their cleanup functions
|
|
891
892
|
instances: /* @__PURE__ */ new Map(),
|
|
892
|
-
// Typeahead state
|
|
893
|
-
_typeaheadBuffer: "",
|
|
894
|
-
_typeaheadTimer: null,
|
|
895
893
|
/**
|
|
896
894
|
* Initialize dropdown components
|
|
897
895
|
*/
|
|
@@ -955,7 +953,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
955
953
|
item.addEventListener("keydown", itemKeydownHandler);
|
|
956
954
|
cleanupFunctions.push(() => item.removeEventListener("keydown", itemKeydownHandler));
|
|
957
955
|
});
|
|
958
|
-
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions });
|
|
956
|
+
this.instances.set(dropdown, { toggle, menu, cleanup: cleanupFunctions, typeaheadBuffer: "", typeaheadTimer: null });
|
|
959
957
|
},
|
|
960
958
|
/**
|
|
961
959
|
* Toggle dropdown
|
|
@@ -1084,16 +1082,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1084
1082
|
break;
|
|
1085
1083
|
default:
|
|
1086
1084
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
1087
|
-
|
|
1088
|
-
|
|
1085
|
+
const instance = this.instances.get(dropdown);
|
|
1086
|
+
if (!instance) break;
|
|
1087
|
+
clearTimeout(instance.typeaheadTimer);
|
|
1088
|
+
instance.typeaheadBuffer += e.key.toLowerCase();
|
|
1089
1089
|
const match = items.find(
|
|
1090
|
-
(item) => item.textContent.trim().toLowerCase().startsWith(
|
|
1090
|
+
(item) => item.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
|
|
1091
1091
|
);
|
|
1092
1092
|
if (match) {
|
|
1093
1093
|
match.focus();
|
|
1094
1094
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1095
|
+
instance.typeaheadTimer = setTimeout(() => {
|
|
1096
|
+
instance.typeaheadBuffer = "";
|
|
1097
1097
|
}, 500);
|
|
1098
1098
|
}
|
|
1099
1099
|
break;
|
|
@@ -1801,9 +1801,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1801
1801
|
}));
|
|
1802
1802
|
if (!this.img.complete) {
|
|
1803
1803
|
this.img.style.opacity = "0";
|
|
1804
|
-
this.
|
|
1804
|
+
this._imgLoadHandler = () => {
|
|
1805
1805
|
this.img.style.opacity = "";
|
|
1806
1806
|
};
|
|
1807
|
+
this.img.addEventListener("load", this._imgLoadHandler, { once: true });
|
|
1807
1808
|
}
|
|
1808
1809
|
},
|
|
1809
1810
|
/**
|
|
@@ -1822,6 +1823,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1822
1823
|
}
|
|
1823
1824
|
setTimeout(() => {
|
|
1824
1825
|
if (!this.isOpen) {
|
|
1826
|
+
if (this._imgLoadHandler) {
|
|
1827
|
+
this.img.removeEventListener("load", this._imgLoadHandler);
|
|
1828
|
+
this._imgLoadHandler = null;
|
|
1829
|
+
}
|
|
1825
1830
|
this.img.src = "";
|
|
1826
1831
|
this.img.alt = "";
|
|
1827
1832
|
}
|
|
@@ -1881,6 +1886,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1881
1886
|
zIndexCounter: 1050,
|
|
1882
1887
|
// Store trigger cleanup functions
|
|
1883
1888
|
_triggerCleanups: [],
|
|
1889
|
+
// Shared ESC key handler (installed once)
|
|
1890
|
+
_sharedEscHandler: null,
|
|
1884
1891
|
/**
|
|
1885
1892
|
* Initialize modals
|
|
1886
1893
|
*/
|
|
@@ -1945,16 +1952,17 @@ module.exports = __toCommonJS(index_exports);
|
|
|
1945
1952
|
};
|
|
1946
1953
|
backdrop.addEventListener("click", backdropClickHandler);
|
|
1947
1954
|
cleanupFunctions.push(() => backdrop.removeEventListener("click", backdropClickHandler));
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1955
|
+
if (!this._sharedEscHandler) {
|
|
1956
|
+
this._sharedEscHandler = (e) => {
|
|
1957
|
+
if (e.key === "Escape" && this.openModals.length > 0) {
|
|
1958
|
+
const topModal = this.openModals[this.openModals.length - 1];
|
|
1959
|
+
if (topModal.dataset.keyboard !== "false") {
|
|
1960
|
+
this.close(topModal);
|
|
1961
|
+
}
|
|
1953
1962
|
}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
cleanupFunctions.push(() => document.removeEventListener("keydown", escKeyHandler));
|
|
1963
|
+
};
|
|
1964
|
+
document.addEventListener("keydown", this._sharedEscHandler);
|
|
1965
|
+
}
|
|
1958
1966
|
this.modals.set(modal, { backdrop, dialog, trapHandler: null, cleanup: cleanupFunctions });
|
|
1959
1967
|
},
|
|
1960
1968
|
/**
|
|
@@ -2134,6 +2142,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2134
2142
|
});
|
|
2135
2143
|
this._triggerCleanups.forEach((fn) => fn());
|
|
2136
2144
|
this._triggerCleanups = [];
|
|
2145
|
+
if (this._sharedEscHandler) {
|
|
2146
|
+
document.removeEventListener("keydown", this._sharedEscHandler);
|
|
2147
|
+
this._sharedEscHandler = null;
|
|
2148
|
+
}
|
|
2137
2149
|
}
|
|
2138
2150
|
};
|
|
2139
2151
|
if (typeof window.Vanduo !== "undefined") {
|
|
@@ -2299,7 +2311,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2299
2311
|
if (overlay) {
|
|
2300
2312
|
overlay.classList.add("is-active");
|
|
2301
2313
|
}
|
|
2302
|
-
document.body.
|
|
2314
|
+
document.body.classList.add("body-navbar-open");
|
|
2303
2315
|
toggle.setAttribute("aria-expanded", "true");
|
|
2304
2316
|
menu.setAttribute("aria-hidden", "false");
|
|
2305
2317
|
},
|
|
@@ -2316,7 +2328,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2316
2328
|
if (overlay) {
|
|
2317
2329
|
overlay.classList.remove("is-active");
|
|
2318
2330
|
}
|
|
2319
|
-
document.body.
|
|
2331
|
+
document.body.classList.remove("body-navbar-open");
|
|
2320
2332
|
const dropdownMenus = menu.querySelectorAll(".vd-navbar-dropdown-menu.is-open");
|
|
2321
2333
|
dropdownMenus.forEach((dropdownMenu) => {
|
|
2322
2334
|
dropdownMenu.classList.remove("is-open");
|
|
@@ -2875,9 +2887,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2875
2887
|
const Select = {
|
|
2876
2888
|
// Store initialized selects and their cleanup functions
|
|
2877
2889
|
instances: /* @__PURE__ */ new Map(),
|
|
2878
|
-
// Typeahead state
|
|
2879
|
-
_typeaheadBuffer: "",
|
|
2880
|
-
_typeaheadTimer: null,
|
|
2881
2890
|
/**
|
|
2882
2891
|
* Initialize select components
|
|
2883
2892
|
*/
|
|
@@ -2958,7 +2967,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
2958
2967
|
};
|
|
2959
2968
|
select.addEventListener("change", changeHandler);
|
|
2960
2969
|
cleanupFunctions.push(() => select.removeEventListener("change", changeHandler));
|
|
2961
|
-
this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions });
|
|
2970
|
+
this.instances.set(select, { wrapper, button, dropdown, cleanup: cleanupFunctions, typeaheadBuffer: "", typeaheadTimer: null });
|
|
2962
2971
|
},
|
|
2963
2972
|
/**
|
|
2964
2973
|
* Build options in dropdown
|
|
@@ -3052,7 +3061,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3052
3061
|
* @param {HTMLElement} dropdown - Dropdown container
|
|
3053
3062
|
*/
|
|
3054
3063
|
updateSelectedOptions: function(select, dropdown) {
|
|
3055
|
-
const options = dropdown.querySelectorAll(".
|
|
3064
|
+
const options = dropdown.querySelectorAll(".custom-select-option");
|
|
3056
3065
|
const selectedValues = Array.from(select.selectedOptions).map((opt) => opt.value);
|
|
3057
3066
|
options.forEach((optionEl) => {
|
|
3058
3067
|
const value = optionEl.dataset.value;
|
|
@@ -3086,7 +3095,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3086
3095
|
openDropdown: function(button, dropdown) {
|
|
3087
3096
|
dropdown.classList.add("is-open");
|
|
3088
3097
|
button.setAttribute("aria-expanded", "true");
|
|
3089
|
-
const firstOption = dropdown.querySelector(".
|
|
3098
|
+
const firstOption = dropdown.querySelector(".custom-select-option:not(.is-disabled)");
|
|
3090
3099
|
if (firstOption) {
|
|
3091
3100
|
firstOption.focus();
|
|
3092
3101
|
}
|
|
@@ -3109,7 +3118,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3109
3118
|
*/
|
|
3110
3119
|
handleKeydown: function(e, select, button, dropdown) {
|
|
3111
3120
|
const isOpen = dropdown.classList.contains("is-open");
|
|
3112
|
-
const options = Array.from(dropdown.querySelectorAll(".
|
|
3121
|
+
const options = Array.from(dropdown.querySelectorAll(".custom-select-option:not(.is-disabled)"));
|
|
3113
3122
|
const currentIndex = options.findIndex((opt) => opt === document.activeElement);
|
|
3114
3123
|
switch (e.key) {
|
|
3115
3124
|
case "Enter":
|
|
@@ -3160,16 +3169,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3160
3169
|
break;
|
|
3161
3170
|
default:
|
|
3162
3171
|
if (isOpen && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
3163
|
-
|
|
3164
|
-
|
|
3172
|
+
const instance = this.instances.get(select);
|
|
3173
|
+
if (!instance) break;
|
|
3174
|
+
clearTimeout(instance.typeaheadTimer);
|
|
3175
|
+
instance.typeaheadBuffer += e.key.toLowerCase();
|
|
3165
3176
|
const match = options.find(
|
|
3166
|
-
(opt) => opt.textContent.trim().toLowerCase().startsWith(
|
|
3177
|
+
(opt) => opt.textContent.trim().toLowerCase().startsWith(instance.typeaheadBuffer)
|
|
3167
3178
|
);
|
|
3168
3179
|
if (match) {
|
|
3169
3180
|
match.focus();
|
|
3170
3181
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3182
|
+
instance.typeaheadTimer = setTimeout(() => {
|
|
3183
|
+
instance.typeaheadBuffer = "";
|
|
3173
3184
|
}, 500);
|
|
3174
3185
|
}
|
|
3175
3186
|
break;
|
|
@@ -3201,7 +3212,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
3201
3212
|
if (element.id) {
|
|
3202
3213
|
return element.id;
|
|
3203
3214
|
}
|
|
3204
|
-
|
|
3215
|
+
const id = "select-" + Math.random().toString(36).substr(2, 9);
|
|
3216
|
+
element.id = id;
|
|
3217
|
+
return id;
|
|
3205
3218
|
},
|
|
3206
3219
|
/**
|
|
3207
3220
|
* Destroy a select instance and clean up event listeners
|
|
@@ -4255,7 +4268,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
4255
4268
|
this.applyNeutral(this.DEFAULTS.NEUTRAL);
|
|
4256
4269
|
this.applyRadius(this.DEFAULTS.RADIUS);
|
|
4257
4270
|
this.applyFont(this.DEFAULTS.FONT);
|
|
4258
|
-
this.applyTheme(this.DEFAULTS.THEME);
|
|
4259
4271
|
this.updateUI();
|
|
4260
4272
|
this.dispatchEvent("reset", { state: { ...this.state } });
|
|
4261
4273
|
},
|
|
@@ -7040,6 +7052,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7040
7052
|
// js/components/suggest.js
|
|
7041
7053
|
(function() {
|
|
7042
7054
|
"use strict";
|
|
7055
|
+
function _escapeHtml(text) {
|
|
7056
|
+
const div = document.createElement("div");
|
|
7057
|
+
div.textContent = text;
|
|
7058
|
+
return div.innerHTML;
|
|
7059
|
+
}
|
|
7043
7060
|
const Suggest = {
|
|
7044
7061
|
instances: /* @__PURE__ */ new Map(),
|
|
7045
7062
|
init: function() {
|
|
@@ -7099,8 +7116,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7099
7116
|
li.id = listId + "-item-" + i;
|
|
7100
7117
|
const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
|
|
7101
7118
|
if (query) {
|
|
7119
|
+
const escaped = _escapeHtml(text);
|
|
7102
7120
|
const re = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
|
|
7103
|
-
li.innerHTML =
|
|
7121
|
+
li.innerHTML = escaped.replace(re, '<span class="vd-suggest-match">$1</span>');
|
|
7104
7122
|
} else {
|
|
7105
7123
|
li.textContent = text;
|
|
7106
7124
|
}
|
|
@@ -7253,14 +7271,20 @@ module.exports = __toCommonJS(index_exports);
|
|
|
7253
7271
|
maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
|
|
7254
7272
|
pattern: (value, param) => {
|
|
7255
7273
|
try {
|
|
7274
|
+
if (param.length > 100) return false;
|
|
7256
7275
|
return new RegExp(param).test(value);
|
|
7257
7276
|
} catch (_e) {
|
|
7258
7277
|
return false;
|
|
7259
7278
|
}
|
|
7260
7279
|
},
|
|
7261
7280
|
match: (value, param) => {
|
|
7262
|
-
|
|
7263
|
-
|
|
7281
|
+
try {
|
|
7282
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(param) : param;
|
|
7283
|
+
const other = document.querySelector('[name="' + escaped + '"]');
|
|
7284
|
+
return other ? value === other.value : false;
|
|
7285
|
+
} catch (_e) {
|
|
7286
|
+
return false;
|
|
7287
|
+
}
|
|
7264
7288
|
}
|
|
7265
7289
|
},
|
|
7266
7290
|
messages: {
|
|
@@ -8541,6 +8565,667 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8541
8565
|
window.VanduoSpotlight = Spotlight;
|
|
8542
8566
|
})();
|
|
8543
8567
|
|
|
8568
|
+
// js/components/music-player.js
|
|
8569
|
+
(function() {
|
|
8570
|
+
"use strict";
|
|
8571
|
+
function shuffleArray(arr) {
|
|
8572
|
+
const shuffled = arr.slice();
|
|
8573
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
8574
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
8575
|
+
const tmp = shuffled[i];
|
|
8576
|
+
shuffled[i] = shuffled[j];
|
|
8577
|
+
shuffled[j] = tmp;
|
|
8578
|
+
}
|
|
8579
|
+
return shuffled;
|
|
8580
|
+
}
|
|
8581
|
+
function formatTime(seconds) {
|
|
8582
|
+
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
8583
|
+
const m = Math.floor(seconds / 60);
|
|
8584
|
+
const s = Math.floor(seconds % 60);
|
|
8585
|
+
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
8586
|
+
}
|
|
8587
|
+
function updateRangeFill(input) {
|
|
8588
|
+
const min = parseFloat(input.min) || 0;
|
|
8589
|
+
const max = parseFloat(input.max) || 1;
|
|
8590
|
+
const val = parseFloat(input.value) || 0;
|
|
8591
|
+
const pct = (val - min) / (max - min) * 100;
|
|
8592
|
+
input.style.setProperty("--fill", pct + "%");
|
|
8593
|
+
input.style.backgroundImage = "linear-gradient(to right, var(--music-player-track-fill, currentColor) 0%, var(--music-player-track-fill, currentColor) " + pct + "%, var(--music-player-track-bg, #ccc) " + pct + "%, var(--music-player-track-bg, #ccc) 100%)";
|
|
8594
|
+
}
|
|
8595
|
+
function icon(name) {
|
|
8596
|
+
const el = document.createElement("i");
|
|
8597
|
+
el.className = "ph ph-" + name;
|
|
8598
|
+
el.setAttribute("aria-hidden", "true");
|
|
8599
|
+
return el;
|
|
8600
|
+
}
|
|
8601
|
+
const MusicPlayer = {
|
|
8602
|
+
/** @type {Map<HTMLElement, Object>} */
|
|
8603
|
+
instances: /* @__PURE__ */ new Map(),
|
|
8604
|
+
/**
|
|
8605
|
+
* Default options.
|
|
8606
|
+
*/
|
|
8607
|
+
defaults: {
|
|
8608
|
+
tracks: [],
|
|
8609
|
+
volume: 0.5,
|
|
8610
|
+
shuffle: false,
|
|
8611
|
+
showProgress: false,
|
|
8612
|
+
showPlaylist: false,
|
|
8613
|
+
autoAdvance: true
|
|
8614
|
+
},
|
|
8615
|
+
/**
|
|
8616
|
+
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
8617
|
+
* Options can be provided via data-music-player-options (JSON string).
|
|
8618
|
+
*/
|
|
8619
|
+
init: function() {
|
|
8620
|
+
document.querySelectorAll(".vd-music-player, [data-music-player]").forEach((el) => {
|
|
8621
|
+
if (this.instances.has(el)) return;
|
|
8622
|
+
let opts = {};
|
|
8623
|
+
const attr = el.getAttribute("data-music-player-options");
|
|
8624
|
+
if (attr) {
|
|
8625
|
+
try {
|
|
8626
|
+
opts = JSON.parse(attr);
|
|
8627
|
+
} catch (_) {
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
this.initPlayer(el, opts);
|
|
8631
|
+
});
|
|
8632
|
+
},
|
|
8633
|
+
/**
|
|
8634
|
+
* Initialize a single player element.
|
|
8635
|
+
* @param {HTMLElement} container
|
|
8636
|
+
* @param {Object} [options]
|
|
8637
|
+
*/
|
|
8638
|
+
initPlayer: function(container, options) {
|
|
8639
|
+
const opts = Object.assign({}, this.defaults, options || {});
|
|
8640
|
+
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
8641
|
+
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
8642
|
+
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
8643
|
+
const state = {
|
|
8644
|
+
tracks: trackList,
|
|
8645
|
+
originalTracks: tracks.slice(),
|
|
8646
|
+
currentIndex: 0,
|
|
8647
|
+
isPlaying: false,
|
|
8648
|
+
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
8649
|
+
shuffle: opts.shuffle,
|
|
8650
|
+
showProgress: opts.showProgress,
|
|
8651
|
+
showPlaylist: opts.showPlaylist,
|
|
8652
|
+
autoAdvance: opts.autoAdvance,
|
|
8653
|
+
audio: null
|
|
8654
|
+
};
|
|
8655
|
+
const audio = new Audio();
|
|
8656
|
+
audio.volume = state.volume;
|
|
8657
|
+
audio.preload = "metadata";
|
|
8658
|
+
state.audio = audio;
|
|
8659
|
+
this._buildDOM(container, state);
|
|
8660
|
+
const refs = {
|
|
8661
|
+
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
8662
|
+
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
8663
|
+
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
8664
|
+
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
8665
|
+
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
8666
|
+
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
8667
|
+
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
8668
|
+
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
8669
|
+
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
8670
|
+
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
8671
|
+
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
8672
|
+
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
8673
|
+
};
|
|
8674
|
+
const renderPlayIcon = () => {
|
|
8675
|
+
const btn = refs.btnPlay;
|
|
8676
|
+
if (!btn) return;
|
|
8677
|
+
btn.innerHTML = "";
|
|
8678
|
+
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
8679
|
+
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
8680
|
+
btn.classList.toggle("is-active", state.isPlaying);
|
|
8681
|
+
};
|
|
8682
|
+
const renderTrackName = () => {
|
|
8683
|
+
const el = refs.trackName;
|
|
8684
|
+
if (!el) return;
|
|
8685
|
+
const track = state.tracks[state.currentIndex];
|
|
8686
|
+
if (track) {
|
|
8687
|
+
el.textContent = track.name || "Unknown Track";
|
|
8688
|
+
el.classList.remove("is-idle");
|
|
8689
|
+
} else {
|
|
8690
|
+
el.textContent = "No tracks loaded";
|
|
8691
|
+
el.classList.add("is-idle");
|
|
8692
|
+
}
|
|
8693
|
+
};
|
|
8694
|
+
const renderVolumeIcon = () => {
|
|
8695
|
+
const el = refs.volumeIcon;
|
|
8696
|
+
if (!el) return;
|
|
8697
|
+
el.innerHTML = "";
|
|
8698
|
+
const v = state.volume;
|
|
8699
|
+
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
8700
|
+
el.appendChild(icon(name));
|
|
8701
|
+
};
|
|
8702
|
+
const renderShuffleBtn = () => {
|
|
8703
|
+
const btn = refs.btnShuffle;
|
|
8704
|
+
if (!btn) return;
|
|
8705
|
+
btn.classList.toggle("is-active", state.shuffle);
|
|
8706
|
+
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
8707
|
+
};
|
|
8708
|
+
const renderPlaylistItems = () => {
|
|
8709
|
+
const panel = refs.playlistPanel;
|
|
8710
|
+
if (!panel) return;
|
|
8711
|
+
panel.innerHTML = "";
|
|
8712
|
+
state.tracks.forEach((track, i) => {
|
|
8713
|
+
const item = document.createElement("button");
|
|
8714
|
+
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
8715
|
+
item.type = "button";
|
|
8716
|
+
item.setAttribute("data-index", String(i));
|
|
8717
|
+
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
8718
|
+
const num = document.createElement("span");
|
|
8719
|
+
num.className = "vd-music-player-playlist-num";
|
|
8720
|
+
num.textContent = String(i + 1);
|
|
8721
|
+
const name = document.createElement("span");
|
|
8722
|
+
name.className = "vd-music-player-playlist-name";
|
|
8723
|
+
name.textContent = track.name || "Track " + (i + 1);
|
|
8724
|
+
item.appendChild(num);
|
|
8725
|
+
item.appendChild(name);
|
|
8726
|
+
panel.appendChild(item);
|
|
8727
|
+
});
|
|
8728
|
+
};
|
|
8729
|
+
const renderProgress = () => {
|
|
8730
|
+
const bar = refs.progressBar;
|
|
8731
|
+
if (!bar || !audio.duration) return;
|
|
8732
|
+
const pct = audio.currentTime / audio.duration * 100;
|
|
8733
|
+
bar.value = String(pct);
|
|
8734
|
+
updateRangeFill(bar);
|
|
8735
|
+
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
8736
|
+
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
8737
|
+
};
|
|
8738
|
+
const loadTrack = (index, autoPlay) => {
|
|
8739
|
+
const track = state.tracks[index];
|
|
8740
|
+
if (!track) return;
|
|
8741
|
+
state.currentIndex = index;
|
|
8742
|
+
audio.src = track.url;
|
|
8743
|
+
renderTrackName();
|
|
8744
|
+
renderPlaylistItems();
|
|
8745
|
+
if (refs.progressBar) {
|
|
8746
|
+
refs.progressBar.value = "0";
|
|
8747
|
+
updateRangeFill(refs.progressBar);
|
|
8748
|
+
}
|
|
8749
|
+
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
8750
|
+
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
8751
|
+
container.dispatchEvent(
|
|
8752
|
+
new CustomEvent("musicplayer:trackchange", {
|
|
8753
|
+
bubbles: true,
|
|
8754
|
+
detail: { index, name: track.name, url: track.url }
|
|
8755
|
+
})
|
|
8756
|
+
);
|
|
8757
|
+
if (autoPlay) {
|
|
8758
|
+
audio.play().catch(() => {
|
|
8759
|
+
});
|
|
8760
|
+
}
|
|
8761
|
+
};
|
|
8762
|
+
const cleanupFunctions = [];
|
|
8763
|
+
const onPlay = () => {
|
|
8764
|
+
state.isPlaying = true;
|
|
8765
|
+
renderPlayIcon();
|
|
8766
|
+
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
8767
|
+
};
|
|
8768
|
+
const onPause = () => {
|
|
8769
|
+
state.isPlaying = false;
|
|
8770
|
+
renderPlayIcon();
|
|
8771
|
+
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
8772
|
+
};
|
|
8773
|
+
const onEnded = () => {
|
|
8774
|
+
if (state.autoAdvance && state.tracks.length > 1) {
|
|
8775
|
+
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
8776
|
+
loadTrack(next, true);
|
|
8777
|
+
} else {
|
|
8778
|
+
state.isPlaying = false;
|
|
8779
|
+
renderPlayIcon();
|
|
8780
|
+
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
8781
|
+
}
|
|
8782
|
+
};
|
|
8783
|
+
const onTimeUpdate = () => {
|
|
8784
|
+
if (state.showProgress) renderProgress();
|
|
8785
|
+
};
|
|
8786
|
+
const onLoadedMetadata = () => {
|
|
8787
|
+
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
8788
|
+
if (refs.progressBar) {
|
|
8789
|
+
refs.progressBar.max = "100";
|
|
8790
|
+
updateRangeFill(refs.progressBar);
|
|
8791
|
+
}
|
|
8792
|
+
};
|
|
8793
|
+
audio.addEventListener("play", onPlay);
|
|
8794
|
+
audio.addEventListener("pause", onPause);
|
|
8795
|
+
audio.addEventListener("ended", onEnded);
|
|
8796
|
+
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
8797
|
+
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
8798
|
+
cleanupFunctions.push(() => {
|
|
8799
|
+
audio.removeEventListener("play", onPlay);
|
|
8800
|
+
audio.removeEventListener("pause", onPause);
|
|
8801
|
+
audio.removeEventListener("ended", onEnded);
|
|
8802
|
+
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
8803
|
+
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
8804
|
+
audio.pause();
|
|
8805
|
+
audio.src = "";
|
|
8806
|
+
});
|
|
8807
|
+
if (refs.btnPlay) {
|
|
8808
|
+
const handler = () => {
|
|
8809
|
+
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
8810
|
+
if (state.isPlaying) {
|
|
8811
|
+
audio.pause();
|
|
8812
|
+
} else {
|
|
8813
|
+
audio.play().catch(() => {
|
|
8814
|
+
});
|
|
8815
|
+
}
|
|
8816
|
+
};
|
|
8817
|
+
refs.btnPlay.addEventListener("click", handler);
|
|
8818
|
+
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
8819
|
+
const keyHandler = (e) => {
|
|
8820
|
+
if (e.key === " " || e.key === "Enter") {
|
|
8821
|
+
e.preventDefault();
|
|
8822
|
+
handler();
|
|
8823
|
+
}
|
|
8824
|
+
};
|
|
8825
|
+
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
8826
|
+
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
8827
|
+
}
|
|
8828
|
+
if (refs.btnPrev) {
|
|
8829
|
+
const handler = () => {
|
|
8830
|
+
if (!state.tracks.length) return;
|
|
8831
|
+
if (audio.currentTime > 3) {
|
|
8832
|
+
audio.currentTime = 0;
|
|
8833
|
+
} else {
|
|
8834
|
+
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
8835
|
+
loadTrack(prev, state.isPlaying);
|
|
8836
|
+
}
|
|
8837
|
+
};
|
|
8838
|
+
refs.btnPrev.addEventListener("click", handler);
|
|
8839
|
+
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
8840
|
+
}
|
|
8841
|
+
if (refs.btnNext) {
|
|
8842
|
+
const handler = () => {
|
|
8843
|
+
if (!state.tracks.length) return;
|
|
8844
|
+
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
8845
|
+
loadTrack(next, state.isPlaying);
|
|
8846
|
+
};
|
|
8847
|
+
refs.btnNext.addEventListener("click", handler);
|
|
8848
|
+
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
8849
|
+
}
|
|
8850
|
+
if (refs.btnShuffle) {
|
|
8851
|
+
const handler = () => {
|
|
8852
|
+
state.shuffle = !state.shuffle;
|
|
8853
|
+
if (state.shuffle) {
|
|
8854
|
+
const current = state.tracks[state.currentIndex];
|
|
8855
|
+
state.tracks = shuffleArray(state.tracks);
|
|
8856
|
+
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
8857
|
+
if (newIdx > 0) {
|
|
8858
|
+
state.tracks.splice(newIdx, 1);
|
|
8859
|
+
state.tracks.unshift(current);
|
|
8860
|
+
}
|
|
8861
|
+
state.currentIndex = 0;
|
|
8862
|
+
} else {
|
|
8863
|
+
const current = state.tracks[state.currentIndex];
|
|
8864
|
+
state.tracks = state.originalTracks.slice();
|
|
8865
|
+
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
8866
|
+
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
8867
|
+
}
|
|
8868
|
+
renderShuffleBtn();
|
|
8869
|
+
renderPlaylistItems();
|
|
8870
|
+
};
|
|
8871
|
+
refs.btnShuffle.addEventListener("click", handler);
|
|
8872
|
+
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
8873
|
+
}
|
|
8874
|
+
if (refs.btnPlaylist) {
|
|
8875
|
+
const handler = () => {
|
|
8876
|
+
const panel = refs.playlistPanel;
|
|
8877
|
+
if (!panel) return;
|
|
8878
|
+
const isOpen = panel.classList.toggle("is-open");
|
|
8879
|
+
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
8880
|
+
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
8881
|
+
};
|
|
8882
|
+
refs.btnPlaylist.addEventListener("click", handler);
|
|
8883
|
+
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
8884
|
+
}
|
|
8885
|
+
if (refs.volumeSlider) {
|
|
8886
|
+
const handler = (e) => {
|
|
8887
|
+
const v = parseFloat(e.target.value);
|
|
8888
|
+
state.volume = v;
|
|
8889
|
+
audio.volume = v;
|
|
8890
|
+
renderVolumeIcon();
|
|
8891
|
+
updateRangeFill(refs.volumeSlider);
|
|
8892
|
+
container.dispatchEvent(
|
|
8893
|
+
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
8894
|
+
);
|
|
8895
|
+
};
|
|
8896
|
+
refs.volumeSlider.addEventListener("input", handler);
|
|
8897
|
+
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
8898
|
+
updateRangeFill(refs.volumeSlider);
|
|
8899
|
+
}
|
|
8900
|
+
if (refs.progressBar) {
|
|
8901
|
+
const handler = (e) => {
|
|
8902
|
+
if (!audio.duration) return;
|
|
8903
|
+
const pct = parseFloat(e.target.value);
|
|
8904
|
+
audio.currentTime = pct / 100 * audio.duration;
|
|
8905
|
+
updateRangeFill(refs.progressBar);
|
|
8906
|
+
};
|
|
8907
|
+
refs.progressBar.addEventListener("input", handler);
|
|
8908
|
+
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
8909
|
+
}
|
|
8910
|
+
if (refs.playlistPanel) {
|
|
8911
|
+
const panelHandler = (e) => {
|
|
8912
|
+
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
8913
|
+
if (!item) return;
|
|
8914
|
+
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
8915
|
+
if (!isNaN(idx)) loadTrack(idx, true);
|
|
8916
|
+
};
|
|
8917
|
+
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
8918
|
+
cleanupFunctions.push(
|
|
8919
|
+
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
8920
|
+
);
|
|
8921
|
+
}
|
|
8922
|
+
renderPlayIcon();
|
|
8923
|
+
renderTrackName();
|
|
8924
|
+
renderVolumeIcon();
|
|
8925
|
+
if (opts.showPlaylist) renderPlaylistItems();
|
|
8926
|
+
this.instances.set(container, { state, audio, refs, cleanup: cleanupFunctions });
|
|
8927
|
+
container.setAttribute("data-music-player-initialized", "true");
|
|
8928
|
+
},
|
|
8929
|
+
/* ─── DOM builder ─────────────────────────────────────── */
|
|
8930
|
+
/**
|
|
8931
|
+
* Build the inner DOM structure inside container.
|
|
8932
|
+
* Pre-existing inner content is replaced only if it has no
|
|
8933
|
+
* recognised child elements (allows server-rendered markup).
|
|
8934
|
+
* @param {HTMLElement} container
|
|
8935
|
+
* @param {Object} state
|
|
8936
|
+
*/
|
|
8937
|
+
_buildDOM: function(container, state) {
|
|
8938
|
+
if (container.querySelector(".vd-music-player-controls")) return;
|
|
8939
|
+
container.setAttribute("role", "region");
|
|
8940
|
+
container.setAttribute("aria-label", "Music Player");
|
|
8941
|
+
if (state.showProgress) container.classList.add("has-progress");
|
|
8942
|
+
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
8943
|
+
const info = document.createElement("div");
|
|
8944
|
+
info.className = "vd-music-player-info";
|
|
8945
|
+
const iconWrap = document.createElement("span");
|
|
8946
|
+
iconWrap.className = "vd-music-player-icon";
|
|
8947
|
+
iconWrap.setAttribute("aria-hidden", "true");
|
|
8948
|
+
iconWrap.appendChild(icon("music-note"));
|
|
8949
|
+
const trackName = document.createElement("span");
|
|
8950
|
+
trackName.className = "vd-music-player-track-name";
|
|
8951
|
+
trackName.setAttribute("aria-live", "polite");
|
|
8952
|
+
trackName.setAttribute("aria-atomic", "true");
|
|
8953
|
+
info.appendChild(iconWrap);
|
|
8954
|
+
info.appendChild(trackName);
|
|
8955
|
+
container.appendChild(info);
|
|
8956
|
+
const controls = document.createElement("div");
|
|
8957
|
+
controls.className = "vd-music-player-controls";
|
|
8958
|
+
controls.setAttribute("role", "group");
|
|
8959
|
+
controls.setAttribute("aria-label", "Playback controls");
|
|
8960
|
+
const btnPrev = document.createElement("button");
|
|
8961
|
+
btnPrev.type = "button";
|
|
8962
|
+
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
8963
|
+
btnPrev.setAttribute("aria-label", "Previous track");
|
|
8964
|
+
btnPrev.appendChild(icon("skip-back"));
|
|
8965
|
+
const btnPlay = document.createElement("button");
|
|
8966
|
+
btnPlay.type = "button";
|
|
8967
|
+
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
8968
|
+
btnPlay.setAttribute("aria-label", "Play");
|
|
8969
|
+
btnPlay.appendChild(icon("play"));
|
|
8970
|
+
const btnNext = document.createElement("button");
|
|
8971
|
+
btnNext.type = "button";
|
|
8972
|
+
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
8973
|
+
btnNext.setAttribute("aria-label", "Next track");
|
|
8974
|
+
btnNext.appendChild(icon("skip-forward"));
|
|
8975
|
+
controls.appendChild(btnPrev);
|
|
8976
|
+
controls.appendChild(btnPlay);
|
|
8977
|
+
controls.appendChild(btnNext);
|
|
8978
|
+
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
8979
|
+
const btnShuffle = document.createElement("button");
|
|
8980
|
+
btnShuffle.type = "button";
|
|
8981
|
+
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
8982
|
+
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
8983
|
+
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
8984
|
+
btnShuffle.appendChild(icon("shuffle"));
|
|
8985
|
+
controls.appendChild(btnShuffle);
|
|
8986
|
+
}
|
|
8987
|
+
const spacer = document.createElement("span");
|
|
8988
|
+
spacer.className = "vd-music-player-spacer";
|
|
8989
|
+
spacer.setAttribute("aria-hidden", "true");
|
|
8990
|
+
controls.appendChild(spacer);
|
|
8991
|
+
const volumeWrap = document.createElement("div");
|
|
8992
|
+
volumeWrap.className = "vd-music-player-volume";
|
|
8993
|
+
const volumeIcon = document.createElement("span");
|
|
8994
|
+
volumeIcon.className = "vd-music-player-volume-icon";
|
|
8995
|
+
volumeIcon.setAttribute("aria-hidden", "true");
|
|
8996
|
+
const volumeSlider = document.createElement("input");
|
|
8997
|
+
volumeSlider.type = "range";
|
|
8998
|
+
volumeSlider.className = "vd-music-player-volume-slider";
|
|
8999
|
+
volumeSlider.min = "0";
|
|
9000
|
+
volumeSlider.max = "1";
|
|
9001
|
+
volumeSlider.step = "0.01";
|
|
9002
|
+
volumeSlider.value = String(state.volume);
|
|
9003
|
+
volumeSlider.setAttribute("aria-label", "Volume");
|
|
9004
|
+
volumeWrap.appendChild(volumeIcon);
|
|
9005
|
+
volumeWrap.appendChild(volumeSlider);
|
|
9006
|
+
controls.appendChild(volumeWrap);
|
|
9007
|
+
if (state.showPlaylist) {
|
|
9008
|
+
const btnPlaylist = document.createElement("button");
|
|
9009
|
+
btnPlaylist.type = "button";
|
|
9010
|
+
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
9011
|
+
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
9012
|
+
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
9013
|
+
btnPlaylist.appendChild(icon("playlist"));
|
|
9014
|
+
controls.appendChild(btnPlaylist);
|
|
9015
|
+
}
|
|
9016
|
+
container.appendChild(controls);
|
|
9017
|
+
if (state.showProgress) {
|
|
9018
|
+
const progressRow = document.createElement("div");
|
|
9019
|
+
progressRow.className = "vd-music-player-progress";
|
|
9020
|
+
const timeElapsed = document.createElement("span");
|
|
9021
|
+
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
9022
|
+
timeElapsed.textContent = "0:00";
|
|
9023
|
+
timeElapsed.setAttribute("aria-hidden", "true");
|
|
9024
|
+
const progressBar = document.createElement("input");
|
|
9025
|
+
progressBar.type = "range";
|
|
9026
|
+
progressBar.className = "vd-music-player-progress-bar";
|
|
9027
|
+
progressBar.min = "0";
|
|
9028
|
+
progressBar.max = "100";
|
|
9029
|
+
progressBar.step = "0.1";
|
|
9030
|
+
progressBar.value = "0";
|
|
9031
|
+
progressBar.setAttribute("aria-label", "Seek");
|
|
9032
|
+
const timeDuration = document.createElement("span");
|
|
9033
|
+
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
9034
|
+
timeDuration.textContent = "0:00";
|
|
9035
|
+
timeDuration.setAttribute("aria-hidden", "true");
|
|
9036
|
+
progressRow.appendChild(timeElapsed);
|
|
9037
|
+
progressRow.appendChild(progressBar);
|
|
9038
|
+
progressRow.appendChild(timeDuration);
|
|
9039
|
+
container.appendChild(progressRow);
|
|
9040
|
+
}
|
|
9041
|
+
if (state.showPlaylist) {
|
|
9042
|
+
const playlist = document.createElement("div");
|
|
9043
|
+
playlist.className = "vd-music-player-playlist";
|
|
9044
|
+
playlist.setAttribute("aria-label", "Playlist");
|
|
9045
|
+
container.appendChild(playlist);
|
|
9046
|
+
}
|
|
9047
|
+
},
|
|
9048
|
+
/* ─── Public API ──────────────────────────────────────── */
|
|
9049
|
+
/**
|
|
9050
|
+
* @param {HTMLElement} container
|
|
9051
|
+
*/
|
|
9052
|
+
play: function(container) {
|
|
9053
|
+
const inst = this.instances.get(container);
|
|
9054
|
+
if (!inst) return;
|
|
9055
|
+
if (!inst.audio.src && inst.state.tracks.length) {
|
|
9056
|
+
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
9057
|
+
}
|
|
9058
|
+
inst.audio.play().catch(() => {
|
|
9059
|
+
});
|
|
9060
|
+
},
|
|
9061
|
+
/**
|
|
9062
|
+
* @param {HTMLElement} container
|
|
9063
|
+
*/
|
|
9064
|
+
pause: function(container) {
|
|
9065
|
+
const inst = this.instances.get(container);
|
|
9066
|
+
if (inst) inst.audio.pause();
|
|
9067
|
+
},
|
|
9068
|
+
/**
|
|
9069
|
+
* @param {HTMLElement} container
|
|
9070
|
+
*/
|
|
9071
|
+
toggle: function(container) {
|
|
9072
|
+
const inst = this.instances.get(container);
|
|
9073
|
+
if (!inst) return;
|
|
9074
|
+
if (inst.state.isPlaying) {
|
|
9075
|
+
this.pause(container);
|
|
9076
|
+
} else {
|
|
9077
|
+
this.play(container);
|
|
9078
|
+
}
|
|
9079
|
+
},
|
|
9080
|
+
/**
|
|
9081
|
+
* @param {HTMLElement} container
|
|
9082
|
+
*/
|
|
9083
|
+
next: function(container) {
|
|
9084
|
+
const inst = this.instances.get(container);
|
|
9085
|
+
if (!inst || !inst.state.tracks.length) return;
|
|
9086
|
+
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
9087
|
+
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
9088
|
+
},
|
|
9089
|
+
/**
|
|
9090
|
+
* @param {HTMLElement} container
|
|
9091
|
+
*/
|
|
9092
|
+
previous: function(container) {
|
|
9093
|
+
const inst = this.instances.get(container);
|
|
9094
|
+
if (!inst || !inst.state.tracks.length) return;
|
|
9095
|
+
const len = inst.state.tracks.length;
|
|
9096
|
+
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
9097
|
+
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
9098
|
+
},
|
|
9099
|
+
/**
|
|
9100
|
+
* @param {HTMLElement} container
|
|
9101
|
+
* @param {number} value - 0 to 1
|
|
9102
|
+
*/
|
|
9103
|
+
setVolume: function(container, value) {
|
|
9104
|
+
const inst = this.instances.get(container);
|
|
9105
|
+
if (!inst) return;
|
|
9106
|
+
const v = Math.max(0, Math.min(1, value));
|
|
9107
|
+
inst.state.volume = v;
|
|
9108
|
+
inst.audio.volume = v;
|
|
9109
|
+
if (inst.refs.volumeSlider) {
|
|
9110
|
+
inst.refs.volumeSlider.value = String(v);
|
|
9111
|
+
updateRangeFill(inst.refs.volumeSlider);
|
|
9112
|
+
}
|
|
9113
|
+
container.dispatchEvent(
|
|
9114
|
+
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
9115
|
+
);
|
|
9116
|
+
},
|
|
9117
|
+
/**
|
|
9118
|
+
* @param {HTMLElement} container
|
|
9119
|
+
* @param {number} index - Track index
|
|
9120
|
+
*/
|
|
9121
|
+
setTrack: function(container, index) {
|
|
9122
|
+
const inst = this.instances.get(container);
|
|
9123
|
+
if (!inst) return;
|
|
9124
|
+
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
9125
|
+
},
|
|
9126
|
+
/**
|
|
9127
|
+
* Shuffle or un-shuffle the track list.
|
|
9128
|
+
* @param {HTMLElement} container
|
|
9129
|
+
*/
|
|
9130
|
+
shuffle: function(container) {
|
|
9131
|
+
const inst = this.instances.get(container);
|
|
9132
|
+
if (!inst || !inst.refs.btnShuffle) return;
|
|
9133
|
+
inst.refs.btnShuffle.click();
|
|
9134
|
+
},
|
|
9135
|
+
/**
|
|
9136
|
+
* Return a shallow copy of the current player state.
|
|
9137
|
+
* @param {HTMLElement} container
|
|
9138
|
+
* @returns {Object|null}
|
|
9139
|
+
*/
|
|
9140
|
+
getState: function(container) {
|
|
9141
|
+
const inst = this.instances.get(container);
|
|
9142
|
+
if (!inst) return null;
|
|
9143
|
+
const s = inst.state;
|
|
9144
|
+
return {
|
|
9145
|
+
isPlaying: s.isPlaying,
|
|
9146
|
+
currentIndex: s.currentIndex,
|
|
9147
|
+
currentTrack: s.tracks[s.currentIndex] || null,
|
|
9148
|
+
volume: s.volume,
|
|
9149
|
+
shuffle: s.shuffle,
|
|
9150
|
+
tracks: s.tracks.slice()
|
|
9151
|
+
};
|
|
9152
|
+
},
|
|
9153
|
+
/**
|
|
9154
|
+
* Stop playback, clean up listeners, remove instance.
|
|
9155
|
+
* @param {HTMLElement} container
|
|
9156
|
+
*/
|
|
9157
|
+
destroy: function(container) {
|
|
9158
|
+
const inst = this.instances.get(container);
|
|
9159
|
+
if (!inst) return;
|
|
9160
|
+
inst.cleanup.forEach((fn) => fn());
|
|
9161
|
+
this.instances.delete(container);
|
|
9162
|
+
container.removeAttribute("data-music-player-initialized");
|
|
9163
|
+
},
|
|
9164
|
+
/**
|
|
9165
|
+
* Destroy all instances.
|
|
9166
|
+
*/
|
|
9167
|
+
destroyAll: function() {
|
|
9168
|
+
this.instances.forEach((_, container) => this.destroy(container));
|
|
9169
|
+
},
|
|
9170
|
+
/* ─── Internal helpers ────────────────────────────────── */
|
|
9171
|
+
/**
|
|
9172
|
+
* Load track by index on an already-initialised instance object.
|
|
9173
|
+
* @param {Object} inst
|
|
9174
|
+
* @param {number} index
|
|
9175
|
+
* @param {boolean} autoPlay
|
|
9176
|
+
*/
|
|
9177
|
+
_loadTrack: function(inst, index, autoPlay) {
|
|
9178
|
+
const track = inst.state.tracks[index];
|
|
9179
|
+
if (!track) return;
|
|
9180
|
+
const container = this._containerOf(inst);
|
|
9181
|
+
inst.state.currentIndex = index;
|
|
9182
|
+
inst.audio.src = track.url;
|
|
9183
|
+
if (inst.refs.trackName) {
|
|
9184
|
+
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
9185
|
+
inst.refs.trackName.classList.remove("is-idle");
|
|
9186
|
+
}
|
|
9187
|
+
if (inst.refs.playlistPanel) {
|
|
9188
|
+
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
9189
|
+
const active = i === index;
|
|
9190
|
+
item.classList.toggle("is-active", active);
|
|
9191
|
+
item.setAttribute("aria-current", active ? "true" : "false");
|
|
9192
|
+
});
|
|
9193
|
+
}
|
|
9194
|
+
if (inst.refs.progressBar) {
|
|
9195
|
+
inst.refs.progressBar.value = "0";
|
|
9196
|
+
updateRangeFill(inst.refs.progressBar);
|
|
9197
|
+
}
|
|
9198
|
+
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
9199
|
+
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
9200
|
+
if (container) {
|
|
9201
|
+
container.dispatchEvent(
|
|
9202
|
+
new CustomEvent("musicplayer:trackchange", {
|
|
9203
|
+
bubbles: true,
|
|
9204
|
+
detail: { index, name: track.name, url: track.url }
|
|
9205
|
+
})
|
|
9206
|
+
);
|
|
9207
|
+
}
|
|
9208
|
+
if (autoPlay) inst.audio.play().catch(() => {
|
|
9209
|
+
});
|
|
9210
|
+
},
|
|
9211
|
+
/**
|
|
9212
|
+
* Reverse-lookup the container element for a given instance object.
|
|
9213
|
+
* @param {Object} inst
|
|
9214
|
+
* @returns {HTMLElement|null}
|
|
9215
|
+
*/
|
|
9216
|
+
_containerOf: function(inst) {
|
|
9217
|
+
for (const [container, i] of this.instances) {
|
|
9218
|
+
if (i === inst) return container;
|
|
9219
|
+
}
|
|
9220
|
+
return null;
|
|
9221
|
+
}
|
|
9222
|
+
};
|
|
9223
|
+
if (typeof window.Vanduo !== "undefined") {
|
|
9224
|
+
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
9225
|
+
}
|
|
9226
|
+
window.VanduoMusicPlayer = MusicPlayer;
|
|
9227
|
+
})();
|
|
9228
|
+
|
|
8544
9229
|
// js/index.js
|
|
8545
9230
|
var Vanduo = window.Vanduo;
|
|
8546
9231
|
var index_default = Vanduo;
|