@vanduo-oss/framework 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/css/vanduo.css +0 -1
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +86 -1085
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +8 -8
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +1 -690
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +86 -1085
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +8 -8
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +86 -1085
- 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 +8 -8
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/code-snippet.js +73 -16
- package/js/components/datepicker.js +27 -1
- package/js/components/suggest.js +18 -15
- package/js/index.js +0 -1
- package/package.json +1 -1
- package/css/components/music-player.css +0 -798
- package/js/components/music-player.js +0 -1294
package/dist/vanduo.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Vanduo v1.4.
|
|
1
|
+
/*! Vanduo v1.4.3 | Built: 2026-05-28T13:14:25.371Z | git:02ef7f6 | development */
|
|
2
2
|
(() => {
|
|
3
3
|
// js/utils/lifecycle.js
|
|
4
4
|
(function() {
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
// js/vanduo.js
|
|
177
177
|
(function() {
|
|
178
178
|
"use strict";
|
|
179
|
-
const VANDUO_VERSION = true ? "1.4.
|
|
179
|
+
const VANDUO_VERSION = true ? "1.4.3" : "0.0.0-dev";
|
|
180
180
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
181
181
|
const Vanduo2 = {
|
|
182
182
|
version: VANDUO_VERSION,
|
|
@@ -849,12 +849,38 @@
|
|
|
849
849
|
* @returns {string} HTML with syntax highlighting spans
|
|
850
850
|
*/
|
|
851
851
|
highlightHtml: function(html) {
|
|
852
|
-
html = html.replace(/(<\/?)([\w-]+)/g, '$1<span class="code-tag">$2</span>');
|
|
853
|
-
html = html.replace(/([\w-]+)(=)(["'])/g, '<span class="code-attr">$1</span>$2$3');
|
|
854
|
-
html = html.replace(/([\w-]+)(=)("|')/g, '<span class="code-attr">$1</span>$2$3');
|
|
855
|
-
html = html.replace(/(["'])([^"']*)(["'])/g, '$1<span class="code-string">$2</span>$3');
|
|
856
|
-
html = html.replace(/("|')([^&]*)("|')/g, '$1<span class="code-string">$2</span>$3');
|
|
857
852
|
html = html.replace(/(<!--)(.*?)(-->)/g, '<span class="code-comment">$1$2$3</span>');
|
|
853
|
+
html = html.replace(/<(?!--)([\s\S]*?)>/g, function(match, inner) {
|
|
854
|
+
const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
|
|
855
|
+
if (closingMatch) {
|
|
856
|
+
return "<" + closingMatch[1] + '<span class="code-tag">' + closingMatch[2] + "</span>>";
|
|
857
|
+
}
|
|
858
|
+
const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
|
|
859
|
+
const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : "";
|
|
860
|
+
const tagSource = selfClosingMatch ? inner.slice(0, inner.length - selfClosingMatch[0].length) : inner;
|
|
861
|
+
const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
|
|
862
|
+
if (!openMatch) {
|
|
863
|
+
return match;
|
|
864
|
+
}
|
|
865
|
+
const tagName = openMatch[1];
|
|
866
|
+
let attrSource = openMatch[2];
|
|
867
|
+
attrSource = attrSource.replace(
|
|
868
|
+
/([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
|
|
869
|
+
function(attrMatch, attrName, separator, quote, value) {
|
|
870
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + quote + '<span class="code-string">' + value + "</span>" + quote;
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
attrSource = attrSource.replace(
|
|
874
|
+
/([\w:-]+)(\s*=\s*)("|')([\s\S]*?)("|')/g,
|
|
875
|
+
function(attrMatch, attrName, separator, openQuote, value, closeQuote) {
|
|
876
|
+
if (openQuote !== closeQuote) {
|
|
877
|
+
return attrMatch;
|
|
878
|
+
}
|
|
879
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + openQuote + '<span class="code-string">' + value + "</span>" + closeQuote;
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
return '<<span class="code-tag">' + tagName + "</span>" + attrSource + selfClosingSuffix + ">";
|
|
883
|
+
});
|
|
858
884
|
return html;
|
|
859
885
|
},
|
|
860
886
|
/**
|
|
@@ -876,16 +902,26 @@
|
|
|
876
902
|
* @returns {string} JS with syntax highlighting spans
|
|
877
903
|
*/
|
|
878
904
|
highlightJs: function(js) {
|
|
905
|
+
const protectedTokens = [];
|
|
906
|
+
const protectToken = (value, className) => {
|
|
907
|
+
const marker = String.fromCharCode(57344 + protectedTokens.length);
|
|
908
|
+
protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + "</span>" });
|
|
909
|
+
return marker;
|
|
910
|
+
};
|
|
911
|
+
js = js.replace(
|
|
912
|
+
/\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
|
|
913
|
+
(match) => protectToken(match, match.startsWith("/") ? "code-comment" : "code-string")
|
|
914
|
+
);
|
|
879
915
|
const keywords = ["const", "let", "var", "function", "return", "if", "else", "for", "while", "switch", "case", "break", "continue", "new", "this", "class", "extends", "import", "export", "default", "async", "await", "try", "catch", "throw", "typeof", "instanceof"];
|
|
880
916
|
keywords.forEach((kw) => {
|
|
881
917
|
const regex = new RegExp(`\\b(${kw})\\b`, "g");
|
|
882
918
|
js = js.replace(regex, '<span class="code-keyword">$1</span>');
|
|
883
919
|
});
|
|
884
|
-
js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
|
|
885
920
|
js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
|
|
886
921
|
js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
|
|
887
|
-
|
|
888
|
-
|
|
922
|
+
protectedTokens.forEach((token) => {
|
|
923
|
+
js = js.replace(token.marker, token.html);
|
|
924
|
+
});
|
|
889
925
|
return js;
|
|
890
926
|
},
|
|
891
927
|
/**
|
|
@@ -8431,11 +8467,6 @@
|
|
|
8431
8467
|
// js/components/suggest.js
|
|
8432
8468
|
(function() {
|
|
8433
8469
|
"use strict";
|
|
8434
|
-
function _escapeHtml(text) {
|
|
8435
|
-
const div = document.createElement("div");
|
|
8436
|
-
div.textContent = text;
|
|
8437
|
-
return div.innerHTML;
|
|
8438
|
-
}
|
|
8439
8470
|
function _isSafeUrl(url, allowlist) {
|
|
8440
8471
|
try {
|
|
8441
8472
|
const resolved = new URL(url, window.location.href);
|
|
@@ -8506,9 +8537,24 @@
|
|
|
8506
8537
|
li.id = listId + "-item-" + i;
|
|
8507
8538
|
const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
|
|
8508
8539
|
if (query) {
|
|
8509
|
-
const
|
|
8510
|
-
const
|
|
8511
|
-
|
|
8540
|
+
const lowerText = text.toLowerCase();
|
|
8541
|
+
const lowerQuery = query.toLowerCase();
|
|
8542
|
+
let start = 0;
|
|
8543
|
+
let matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8544
|
+
while (matchIndex !== -1) {
|
|
8545
|
+
if (matchIndex > start) {
|
|
8546
|
+
li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
|
|
8547
|
+
}
|
|
8548
|
+
const matchSpan = document.createElement("span");
|
|
8549
|
+
matchSpan.className = "vd-suggest-match";
|
|
8550
|
+
matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
|
|
8551
|
+
li.appendChild(matchSpan);
|
|
8552
|
+
start = matchIndex + query.length;
|
|
8553
|
+
matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8554
|
+
}
|
|
8555
|
+
if (start < text.length) {
|
|
8556
|
+
li.appendChild(document.createTextNode(text.slice(start)));
|
|
8557
|
+
}
|
|
8512
8558
|
} else {
|
|
8513
8559
|
li.textContent = text;
|
|
8514
8560
|
}
|
|
@@ -8978,6 +9024,7 @@
|
|
|
8978
9024
|
let viewMode = "days";
|
|
8979
9025
|
let focusedDate = null;
|
|
8980
9026
|
let skipNextFocusOpen = false;
|
|
9027
|
+
let ignoreOutsideUntil = 0;
|
|
8981
9028
|
const isDisabled = (d) => {
|
|
8982
9029
|
if (minDate) {
|
|
8983
9030
|
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
@@ -9343,7 +9390,11 @@
|
|
|
9343
9390
|
const repositionHandler = () => {
|
|
9344
9391
|
positionPopup();
|
|
9345
9392
|
};
|
|
9393
|
+
const markIgnoreOutside = () => {
|
|
9394
|
+
ignoreOutsideUntil = Date.now() + 100;
|
|
9395
|
+
};
|
|
9346
9396
|
const open = () => {
|
|
9397
|
+
markIgnoreOutside();
|
|
9347
9398
|
viewMode = "days";
|
|
9348
9399
|
if (selectedDate) {
|
|
9349
9400
|
viewYear = selectedDate.getFullYear();
|
|
@@ -9377,8 +9428,22 @@
|
|
|
9377
9428
|
}
|
|
9378
9429
|
open();
|
|
9379
9430
|
};
|
|
9431
|
+
const clickHandler = () => {
|
|
9432
|
+
if (!popup.classList.contains("is-open")) open();
|
|
9433
|
+
};
|
|
9434
|
+
const isOpenAnchorTarget = (target) => {
|
|
9435
|
+
if (!target || !(target instanceof Node)) return false;
|
|
9436
|
+
if (target === input || input.contains(target) || popup.contains(target)) return true;
|
|
9437
|
+
const inputId = input.id;
|
|
9438
|
+
if (inputId) {
|
|
9439
|
+
const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
|
|
9440
|
+
if (label && (target === label || label.contains(target))) return true;
|
|
9441
|
+
}
|
|
9442
|
+
return false;
|
|
9443
|
+
};
|
|
9380
9444
|
const outsideHandler = (e) => {
|
|
9381
|
-
if (
|
|
9445
|
+
if (Date.now() < ignoreOutsideUntil) return;
|
|
9446
|
+
if (!isOpenAnchorTarget(e.target)) close();
|
|
9382
9447
|
};
|
|
9383
9448
|
const escHandler = (e) => {
|
|
9384
9449
|
if (e.key === "Escape" && popup.classList.contains("is-open")) {
|
|
@@ -9388,6 +9453,7 @@
|
|
|
9388
9453
|
}
|
|
9389
9454
|
};
|
|
9390
9455
|
input.addEventListener("focus", focusHandler);
|
|
9456
|
+
input.addEventListener("click", clickHandler);
|
|
9391
9457
|
document.addEventListener("click", outsideHandler, true);
|
|
9392
9458
|
document.addEventListener("keydown", escHandler);
|
|
9393
9459
|
popup.addEventListener("keydown", handleGridKeydown);
|
|
@@ -9398,6 +9464,7 @@
|
|
|
9398
9464
|
input.setAttribute("autocomplete", "off");
|
|
9399
9465
|
cleanup.push(
|
|
9400
9466
|
() => input.removeEventListener("focus", focusHandler),
|
|
9467
|
+
() => input.removeEventListener("click", clickHandler),
|
|
9401
9468
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9402
9469
|
() => document.removeEventListener("keydown", escHandler),
|
|
9403
9470
|
() => popup.removeEventListener("keydown", handleGridKeydown),
|
|
@@ -10330,1072 +10397,6 @@
|
|
|
10330
10397
|
window.VanduoSpotlight = Spotlight;
|
|
10331
10398
|
})();
|
|
10332
10399
|
|
|
10333
|
-
// js/components/music-player.js
|
|
10334
|
-
(function() {
|
|
10335
|
-
"use strict";
|
|
10336
|
-
function shuffleArray(arr) {
|
|
10337
|
-
const shuffled = arr.slice();
|
|
10338
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
10339
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
10340
|
-
const tmp = shuffled[i];
|
|
10341
|
-
shuffled[i] = shuffled[j];
|
|
10342
|
-
shuffled[j] = tmp;
|
|
10343
|
-
}
|
|
10344
|
-
return shuffled;
|
|
10345
|
-
}
|
|
10346
|
-
function formatTime(seconds) {
|
|
10347
|
-
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
10348
|
-
const m = Math.floor(seconds / 60);
|
|
10349
|
-
const s = Math.floor(seconds % 60);
|
|
10350
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
10351
|
-
}
|
|
10352
|
-
function persistStorageKey(id) {
|
|
10353
|
-
return "vanduo:music-player:" + (id && id.trim() ? id.trim() : "default") + ":pos";
|
|
10354
|
-
}
|
|
10355
|
-
function updateRangeFill(input) {
|
|
10356
|
-
const min = parseFloat(input.min) || 0;
|
|
10357
|
-
const max = parseFloat(input.max) || 1;
|
|
10358
|
-
const val = parseFloat(input.value) || 0;
|
|
10359
|
-
const pct = (val - min) / (max - min) * 100;
|
|
10360
|
-
input.style.setProperty("--vd-fill", pct + "%");
|
|
10361
|
-
input.style.backgroundImage = "linear-gradient(to right, var(--vd-music-player-track-fill, currentColor) 0%, var(--vd-music-player-track-fill, currentColor) " + pct + "%, var(--vd-music-player-track-bg, #ccc) " + pct + "%, var(--vd-music-player-track-bg, #ccc) 100%)";
|
|
10362
|
-
}
|
|
10363
|
-
function icon(name) {
|
|
10364
|
-
const el = document.createElement("i");
|
|
10365
|
-
el.className = "ph ph-" + name;
|
|
10366
|
-
el.setAttribute("aria-hidden", "true");
|
|
10367
|
-
return el;
|
|
10368
|
-
}
|
|
10369
|
-
const MusicPlayer = {
|
|
10370
|
-
/** @type {Map<HTMLElement, Object>} */
|
|
10371
|
-
instances: /* @__PURE__ */ new Map(),
|
|
10372
|
-
/**
|
|
10373
|
-
* Default options.
|
|
10374
|
-
*/
|
|
10375
|
-
defaults: {
|
|
10376
|
-
tracks: [],
|
|
10377
|
-
volume: 0.5,
|
|
10378
|
-
shuffle: false,
|
|
10379
|
-
showProgress: false,
|
|
10380
|
-
showPlaylist: false,
|
|
10381
|
-
autoAdvance: true,
|
|
10382
|
-
glass: false,
|
|
10383
|
-
detachable: false,
|
|
10384
|
-
/** @type {null|string} */
|
|
10385
|
-
floatingPosition: null,
|
|
10386
|
-
draggable: false,
|
|
10387
|
-
minimizable: false,
|
|
10388
|
-
startMinimized: false,
|
|
10389
|
-
persistPosition: false,
|
|
10390
|
-
persistKey: ""
|
|
10391
|
-
},
|
|
10392
|
-
/**
|
|
10393
|
-
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
10394
|
-
* Options can be provided via data-music-player-options (JSON string).
|
|
10395
|
-
*/
|
|
10396
|
-
init: function(root) {
|
|
10397
|
-
window.Vanduo.queryAll(root, ".vd-music-player, [data-music-player]").forEach((el) => {
|
|
10398
|
-
if (this.instances.has(el)) return;
|
|
10399
|
-
let opts = {};
|
|
10400
|
-
const attr = el.getAttribute("data-music-player-options");
|
|
10401
|
-
if (attr) {
|
|
10402
|
-
try {
|
|
10403
|
-
opts = JSON.parse(attr);
|
|
10404
|
-
} catch (_) {
|
|
10405
|
-
}
|
|
10406
|
-
}
|
|
10407
|
-
this.initPlayer(el, opts);
|
|
10408
|
-
});
|
|
10409
|
-
},
|
|
10410
|
-
/**
|
|
10411
|
-
* Initialize a single player element.
|
|
10412
|
-
* @param {HTMLElement} container
|
|
10413
|
-
* @param {Object} [options]
|
|
10414
|
-
*/
|
|
10415
|
-
initPlayer: function(container, options) {
|
|
10416
|
-
const opts = Object.assign({}, this.defaults, options || {});
|
|
10417
|
-
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
10418
|
-
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
10419
|
-
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
10420
|
-
const state = {
|
|
10421
|
-
tracks: trackList,
|
|
10422
|
-
originalTracks: tracks.slice(),
|
|
10423
|
-
currentIndex: 0,
|
|
10424
|
-
isPlaying: false,
|
|
10425
|
-
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
10426
|
-
shuffle: opts.shuffle,
|
|
10427
|
-
showProgress: opts.showProgress,
|
|
10428
|
-
showPlaylist: opts.showPlaylist,
|
|
10429
|
-
autoAdvance: opts.autoAdvance,
|
|
10430
|
-
audio: null,
|
|
10431
|
-
glass: Boolean(opts.glass),
|
|
10432
|
-
detachable: Boolean(opts.detachable),
|
|
10433
|
-
floatingPosition: opts.floatingPosition || "bottom-right",
|
|
10434
|
-
draggable: Boolean(opts.draggable) && Boolean(opts.detachable),
|
|
10435
|
-
minimizable: Boolean(opts.minimizable),
|
|
10436
|
-
startMinimized: Boolean(opts.startMinimized),
|
|
10437
|
-
persistPosition: Boolean(opts.persistPosition),
|
|
10438
|
-
persistKey: typeof opts.persistKey === "string" ? opts.persistKey : "",
|
|
10439
|
-
isDetached: false,
|
|
10440
|
-
isMinimized: false,
|
|
10441
|
-
_startMinimizeApplied: false
|
|
10442
|
-
};
|
|
10443
|
-
const audio = new Audio();
|
|
10444
|
-
audio.volume = state.volume;
|
|
10445
|
-
audio.preload = "metadata";
|
|
10446
|
-
state.audio = audio;
|
|
10447
|
-
this._buildDOM(container, state);
|
|
10448
|
-
const refs = {
|
|
10449
|
-
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
10450
|
-
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
10451
|
-
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
10452
|
-
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
10453
|
-
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
10454
|
-
btnDetach: container.querySelector(".vd-music-player-btn-detach"),
|
|
10455
|
-
btnAttach: container.querySelector(".vd-music-player-btn-attach"),
|
|
10456
|
-
btnMinimize: container.querySelector(".vd-music-player-btn-minimize"),
|
|
10457
|
-
dragHandle: container.querySelector(".vd-music-player-drag-handle"),
|
|
10458
|
-
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
10459
|
-
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
10460
|
-
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
10461
|
-
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
10462
|
-
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
10463
|
-
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
10464
|
-
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
10465
|
-
};
|
|
10466
|
-
const renderPlayIcon = () => {
|
|
10467
|
-
const btn = refs.btnPlay;
|
|
10468
|
-
if (!btn) return;
|
|
10469
|
-
btn.innerHTML = "";
|
|
10470
|
-
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
10471
|
-
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
10472
|
-
btn.classList.toggle("is-active", state.isPlaying);
|
|
10473
|
-
};
|
|
10474
|
-
const renderTrackName = () => {
|
|
10475
|
-
const el = refs.trackName;
|
|
10476
|
-
if (!el) return;
|
|
10477
|
-
const track = state.tracks[state.currentIndex];
|
|
10478
|
-
if (track) {
|
|
10479
|
-
el.textContent = track.name || "Unknown Track";
|
|
10480
|
-
el.classList.remove("is-idle");
|
|
10481
|
-
} else {
|
|
10482
|
-
el.textContent = "No tracks loaded";
|
|
10483
|
-
el.classList.add("is-idle");
|
|
10484
|
-
}
|
|
10485
|
-
};
|
|
10486
|
-
const renderVolumeIcon = () => {
|
|
10487
|
-
const el = refs.volumeIcon;
|
|
10488
|
-
if (!el) return;
|
|
10489
|
-
el.innerHTML = "";
|
|
10490
|
-
const v = state.volume;
|
|
10491
|
-
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
10492
|
-
el.appendChild(icon(name));
|
|
10493
|
-
};
|
|
10494
|
-
const renderShuffleBtn = () => {
|
|
10495
|
-
const btn = refs.btnShuffle;
|
|
10496
|
-
if (!btn) return;
|
|
10497
|
-
btn.classList.toggle("is-active", state.shuffle);
|
|
10498
|
-
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10499
|
-
};
|
|
10500
|
-
const renderPlaylistItems = () => {
|
|
10501
|
-
const panel = refs.playlistPanel;
|
|
10502
|
-
if (!panel) return;
|
|
10503
|
-
panel.innerHTML = "";
|
|
10504
|
-
state.tracks.forEach((track, i) => {
|
|
10505
|
-
const item = document.createElement("button");
|
|
10506
|
-
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
10507
|
-
item.type = "button";
|
|
10508
|
-
item.setAttribute("data-index", String(i));
|
|
10509
|
-
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
10510
|
-
const num = document.createElement("span");
|
|
10511
|
-
num.className = "vd-music-player-playlist-num";
|
|
10512
|
-
num.textContent = String(i + 1);
|
|
10513
|
-
const name = document.createElement("span");
|
|
10514
|
-
name.className = "vd-music-player-playlist-name";
|
|
10515
|
-
name.textContent = track.name || "Track " + (i + 1);
|
|
10516
|
-
item.appendChild(num);
|
|
10517
|
-
item.appendChild(name);
|
|
10518
|
-
panel.appendChild(item);
|
|
10519
|
-
});
|
|
10520
|
-
};
|
|
10521
|
-
const renderProgress = () => {
|
|
10522
|
-
const bar = refs.progressBar;
|
|
10523
|
-
if (!bar || !audio.duration) return;
|
|
10524
|
-
const pct = audio.currentTime / audio.duration * 100;
|
|
10525
|
-
bar.value = String(pct);
|
|
10526
|
-
updateRangeFill(bar);
|
|
10527
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
10528
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10529
|
-
};
|
|
10530
|
-
const loadTrack = (index, autoPlay) => {
|
|
10531
|
-
const track = state.tracks[index];
|
|
10532
|
-
if (!track) return;
|
|
10533
|
-
state.currentIndex = index;
|
|
10534
|
-
audio.src = track.url;
|
|
10535
|
-
renderTrackName();
|
|
10536
|
-
renderPlaylistItems();
|
|
10537
|
-
if (refs.progressBar) {
|
|
10538
|
-
refs.progressBar.value = "0";
|
|
10539
|
-
updateRangeFill(refs.progressBar);
|
|
10540
|
-
}
|
|
10541
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
10542
|
-
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
10543
|
-
container.dispatchEvent(
|
|
10544
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
10545
|
-
bubbles: true,
|
|
10546
|
-
detail: { index, name: track.name, url: track.url }
|
|
10547
|
-
})
|
|
10548
|
-
);
|
|
10549
|
-
if (autoPlay) {
|
|
10550
|
-
audio.play().catch(() => {
|
|
10551
|
-
});
|
|
10552
|
-
}
|
|
10553
|
-
};
|
|
10554
|
-
const cleanupFunctions = [];
|
|
10555
|
-
const onPlay = () => {
|
|
10556
|
-
state.isPlaying = true;
|
|
10557
|
-
renderPlayIcon();
|
|
10558
|
-
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
10559
|
-
};
|
|
10560
|
-
const onPause = () => {
|
|
10561
|
-
state.isPlaying = false;
|
|
10562
|
-
renderPlayIcon();
|
|
10563
|
-
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
10564
|
-
};
|
|
10565
|
-
const onEnded = () => {
|
|
10566
|
-
if (state.autoAdvance && state.tracks.length > 1) {
|
|
10567
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10568
|
-
loadTrack(next, true);
|
|
10569
|
-
} else {
|
|
10570
|
-
state.isPlaying = false;
|
|
10571
|
-
renderPlayIcon();
|
|
10572
|
-
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
10573
|
-
}
|
|
10574
|
-
};
|
|
10575
|
-
const onTimeUpdate = () => {
|
|
10576
|
-
if (state.showProgress) renderProgress();
|
|
10577
|
-
};
|
|
10578
|
-
const onLoadedMetadata = () => {
|
|
10579
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10580
|
-
if (refs.progressBar) {
|
|
10581
|
-
refs.progressBar.max = "100";
|
|
10582
|
-
updateRangeFill(refs.progressBar);
|
|
10583
|
-
}
|
|
10584
|
-
};
|
|
10585
|
-
audio.addEventListener("play", onPlay);
|
|
10586
|
-
audio.addEventListener("pause", onPause);
|
|
10587
|
-
audio.addEventListener("ended", onEnded);
|
|
10588
|
-
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
10589
|
-
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
10590
|
-
cleanupFunctions.push(() => {
|
|
10591
|
-
audio.removeEventListener("play", onPlay);
|
|
10592
|
-
audio.removeEventListener("pause", onPause);
|
|
10593
|
-
audio.removeEventListener("ended", onEnded);
|
|
10594
|
-
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
10595
|
-
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
10596
|
-
audio.pause();
|
|
10597
|
-
audio.src = "";
|
|
10598
|
-
});
|
|
10599
|
-
if (refs.btnPlay) {
|
|
10600
|
-
const handler = () => {
|
|
10601
|
-
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
10602
|
-
if (state.isPlaying) {
|
|
10603
|
-
audio.pause();
|
|
10604
|
-
} else {
|
|
10605
|
-
audio.play().catch(() => {
|
|
10606
|
-
});
|
|
10607
|
-
}
|
|
10608
|
-
};
|
|
10609
|
-
refs.btnPlay.addEventListener("click", handler);
|
|
10610
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
10611
|
-
const keyHandler = (e) => {
|
|
10612
|
-
if (e.key === " " || e.key === "Enter") {
|
|
10613
|
-
e.preventDefault();
|
|
10614
|
-
handler();
|
|
10615
|
-
}
|
|
10616
|
-
};
|
|
10617
|
-
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
10618
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
10619
|
-
}
|
|
10620
|
-
if (refs.btnPrev) {
|
|
10621
|
-
const handler = () => {
|
|
10622
|
-
if (!state.tracks.length) return;
|
|
10623
|
-
if (audio.currentTime > 3) {
|
|
10624
|
-
audio.currentTime = 0;
|
|
10625
|
-
} else {
|
|
10626
|
-
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
10627
|
-
loadTrack(prev, state.isPlaying);
|
|
10628
|
-
}
|
|
10629
|
-
};
|
|
10630
|
-
refs.btnPrev.addEventListener("click", handler);
|
|
10631
|
-
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
10632
|
-
}
|
|
10633
|
-
if (refs.btnNext) {
|
|
10634
|
-
const handler = () => {
|
|
10635
|
-
if (!state.tracks.length) return;
|
|
10636
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10637
|
-
loadTrack(next, state.isPlaying);
|
|
10638
|
-
};
|
|
10639
|
-
refs.btnNext.addEventListener("click", handler);
|
|
10640
|
-
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
10641
|
-
}
|
|
10642
|
-
if (refs.btnShuffle) {
|
|
10643
|
-
const handler = () => {
|
|
10644
|
-
state.shuffle = !state.shuffle;
|
|
10645
|
-
if (state.shuffle) {
|
|
10646
|
-
const current = state.tracks[state.currentIndex];
|
|
10647
|
-
state.tracks = shuffleArray(state.tracks);
|
|
10648
|
-
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
10649
|
-
if (newIdx > 0) {
|
|
10650
|
-
state.tracks.splice(newIdx, 1);
|
|
10651
|
-
state.tracks.unshift(current);
|
|
10652
|
-
}
|
|
10653
|
-
state.currentIndex = 0;
|
|
10654
|
-
} else {
|
|
10655
|
-
const current = state.tracks[state.currentIndex];
|
|
10656
|
-
state.tracks = state.originalTracks.slice();
|
|
10657
|
-
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
10658
|
-
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
10659
|
-
}
|
|
10660
|
-
renderShuffleBtn();
|
|
10661
|
-
renderPlaylistItems();
|
|
10662
|
-
};
|
|
10663
|
-
refs.btnShuffle.addEventListener("click", handler);
|
|
10664
|
-
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
10665
|
-
}
|
|
10666
|
-
if (refs.btnPlaylist) {
|
|
10667
|
-
const handler = () => {
|
|
10668
|
-
const panel = refs.playlistPanel;
|
|
10669
|
-
if (!panel) return;
|
|
10670
|
-
const isOpen = panel.classList.toggle("is-open");
|
|
10671
|
-
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
10672
|
-
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
10673
|
-
};
|
|
10674
|
-
refs.btnPlaylist.addEventListener("click", handler);
|
|
10675
|
-
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
10676
|
-
}
|
|
10677
|
-
if (refs.volumeSlider) {
|
|
10678
|
-
const handler = (e) => {
|
|
10679
|
-
const v = parseFloat(e.target.value);
|
|
10680
|
-
state.volume = v;
|
|
10681
|
-
audio.volume = v;
|
|
10682
|
-
renderVolumeIcon();
|
|
10683
|
-
updateRangeFill(refs.volumeSlider);
|
|
10684
|
-
container.dispatchEvent(
|
|
10685
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10686
|
-
);
|
|
10687
|
-
};
|
|
10688
|
-
refs.volumeSlider.addEventListener("input", handler);
|
|
10689
|
-
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
10690
|
-
updateRangeFill(refs.volumeSlider);
|
|
10691
|
-
}
|
|
10692
|
-
if (refs.progressBar) {
|
|
10693
|
-
const handler = (e) => {
|
|
10694
|
-
if (!audio.duration) return;
|
|
10695
|
-
const pct = parseFloat(e.target.value);
|
|
10696
|
-
audio.currentTime = pct / 100 * audio.duration;
|
|
10697
|
-
updateRangeFill(refs.progressBar);
|
|
10698
|
-
};
|
|
10699
|
-
refs.progressBar.addEventListener("input", handler);
|
|
10700
|
-
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
10701
|
-
}
|
|
10702
|
-
if (refs.playlistPanel) {
|
|
10703
|
-
const panelHandler = (e) => {
|
|
10704
|
-
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
10705
|
-
if (!item) return;
|
|
10706
|
-
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
10707
|
-
if (!isNaN(idx)) loadTrack(idx, true);
|
|
10708
|
-
};
|
|
10709
|
-
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
10710
|
-
cleanupFunctions.push(
|
|
10711
|
-
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
10712
|
-
);
|
|
10713
|
-
}
|
|
10714
|
-
if (refs.btnDetach) {
|
|
10715
|
-
const h = () => {
|
|
10716
|
-
this.detach(container);
|
|
10717
|
-
};
|
|
10718
|
-
refs.btnDetach.addEventListener("click", h);
|
|
10719
|
-
cleanupFunctions.push(() => refs.btnDetach.removeEventListener("click", h));
|
|
10720
|
-
}
|
|
10721
|
-
if (refs.btnAttach) {
|
|
10722
|
-
const h = () => {
|
|
10723
|
-
this.attach(container);
|
|
10724
|
-
};
|
|
10725
|
-
refs.btnAttach.addEventListener("click", h);
|
|
10726
|
-
cleanupFunctions.push(() => refs.btnAttach.removeEventListener("click", h));
|
|
10727
|
-
}
|
|
10728
|
-
if (refs.btnMinimize) {
|
|
10729
|
-
const h = () => {
|
|
10730
|
-
this.toggleMinimize(container);
|
|
10731
|
-
};
|
|
10732
|
-
refs.btnMinimize.addEventListener("click", h);
|
|
10733
|
-
cleanupFunctions.push(() => refs.btnMinimize.removeEventListener("click", h));
|
|
10734
|
-
}
|
|
10735
|
-
renderPlayIcon();
|
|
10736
|
-
renderTrackName();
|
|
10737
|
-
renderVolumeIcon();
|
|
10738
|
-
if (opts.showPlaylist) renderPlaylistItems();
|
|
10739
|
-
this.instances.set(container, {
|
|
10740
|
-
state,
|
|
10741
|
-
audio,
|
|
10742
|
-
refs,
|
|
10743
|
-
cleanup: cleanupFunctions,
|
|
10744
|
-
ui: { restore: null, unbindDrag: null }
|
|
10745
|
-
});
|
|
10746
|
-
container.setAttribute("data-music-player-initialized", "true");
|
|
10747
|
-
},
|
|
10748
|
-
/* ─── DOM builder ─────────────────────────────────────── */
|
|
10749
|
-
/**
|
|
10750
|
-
* Build the inner DOM structure inside container.
|
|
10751
|
-
* Pre-existing inner content is replaced only if it has no
|
|
10752
|
-
* recognised child elements (allows server-rendered markup).
|
|
10753
|
-
* @param {HTMLElement} container
|
|
10754
|
-
* @param {Object} state
|
|
10755
|
-
*/
|
|
10756
|
-
_buildDOM: function(container, state) {
|
|
10757
|
-
if (container.querySelector(".vd-music-player-controls")) return;
|
|
10758
|
-
container.setAttribute("role", "region");
|
|
10759
|
-
container.setAttribute("aria-label", "Music Player");
|
|
10760
|
-
if (state.showProgress) container.classList.add("has-progress");
|
|
10761
|
-
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
10762
|
-
if (state.glass) container.classList.add("vd-music-player-glass");
|
|
10763
|
-
if (state.draggable) container.classList.add("vd-music-player-draggable");
|
|
10764
|
-
if (state.detachable || state.minimizable) {
|
|
10765
|
-
const tb = document.createElement("div");
|
|
10766
|
-
tb.className = "vd-music-player-toolbar";
|
|
10767
|
-
tb.setAttribute("role", "toolbar");
|
|
10768
|
-
tb.setAttribute("aria-label", "Player window");
|
|
10769
|
-
if (state.draggable) {
|
|
10770
|
-
const h = document.createElement("button");
|
|
10771
|
-
h.type = "button";
|
|
10772
|
-
h.className = "vd-music-player-drag-handle";
|
|
10773
|
-
h.setAttribute("aria-label", "Drag to move player");
|
|
10774
|
-
h.appendChild(icon("dots-six-vertical"));
|
|
10775
|
-
tb.appendChild(h);
|
|
10776
|
-
}
|
|
10777
|
-
const tSp = document.createElement("span");
|
|
10778
|
-
tSp.className = "vd-music-player-toolbar-spacer";
|
|
10779
|
-
tSp.setAttribute("aria-hidden", "true");
|
|
10780
|
-
tb.appendChild(tSp);
|
|
10781
|
-
if (state.minimizable) {
|
|
10782
|
-
const bMin = document.createElement("button");
|
|
10783
|
-
bMin.type = "button";
|
|
10784
|
-
bMin.className = "vd-music-player-btn vd-music-player-btn-minimize";
|
|
10785
|
-
bMin.setAttribute("aria-label", "Minimize player");
|
|
10786
|
-
bMin.setAttribute("aria-expanded", "true");
|
|
10787
|
-
bMin.appendChild(icon("minus"));
|
|
10788
|
-
tb.appendChild(bMin);
|
|
10789
|
-
}
|
|
10790
|
-
if (state.detachable) {
|
|
10791
|
-
const bOut = document.createElement("button");
|
|
10792
|
-
bOut.type = "button";
|
|
10793
|
-
bOut.className = "vd-music-player-btn vd-music-player-btn-detach";
|
|
10794
|
-
bOut.setAttribute("aria-label", "Detach player");
|
|
10795
|
-
bOut.appendChild(icon("arrows-out"));
|
|
10796
|
-
tb.appendChild(bOut);
|
|
10797
|
-
const bIn = document.createElement("button");
|
|
10798
|
-
bIn.type = "button";
|
|
10799
|
-
bIn.className = "vd-music-player-btn vd-music-player-btn-attach";
|
|
10800
|
-
bIn.setAttribute("aria-label", "Attach player");
|
|
10801
|
-
bIn.appendChild(icon("arrows-in"));
|
|
10802
|
-
tb.appendChild(bIn);
|
|
10803
|
-
}
|
|
10804
|
-
container.classList.add("vd-music-player-has-chrome");
|
|
10805
|
-
container.appendChild(tb);
|
|
10806
|
-
}
|
|
10807
|
-
const info = document.createElement("div");
|
|
10808
|
-
info.className = "vd-music-player-info";
|
|
10809
|
-
const iconWrap = document.createElement("span");
|
|
10810
|
-
iconWrap.className = "vd-music-player-icon";
|
|
10811
|
-
iconWrap.setAttribute("aria-hidden", "true");
|
|
10812
|
-
iconWrap.appendChild(icon("music-note"));
|
|
10813
|
-
const trackName = document.createElement("span");
|
|
10814
|
-
trackName.className = "vd-music-player-track-name";
|
|
10815
|
-
trackName.setAttribute("aria-live", "polite");
|
|
10816
|
-
trackName.setAttribute("aria-atomic", "true");
|
|
10817
|
-
info.appendChild(iconWrap);
|
|
10818
|
-
info.appendChild(trackName);
|
|
10819
|
-
container.appendChild(info);
|
|
10820
|
-
const controls = document.createElement("div");
|
|
10821
|
-
controls.className = "vd-music-player-controls";
|
|
10822
|
-
controls.setAttribute("role", "group");
|
|
10823
|
-
controls.setAttribute("aria-label", "Playback controls");
|
|
10824
|
-
const btnPrev = document.createElement("button");
|
|
10825
|
-
btnPrev.type = "button";
|
|
10826
|
-
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
10827
|
-
btnPrev.setAttribute("aria-label", "Previous track");
|
|
10828
|
-
btnPrev.appendChild(icon("skip-back"));
|
|
10829
|
-
const btnPlay = document.createElement("button");
|
|
10830
|
-
btnPlay.type = "button";
|
|
10831
|
-
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
10832
|
-
btnPlay.setAttribute("aria-label", "Play");
|
|
10833
|
-
btnPlay.appendChild(icon("play"));
|
|
10834
|
-
const btnNext = document.createElement("button");
|
|
10835
|
-
btnNext.type = "button";
|
|
10836
|
-
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
10837
|
-
btnNext.setAttribute("aria-label", "Next track");
|
|
10838
|
-
btnNext.appendChild(icon("skip-forward"));
|
|
10839
|
-
controls.appendChild(btnPrev);
|
|
10840
|
-
controls.appendChild(btnPlay);
|
|
10841
|
-
controls.appendChild(btnNext);
|
|
10842
|
-
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
10843
|
-
const btnShuffle = document.createElement("button");
|
|
10844
|
-
btnShuffle.type = "button";
|
|
10845
|
-
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
10846
|
-
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
10847
|
-
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10848
|
-
btnShuffle.appendChild(icon("shuffle"));
|
|
10849
|
-
controls.appendChild(btnShuffle);
|
|
10850
|
-
}
|
|
10851
|
-
const spacer = document.createElement("span");
|
|
10852
|
-
spacer.className = "vd-music-player-spacer";
|
|
10853
|
-
spacer.setAttribute("aria-hidden", "true");
|
|
10854
|
-
controls.appendChild(spacer);
|
|
10855
|
-
const volumeWrap = document.createElement("div");
|
|
10856
|
-
volumeWrap.className = "vd-music-player-volume";
|
|
10857
|
-
const volumeIcon = document.createElement("span");
|
|
10858
|
-
volumeIcon.className = "vd-music-player-volume-icon";
|
|
10859
|
-
volumeIcon.setAttribute("aria-hidden", "true");
|
|
10860
|
-
const volumeSlider = document.createElement("input");
|
|
10861
|
-
volumeSlider.type = "range";
|
|
10862
|
-
volumeSlider.className = "vd-music-player-volume-slider";
|
|
10863
|
-
volumeSlider.min = "0";
|
|
10864
|
-
volumeSlider.max = "1";
|
|
10865
|
-
volumeSlider.step = "0.01";
|
|
10866
|
-
volumeSlider.value = String(state.volume);
|
|
10867
|
-
volumeSlider.setAttribute("aria-label", "Volume");
|
|
10868
|
-
volumeWrap.appendChild(volumeIcon);
|
|
10869
|
-
volumeWrap.appendChild(volumeSlider);
|
|
10870
|
-
controls.appendChild(volumeWrap);
|
|
10871
|
-
if (state.showPlaylist) {
|
|
10872
|
-
const btnPlaylist = document.createElement("button");
|
|
10873
|
-
btnPlaylist.type = "button";
|
|
10874
|
-
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
10875
|
-
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
10876
|
-
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
10877
|
-
btnPlaylist.appendChild(icon("playlist"));
|
|
10878
|
-
controls.appendChild(btnPlaylist);
|
|
10879
|
-
}
|
|
10880
|
-
container.appendChild(controls);
|
|
10881
|
-
if (state.showProgress) {
|
|
10882
|
-
const progressRow = document.createElement("div");
|
|
10883
|
-
progressRow.className = "vd-music-player-progress";
|
|
10884
|
-
const timeElapsed = document.createElement("span");
|
|
10885
|
-
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
10886
|
-
timeElapsed.textContent = "0:00";
|
|
10887
|
-
timeElapsed.setAttribute("aria-hidden", "true");
|
|
10888
|
-
const progressBar = document.createElement("input");
|
|
10889
|
-
progressBar.type = "range";
|
|
10890
|
-
progressBar.className = "vd-music-player-progress-bar";
|
|
10891
|
-
progressBar.min = "0";
|
|
10892
|
-
progressBar.max = "100";
|
|
10893
|
-
progressBar.step = "0.1";
|
|
10894
|
-
progressBar.value = "0";
|
|
10895
|
-
progressBar.setAttribute("aria-label", "Seek");
|
|
10896
|
-
const timeDuration = document.createElement("span");
|
|
10897
|
-
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
10898
|
-
timeDuration.textContent = "0:00";
|
|
10899
|
-
timeDuration.setAttribute("aria-hidden", "true");
|
|
10900
|
-
progressRow.appendChild(timeElapsed);
|
|
10901
|
-
progressRow.appendChild(progressBar);
|
|
10902
|
-
progressRow.appendChild(timeDuration);
|
|
10903
|
-
container.appendChild(progressRow);
|
|
10904
|
-
}
|
|
10905
|
-
if (state.showPlaylist) {
|
|
10906
|
-
const playlist = document.createElement("div");
|
|
10907
|
-
playlist.className = "vd-music-player-playlist";
|
|
10908
|
-
playlist.setAttribute("aria-label", "Playlist");
|
|
10909
|
-
container.appendChild(playlist);
|
|
10910
|
-
}
|
|
10911
|
-
},
|
|
10912
|
-
/* ─── Public API ──────────────────────────────────────── */
|
|
10913
|
-
/**
|
|
10914
|
-
* @param {HTMLElement} container
|
|
10915
|
-
*/
|
|
10916
|
-
play: function(container) {
|
|
10917
|
-
const inst = this.instances.get(container);
|
|
10918
|
-
if (!inst) return;
|
|
10919
|
-
if (!inst.audio.src && inst.state.tracks.length) {
|
|
10920
|
-
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
10921
|
-
}
|
|
10922
|
-
inst.audio.play().catch(() => {
|
|
10923
|
-
});
|
|
10924
|
-
},
|
|
10925
|
-
/**
|
|
10926
|
-
* @param {HTMLElement} container
|
|
10927
|
-
*/
|
|
10928
|
-
pause: function(container) {
|
|
10929
|
-
const inst = this.instances.get(container);
|
|
10930
|
-
if (inst) inst.audio.pause();
|
|
10931
|
-
},
|
|
10932
|
-
/**
|
|
10933
|
-
* @param {HTMLElement} container
|
|
10934
|
-
*/
|
|
10935
|
-
toggle: function(container) {
|
|
10936
|
-
const inst = this.instances.get(container);
|
|
10937
|
-
if (!inst) return;
|
|
10938
|
-
if (inst.state.isPlaying) {
|
|
10939
|
-
this.pause(container);
|
|
10940
|
-
} else {
|
|
10941
|
-
this.play(container);
|
|
10942
|
-
}
|
|
10943
|
-
},
|
|
10944
|
-
/**
|
|
10945
|
-
* @param {HTMLElement} container
|
|
10946
|
-
*/
|
|
10947
|
-
next: function(container) {
|
|
10948
|
-
const inst = this.instances.get(container);
|
|
10949
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10950
|
-
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
10951
|
-
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
10952
|
-
},
|
|
10953
|
-
/**
|
|
10954
|
-
* @param {HTMLElement} container
|
|
10955
|
-
*/
|
|
10956
|
-
previous: function(container) {
|
|
10957
|
-
const inst = this.instances.get(container);
|
|
10958
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10959
|
-
const len = inst.state.tracks.length;
|
|
10960
|
-
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
10961
|
-
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
10962
|
-
},
|
|
10963
|
-
/**
|
|
10964
|
-
* @param {HTMLElement} container
|
|
10965
|
-
* @param {number} value - 0 to 1
|
|
10966
|
-
*/
|
|
10967
|
-
setVolume: function(container, value) {
|
|
10968
|
-
const inst = this.instances.get(container);
|
|
10969
|
-
if (!inst) return;
|
|
10970
|
-
const v = Math.max(0, Math.min(1, value));
|
|
10971
|
-
inst.state.volume = v;
|
|
10972
|
-
inst.audio.volume = v;
|
|
10973
|
-
if (inst.refs.volumeSlider) {
|
|
10974
|
-
inst.refs.volumeSlider.value = String(v);
|
|
10975
|
-
updateRangeFill(inst.refs.volumeSlider);
|
|
10976
|
-
}
|
|
10977
|
-
container.dispatchEvent(
|
|
10978
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10979
|
-
);
|
|
10980
|
-
},
|
|
10981
|
-
/**
|
|
10982
|
-
* @param {HTMLElement} container
|
|
10983
|
-
* @param {number} index - Track index
|
|
10984
|
-
*/
|
|
10985
|
-
setTrack: function(container, index) {
|
|
10986
|
-
const inst = this.instances.get(container);
|
|
10987
|
-
if (!inst) return;
|
|
10988
|
-
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
10989
|
-
},
|
|
10990
|
-
/**
|
|
10991
|
-
* Shuffle or un-shuffle the track list.
|
|
10992
|
-
* @param {HTMLElement} container
|
|
10993
|
-
*/
|
|
10994
|
-
shuffle: function(container) {
|
|
10995
|
-
const inst = this.instances.get(container);
|
|
10996
|
-
if (!inst || !inst.refs.btnShuffle) return;
|
|
10997
|
-
inst.refs.btnShuffle.click();
|
|
10998
|
-
},
|
|
10999
|
-
/**
|
|
11000
|
-
* Float the player above the page. Requires { detachable: true } at init.
|
|
11001
|
-
* @param {HTMLElement} container
|
|
11002
|
-
* @param {string} [position] 'bottom-left' or 'bottom-right'
|
|
11003
|
-
*/
|
|
11004
|
-
detach: function(container, position) {
|
|
11005
|
-
const inst = this.instances.get(container);
|
|
11006
|
-
if (!inst || !inst.state.detachable || inst.state.isDetached) return;
|
|
11007
|
-
const s = inst.state;
|
|
11008
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11009
|
-
s.isDetached = true;
|
|
11010
|
-
inst.ui.restore = {
|
|
11011
|
-
parent: container.parentNode,
|
|
11012
|
-
next: container.nextSibling
|
|
11013
|
-
};
|
|
11014
|
-
document.body.appendChild(container);
|
|
11015
|
-
container.classList.add("vd-music-player-floating", "vd-music-player-detached");
|
|
11016
|
-
const pos = position != null && position !== void 0 ? position : s.floatingPosition;
|
|
11017
|
-
this._setCornerPosition(
|
|
11018
|
-
container,
|
|
11019
|
-
pos === "bottom-left" || pos === "bottom-right" ? pos : "bottom-right"
|
|
11020
|
-
);
|
|
11021
|
-
this._loadPersistedPosition(container, inst);
|
|
11022
|
-
if (s.startMinimized && !s._startMinimizeApplied) {
|
|
11023
|
-
s._startMinimizeApplied = true;
|
|
11024
|
-
this.minimize(container);
|
|
11025
|
-
}
|
|
11026
|
-
this._bindFloatingDrag(inst);
|
|
11027
|
-
container.dispatchEvent(new CustomEvent("musicplayer:detach", { bubbles: true }));
|
|
11028
|
-
},
|
|
11029
|
-
/**
|
|
11030
|
-
* Return a detached player to its original place in the document.
|
|
11031
|
-
* @param {HTMLElement} container
|
|
11032
|
-
*/
|
|
11033
|
-
attach: function(container) {
|
|
11034
|
-
const inst = this.instances.get(container);
|
|
11035
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11036
|
-
this._unbindFloatingDrag(inst);
|
|
11037
|
-
inst.state.isDetached = false;
|
|
11038
|
-
const r = inst.ui && inst.ui.restore;
|
|
11039
|
-
container.classList.remove(
|
|
11040
|
-
"vd-music-player-floating",
|
|
11041
|
-
"vd-music-player-detached",
|
|
11042
|
-
"vd-music-player-floating-bottom-left",
|
|
11043
|
-
"vd-music-player-floating-bottom-right",
|
|
11044
|
-
"is-position-custom"
|
|
11045
|
-
);
|
|
11046
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11047
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11048
|
-
if (r && r.parent && r.parent.isConnected) {
|
|
11049
|
-
r.parent.insertBefore(container, r.next);
|
|
11050
|
-
}
|
|
11051
|
-
if (inst.ui) {
|
|
11052
|
-
inst.ui.restore = null;
|
|
11053
|
-
inst.ui.unbindDrag = null;
|
|
11054
|
-
}
|
|
11055
|
-
container.dispatchEvent(new CustomEvent("musicplayer:attach", { bubbles: true }));
|
|
11056
|
-
},
|
|
11057
|
-
/**
|
|
11058
|
-
* Collapse to essential controls. Requires { minimizable: true } at init.
|
|
11059
|
-
* @param {HTMLElement} container
|
|
11060
|
-
*/
|
|
11061
|
-
minimize: function(container) {
|
|
11062
|
-
const inst = this.instances.get(container);
|
|
11063
|
-
if (!inst || !inst.state.minimizable || inst.state.isMinimized) return;
|
|
11064
|
-
const s = inst.state;
|
|
11065
|
-
s.isMinimized = true;
|
|
11066
|
-
container.classList.add("vd-music-player-minimized");
|
|
11067
|
-
this._setMinimizeButtonState(inst, true);
|
|
11068
|
-
if (inst.refs.playlistPanel && inst.refs.playlistPanel.classList.contains("is-open") && inst.refs.btnPlaylist) {
|
|
11069
|
-
inst.refs.playlistPanel.classList.remove("is-open");
|
|
11070
|
-
inst.refs.btnPlaylist.classList.remove("is-active");
|
|
11071
|
-
inst.refs.btnPlaylist.setAttribute("aria-expanded", "false");
|
|
11072
|
-
}
|
|
11073
|
-
container.dispatchEvent(new CustomEvent("musicplayer:minimize", { bubbles: true }));
|
|
11074
|
-
},
|
|
11075
|
-
/**
|
|
11076
|
-
* Restore from minimized state.
|
|
11077
|
-
* @param {HTMLElement} container
|
|
11078
|
-
*/
|
|
11079
|
-
expand: function(container) {
|
|
11080
|
-
const inst = this.instances.get(container);
|
|
11081
|
-
if (!inst || !inst.state.minimizable || !inst.state.isMinimized) return;
|
|
11082
|
-
inst.state.isMinimized = false;
|
|
11083
|
-
container.classList.remove("vd-music-player-minimized");
|
|
11084
|
-
this._setMinimizeButtonState(inst, false);
|
|
11085
|
-
container.dispatchEvent(new CustomEvent("musicplayer:expand", { bubbles: true }));
|
|
11086
|
-
},
|
|
11087
|
-
/**
|
|
11088
|
-
* Toggle minimize / expand.
|
|
11089
|
-
* @param {HTMLElement} container
|
|
11090
|
-
*/
|
|
11091
|
-
toggleMinimize: function(container) {
|
|
11092
|
-
const inst = this.instances.get(container);
|
|
11093
|
-
if (!inst || !inst.state.minimizable) return;
|
|
11094
|
-
if (inst.state.isMinimized) {
|
|
11095
|
-
this.expand(container);
|
|
11096
|
-
} else {
|
|
11097
|
-
this.minimize(container);
|
|
11098
|
-
}
|
|
11099
|
-
},
|
|
11100
|
-
/**
|
|
11101
|
-
* Set floating corner or pixel position (detached only).
|
|
11102
|
-
* @param {HTMLElement} container
|
|
11103
|
-
* @param {string|{x:number,y:number}} position 'bottom-left' | 'bottom-right' | { x, y } viewport
|
|
11104
|
-
*/
|
|
11105
|
-
setPosition: function(container, position) {
|
|
11106
|
-
const inst = this.instances.get(container);
|
|
11107
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11108
|
-
if (typeof position === "string") {
|
|
11109
|
-
this._setCornerPosition(
|
|
11110
|
-
container,
|
|
11111
|
-
position === "bottom-left" || position === "bottom-right" ? position : "bottom-right"
|
|
11112
|
-
);
|
|
11113
|
-
} else if (position && typeof position.x === "number" && typeof position.y === "number") {
|
|
11114
|
-
this._setCustomPositionFromRect(container, position.x, position.y);
|
|
11115
|
-
}
|
|
11116
|
-
if (inst.state.persistPosition) {
|
|
11117
|
-
const r = container.getBoundingClientRect();
|
|
11118
|
-
this._savePositionPixels(inst, r.left, r.top);
|
|
11119
|
-
}
|
|
11120
|
-
},
|
|
11121
|
-
/**
|
|
11122
|
-
* @param {Object} inst
|
|
11123
|
-
* @param {boolean} minimized
|
|
11124
|
-
*/
|
|
11125
|
-
_setMinimizeButtonState: function(inst, minimized) {
|
|
11126
|
-
const b = inst.refs && inst.refs.btnMinimize;
|
|
11127
|
-
if (!b) return;
|
|
11128
|
-
b.innerHTML = "";
|
|
11129
|
-
b.appendChild(icon(minimized ? "plus" : "minus"));
|
|
11130
|
-
b.setAttribute("aria-label", minimized ? "Expand player" : "Minimize player");
|
|
11131
|
-
b.setAttribute("aria-expanded", minimized ? "false" : "true");
|
|
11132
|
-
},
|
|
11133
|
-
/**
|
|
11134
|
-
* @param {HTMLElement} container
|
|
11135
|
-
* @param {string} which 'bottom-left' | 'bottom-right'
|
|
11136
|
-
*/
|
|
11137
|
-
_setCornerPosition: function(container, which) {
|
|
11138
|
-
container.classList.remove("is-position-custom", "vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11139
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11140
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11141
|
-
if (which === "bottom-left") {
|
|
11142
|
-
container.classList.add("vd-music-player-floating-bottom-left");
|
|
11143
|
-
} else {
|
|
11144
|
-
container.classList.add("vd-music-player-floating-bottom-right");
|
|
11145
|
-
}
|
|
11146
|
-
},
|
|
11147
|
-
/**
|
|
11148
|
-
* @param {HTMLElement} container
|
|
11149
|
-
* @param {number} left
|
|
11150
|
-
* @param {number} top
|
|
11151
|
-
*/
|
|
11152
|
-
_setCustomPositionFromRect: function(container, left, top) {
|
|
11153
|
-
container.classList.remove("vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11154
|
-
container.classList.add("is-position-custom");
|
|
11155
|
-
container.style.setProperty("--vd-music-player-floating-left", left + "px");
|
|
11156
|
-
container.style.setProperty("--vd-music-player-floating-top", top + "px");
|
|
11157
|
-
},
|
|
11158
|
-
/**
|
|
11159
|
-
* @param {HTMLElement} container
|
|
11160
|
-
* @param {Object} inst
|
|
11161
|
-
*/
|
|
11162
|
-
_loadPersistedPosition: function(container, inst) {
|
|
11163
|
-
if (!inst.state.persistPosition) return;
|
|
11164
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11165
|
-
let raw = null;
|
|
11166
|
-
if (typeof window.safeStorageGet === "function") {
|
|
11167
|
-
raw = window.safeStorageGet(key, null);
|
|
11168
|
-
} else {
|
|
11169
|
-
try {
|
|
11170
|
-
raw = localStorage.getItem(key);
|
|
11171
|
-
} catch (_e) {
|
|
11172
|
-
}
|
|
11173
|
-
}
|
|
11174
|
-
if (!raw) return;
|
|
11175
|
-
try {
|
|
11176
|
-
const o = JSON.parse(raw);
|
|
11177
|
-
if (o && typeof o.x === "number" && typeof o.y === "number") {
|
|
11178
|
-
this._setCustomPositionFromRect(container, o.x, o.y);
|
|
11179
|
-
}
|
|
11180
|
-
} catch (_err) {
|
|
11181
|
-
}
|
|
11182
|
-
},
|
|
11183
|
-
/**
|
|
11184
|
-
* @param {Object} inst
|
|
11185
|
-
* @param {number} x
|
|
11186
|
-
* @param {number} y
|
|
11187
|
-
*/
|
|
11188
|
-
_savePositionPixels: function(inst, x, y) {
|
|
11189
|
-
if (!inst.state.persistPosition) return;
|
|
11190
|
-
const container = this._containerOf(inst);
|
|
11191
|
-
if (!container) return;
|
|
11192
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11193
|
-
const val = JSON.stringify({ x, y });
|
|
11194
|
-
if (typeof window.safeStorageSet === "function") {
|
|
11195
|
-
window.safeStorageSet(key, val);
|
|
11196
|
-
} else {
|
|
11197
|
-
try {
|
|
11198
|
-
localStorage.setItem(key, val);
|
|
11199
|
-
} catch (_e) {
|
|
11200
|
-
}
|
|
11201
|
-
}
|
|
11202
|
-
},
|
|
11203
|
-
/**
|
|
11204
|
-
* @param {Object} inst
|
|
11205
|
-
* @param {HTMLElement} container
|
|
11206
|
-
* @returns {string}
|
|
11207
|
-
*/
|
|
11208
|
-
_persistKeyForInstance: function(inst, container) {
|
|
11209
|
-
const pk = inst.state.persistKey;
|
|
11210
|
-
if (pk && String(pk).trim()) return persistStorageKey(String(pk).trim());
|
|
11211
|
-
return persistStorageKey(container.id || "");
|
|
11212
|
-
},
|
|
11213
|
-
/**
|
|
11214
|
-
* @param {Object} inst
|
|
11215
|
-
*/
|
|
11216
|
-
_unbindFloatingDrag: function(inst) {
|
|
11217
|
-
if (inst.ui && typeof inst.ui.unbindDrag === "function") {
|
|
11218
|
-
inst.ui.unbindDrag();
|
|
11219
|
-
inst.ui.unbindDrag = null;
|
|
11220
|
-
}
|
|
11221
|
-
},
|
|
11222
|
-
/**
|
|
11223
|
-
* Free-form pointer drag on the handle. Vanduo's `draggable` component uses HTML5
|
|
11224
|
-
* drag/drop for list reordering; floating players use pointer events on the handle instead.
|
|
11225
|
-
* @param {Object} inst
|
|
11226
|
-
*/
|
|
11227
|
-
_bindFloatingDrag: function(inst) {
|
|
11228
|
-
this._unbindFloatingDrag(inst);
|
|
11229
|
-
const h = inst.refs && inst.refs.dragHandle;
|
|
11230
|
-
if (!h || !inst.state || !inst.state.draggable) return;
|
|
11231
|
-
const self = this;
|
|
11232
|
-
const container = this._containerOf(inst);
|
|
11233
|
-
if (!container) return;
|
|
11234
|
-
let startX = 0;
|
|
11235
|
-
let startY = 0;
|
|
11236
|
-
let origL = 0;
|
|
11237
|
-
let origT = 0;
|
|
11238
|
-
let activeDrag = false;
|
|
11239
|
-
const onDown = function(e) {
|
|
11240
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
11241
|
-
e.preventDefault();
|
|
11242
|
-
activeDrag = true;
|
|
11243
|
-
const r = container.getBoundingClientRect();
|
|
11244
|
-
origL = r.left;
|
|
11245
|
-
origT = r.top;
|
|
11246
|
-
startX = e.clientX;
|
|
11247
|
-
startY = e.clientY;
|
|
11248
|
-
self._setCustomPositionFromRect(container, origL, origT);
|
|
11249
|
-
try {
|
|
11250
|
-
h.setPointerCapture(e.pointerId);
|
|
11251
|
-
} catch (_err) {
|
|
11252
|
-
}
|
|
11253
|
-
};
|
|
11254
|
-
const onMove = function(e) {
|
|
11255
|
-
if (!activeDrag) return;
|
|
11256
|
-
const dx = e.clientX - startX;
|
|
11257
|
-
const dy = e.clientY - startY;
|
|
11258
|
-
let nl = origL + dx;
|
|
11259
|
-
let nt = origT + dy;
|
|
11260
|
-
const r = container.getBoundingClientRect();
|
|
11261
|
-
const w = r.width;
|
|
11262
|
-
const ph = r.height;
|
|
11263
|
-
const vw = window.innerWidth;
|
|
11264
|
-
const vh = window.innerHeight;
|
|
11265
|
-
const pad = 8;
|
|
11266
|
-
nl = Math.max(pad, Math.min(nl, vw - w - pad));
|
|
11267
|
-
nt = Math.max(pad, Math.min(nt, vh - ph - pad));
|
|
11268
|
-
self._setCustomPositionFromRect(container, nl, nt);
|
|
11269
|
-
};
|
|
11270
|
-
const onUp = function(e) {
|
|
11271
|
-
if (!activeDrag) return;
|
|
11272
|
-
activeDrag = false;
|
|
11273
|
-
if (typeof h.hasPointerCapture === "function" && h.hasPointerCapture(e.pointerId)) {
|
|
11274
|
-
try {
|
|
11275
|
-
h.releasePointerCapture(e.pointerId);
|
|
11276
|
-
} catch (_e) {
|
|
11277
|
-
}
|
|
11278
|
-
}
|
|
11279
|
-
if (inst.state.persistPosition) {
|
|
11280
|
-
const r = container.getBoundingClientRect();
|
|
11281
|
-
self._savePositionPixels(inst, r.left, r.top);
|
|
11282
|
-
}
|
|
11283
|
-
};
|
|
11284
|
-
h.addEventListener("pointerdown", onDown);
|
|
11285
|
-
h.addEventListener("pointermove", onMove);
|
|
11286
|
-
h.addEventListener("pointerup", onUp);
|
|
11287
|
-
h.addEventListener("pointercancel", onUp);
|
|
11288
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11289
|
-
inst.ui.unbindDrag = function() {
|
|
11290
|
-
h.removeEventListener("pointerdown", onDown);
|
|
11291
|
-
h.removeEventListener("pointermove", onMove);
|
|
11292
|
-
h.removeEventListener("pointerup", onUp);
|
|
11293
|
-
h.removeEventListener("pointercancel", onUp);
|
|
11294
|
-
};
|
|
11295
|
-
},
|
|
11296
|
-
/**
|
|
11297
|
-
* Return a shallow copy of the current player state.
|
|
11298
|
-
* @param {HTMLElement} container
|
|
11299
|
-
* @returns {Object|null}
|
|
11300
|
-
*/
|
|
11301
|
-
getState: function(container) {
|
|
11302
|
-
const inst = this.instances.get(container);
|
|
11303
|
-
if (!inst) return null;
|
|
11304
|
-
const s = inst.state;
|
|
11305
|
-
return {
|
|
11306
|
-
isPlaying: s.isPlaying,
|
|
11307
|
-
currentIndex: s.currentIndex,
|
|
11308
|
-
currentTrack: s.tracks[s.currentIndex] || null,
|
|
11309
|
-
volume: s.volume,
|
|
11310
|
-
shuffle: s.shuffle,
|
|
11311
|
-
tracks: s.tracks.slice(),
|
|
11312
|
-
isDetached: Boolean(s.isDetached),
|
|
11313
|
-
isMinimized: Boolean(s.isMinimized)
|
|
11314
|
-
};
|
|
11315
|
-
},
|
|
11316
|
-
/**
|
|
11317
|
-
* Stop playback, clean up listeners, remove instance.
|
|
11318
|
-
* @param {HTMLElement} container
|
|
11319
|
-
*/
|
|
11320
|
-
destroy: function(container) {
|
|
11321
|
-
const inst = this.instances.get(container);
|
|
11322
|
-
if (!inst) return;
|
|
11323
|
-
this._unbindFloatingDrag(inst);
|
|
11324
|
-
if (inst.state && inst.state.isDetached) {
|
|
11325
|
-
try {
|
|
11326
|
-
this.attach(container);
|
|
11327
|
-
} catch (_e) {
|
|
11328
|
-
}
|
|
11329
|
-
}
|
|
11330
|
-
inst.cleanup.forEach((fn) => fn());
|
|
11331
|
-
this.instances.delete(container);
|
|
11332
|
-
container.removeAttribute("data-music-player-initialized");
|
|
11333
|
-
},
|
|
11334
|
-
/**
|
|
11335
|
-
* Destroy all instances.
|
|
11336
|
-
*/
|
|
11337
|
-
destroyAll: function() {
|
|
11338
|
-
this.instances.forEach((_, container) => this.destroy(container));
|
|
11339
|
-
},
|
|
11340
|
-
/* ─── Internal helpers ────────────────────────────────── */
|
|
11341
|
-
/**
|
|
11342
|
-
* Load track by index on an already-initialised instance object.
|
|
11343
|
-
* @param {Object} inst
|
|
11344
|
-
* @param {number} index
|
|
11345
|
-
* @param {boolean} autoPlay
|
|
11346
|
-
*/
|
|
11347
|
-
_loadTrack: function(inst, index, autoPlay) {
|
|
11348
|
-
const track = inst.state.tracks[index];
|
|
11349
|
-
if (!track) return;
|
|
11350
|
-
const container = this._containerOf(inst);
|
|
11351
|
-
inst.state.currentIndex = index;
|
|
11352
|
-
inst.audio.src = track.url;
|
|
11353
|
-
if (inst.refs.trackName) {
|
|
11354
|
-
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
11355
|
-
inst.refs.trackName.classList.remove("is-idle");
|
|
11356
|
-
}
|
|
11357
|
-
if (inst.refs.playlistPanel) {
|
|
11358
|
-
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
11359
|
-
const active = i === index;
|
|
11360
|
-
item.classList.toggle("is-active", active);
|
|
11361
|
-
item.setAttribute("aria-current", active ? "true" : "false");
|
|
11362
|
-
});
|
|
11363
|
-
}
|
|
11364
|
-
if (inst.refs.progressBar) {
|
|
11365
|
-
inst.refs.progressBar.value = "0";
|
|
11366
|
-
updateRangeFill(inst.refs.progressBar);
|
|
11367
|
-
}
|
|
11368
|
-
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
11369
|
-
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
11370
|
-
if (container) {
|
|
11371
|
-
container.dispatchEvent(
|
|
11372
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
11373
|
-
bubbles: true,
|
|
11374
|
-
detail: { index, name: track.name, url: track.url }
|
|
11375
|
-
})
|
|
11376
|
-
);
|
|
11377
|
-
}
|
|
11378
|
-
if (autoPlay) inst.audio.play().catch(() => {
|
|
11379
|
-
});
|
|
11380
|
-
},
|
|
11381
|
-
/**
|
|
11382
|
-
* Reverse-lookup the container element for a given instance object.
|
|
11383
|
-
* @param {Object} inst
|
|
11384
|
-
* @returns {HTMLElement|null}
|
|
11385
|
-
*/
|
|
11386
|
-
_containerOf: function(inst) {
|
|
11387
|
-
for (const [container, i] of this.instances) {
|
|
11388
|
-
if (i === inst) return container;
|
|
11389
|
-
}
|
|
11390
|
-
return null;
|
|
11391
|
-
}
|
|
11392
|
-
};
|
|
11393
|
-
if (typeof window.Vanduo !== "undefined") {
|
|
11394
|
-
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
11395
|
-
}
|
|
11396
|
-
window.VanduoMusicPlayer = MusicPlayer;
|
|
11397
|
-
})();
|
|
11398
|
-
|
|
11399
10400
|
// js/index.js
|
|
11400
10401
|
var Vanduo = window.Vanduo;
|
|
11401
10402
|
var index_default = Vanduo;
|