@vanduo-oss/framework 1.4.1 → 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/components/datepicker.css +3 -1
- package/css/components/modals.css +10 -6
- package/css/components/timepicker.css +3 -1
- package/css/vanduo.css +0 -1
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +169 -1093
- 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 +17 -700
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +169 -1093
- 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 +169 -1093
- 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 +77 -4
- package/js/components/suggest.js +18 -15
- package/js/components/timepicker.js +48 -6
- 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.esm.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
|
}
|
|
@@ -8934,6 +8980,27 @@
|
|
|
8934
8980
|
x.setDate(x.getDate() + (6 - day));
|
|
8935
8981
|
return x;
|
|
8936
8982
|
}
|
|
8983
|
+
function positionAnchoredPopup(anchor, popup, gap) {
|
|
8984
|
+
const padding = 8;
|
|
8985
|
+
const offset = gap != null ? gap : 4;
|
|
8986
|
+
const rect = anchor.getBoundingClientRect();
|
|
8987
|
+
popup.style.minWidth = Math.max(rect.width, 0) + "px";
|
|
8988
|
+
let top = rect.bottom + offset;
|
|
8989
|
+
let left = rect.left;
|
|
8990
|
+
popup.style.top = top + "px";
|
|
8991
|
+
popup.style.left = left + "px";
|
|
8992
|
+
const popRect = popup.getBoundingClientRect();
|
|
8993
|
+
if (popRect.bottom > window.innerHeight - padding && rect.top - popRect.height > padding) {
|
|
8994
|
+
top = rect.top - popRect.height - offset;
|
|
8995
|
+
popup.style.top = top + "px";
|
|
8996
|
+
}
|
|
8997
|
+
const alignedRect = popup.getBoundingClientRect();
|
|
8998
|
+
left = rect.left;
|
|
8999
|
+
if (left + alignedRect.width > window.innerWidth - padding) {
|
|
9000
|
+
left = window.innerWidth - alignedRect.width - padding;
|
|
9001
|
+
}
|
|
9002
|
+
popup.style.left = Math.max(padding, left) + "px";
|
|
9003
|
+
}
|
|
8937
9004
|
const Datepicker = {
|
|
8938
9005
|
instances: /* @__PURE__ */ new Map(),
|
|
8939
9006
|
init: function(root) {
|
|
@@ -8957,6 +9024,7 @@
|
|
|
8957
9024
|
let viewMode = "days";
|
|
8958
9025
|
let focusedDate = null;
|
|
8959
9026
|
let skipNextFocusOpen = false;
|
|
9027
|
+
let ignoreOutsideUntil = 0;
|
|
8960
9028
|
const isDisabled = (d) => {
|
|
8961
9029
|
if (minDate) {
|
|
8962
9030
|
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
@@ -9015,7 +9083,7 @@
|
|
|
9015
9083
|
wrapper.style.display = "inline-block";
|
|
9016
9084
|
input.parentNode.insertBefore(wrapper, input);
|
|
9017
9085
|
wrapper.appendChild(input);
|
|
9018
|
-
|
|
9086
|
+
document.body.appendChild(popup);
|
|
9019
9087
|
const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
9020
9088
|
const selectDate = (date) => {
|
|
9021
9089
|
selectedDate = date;
|
|
@@ -9245,6 +9313,9 @@
|
|
|
9245
9313
|
}
|
|
9246
9314
|
popup.appendChild(grid);
|
|
9247
9315
|
}
|
|
9316
|
+
if (popup.classList.contains("is-open")) {
|
|
9317
|
+
requestAnimationFrame(positionPopup);
|
|
9318
|
+
}
|
|
9248
9319
|
};
|
|
9249
9320
|
const handleGridKeydown = (e) => {
|
|
9250
9321
|
if (!popup.classList.contains("is-open") || viewMode !== "days") return;
|
|
@@ -9312,7 +9383,18 @@
|
|
|
9312
9383
|
render();
|
|
9313
9384
|
requestAnimationFrame(focusFocusedDay);
|
|
9314
9385
|
};
|
|
9386
|
+
const positionPopup = () => {
|
|
9387
|
+
if (!popup.classList.contains("is-open")) return;
|
|
9388
|
+
positionAnchoredPopup(input, popup);
|
|
9389
|
+
};
|
|
9390
|
+
const repositionHandler = () => {
|
|
9391
|
+
positionPopup();
|
|
9392
|
+
};
|
|
9393
|
+
const markIgnoreOutside = () => {
|
|
9394
|
+
ignoreOutsideUntil = Date.now() + 100;
|
|
9395
|
+
};
|
|
9315
9396
|
const open = () => {
|
|
9397
|
+
markIgnoreOutside();
|
|
9316
9398
|
viewMode = "days";
|
|
9317
9399
|
if (selectedDate) {
|
|
9318
9400
|
viewYear = selectedDate.getFullYear();
|
|
@@ -9329,7 +9411,10 @@
|
|
|
9329
9411
|
render();
|
|
9330
9412
|
popup.classList.add("is-open");
|
|
9331
9413
|
input.setAttribute("aria-expanded", "true");
|
|
9332
|
-
requestAnimationFrame(
|
|
9414
|
+
requestAnimationFrame(() => {
|
|
9415
|
+
positionPopup();
|
|
9416
|
+
focusFocusedDay();
|
|
9417
|
+
});
|
|
9333
9418
|
};
|
|
9334
9419
|
const close = () => {
|
|
9335
9420
|
popup.classList.remove("is-open");
|
|
@@ -9343,8 +9428,22 @@
|
|
|
9343
9428
|
}
|
|
9344
9429
|
open();
|
|
9345
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
|
+
};
|
|
9346
9444
|
const outsideHandler = (e) => {
|
|
9347
|
-
if (
|
|
9445
|
+
if (Date.now() < ignoreOutsideUntil) return;
|
|
9446
|
+
if (!isOpenAnchorTarget(e.target)) close();
|
|
9348
9447
|
};
|
|
9349
9448
|
const escHandler = (e) => {
|
|
9350
9449
|
if (e.key === "Escape" && popup.classList.contains("is-open")) {
|
|
@@ -9354,17 +9453,24 @@
|
|
|
9354
9453
|
}
|
|
9355
9454
|
};
|
|
9356
9455
|
input.addEventListener("focus", focusHandler);
|
|
9456
|
+
input.addEventListener("click", clickHandler);
|
|
9357
9457
|
document.addEventListener("click", outsideHandler, true);
|
|
9358
9458
|
document.addEventListener("keydown", escHandler);
|
|
9359
9459
|
popup.addEventListener("keydown", handleGridKeydown);
|
|
9460
|
+
window.addEventListener("resize", repositionHandler);
|
|
9461
|
+
window.addEventListener("scroll", repositionHandler, true);
|
|
9360
9462
|
input.setAttribute("aria-haspopup", "dialog");
|
|
9361
9463
|
input.setAttribute("aria-expanded", "false");
|
|
9362
9464
|
input.setAttribute("autocomplete", "off");
|
|
9363
9465
|
cleanup.push(
|
|
9364
9466
|
() => input.removeEventListener("focus", focusHandler),
|
|
9467
|
+
() => input.removeEventListener("click", clickHandler),
|
|
9365
9468
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9366
9469
|
() => document.removeEventListener("keydown", escHandler),
|
|
9367
|
-
() => popup.removeEventListener("keydown", handleGridKeydown)
|
|
9470
|
+
() => popup.removeEventListener("keydown", handleGridKeydown),
|
|
9471
|
+
() => window.removeEventListener("resize", repositionHandler),
|
|
9472
|
+
() => window.removeEventListener("scroll", repositionHandler, true),
|
|
9473
|
+
() => popup.remove()
|
|
9368
9474
|
);
|
|
9369
9475
|
this.instances.set(input, { cleanup, open, close, popup });
|
|
9370
9476
|
},
|
|
@@ -9387,6 +9493,27 @@
|
|
|
9387
9493
|
// js/components/timepicker.js
|
|
9388
9494
|
(function() {
|
|
9389
9495
|
"use strict";
|
|
9496
|
+
function positionAnchoredPopup(anchor, popup, gap) {
|
|
9497
|
+
const padding = 8;
|
|
9498
|
+
const offset = gap != null ? gap : 4;
|
|
9499
|
+
const rect = anchor.getBoundingClientRect();
|
|
9500
|
+
popup.style.minWidth = Math.max(rect.width, 0) + "px";
|
|
9501
|
+
let top = rect.bottom + offset;
|
|
9502
|
+
let left = rect.left;
|
|
9503
|
+
popup.style.top = top + "px";
|
|
9504
|
+
popup.style.left = left + "px";
|
|
9505
|
+
const popRect = popup.getBoundingClientRect();
|
|
9506
|
+
if (popRect.bottom > window.innerHeight - padding && rect.top - popRect.height > padding) {
|
|
9507
|
+
top = rect.top - popRect.height - offset;
|
|
9508
|
+
popup.style.top = top + "px";
|
|
9509
|
+
}
|
|
9510
|
+
const alignedRect = popup.getBoundingClientRect();
|
|
9511
|
+
left = rect.left;
|
|
9512
|
+
if (left + alignedRect.width > window.innerWidth - padding) {
|
|
9513
|
+
left = window.innerWidth - alignedRect.width - padding;
|
|
9514
|
+
}
|
|
9515
|
+
popup.style.left = Math.max(padding, left) + "px";
|
|
9516
|
+
}
|
|
9390
9517
|
const Timepicker = {
|
|
9391
9518
|
instances: /* @__PURE__ */ new Map(),
|
|
9392
9519
|
init: function(root) {
|
|
@@ -9411,7 +9538,7 @@
|
|
|
9411
9538
|
const popup = document.createElement("div");
|
|
9412
9539
|
popup.className = "vd-timepicker-popup";
|
|
9413
9540
|
popup.setAttribute("role", "listbox");
|
|
9414
|
-
|
|
9541
|
+
document.body.appendChild(popup);
|
|
9415
9542
|
const times = [];
|
|
9416
9543
|
for (let h = 0; h < 24; h++) {
|
|
9417
9544
|
for (let m = 0; m < 60; m += step) {
|
|
@@ -9451,12 +9578,22 @@
|
|
|
9451
9578
|
popup.appendChild(item);
|
|
9452
9579
|
});
|
|
9453
9580
|
};
|
|
9581
|
+
const positionPopup = () => {
|
|
9582
|
+
if (!popup.classList.contains("is-open")) return;
|
|
9583
|
+
positionAnchoredPopup(input, popup);
|
|
9584
|
+
};
|
|
9585
|
+
const repositionHandler = () => {
|
|
9586
|
+
positionPopup();
|
|
9587
|
+
};
|
|
9454
9588
|
const open = () => {
|
|
9455
9589
|
render();
|
|
9456
9590
|
popup.classList.add("is-open");
|
|
9457
9591
|
input.setAttribute("aria-expanded", "true");
|
|
9458
|
-
|
|
9459
|
-
|
|
9592
|
+
requestAnimationFrame(() => {
|
|
9593
|
+
positionPopup();
|
|
9594
|
+
const selected = popup.querySelector(".is-selected");
|
|
9595
|
+
if (selected) selected.scrollIntoView({ block: "center" });
|
|
9596
|
+
});
|
|
9460
9597
|
};
|
|
9461
9598
|
const close = () => {
|
|
9462
9599
|
popup.classList.remove("is-open");
|
|
@@ -9464,7 +9601,7 @@
|
|
|
9464
9601
|
};
|
|
9465
9602
|
const focusHandler = () => open();
|
|
9466
9603
|
const outsideHandler = (e) => {
|
|
9467
|
-
if (!
|
|
9604
|
+
if (!input.contains(e.target) && !popup.contains(e.target)) close();
|
|
9468
9605
|
};
|
|
9469
9606
|
const escHandler = (e) => {
|
|
9470
9607
|
if (e.key === "Escape") close();
|
|
@@ -9472,6 +9609,8 @@
|
|
|
9472
9609
|
input.addEventListener("focus", focusHandler);
|
|
9473
9610
|
document.addEventListener("click", outsideHandler, true);
|
|
9474
9611
|
document.addEventListener("keydown", escHandler);
|
|
9612
|
+
window.addEventListener("resize", repositionHandler);
|
|
9613
|
+
window.addEventListener("scroll", repositionHandler, true);
|
|
9475
9614
|
input.setAttribute("aria-haspopup", "listbox");
|
|
9476
9615
|
input.setAttribute("aria-expanded", "false");
|
|
9477
9616
|
input.setAttribute("autocomplete", "off");
|
|
@@ -9479,7 +9618,10 @@
|
|
|
9479
9618
|
cleanup.push(
|
|
9480
9619
|
() => input.removeEventListener("focus", focusHandler),
|
|
9481
9620
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9482
|
-
() => document.removeEventListener("keydown", escHandler)
|
|
9621
|
+
() => document.removeEventListener("keydown", escHandler),
|
|
9622
|
+
() => window.removeEventListener("resize", repositionHandler),
|
|
9623
|
+
() => window.removeEventListener("scroll", repositionHandler, true),
|
|
9624
|
+
() => popup.remove()
|
|
9483
9625
|
);
|
|
9484
9626
|
this.instances.set(input, { cleanup, open, close });
|
|
9485
9627
|
},
|
|
@@ -10255,1072 +10397,6 @@
|
|
|
10255
10397
|
window.VanduoSpotlight = Spotlight;
|
|
10256
10398
|
})();
|
|
10257
10399
|
|
|
10258
|
-
// js/components/music-player.js
|
|
10259
|
-
(function() {
|
|
10260
|
-
"use strict";
|
|
10261
|
-
function shuffleArray(arr) {
|
|
10262
|
-
const shuffled = arr.slice();
|
|
10263
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
10264
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
10265
|
-
const tmp = shuffled[i];
|
|
10266
|
-
shuffled[i] = shuffled[j];
|
|
10267
|
-
shuffled[j] = tmp;
|
|
10268
|
-
}
|
|
10269
|
-
return shuffled;
|
|
10270
|
-
}
|
|
10271
|
-
function formatTime(seconds) {
|
|
10272
|
-
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
10273
|
-
const m = Math.floor(seconds / 60);
|
|
10274
|
-
const s = Math.floor(seconds % 60);
|
|
10275
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
10276
|
-
}
|
|
10277
|
-
function persistStorageKey(id) {
|
|
10278
|
-
return "vanduo:music-player:" + (id && id.trim() ? id.trim() : "default") + ":pos";
|
|
10279
|
-
}
|
|
10280
|
-
function updateRangeFill(input) {
|
|
10281
|
-
const min = parseFloat(input.min) || 0;
|
|
10282
|
-
const max = parseFloat(input.max) || 1;
|
|
10283
|
-
const val = parseFloat(input.value) || 0;
|
|
10284
|
-
const pct = (val - min) / (max - min) * 100;
|
|
10285
|
-
input.style.setProperty("--vd-fill", pct + "%");
|
|
10286
|
-
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%)";
|
|
10287
|
-
}
|
|
10288
|
-
function icon(name) {
|
|
10289
|
-
const el = document.createElement("i");
|
|
10290
|
-
el.className = "ph ph-" + name;
|
|
10291
|
-
el.setAttribute("aria-hidden", "true");
|
|
10292
|
-
return el;
|
|
10293
|
-
}
|
|
10294
|
-
const MusicPlayer = {
|
|
10295
|
-
/** @type {Map<HTMLElement, Object>} */
|
|
10296
|
-
instances: /* @__PURE__ */ new Map(),
|
|
10297
|
-
/**
|
|
10298
|
-
* Default options.
|
|
10299
|
-
*/
|
|
10300
|
-
defaults: {
|
|
10301
|
-
tracks: [],
|
|
10302
|
-
volume: 0.5,
|
|
10303
|
-
shuffle: false,
|
|
10304
|
-
showProgress: false,
|
|
10305
|
-
showPlaylist: false,
|
|
10306
|
-
autoAdvance: true,
|
|
10307
|
-
glass: false,
|
|
10308
|
-
detachable: false,
|
|
10309
|
-
/** @type {null|string} */
|
|
10310
|
-
floatingPosition: null,
|
|
10311
|
-
draggable: false,
|
|
10312
|
-
minimizable: false,
|
|
10313
|
-
startMinimized: false,
|
|
10314
|
-
persistPosition: false,
|
|
10315
|
-
persistKey: ""
|
|
10316
|
-
},
|
|
10317
|
-
/**
|
|
10318
|
-
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
10319
|
-
* Options can be provided via data-music-player-options (JSON string).
|
|
10320
|
-
*/
|
|
10321
|
-
init: function(root) {
|
|
10322
|
-
window.Vanduo.queryAll(root, ".vd-music-player, [data-music-player]").forEach((el) => {
|
|
10323
|
-
if (this.instances.has(el)) return;
|
|
10324
|
-
let opts = {};
|
|
10325
|
-
const attr = el.getAttribute("data-music-player-options");
|
|
10326
|
-
if (attr) {
|
|
10327
|
-
try {
|
|
10328
|
-
opts = JSON.parse(attr);
|
|
10329
|
-
} catch (_) {
|
|
10330
|
-
}
|
|
10331
|
-
}
|
|
10332
|
-
this.initPlayer(el, opts);
|
|
10333
|
-
});
|
|
10334
|
-
},
|
|
10335
|
-
/**
|
|
10336
|
-
* Initialize a single player element.
|
|
10337
|
-
* @param {HTMLElement} container
|
|
10338
|
-
* @param {Object} [options]
|
|
10339
|
-
*/
|
|
10340
|
-
initPlayer: function(container, options) {
|
|
10341
|
-
const opts = Object.assign({}, this.defaults, options || {});
|
|
10342
|
-
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
10343
|
-
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
10344
|
-
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
10345
|
-
const state = {
|
|
10346
|
-
tracks: trackList,
|
|
10347
|
-
originalTracks: tracks.slice(),
|
|
10348
|
-
currentIndex: 0,
|
|
10349
|
-
isPlaying: false,
|
|
10350
|
-
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
10351
|
-
shuffle: opts.shuffle,
|
|
10352
|
-
showProgress: opts.showProgress,
|
|
10353
|
-
showPlaylist: opts.showPlaylist,
|
|
10354
|
-
autoAdvance: opts.autoAdvance,
|
|
10355
|
-
audio: null,
|
|
10356
|
-
glass: Boolean(opts.glass),
|
|
10357
|
-
detachable: Boolean(opts.detachable),
|
|
10358
|
-
floatingPosition: opts.floatingPosition || "bottom-right",
|
|
10359
|
-
draggable: Boolean(opts.draggable) && Boolean(opts.detachable),
|
|
10360
|
-
minimizable: Boolean(opts.minimizable),
|
|
10361
|
-
startMinimized: Boolean(opts.startMinimized),
|
|
10362
|
-
persistPosition: Boolean(opts.persistPosition),
|
|
10363
|
-
persistKey: typeof opts.persistKey === "string" ? opts.persistKey : "",
|
|
10364
|
-
isDetached: false,
|
|
10365
|
-
isMinimized: false,
|
|
10366
|
-
_startMinimizeApplied: false
|
|
10367
|
-
};
|
|
10368
|
-
const audio = new Audio();
|
|
10369
|
-
audio.volume = state.volume;
|
|
10370
|
-
audio.preload = "metadata";
|
|
10371
|
-
state.audio = audio;
|
|
10372
|
-
this._buildDOM(container, state);
|
|
10373
|
-
const refs = {
|
|
10374
|
-
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
10375
|
-
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
10376
|
-
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
10377
|
-
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
10378
|
-
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
10379
|
-
btnDetach: container.querySelector(".vd-music-player-btn-detach"),
|
|
10380
|
-
btnAttach: container.querySelector(".vd-music-player-btn-attach"),
|
|
10381
|
-
btnMinimize: container.querySelector(".vd-music-player-btn-minimize"),
|
|
10382
|
-
dragHandle: container.querySelector(".vd-music-player-drag-handle"),
|
|
10383
|
-
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
10384
|
-
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
10385
|
-
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
10386
|
-
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
10387
|
-
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
10388
|
-
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
10389
|
-
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
10390
|
-
};
|
|
10391
|
-
const renderPlayIcon = () => {
|
|
10392
|
-
const btn = refs.btnPlay;
|
|
10393
|
-
if (!btn) return;
|
|
10394
|
-
btn.innerHTML = "";
|
|
10395
|
-
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
10396
|
-
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
10397
|
-
btn.classList.toggle("is-active", state.isPlaying);
|
|
10398
|
-
};
|
|
10399
|
-
const renderTrackName = () => {
|
|
10400
|
-
const el = refs.trackName;
|
|
10401
|
-
if (!el) return;
|
|
10402
|
-
const track = state.tracks[state.currentIndex];
|
|
10403
|
-
if (track) {
|
|
10404
|
-
el.textContent = track.name || "Unknown Track";
|
|
10405
|
-
el.classList.remove("is-idle");
|
|
10406
|
-
} else {
|
|
10407
|
-
el.textContent = "No tracks loaded";
|
|
10408
|
-
el.classList.add("is-idle");
|
|
10409
|
-
}
|
|
10410
|
-
};
|
|
10411
|
-
const renderVolumeIcon = () => {
|
|
10412
|
-
const el = refs.volumeIcon;
|
|
10413
|
-
if (!el) return;
|
|
10414
|
-
el.innerHTML = "";
|
|
10415
|
-
const v = state.volume;
|
|
10416
|
-
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
10417
|
-
el.appendChild(icon(name));
|
|
10418
|
-
};
|
|
10419
|
-
const renderShuffleBtn = () => {
|
|
10420
|
-
const btn = refs.btnShuffle;
|
|
10421
|
-
if (!btn) return;
|
|
10422
|
-
btn.classList.toggle("is-active", state.shuffle);
|
|
10423
|
-
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10424
|
-
};
|
|
10425
|
-
const renderPlaylistItems = () => {
|
|
10426
|
-
const panel = refs.playlistPanel;
|
|
10427
|
-
if (!panel) return;
|
|
10428
|
-
panel.innerHTML = "";
|
|
10429
|
-
state.tracks.forEach((track, i) => {
|
|
10430
|
-
const item = document.createElement("button");
|
|
10431
|
-
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
10432
|
-
item.type = "button";
|
|
10433
|
-
item.setAttribute("data-index", String(i));
|
|
10434
|
-
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
10435
|
-
const num = document.createElement("span");
|
|
10436
|
-
num.className = "vd-music-player-playlist-num";
|
|
10437
|
-
num.textContent = String(i + 1);
|
|
10438
|
-
const name = document.createElement("span");
|
|
10439
|
-
name.className = "vd-music-player-playlist-name";
|
|
10440
|
-
name.textContent = track.name || "Track " + (i + 1);
|
|
10441
|
-
item.appendChild(num);
|
|
10442
|
-
item.appendChild(name);
|
|
10443
|
-
panel.appendChild(item);
|
|
10444
|
-
});
|
|
10445
|
-
};
|
|
10446
|
-
const renderProgress = () => {
|
|
10447
|
-
const bar = refs.progressBar;
|
|
10448
|
-
if (!bar || !audio.duration) return;
|
|
10449
|
-
const pct = audio.currentTime / audio.duration * 100;
|
|
10450
|
-
bar.value = String(pct);
|
|
10451
|
-
updateRangeFill(bar);
|
|
10452
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
10453
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10454
|
-
};
|
|
10455
|
-
const loadTrack = (index, autoPlay) => {
|
|
10456
|
-
const track = state.tracks[index];
|
|
10457
|
-
if (!track) return;
|
|
10458
|
-
state.currentIndex = index;
|
|
10459
|
-
audio.src = track.url;
|
|
10460
|
-
renderTrackName();
|
|
10461
|
-
renderPlaylistItems();
|
|
10462
|
-
if (refs.progressBar) {
|
|
10463
|
-
refs.progressBar.value = "0";
|
|
10464
|
-
updateRangeFill(refs.progressBar);
|
|
10465
|
-
}
|
|
10466
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
10467
|
-
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
10468
|
-
container.dispatchEvent(
|
|
10469
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
10470
|
-
bubbles: true,
|
|
10471
|
-
detail: { index, name: track.name, url: track.url }
|
|
10472
|
-
})
|
|
10473
|
-
);
|
|
10474
|
-
if (autoPlay) {
|
|
10475
|
-
audio.play().catch(() => {
|
|
10476
|
-
});
|
|
10477
|
-
}
|
|
10478
|
-
};
|
|
10479
|
-
const cleanupFunctions = [];
|
|
10480
|
-
const onPlay = () => {
|
|
10481
|
-
state.isPlaying = true;
|
|
10482
|
-
renderPlayIcon();
|
|
10483
|
-
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
10484
|
-
};
|
|
10485
|
-
const onPause = () => {
|
|
10486
|
-
state.isPlaying = false;
|
|
10487
|
-
renderPlayIcon();
|
|
10488
|
-
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
10489
|
-
};
|
|
10490
|
-
const onEnded = () => {
|
|
10491
|
-
if (state.autoAdvance && state.tracks.length > 1) {
|
|
10492
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10493
|
-
loadTrack(next, true);
|
|
10494
|
-
} else {
|
|
10495
|
-
state.isPlaying = false;
|
|
10496
|
-
renderPlayIcon();
|
|
10497
|
-
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
10498
|
-
}
|
|
10499
|
-
};
|
|
10500
|
-
const onTimeUpdate = () => {
|
|
10501
|
-
if (state.showProgress) renderProgress();
|
|
10502
|
-
};
|
|
10503
|
-
const onLoadedMetadata = () => {
|
|
10504
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10505
|
-
if (refs.progressBar) {
|
|
10506
|
-
refs.progressBar.max = "100";
|
|
10507
|
-
updateRangeFill(refs.progressBar);
|
|
10508
|
-
}
|
|
10509
|
-
};
|
|
10510
|
-
audio.addEventListener("play", onPlay);
|
|
10511
|
-
audio.addEventListener("pause", onPause);
|
|
10512
|
-
audio.addEventListener("ended", onEnded);
|
|
10513
|
-
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
10514
|
-
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
10515
|
-
cleanupFunctions.push(() => {
|
|
10516
|
-
audio.removeEventListener("play", onPlay);
|
|
10517
|
-
audio.removeEventListener("pause", onPause);
|
|
10518
|
-
audio.removeEventListener("ended", onEnded);
|
|
10519
|
-
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
10520
|
-
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
10521
|
-
audio.pause();
|
|
10522
|
-
audio.src = "";
|
|
10523
|
-
});
|
|
10524
|
-
if (refs.btnPlay) {
|
|
10525
|
-
const handler = () => {
|
|
10526
|
-
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
10527
|
-
if (state.isPlaying) {
|
|
10528
|
-
audio.pause();
|
|
10529
|
-
} else {
|
|
10530
|
-
audio.play().catch(() => {
|
|
10531
|
-
});
|
|
10532
|
-
}
|
|
10533
|
-
};
|
|
10534
|
-
refs.btnPlay.addEventListener("click", handler);
|
|
10535
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
10536
|
-
const keyHandler = (e) => {
|
|
10537
|
-
if (e.key === " " || e.key === "Enter") {
|
|
10538
|
-
e.preventDefault();
|
|
10539
|
-
handler();
|
|
10540
|
-
}
|
|
10541
|
-
};
|
|
10542
|
-
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
10543
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
10544
|
-
}
|
|
10545
|
-
if (refs.btnPrev) {
|
|
10546
|
-
const handler = () => {
|
|
10547
|
-
if (!state.tracks.length) return;
|
|
10548
|
-
if (audio.currentTime > 3) {
|
|
10549
|
-
audio.currentTime = 0;
|
|
10550
|
-
} else {
|
|
10551
|
-
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
10552
|
-
loadTrack(prev, state.isPlaying);
|
|
10553
|
-
}
|
|
10554
|
-
};
|
|
10555
|
-
refs.btnPrev.addEventListener("click", handler);
|
|
10556
|
-
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
10557
|
-
}
|
|
10558
|
-
if (refs.btnNext) {
|
|
10559
|
-
const handler = () => {
|
|
10560
|
-
if (!state.tracks.length) return;
|
|
10561
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10562
|
-
loadTrack(next, state.isPlaying);
|
|
10563
|
-
};
|
|
10564
|
-
refs.btnNext.addEventListener("click", handler);
|
|
10565
|
-
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
10566
|
-
}
|
|
10567
|
-
if (refs.btnShuffle) {
|
|
10568
|
-
const handler = () => {
|
|
10569
|
-
state.shuffle = !state.shuffle;
|
|
10570
|
-
if (state.shuffle) {
|
|
10571
|
-
const current = state.tracks[state.currentIndex];
|
|
10572
|
-
state.tracks = shuffleArray(state.tracks);
|
|
10573
|
-
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
10574
|
-
if (newIdx > 0) {
|
|
10575
|
-
state.tracks.splice(newIdx, 1);
|
|
10576
|
-
state.tracks.unshift(current);
|
|
10577
|
-
}
|
|
10578
|
-
state.currentIndex = 0;
|
|
10579
|
-
} else {
|
|
10580
|
-
const current = state.tracks[state.currentIndex];
|
|
10581
|
-
state.tracks = state.originalTracks.slice();
|
|
10582
|
-
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
10583
|
-
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
10584
|
-
}
|
|
10585
|
-
renderShuffleBtn();
|
|
10586
|
-
renderPlaylistItems();
|
|
10587
|
-
};
|
|
10588
|
-
refs.btnShuffle.addEventListener("click", handler);
|
|
10589
|
-
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
10590
|
-
}
|
|
10591
|
-
if (refs.btnPlaylist) {
|
|
10592
|
-
const handler = () => {
|
|
10593
|
-
const panel = refs.playlistPanel;
|
|
10594
|
-
if (!panel) return;
|
|
10595
|
-
const isOpen = panel.classList.toggle("is-open");
|
|
10596
|
-
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
10597
|
-
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
10598
|
-
};
|
|
10599
|
-
refs.btnPlaylist.addEventListener("click", handler);
|
|
10600
|
-
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
10601
|
-
}
|
|
10602
|
-
if (refs.volumeSlider) {
|
|
10603
|
-
const handler = (e) => {
|
|
10604
|
-
const v = parseFloat(e.target.value);
|
|
10605
|
-
state.volume = v;
|
|
10606
|
-
audio.volume = v;
|
|
10607
|
-
renderVolumeIcon();
|
|
10608
|
-
updateRangeFill(refs.volumeSlider);
|
|
10609
|
-
container.dispatchEvent(
|
|
10610
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10611
|
-
);
|
|
10612
|
-
};
|
|
10613
|
-
refs.volumeSlider.addEventListener("input", handler);
|
|
10614
|
-
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
10615
|
-
updateRangeFill(refs.volumeSlider);
|
|
10616
|
-
}
|
|
10617
|
-
if (refs.progressBar) {
|
|
10618
|
-
const handler = (e) => {
|
|
10619
|
-
if (!audio.duration) return;
|
|
10620
|
-
const pct = parseFloat(e.target.value);
|
|
10621
|
-
audio.currentTime = pct / 100 * audio.duration;
|
|
10622
|
-
updateRangeFill(refs.progressBar);
|
|
10623
|
-
};
|
|
10624
|
-
refs.progressBar.addEventListener("input", handler);
|
|
10625
|
-
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
10626
|
-
}
|
|
10627
|
-
if (refs.playlistPanel) {
|
|
10628
|
-
const panelHandler = (e) => {
|
|
10629
|
-
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
10630
|
-
if (!item) return;
|
|
10631
|
-
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
10632
|
-
if (!isNaN(idx)) loadTrack(idx, true);
|
|
10633
|
-
};
|
|
10634
|
-
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
10635
|
-
cleanupFunctions.push(
|
|
10636
|
-
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
10637
|
-
);
|
|
10638
|
-
}
|
|
10639
|
-
if (refs.btnDetach) {
|
|
10640
|
-
const h = () => {
|
|
10641
|
-
this.detach(container);
|
|
10642
|
-
};
|
|
10643
|
-
refs.btnDetach.addEventListener("click", h);
|
|
10644
|
-
cleanupFunctions.push(() => refs.btnDetach.removeEventListener("click", h));
|
|
10645
|
-
}
|
|
10646
|
-
if (refs.btnAttach) {
|
|
10647
|
-
const h = () => {
|
|
10648
|
-
this.attach(container);
|
|
10649
|
-
};
|
|
10650
|
-
refs.btnAttach.addEventListener("click", h);
|
|
10651
|
-
cleanupFunctions.push(() => refs.btnAttach.removeEventListener("click", h));
|
|
10652
|
-
}
|
|
10653
|
-
if (refs.btnMinimize) {
|
|
10654
|
-
const h = () => {
|
|
10655
|
-
this.toggleMinimize(container);
|
|
10656
|
-
};
|
|
10657
|
-
refs.btnMinimize.addEventListener("click", h);
|
|
10658
|
-
cleanupFunctions.push(() => refs.btnMinimize.removeEventListener("click", h));
|
|
10659
|
-
}
|
|
10660
|
-
renderPlayIcon();
|
|
10661
|
-
renderTrackName();
|
|
10662
|
-
renderVolumeIcon();
|
|
10663
|
-
if (opts.showPlaylist) renderPlaylistItems();
|
|
10664
|
-
this.instances.set(container, {
|
|
10665
|
-
state,
|
|
10666
|
-
audio,
|
|
10667
|
-
refs,
|
|
10668
|
-
cleanup: cleanupFunctions,
|
|
10669
|
-
ui: { restore: null, unbindDrag: null }
|
|
10670
|
-
});
|
|
10671
|
-
container.setAttribute("data-music-player-initialized", "true");
|
|
10672
|
-
},
|
|
10673
|
-
/* ─── DOM builder ─────────────────────────────────────── */
|
|
10674
|
-
/**
|
|
10675
|
-
* Build the inner DOM structure inside container.
|
|
10676
|
-
* Pre-existing inner content is replaced only if it has no
|
|
10677
|
-
* recognised child elements (allows server-rendered markup).
|
|
10678
|
-
* @param {HTMLElement} container
|
|
10679
|
-
* @param {Object} state
|
|
10680
|
-
*/
|
|
10681
|
-
_buildDOM: function(container, state) {
|
|
10682
|
-
if (container.querySelector(".vd-music-player-controls")) return;
|
|
10683
|
-
container.setAttribute("role", "region");
|
|
10684
|
-
container.setAttribute("aria-label", "Music Player");
|
|
10685
|
-
if (state.showProgress) container.classList.add("has-progress");
|
|
10686
|
-
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
10687
|
-
if (state.glass) container.classList.add("vd-music-player-glass");
|
|
10688
|
-
if (state.draggable) container.classList.add("vd-music-player-draggable");
|
|
10689
|
-
if (state.detachable || state.minimizable) {
|
|
10690
|
-
const tb = document.createElement("div");
|
|
10691
|
-
tb.className = "vd-music-player-toolbar";
|
|
10692
|
-
tb.setAttribute("role", "toolbar");
|
|
10693
|
-
tb.setAttribute("aria-label", "Player window");
|
|
10694
|
-
if (state.draggable) {
|
|
10695
|
-
const h = document.createElement("button");
|
|
10696
|
-
h.type = "button";
|
|
10697
|
-
h.className = "vd-music-player-drag-handle";
|
|
10698
|
-
h.setAttribute("aria-label", "Drag to move player");
|
|
10699
|
-
h.appendChild(icon("dots-six-vertical"));
|
|
10700
|
-
tb.appendChild(h);
|
|
10701
|
-
}
|
|
10702
|
-
const tSp = document.createElement("span");
|
|
10703
|
-
tSp.className = "vd-music-player-toolbar-spacer";
|
|
10704
|
-
tSp.setAttribute("aria-hidden", "true");
|
|
10705
|
-
tb.appendChild(tSp);
|
|
10706
|
-
if (state.minimizable) {
|
|
10707
|
-
const bMin = document.createElement("button");
|
|
10708
|
-
bMin.type = "button";
|
|
10709
|
-
bMin.className = "vd-music-player-btn vd-music-player-btn-minimize";
|
|
10710
|
-
bMin.setAttribute("aria-label", "Minimize player");
|
|
10711
|
-
bMin.setAttribute("aria-expanded", "true");
|
|
10712
|
-
bMin.appendChild(icon("minus"));
|
|
10713
|
-
tb.appendChild(bMin);
|
|
10714
|
-
}
|
|
10715
|
-
if (state.detachable) {
|
|
10716
|
-
const bOut = document.createElement("button");
|
|
10717
|
-
bOut.type = "button";
|
|
10718
|
-
bOut.className = "vd-music-player-btn vd-music-player-btn-detach";
|
|
10719
|
-
bOut.setAttribute("aria-label", "Detach player");
|
|
10720
|
-
bOut.appendChild(icon("arrows-out"));
|
|
10721
|
-
tb.appendChild(bOut);
|
|
10722
|
-
const bIn = document.createElement("button");
|
|
10723
|
-
bIn.type = "button";
|
|
10724
|
-
bIn.className = "vd-music-player-btn vd-music-player-btn-attach";
|
|
10725
|
-
bIn.setAttribute("aria-label", "Attach player");
|
|
10726
|
-
bIn.appendChild(icon("arrows-in"));
|
|
10727
|
-
tb.appendChild(bIn);
|
|
10728
|
-
}
|
|
10729
|
-
container.classList.add("vd-music-player-has-chrome");
|
|
10730
|
-
container.appendChild(tb);
|
|
10731
|
-
}
|
|
10732
|
-
const info = document.createElement("div");
|
|
10733
|
-
info.className = "vd-music-player-info";
|
|
10734
|
-
const iconWrap = document.createElement("span");
|
|
10735
|
-
iconWrap.className = "vd-music-player-icon";
|
|
10736
|
-
iconWrap.setAttribute("aria-hidden", "true");
|
|
10737
|
-
iconWrap.appendChild(icon("music-note"));
|
|
10738
|
-
const trackName = document.createElement("span");
|
|
10739
|
-
trackName.className = "vd-music-player-track-name";
|
|
10740
|
-
trackName.setAttribute("aria-live", "polite");
|
|
10741
|
-
trackName.setAttribute("aria-atomic", "true");
|
|
10742
|
-
info.appendChild(iconWrap);
|
|
10743
|
-
info.appendChild(trackName);
|
|
10744
|
-
container.appendChild(info);
|
|
10745
|
-
const controls = document.createElement("div");
|
|
10746
|
-
controls.className = "vd-music-player-controls";
|
|
10747
|
-
controls.setAttribute("role", "group");
|
|
10748
|
-
controls.setAttribute("aria-label", "Playback controls");
|
|
10749
|
-
const btnPrev = document.createElement("button");
|
|
10750
|
-
btnPrev.type = "button";
|
|
10751
|
-
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
10752
|
-
btnPrev.setAttribute("aria-label", "Previous track");
|
|
10753
|
-
btnPrev.appendChild(icon("skip-back"));
|
|
10754
|
-
const btnPlay = document.createElement("button");
|
|
10755
|
-
btnPlay.type = "button";
|
|
10756
|
-
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
10757
|
-
btnPlay.setAttribute("aria-label", "Play");
|
|
10758
|
-
btnPlay.appendChild(icon("play"));
|
|
10759
|
-
const btnNext = document.createElement("button");
|
|
10760
|
-
btnNext.type = "button";
|
|
10761
|
-
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
10762
|
-
btnNext.setAttribute("aria-label", "Next track");
|
|
10763
|
-
btnNext.appendChild(icon("skip-forward"));
|
|
10764
|
-
controls.appendChild(btnPrev);
|
|
10765
|
-
controls.appendChild(btnPlay);
|
|
10766
|
-
controls.appendChild(btnNext);
|
|
10767
|
-
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
10768
|
-
const btnShuffle = document.createElement("button");
|
|
10769
|
-
btnShuffle.type = "button";
|
|
10770
|
-
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
10771
|
-
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
10772
|
-
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10773
|
-
btnShuffle.appendChild(icon("shuffle"));
|
|
10774
|
-
controls.appendChild(btnShuffle);
|
|
10775
|
-
}
|
|
10776
|
-
const spacer = document.createElement("span");
|
|
10777
|
-
spacer.className = "vd-music-player-spacer";
|
|
10778
|
-
spacer.setAttribute("aria-hidden", "true");
|
|
10779
|
-
controls.appendChild(spacer);
|
|
10780
|
-
const volumeWrap = document.createElement("div");
|
|
10781
|
-
volumeWrap.className = "vd-music-player-volume";
|
|
10782
|
-
const volumeIcon = document.createElement("span");
|
|
10783
|
-
volumeIcon.className = "vd-music-player-volume-icon";
|
|
10784
|
-
volumeIcon.setAttribute("aria-hidden", "true");
|
|
10785
|
-
const volumeSlider = document.createElement("input");
|
|
10786
|
-
volumeSlider.type = "range";
|
|
10787
|
-
volumeSlider.className = "vd-music-player-volume-slider";
|
|
10788
|
-
volumeSlider.min = "0";
|
|
10789
|
-
volumeSlider.max = "1";
|
|
10790
|
-
volumeSlider.step = "0.01";
|
|
10791
|
-
volumeSlider.value = String(state.volume);
|
|
10792
|
-
volumeSlider.setAttribute("aria-label", "Volume");
|
|
10793
|
-
volumeWrap.appendChild(volumeIcon);
|
|
10794
|
-
volumeWrap.appendChild(volumeSlider);
|
|
10795
|
-
controls.appendChild(volumeWrap);
|
|
10796
|
-
if (state.showPlaylist) {
|
|
10797
|
-
const btnPlaylist = document.createElement("button");
|
|
10798
|
-
btnPlaylist.type = "button";
|
|
10799
|
-
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
10800
|
-
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
10801
|
-
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
10802
|
-
btnPlaylist.appendChild(icon("playlist"));
|
|
10803
|
-
controls.appendChild(btnPlaylist);
|
|
10804
|
-
}
|
|
10805
|
-
container.appendChild(controls);
|
|
10806
|
-
if (state.showProgress) {
|
|
10807
|
-
const progressRow = document.createElement("div");
|
|
10808
|
-
progressRow.className = "vd-music-player-progress";
|
|
10809
|
-
const timeElapsed = document.createElement("span");
|
|
10810
|
-
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
10811
|
-
timeElapsed.textContent = "0:00";
|
|
10812
|
-
timeElapsed.setAttribute("aria-hidden", "true");
|
|
10813
|
-
const progressBar = document.createElement("input");
|
|
10814
|
-
progressBar.type = "range";
|
|
10815
|
-
progressBar.className = "vd-music-player-progress-bar";
|
|
10816
|
-
progressBar.min = "0";
|
|
10817
|
-
progressBar.max = "100";
|
|
10818
|
-
progressBar.step = "0.1";
|
|
10819
|
-
progressBar.value = "0";
|
|
10820
|
-
progressBar.setAttribute("aria-label", "Seek");
|
|
10821
|
-
const timeDuration = document.createElement("span");
|
|
10822
|
-
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
10823
|
-
timeDuration.textContent = "0:00";
|
|
10824
|
-
timeDuration.setAttribute("aria-hidden", "true");
|
|
10825
|
-
progressRow.appendChild(timeElapsed);
|
|
10826
|
-
progressRow.appendChild(progressBar);
|
|
10827
|
-
progressRow.appendChild(timeDuration);
|
|
10828
|
-
container.appendChild(progressRow);
|
|
10829
|
-
}
|
|
10830
|
-
if (state.showPlaylist) {
|
|
10831
|
-
const playlist = document.createElement("div");
|
|
10832
|
-
playlist.className = "vd-music-player-playlist";
|
|
10833
|
-
playlist.setAttribute("aria-label", "Playlist");
|
|
10834
|
-
container.appendChild(playlist);
|
|
10835
|
-
}
|
|
10836
|
-
},
|
|
10837
|
-
/* ─── Public API ──────────────────────────────────────── */
|
|
10838
|
-
/**
|
|
10839
|
-
* @param {HTMLElement} container
|
|
10840
|
-
*/
|
|
10841
|
-
play: function(container) {
|
|
10842
|
-
const inst = this.instances.get(container);
|
|
10843
|
-
if (!inst) return;
|
|
10844
|
-
if (!inst.audio.src && inst.state.tracks.length) {
|
|
10845
|
-
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
10846
|
-
}
|
|
10847
|
-
inst.audio.play().catch(() => {
|
|
10848
|
-
});
|
|
10849
|
-
},
|
|
10850
|
-
/**
|
|
10851
|
-
* @param {HTMLElement} container
|
|
10852
|
-
*/
|
|
10853
|
-
pause: function(container) {
|
|
10854
|
-
const inst = this.instances.get(container);
|
|
10855
|
-
if (inst) inst.audio.pause();
|
|
10856
|
-
},
|
|
10857
|
-
/**
|
|
10858
|
-
* @param {HTMLElement} container
|
|
10859
|
-
*/
|
|
10860
|
-
toggle: function(container) {
|
|
10861
|
-
const inst = this.instances.get(container);
|
|
10862
|
-
if (!inst) return;
|
|
10863
|
-
if (inst.state.isPlaying) {
|
|
10864
|
-
this.pause(container);
|
|
10865
|
-
} else {
|
|
10866
|
-
this.play(container);
|
|
10867
|
-
}
|
|
10868
|
-
},
|
|
10869
|
-
/**
|
|
10870
|
-
* @param {HTMLElement} container
|
|
10871
|
-
*/
|
|
10872
|
-
next: function(container) {
|
|
10873
|
-
const inst = this.instances.get(container);
|
|
10874
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10875
|
-
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
10876
|
-
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
10877
|
-
},
|
|
10878
|
-
/**
|
|
10879
|
-
* @param {HTMLElement} container
|
|
10880
|
-
*/
|
|
10881
|
-
previous: function(container) {
|
|
10882
|
-
const inst = this.instances.get(container);
|
|
10883
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10884
|
-
const len = inst.state.tracks.length;
|
|
10885
|
-
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
10886
|
-
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
10887
|
-
},
|
|
10888
|
-
/**
|
|
10889
|
-
* @param {HTMLElement} container
|
|
10890
|
-
* @param {number} value - 0 to 1
|
|
10891
|
-
*/
|
|
10892
|
-
setVolume: function(container, value) {
|
|
10893
|
-
const inst = this.instances.get(container);
|
|
10894
|
-
if (!inst) return;
|
|
10895
|
-
const v = Math.max(0, Math.min(1, value));
|
|
10896
|
-
inst.state.volume = v;
|
|
10897
|
-
inst.audio.volume = v;
|
|
10898
|
-
if (inst.refs.volumeSlider) {
|
|
10899
|
-
inst.refs.volumeSlider.value = String(v);
|
|
10900
|
-
updateRangeFill(inst.refs.volumeSlider);
|
|
10901
|
-
}
|
|
10902
|
-
container.dispatchEvent(
|
|
10903
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10904
|
-
);
|
|
10905
|
-
},
|
|
10906
|
-
/**
|
|
10907
|
-
* @param {HTMLElement} container
|
|
10908
|
-
* @param {number} index - Track index
|
|
10909
|
-
*/
|
|
10910
|
-
setTrack: function(container, index) {
|
|
10911
|
-
const inst = this.instances.get(container);
|
|
10912
|
-
if (!inst) return;
|
|
10913
|
-
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
10914
|
-
},
|
|
10915
|
-
/**
|
|
10916
|
-
* Shuffle or un-shuffle the track list.
|
|
10917
|
-
* @param {HTMLElement} container
|
|
10918
|
-
*/
|
|
10919
|
-
shuffle: function(container) {
|
|
10920
|
-
const inst = this.instances.get(container);
|
|
10921
|
-
if (!inst || !inst.refs.btnShuffle) return;
|
|
10922
|
-
inst.refs.btnShuffle.click();
|
|
10923
|
-
},
|
|
10924
|
-
/**
|
|
10925
|
-
* Float the player above the page. Requires { detachable: true } at init.
|
|
10926
|
-
* @param {HTMLElement} container
|
|
10927
|
-
* @param {string} [position] 'bottom-left' or 'bottom-right'
|
|
10928
|
-
*/
|
|
10929
|
-
detach: function(container, position) {
|
|
10930
|
-
const inst = this.instances.get(container);
|
|
10931
|
-
if (!inst || !inst.state.detachable || inst.state.isDetached) return;
|
|
10932
|
-
const s = inst.state;
|
|
10933
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
10934
|
-
s.isDetached = true;
|
|
10935
|
-
inst.ui.restore = {
|
|
10936
|
-
parent: container.parentNode,
|
|
10937
|
-
next: container.nextSibling
|
|
10938
|
-
};
|
|
10939
|
-
document.body.appendChild(container);
|
|
10940
|
-
container.classList.add("vd-music-player-floating", "vd-music-player-detached");
|
|
10941
|
-
const pos = position != null && position !== void 0 ? position : s.floatingPosition;
|
|
10942
|
-
this._setCornerPosition(
|
|
10943
|
-
container,
|
|
10944
|
-
pos === "bottom-left" || pos === "bottom-right" ? pos : "bottom-right"
|
|
10945
|
-
);
|
|
10946
|
-
this._loadPersistedPosition(container, inst);
|
|
10947
|
-
if (s.startMinimized && !s._startMinimizeApplied) {
|
|
10948
|
-
s._startMinimizeApplied = true;
|
|
10949
|
-
this.minimize(container);
|
|
10950
|
-
}
|
|
10951
|
-
this._bindFloatingDrag(inst);
|
|
10952
|
-
container.dispatchEvent(new CustomEvent("musicplayer:detach", { bubbles: true }));
|
|
10953
|
-
},
|
|
10954
|
-
/**
|
|
10955
|
-
* Return a detached player to its original place in the document.
|
|
10956
|
-
* @param {HTMLElement} container
|
|
10957
|
-
*/
|
|
10958
|
-
attach: function(container) {
|
|
10959
|
-
const inst = this.instances.get(container);
|
|
10960
|
-
if (!inst || !inst.state.isDetached) return;
|
|
10961
|
-
this._unbindFloatingDrag(inst);
|
|
10962
|
-
inst.state.isDetached = false;
|
|
10963
|
-
const r = inst.ui && inst.ui.restore;
|
|
10964
|
-
container.classList.remove(
|
|
10965
|
-
"vd-music-player-floating",
|
|
10966
|
-
"vd-music-player-detached",
|
|
10967
|
-
"vd-music-player-floating-bottom-left",
|
|
10968
|
-
"vd-music-player-floating-bottom-right",
|
|
10969
|
-
"is-position-custom"
|
|
10970
|
-
);
|
|
10971
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
10972
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
10973
|
-
if (r && r.parent && r.parent.isConnected) {
|
|
10974
|
-
r.parent.insertBefore(container, r.next);
|
|
10975
|
-
}
|
|
10976
|
-
if (inst.ui) {
|
|
10977
|
-
inst.ui.restore = null;
|
|
10978
|
-
inst.ui.unbindDrag = null;
|
|
10979
|
-
}
|
|
10980
|
-
container.dispatchEvent(new CustomEvent("musicplayer:attach", { bubbles: true }));
|
|
10981
|
-
},
|
|
10982
|
-
/**
|
|
10983
|
-
* Collapse to essential controls. Requires { minimizable: true } at init.
|
|
10984
|
-
* @param {HTMLElement} container
|
|
10985
|
-
*/
|
|
10986
|
-
minimize: function(container) {
|
|
10987
|
-
const inst = this.instances.get(container);
|
|
10988
|
-
if (!inst || !inst.state.minimizable || inst.state.isMinimized) return;
|
|
10989
|
-
const s = inst.state;
|
|
10990
|
-
s.isMinimized = true;
|
|
10991
|
-
container.classList.add("vd-music-player-minimized");
|
|
10992
|
-
this._setMinimizeButtonState(inst, true);
|
|
10993
|
-
if (inst.refs.playlistPanel && inst.refs.playlistPanel.classList.contains("is-open") && inst.refs.btnPlaylist) {
|
|
10994
|
-
inst.refs.playlistPanel.classList.remove("is-open");
|
|
10995
|
-
inst.refs.btnPlaylist.classList.remove("is-active");
|
|
10996
|
-
inst.refs.btnPlaylist.setAttribute("aria-expanded", "false");
|
|
10997
|
-
}
|
|
10998
|
-
container.dispatchEvent(new CustomEvent("musicplayer:minimize", { bubbles: true }));
|
|
10999
|
-
},
|
|
11000
|
-
/**
|
|
11001
|
-
* Restore from minimized state.
|
|
11002
|
-
* @param {HTMLElement} container
|
|
11003
|
-
*/
|
|
11004
|
-
expand: function(container) {
|
|
11005
|
-
const inst = this.instances.get(container);
|
|
11006
|
-
if (!inst || !inst.state.minimizable || !inst.state.isMinimized) return;
|
|
11007
|
-
inst.state.isMinimized = false;
|
|
11008
|
-
container.classList.remove("vd-music-player-minimized");
|
|
11009
|
-
this._setMinimizeButtonState(inst, false);
|
|
11010
|
-
container.dispatchEvent(new CustomEvent("musicplayer:expand", { bubbles: true }));
|
|
11011
|
-
},
|
|
11012
|
-
/**
|
|
11013
|
-
* Toggle minimize / expand.
|
|
11014
|
-
* @param {HTMLElement} container
|
|
11015
|
-
*/
|
|
11016
|
-
toggleMinimize: function(container) {
|
|
11017
|
-
const inst = this.instances.get(container);
|
|
11018
|
-
if (!inst || !inst.state.minimizable) return;
|
|
11019
|
-
if (inst.state.isMinimized) {
|
|
11020
|
-
this.expand(container);
|
|
11021
|
-
} else {
|
|
11022
|
-
this.minimize(container);
|
|
11023
|
-
}
|
|
11024
|
-
},
|
|
11025
|
-
/**
|
|
11026
|
-
* Set floating corner or pixel position (detached only).
|
|
11027
|
-
* @param {HTMLElement} container
|
|
11028
|
-
* @param {string|{x:number,y:number}} position 'bottom-left' | 'bottom-right' | { x, y } viewport
|
|
11029
|
-
*/
|
|
11030
|
-
setPosition: function(container, position) {
|
|
11031
|
-
const inst = this.instances.get(container);
|
|
11032
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11033
|
-
if (typeof position === "string") {
|
|
11034
|
-
this._setCornerPosition(
|
|
11035
|
-
container,
|
|
11036
|
-
position === "bottom-left" || position === "bottom-right" ? position : "bottom-right"
|
|
11037
|
-
);
|
|
11038
|
-
} else if (position && typeof position.x === "number" && typeof position.y === "number") {
|
|
11039
|
-
this._setCustomPositionFromRect(container, position.x, position.y);
|
|
11040
|
-
}
|
|
11041
|
-
if (inst.state.persistPosition) {
|
|
11042
|
-
const r = container.getBoundingClientRect();
|
|
11043
|
-
this._savePositionPixels(inst, r.left, r.top);
|
|
11044
|
-
}
|
|
11045
|
-
},
|
|
11046
|
-
/**
|
|
11047
|
-
* @param {Object} inst
|
|
11048
|
-
* @param {boolean} minimized
|
|
11049
|
-
*/
|
|
11050
|
-
_setMinimizeButtonState: function(inst, minimized) {
|
|
11051
|
-
const b = inst.refs && inst.refs.btnMinimize;
|
|
11052
|
-
if (!b) return;
|
|
11053
|
-
b.innerHTML = "";
|
|
11054
|
-
b.appendChild(icon(minimized ? "plus" : "minus"));
|
|
11055
|
-
b.setAttribute("aria-label", minimized ? "Expand player" : "Minimize player");
|
|
11056
|
-
b.setAttribute("aria-expanded", minimized ? "false" : "true");
|
|
11057
|
-
},
|
|
11058
|
-
/**
|
|
11059
|
-
* @param {HTMLElement} container
|
|
11060
|
-
* @param {string} which 'bottom-left' | 'bottom-right'
|
|
11061
|
-
*/
|
|
11062
|
-
_setCornerPosition: function(container, which) {
|
|
11063
|
-
container.classList.remove("is-position-custom", "vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11064
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11065
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11066
|
-
if (which === "bottom-left") {
|
|
11067
|
-
container.classList.add("vd-music-player-floating-bottom-left");
|
|
11068
|
-
} else {
|
|
11069
|
-
container.classList.add("vd-music-player-floating-bottom-right");
|
|
11070
|
-
}
|
|
11071
|
-
},
|
|
11072
|
-
/**
|
|
11073
|
-
* @param {HTMLElement} container
|
|
11074
|
-
* @param {number} left
|
|
11075
|
-
* @param {number} top
|
|
11076
|
-
*/
|
|
11077
|
-
_setCustomPositionFromRect: function(container, left, top) {
|
|
11078
|
-
container.classList.remove("vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11079
|
-
container.classList.add("is-position-custom");
|
|
11080
|
-
container.style.setProperty("--vd-music-player-floating-left", left + "px");
|
|
11081
|
-
container.style.setProperty("--vd-music-player-floating-top", top + "px");
|
|
11082
|
-
},
|
|
11083
|
-
/**
|
|
11084
|
-
* @param {HTMLElement} container
|
|
11085
|
-
* @param {Object} inst
|
|
11086
|
-
*/
|
|
11087
|
-
_loadPersistedPosition: function(container, inst) {
|
|
11088
|
-
if (!inst.state.persistPosition) return;
|
|
11089
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11090
|
-
let raw = null;
|
|
11091
|
-
if (typeof window.safeStorageGet === "function") {
|
|
11092
|
-
raw = window.safeStorageGet(key, null);
|
|
11093
|
-
} else {
|
|
11094
|
-
try {
|
|
11095
|
-
raw = localStorage.getItem(key);
|
|
11096
|
-
} catch (_e) {
|
|
11097
|
-
}
|
|
11098
|
-
}
|
|
11099
|
-
if (!raw) return;
|
|
11100
|
-
try {
|
|
11101
|
-
const o = JSON.parse(raw);
|
|
11102
|
-
if (o && typeof o.x === "number" && typeof o.y === "number") {
|
|
11103
|
-
this._setCustomPositionFromRect(container, o.x, o.y);
|
|
11104
|
-
}
|
|
11105
|
-
} catch (_err) {
|
|
11106
|
-
}
|
|
11107
|
-
},
|
|
11108
|
-
/**
|
|
11109
|
-
* @param {Object} inst
|
|
11110
|
-
* @param {number} x
|
|
11111
|
-
* @param {number} y
|
|
11112
|
-
*/
|
|
11113
|
-
_savePositionPixels: function(inst, x, y) {
|
|
11114
|
-
if (!inst.state.persistPosition) return;
|
|
11115
|
-
const container = this._containerOf(inst);
|
|
11116
|
-
if (!container) return;
|
|
11117
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11118
|
-
const val = JSON.stringify({ x, y });
|
|
11119
|
-
if (typeof window.safeStorageSet === "function") {
|
|
11120
|
-
window.safeStorageSet(key, val);
|
|
11121
|
-
} else {
|
|
11122
|
-
try {
|
|
11123
|
-
localStorage.setItem(key, val);
|
|
11124
|
-
} catch (_e) {
|
|
11125
|
-
}
|
|
11126
|
-
}
|
|
11127
|
-
},
|
|
11128
|
-
/**
|
|
11129
|
-
* @param {Object} inst
|
|
11130
|
-
* @param {HTMLElement} container
|
|
11131
|
-
* @returns {string}
|
|
11132
|
-
*/
|
|
11133
|
-
_persistKeyForInstance: function(inst, container) {
|
|
11134
|
-
const pk = inst.state.persistKey;
|
|
11135
|
-
if (pk && String(pk).trim()) return persistStorageKey(String(pk).trim());
|
|
11136
|
-
return persistStorageKey(container.id || "");
|
|
11137
|
-
},
|
|
11138
|
-
/**
|
|
11139
|
-
* @param {Object} inst
|
|
11140
|
-
*/
|
|
11141
|
-
_unbindFloatingDrag: function(inst) {
|
|
11142
|
-
if (inst.ui && typeof inst.ui.unbindDrag === "function") {
|
|
11143
|
-
inst.ui.unbindDrag();
|
|
11144
|
-
inst.ui.unbindDrag = null;
|
|
11145
|
-
}
|
|
11146
|
-
},
|
|
11147
|
-
/**
|
|
11148
|
-
* Free-form pointer drag on the handle. Vanduo's `draggable` component uses HTML5
|
|
11149
|
-
* drag/drop for list reordering; floating players use pointer events on the handle instead.
|
|
11150
|
-
* @param {Object} inst
|
|
11151
|
-
*/
|
|
11152
|
-
_bindFloatingDrag: function(inst) {
|
|
11153
|
-
this._unbindFloatingDrag(inst);
|
|
11154
|
-
const h = inst.refs && inst.refs.dragHandle;
|
|
11155
|
-
if (!h || !inst.state || !inst.state.draggable) return;
|
|
11156
|
-
const self = this;
|
|
11157
|
-
const container = this._containerOf(inst);
|
|
11158
|
-
if (!container) return;
|
|
11159
|
-
let startX = 0;
|
|
11160
|
-
let startY = 0;
|
|
11161
|
-
let origL = 0;
|
|
11162
|
-
let origT = 0;
|
|
11163
|
-
let activeDrag = false;
|
|
11164
|
-
const onDown = function(e) {
|
|
11165
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
11166
|
-
e.preventDefault();
|
|
11167
|
-
activeDrag = true;
|
|
11168
|
-
const r = container.getBoundingClientRect();
|
|
11169
|
-
origL = r.left;
|
|
11170
|
-
origT = r.top;
|
|
11171
|
-
startX = e.clientX;
|
|
11172
|
-
startY = e.clientY;
|
|
11173
|
-
self._setCustomPositionFromRect(container, origL, origT);
|
|
11174
|
-
try {
|
|
11175
|
-
h.setPointerCapture(e.pointerId);
|
|
11176
|
-
} catch (_err) {
|
|
11177
|
-
}
|
|
11178
|
-
};
|
|
11179
|
-
const onMove = function(e) {
|
|
11180
|
-
if (!activeDrag) return;
|
|
11181
|
-
const dx = e.clientX - startX;
|
|
11182
|
-
const dy = e.clientY - startY;
|
|
11183
|
-
let nl = origL + dx;
|
|
11184
|
-
let nt = origT + dy;
|
|
11185
|
-
const r = container.getBoundingClientRect();
|
|
11186
|
-
const w = r.width;
|
|
11187
|
-
const ph = r.height;
|
|
11188
|
-
const vw = window.innerWidth;
|
|
11189
|
-
const vh = window.innerHeight;
|
|
11190
|
-
const pad = 8;
|
|
11191
|
-
nl = Math.max(pad, Math.min(nl, vw - w - pad));
|
|
11192
|
-
nt = Math.max(pad, Math.min(nt, vh - ph - pad));
|
|
11193
|
-
self._setCustomPositionFromRect(container, nl, nt);
|
|
11194
|
-
};
|
|
11195
|
-
const onUp = function(e) {
|
|
11196
|
-
if (!activeDrag) return;
|
|
11197
|
-
activeDrag = false;
|
|
11198
|
-
if (typeof h.hasPointerCapture === "function" && h.hasPointerCapture(e.pointerId)) {
|
|
11199
|
-
try {
|
|
11200
|
-
h.releasePointerCapture(e.pointerId);
|
|
11201
|
-
} catch (_e) {
|
|
11202
|
-
}
|
|
11203
|
-
}
|
|
11204
|
-
if (inst.state.persistPosition) {
|
|
11205
|
-
const r = container.getBoundingClientRect();
|
|
11206
|
-
self._savePositionPixels(inst, r.left, r.top);
|
|
11207
|
-
}
|
|
11208
|
-
};
|
|
11209
|
-
h.addEventListener("pointerdown", onDown);
|
|
11210
|
-
h.addEventListener("pointermove", onMove);
|
|
11211
|
-
h.addEventListener("pointerup", onUp);
|
|
11212
|
-
h.addEventListener("pointercancel", onUp);
|
|
11213
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11214
|
-
inst.ui.unbindDrag = function() {
|
|
11215
|
-
h.removeEventListener("pointerdown", onDown);
|
|
11216
|
-
h.removeEventListener("pointermove", onMove);
|
|
11217
|
-
h.removeEventListener("pointerup", onUp);
|
|
11218
|
-
h.removeEventListener("pointercancel", onUp);
|
|
11219
|
-
};
|
|
11220
|
-
},
|
|
11221
|
-
/**
|
|
11222
|
-
* Return a shallow copy of the current player state.
|
|
11223
|
-
* @param {HTMLElement} container
|
|
11224
|
-
* @returns {Object|null}
|
|
11225
|
-
*/
|
|
11226
|
-
getState: function(container) {
|
|
11227
|
-
const inst = this.instances.get(container);
|
|
11228
|
-
if (!inst) return null;
|
|
11229
|
-
const s = inst.state;
|
|
11230
|
-
return {
|
|
11231
|
-
isPlaying: s.isPlaying,
|
|
11232
|
-
currentIndex: s.currentIndex,
|
|
11233
|
-
currentTrack: s.tracks[s.currentIndex] || null,
|
|
11234
|
-
volume: s.volume,
|
|
11235
|
-
shuffle: s.shuffle,
|
|
11236
|
-
tracks: s.tracks.slice(),
|
|
11237
|
-
isDetached: Boolean(s.isDetached),
|
|
11238
|
-
isMinimized: Boolean(s.isMinimized)
|
|
11239
|
-
};
|
|
11240
|
-
},
|
|
11241
|
-
/**
|
|
11242
|
-
* Stop playback, clean up listeners, remove instance.
|
|
11243
|
-
* @param {HTMLElement} container
|
|
11244
|
-
*/
|
|
11245
|
-
destroy: function(container) {
|
|
11246
|
-
const inst = this.instances.get(container);
|
|
11247
|
-
if (!inst) return;
|
|
11248
|
-
this._unbindFloatingDrag(inst);
|
|
11249
|
-
if (inst.state && inst.state.isDetached) {
|
|
11250
|
-
try {
|
|
11251
|
-
this.attach(container);
|
|
11252
|
-
} catch (_e) {
|
|
11253
|
-
}
|
|
11254
|
-
}
|
|
11255
|
-
inst.cleanup.forEach((fn) => fn());
|
|
11256
|
-
this.instances.delete(container);
|
|
11257
|
-
container.removeAttribute("data-music-player-initialized");
|
|
11258
|
-
},
|
|
11259
|
-
/**
|
|
11260
|
-
* Destroy all instances.
|
|
11261
|
-
*/
|
|
11262
|
-
destroyAll: function() {
|
|
11263
|
-
this.instances.forEach((_, container) => this.destroy(container));
|
|
11264
|
-
},
|
|
11265
|
-
/* ─── Internal helpers ────────────────────────────────── */
|
|
11266
|
-
/**
|
|
11267
|
-
* Load track by index on an already-initialised instance object.
|
|
11268
|
-
* @param {Object} inst
|
|
11269
|
-
* @param {number} index
|
|
11270
|
-
* @param {boolean} autoPlay
|
|
11271
|
-
*/
|
|
11272
|
-
_loadTrack: function(inst, index, autoPlay) {
|
|
11273
|
-
const track = inst.state.tracks[index];
|
|
11274
|
-
if (!track) return;
|
|
11275
|
-
const container = this._containerOf(inst);
|
|
11276
|
-
inst.state.currentIndex = index;
|
|
11277
|
-
inst.audio.src = track.url;
|
|
11278
|
-
if (inst.refs.trackName) {
|
|
11279
|
-
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
11280
|
-
inst.refs.trackName.classList.remove("is-idle");
|
|
11281
|
-
}
|
|
11282
|
-
if (inst.refs.playlistPanel) {
|
|
11283
|
-
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
11284
|
-
const active = i === index;
|
|
11285
|
-
item.classList.toggle("is-active", active);
|
|
11286
|
-
item.setAttribute("aria-current", active ? "true" : "false");
|
|
11287
|
-
});
|
|
11288
|
-
}
|
|
11289
|
-
if (inst.refs.progressBar) {
|
|
11290
|
-
inst.refs.progressBar.value = "0";
|
|
11291
|
-
updateRangeFill(inst.refs.progressBar);
|
|
11292
|
-
}
|
|
11293
|
-
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
11294
|
-
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
11295
|
-
if (container) {
|
|
11296
|
-
container.dispatchEvent(
|
|
11297
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
11298
|
-
bubbles: true,
|
|
11299
|
-
detail: { index, name: track.name, url: track.url }
|
|
11300
|
-
})
|
|
11301
|
-
);
|
|
11302
|
-
}
|
|
11303
|
-
if (autoPlay) inst.audio.play().catch(() => {
|
|
11304
|
-
});
|
|
11305
|
-
},
|
|
11306
|
-
/**
|
|
11307
|
-
* Reverse-lookup the container element for a given instance object.
|
|
11308
|
-
* @param {Object} inst
|
|
11309
|
-
* @returns {HTMLElement|null}
|
|
11310
|
-
*/
|
|
11311
|
-
_containerOf: function(inst) {
|
|
11312
|
-
for (const [container, i] of this.instances) {
|
|
11313
|
-
if (i === inst) return container;
|
|
11314
|
-
}
|
|
11315
|
-
return null;
|
|
11316
|
-
}
|
|
11317
|
-
};
|
|
11318
|
-
if (typeof window.Vanduo !== "undefined") {
|
|
11319
|
-
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
11320
|
-
}
|
|
11321
|
-
window.VanduoMusicPlayer = MusicPlayer;
|
|
11322
|
-
})();
|
|
11323
|
-
|
|
11324
10400
|
// js/index.js
|
|
11325
10401
|
var Vanduo = window.Vanduo;
|
|
11326
10402
|
var index_default = Vanduo;
|