@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.cjs.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
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -201,7 +201,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
201
201
|
// js/vanduo.js
|
|
202
202
|
(function() {
|
|
203
203
|
"use strict";
|
|
204
|
-
const VANDUO_VERSION = true ? "1.4.
|
|
204
|
+
const VANDUO_VERSION = true ? "1.4.3" : "0.0.0-dev";
|
|
205
205
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
206
206
|
const Vanduo2 = {
|
|
207
207
|
version: VANDUO_VERSION,
|
|
@@ -874,12 +874,38 @@ module.exports = __toCommonJS(index_exports);
|
|
|
874
874
|
* @returns {string} HTML with syntax highlighting spans
|
|
875
875
|
*/
|
|
876
876
|
highlightHtml: function(html) {
|
|
877
|
-
html = html.replace(/(<\/?)([\w-]+)/g, '$1<span class="code-tag">$2</span>');
|
|
878
|
-
html = html.replace(/([\w-]+)(=)(["'])/g, '<span class="code-attr">$1</span>$2$3');
|
|
879
|
-
html = html.replace(/([\w-]+)(=)("|')/g, '<span class="code-attr">$1</span>$2$3');
|
|
880
|
-
html = html.replace(/(["'])([^"']*)(["'])/g, '$1<span class="code-string">$2</span>$3');
|
|
881
|
-
html = html.replace(/("|')([^&]*)("|')/g, '$1<span class="code-string">$2</span>$3');
|
|
882
877
|
html = html.replace(/(<!--)(.*?)(-->)/g, '<span class="code-comment">$1$2$3</span>');
|
|
878
|
+
html = html.replace(/<(?!--)([\s\S]*?)>/g, function(match, inner) {
|
|
879
|
+
const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
|
|
880
|
+
if (closingMatch) {
|
|
881
|
+
return "<" + closingMatch[1] + '<span class="code-tag">' + closingMatch[2] + "</span>>";
|
|
882
|
+
}
|
|
883
|
+
const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
|
|
884
|
+
const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : "";
|
|
885
|
+
const tagSource = selfClosingMatch ? inner.slice(0, inner.length - selfClosingMatch[0].length) : inner;
|
|
886
|
+
const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
|
|
887
|
+
if (!openMatch) {
|
|
888
|
+
return match;
|
|
889
|
+
}
|
|
890
|
+
const tagName = openMatch[1];
|
|
891
|
+
let attrSource = openMatch[2];
|
|
892
|
+
attrSource = attrSource.replace(
|
|
893
|
+
/([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
|
|
894
|
+
function(attrMatch, attrName, separator, quote, value) {
|
|
895
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + quote + '<span class="code-string">' + value + "</span>" + quote;
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
attrSource = attrSource.replace(
|
|
899
|
+
/([\w:-]+)(\s*=\s*)("|')([\s\S]*?)("|')/g,
|
|
900
|
+
function(attrMatch, attrName, separator, openQuote, value, closeQuote) {
|
|
901
|
+
if (openQuote !== closeQuote) {
|
|
902
|
+
return attrMatch;
|
|
903
|
+
}
|
|
904
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + openQuote + '<span class="code-string">' + value + "</span>" + closeQuote;
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
return '<<span class="code-tag">' + tagName + "</span>" + attrSource + selfClosingSuffix + ">";
|
|
908
|
+
});
|
|
883
909
|
return html;
|
|
884
910
|
},
|
|
885
911
|
/**
|
|
@@ -901,16 +927,26 @@ module.exports = __toCommonJS(index_exports);
|
|
|
901
927
|
* @returns {string} JS with syntax highlighting spans
|
|
902
928
|
*/
|
|
903
929
|
highlightJs: function(js) {
|
|
930
|
+
const protectedTokens = [];
|
|
931
|
+
const protectToken = (value, className) => {
|
|
932
|
+
const marker = String.fromCharCode(57344 + protectedTokens.length);
|
|
933
|
+
protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + "</span>" });
|
|
934
|
+
return marker;
|
|
935
|
+
};
|
|
936
|
+
js = js.replace(
|
|
937
|
+
/\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
|
|
938
|
+
(match) => protectToken(match, match.startsWith("/") ? "code-comment" : "code-string")
|
|
939
|
+
);
|
|
904
940
|
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"];
|
|
905
941
|
keywords.forEach((kw) => {
|
|
906
942
|
const regex = new RegExp(`\\b(${kw})\\b`, "g");
|
|
907
943
|
js = js.replace(regex, '<span class="code-keyword">$1</span>');
|
|
908
944
|
});
|
|
909
|
-
js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
|
|
910
945
|
js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
|
|
911
946
|
js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
|
|
912
|
-
|
|
913
|
-
|
|
947
|
+
protectedTokens.forEach((token) => {
|
|
948
|
+
js = js.replace(token.marker, token.html);
|
|
949
|
+
});
|
|
914
950
|
return js;
|
|
915
951
|
},
|
|
916
952
|
/**
|
|
@@ -8456,11 +8492,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8456
8492
|
// js/components/suggest.js
|
|
8457
8493
|
(function() {
|
|
8458
8494
|
"use strict";
|
|
8459
|
-
function _escapeHtml(text) {
|
|
8460
|
-
const div = document.createElement("div");
|
|
8461
|
-
div.textContent = text;
|
|
8462
|
-
return div.innerHTML;
|
|
8463
|
-
}
|
|
8464
8495
|
function _isSafeUrl(url, allowlist) {
|
|
8465
8496
|
try {
|
|
8466
8497
|
const resolved = new URL(url, window.location.href);
|
|
@@ -8531,9 +8562,24 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8531
8562
|
li.id = listId + "-item-" + i;
|
|
8532
8563
|
const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
|
|
8533
8564
|
if (query) {
|
|
8534
|
-
const
|
|
8535
|
-
const
|
|
8536
|
-
|
|
8565
|
+
const lowerText = text.toLowerCase();
|
|
8566
|
+
const lowerQuery = query.toLowerCase();
|
|
8567
|
+
let start = 0;
|
|
8568
|
+
let matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8569
|
+
while (matchIndex !== -1) {
|
|
8570
|
+
if (matchIndex > start) {
|
|
8571
|
+
li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
|
|
8572
|
+
}
|
|
8573
|
+
const matchSpan = document.createElement("span");
|
|
8574
|
+
matchSpan.className = "vd-suggest-match";
|
|
8575
|
+
matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
|
|
8576
|
+
li.appendChild(matchSpan);
|
|
8577
|
+
start = matchIndex + query.length;
|
|
8578
|
+
matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8579
|
+
}
|
|
8580
|
+
if (start < text.length) {
|
|
8581
|
+
li.appendChild(document.createTextNode(text.slice(start)));
|
|
8582
|
+
}
|
|
8537
8583
|
} else {
|
|
8538
8584
|
li.textContent = text;
|
|
8539
8585
|
}
|
|
@@ -8959,6 +9005,27 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8959
9005
|
x.setDate(x.getDate() + (6 - day));
|
|
8960
9006
|
return x;
|
|
8961
9007
|
}
|
|
9008
|
+
function positionAnchoredPopup(anchor, popup, gap) {
|
|
9009
|
+
const padding = 8;
|
|
9010
|
+
const offset = gap != null ? gap : 4;
|
|
9011
|
+
const rect = anchor.getBoundingClientRect();
|
|
9012
|
+
popup.style.minWidth = Math.max(rect.width, 0) + "px";
|
|
9013
|
+
let top = rect.bottom + offset;
|
|
9014
|
+
let left = rect.left;
|
|
9015
|
+
popup.style.top = top + "px";
|
|
9016
|
+
popup.style.left = left + "px";
|
|
9017
|
+
const popRect = popup.getBoundingClientRect();
|
|
9018
|
+
if (popRect.bottom > window.innerHeight - padding && rect.top - popRect.height > padding) {
|
|
9019
|
+
top = rect.top - popRect.height - offset;
|
|
9020
|
+
popup.style.top = top + "px";
|
|
9021
|
+
}
|
|
9022
|
+
const alignedRect = popup.getBoundingClientRect();
|
|
9023
|
+
left = rect.left;
|
|
9024
|
+
if (left + alignedRect.width > window.innerWidth - padding) {
|
|
9025
|
+
left = window.innerWidth - alignedRect.width - padding;
|
|
9026
|
+
}
|
|
9027
|
+
popup.style.left = Math.max(padding, left) + "px";
|
|
9028
|
+
}
|
|
8962
9029
|
const Datepicker = {
|
|
8963
9030
|
instances: /* @__PURE__ */ new Map(),
|
|
8964
9031
|
init: function(root) {
|
|
@@ -8982,6 +9049,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8982
9049
|
let viewMode = "days";
|
|
8983
9050
|
let focusedDate = null;
|
|
8984
9051
|
let skipNextFocusOpen = false;
|
|
9052
|
+
let ignoreOutsideUntil = 0;
|
|
8985
9053
|
const isDisabled = (d) => {
|
|
8986
9054
|
if (minDate) {
|
|
8987
9055
|
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
@@ -9040,7 +9108,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9040
9108
|
wrapper.style.display = "inline-block";
|
|
9041
9109
|
input.parentNode.insertBefore(wrapper, input);
|
|
9042
9110
|
wrapper.appendChild(input);
|
|
9043
|
-
|
|
9111
|
+
document.body.appendChild(popup);
|
|
9044
9112
|
const isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
9045
9113
|
const selectDate = (date) => {
|
|
9046
9114
|
selectedDate = date;
|
|
@@ -9270,6 +9338,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9270
9338
|
}
|
|
9271
9339
|
popup.appendChild(grid);
|
|
9272
9340
|
}
|
|
9341
|
+
if (popup.classList.contains("is-open")) {
|
|
9342
|
+
requestAnimationFrame(positionPopup);
|
|
9343
|
+
}
|
|
9273
9344
|
};
|
|
9274
9345
|
const handleGridKeydown = (e) => {
|
|
9275
9346
|
if (!popup.classList.contains("is-open") || viewMode !== "days") return;
|
|
@@ -9337,7 +9408,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9337
9408
|
render();
|
|
9338
9409
|
requestAnimationFrame(focusFocusedDay);
|
|
9339
9410
|
};
|
|
9411
|
+
const positionPopup = () => {
|
|
9412
|
+
if (!popup.classList.contains("is-open")) return;
|
|
9413
|
+
positionAnchoredPopup(input, popup);
|
|
9414
|
+
};
|
|
9415
|
+
const repositionHandler = () => {
|
|
9416
|
+
positionPopup();
|
|
9417
|
+
};
|
|
9418
|
+
const markIgnoreOutside = () => {
|
|
9419
|
+
ignoreOutsideUntil = Date.now() + 100;
|
|
9420
|
+
};
|
|
9340
9421
|
const open = () => {
|
|
9422
|
+
markIgnoreOutside();
|
|
9341
9423
|
viewMode = "days";
|
|
9342
9424
|
if (selectedDate) {
|
|
9343
9425
|
viewYear = selectedDate.getFullYear();
|
|
@@ -9354,7 +9436,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9354
9436
|
render();
|
|
9355
9437
|
popup.classList.add("is-open");
|
|
9356
9438
|
input.setAttribute("aria-expanded", "true");
|
|
9357
|
-
requestAnimationFrame(
|
|
9439
|
+
requestAnimationFrame(() => {
|
|
9440
|
+
positionPopup();
|
|
9441
|
+
focusFocusedDay();
|
|
9442
|
+
});
|
|
9358
9443
|
};
|
|
9359
9444
|
const close = () => {
|
|
9360
9445
|
popup.classList.remove("is-open");
|
|
@@ -9368,8 +9453,22 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9368
9453
|
}
|
|
9369
9454
|
open();
|
|
9370
9455
|
};
|
|
9456
|
+
const clickHandler = () => {
|
|
9457
|
+
if (!popup.classList.contains("is-open")) open();
|
|
9458
|
+
};
|
|
9459
|
+
const isOpenAnchorTarget = (target) => {
|
|
9460
|
+
if (!target || !(target instanceof Node)) return false;
|
|
9461
|
+
if (target === input || input.contains(target) || popup.contains(target)) return true;
|
|
9462
|
+
const inputId = input.id;
|
|
9463
|
+
if (inputId) {
|
|
9464
|
+
const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
|
|
9465
|
+
if (label && (target === label || label.contains(target))) return true;
|
|
9466
|
+
}
|
|
9467
|
+
return false;
|
|
9468
|
+
};
|
|
9371
9469
|
const outsideHandler = (e) => {
|
|
9372
|
-
if (
|
|
9470
|
+
if (Date.now() < ignoreOutsideUntil) return;
|
|
9471
|
+
if (!isOpenAnchorTarget(e.target)) close();
|
|
9373
9472
|
};
|
|
9374
9473
|
const escHandler = (e) => {
|
|
9375
9474
|
if (e.key === "Escape" && popup.classList.contains("is-open")) {
|
|
@@ -9379,17 +9478,24 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9379
9478
|
}
|
|
9380
9479
|
};
|
|
9381
9480
|
input.addEventListener("focus", focusHandler);
|
|
9481
|
+
input.addEventListener("click", clickHandler);
|
|
9382
9482
|
document.addEventListener("click", outsideHandler, true);
|
|
9383
9483
|
document.addEventListener("keydown", escHandler);
|
|
9384
9484
|
popup.addEventListener("keydown", handleGridKeydown);
|
|
9485
|
+
window.addEventListener("resize", repositionHandler);
|
|
9486
|
+
window.addEventListener("scroll", repositionHandler, true);
|
|
9385
9487
|
input.setAttribute("aria-haspopup", "dialog");
|
|
9386
9488
|
input.setAttribute("aria-expanded", "false");
|
|
9387
9489
|
input.setAttribute("autocomplete", "off");
|
|
9388
9490
|
cleanup.push(
|
|
9389
9491
|
() => input.removeEventListener("focus", focusHandler),
|
|
9492
|
+
() => input.removeEventListener("click", clickHandler),
|
|
9390
9493
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9391
9494
|
() => document.removeEventListener("keydown", escHandler),
|
|
9392
|
-
() => popup.removeEventListener("keydown", handleGridKeydown)
|
|
9495
|
+
() => popup.removeEventListener("keydown", handleGridKeydown),
|
|
9496
|
+
() => window.removeEventListener("resize", repositionHandler),
|
|
9497
|
+
() => window.removeEventListener("scroll", repositionHandler, true),
|
|
9498
|
+
() => popup.remove()
|
|
9393
9499
|
);
|
|
9394
9500
|
this.instances.set(input, { cleanup, open, close, popup });
|
|
9395
9501
|
},
|
|
@@ -9412,6 +9518,27 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9412
9518
|
// js/components/timepicker.js
|
|
9413
9519
|
(function() {
|
|
9414
9520
|
"use strict";
|
|
9521
|
+
function positionAnchoredPopup(anchor, popup, gap) {
|
|
9522
|
+
const padding = 8;
|
|
9523
|
+
const offset = gap != null ? gap : 4;
|
|
9524
|
+
const rect = anchor.getBoundingClientRect();
|
|
9525
|
+
popup.style.minWidth = Math.max(rect.width, 0) + "px";
|
|
9526
|
+
let top = rect.bottom + offset;
|
|
9527
|
+
let left = rect.left;
|
|
9528
|
+
popup.style.top = top + "px";
|
|
9529
|
+
popup.style.left = left + "px";
|
|
9530
|
+
const popRect = popup.getBoundingClientRect();
|
|
9531
|
+
if (popRect.bottom > window.innerHeight - padding && rect.top - popRect.height > padding) {
|
|
9532
|
+
top = rect.top - popRect.height - offset;
|
|
9533
|
+
popup.style.top = top + "px";
|
|
9534
|
+
}
|
|
9535
|
+
const alignedRect = popup.getBoundingClientRect();
|
|
9536
|
+
left = rect.left;
|
|
9537
|
+
if (left + alignedRect.width > window.innerWidth - padding) {
|
|
9538
|
+
left = window.innerWidth - alignedRect.width - padding;
|
|
9539
|
+
}
|
|
9540
|
+
popup.style.left = Math.max(padding, left) + "px";
|
|
9541
|
+
}
|
|
9415
9542
|
const Timepicker = {
|
|
9416
9543
|
instances: /* @__PURE__ */ new Map(),
|
|
9417
9544
|
init: function(root) {
|
|
@@ -9436,7 +9563,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9436
9563
|
const popup = document.createElement("div");
|
|
9437
9564
|
popup.className = "vd-timepicker-popup";
|
|
9438
9565
|
popup.setAttribute("role", "listbox");
|
|
9439
|
-
|
|
9566
|
+
document.body.appendChild(popup);
|
|
9440
9567
|
const times = [];
|
|
9441
9568
|
for (let h = 0; h < 24; h++) {
|
|
9442
9569
|
for (let m = 0; m < 60; m += step) {
|
|
@@ -9476,12 +9603,22 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9476
9603
|
popup.appendChild(item);
|
|
9477
9604
|
});
|
|
9478
9605
|
};
|
|
9606
|
+
const positionPopup = () => {
|
|
9607
|
+
if (!popup.classList.contains("is-open")) return;
|
|
9608
|
+
positionAnchoredPopup(input, popup);
|
|
9609
|
+
};
|
|
9610
|
+
const repositionHandler = () => {
|
|
9611
|
+
positionPopup();
|
|
9612
|
+
};
|
|
9479
9613
|
const open = () => {
|
|
9480
9614
|
render();
|
|
9481
9615
|
popup.classList.add("is-open");
|
|
9482
9616
|
input.setAttribute("aria-expanded", "true");
|
|
9483
|
-
|
|
9484
|
-
|
|
9617
|
+
requestAnimationFrame(() => {
|
|
9618
|
+
positionPopup();
|
|
9619
|
+
const selected = popup.querySelector(".is-selected");
|
|
9620
|
+
if (selected) selected.scrollIntoView({ block: "center" });
|
|
9621
|
+
});
|
|
9485
9622
|
};
|
|
9486
9623
|
const close = () => {
|
|
9487
9624
|
popup.classList.remove("is-open");
|
|
@@ -9489,7 +9626,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9489
9626
|
};
|
|
9490
9627
|
const focusHandler = () => open();
|
|
9491
9628
|
const outsideHandler = (e) => {
|
|
9492
|
-
if (!
|
|
9629
|
+
if (!input.contains(e.target) && !popup.contains(e.target)) close();
|
|
9493
9630
|
};
|
|
9494
9631
|
const escHandler = (e) => {
|
|
9495
9632
|
if (e.key === "Escape") close();
|
|
@@ -9497,6 +9634,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9497
9634
|
input.addEventListener("focus", focusHandler);
|
|
9498
9635
|
document.addEventListener("click", outsideHandler, true);
|
|
9499
9636
|
document.addEventListener("keydown", escHandler);
|
|
9637
|
+
window.addEventListener("resize", repositionHandler);
|
|
9638
|
+
window.addEventListener("scroll", repositionHandler, true);
|
|
9500
9639
|
input.setAttribute("aria-haspopup", "listbox");
|
|
9501
9640
|
input.setAttribute("aria-expanded", "false");
|
|
9502
9641
|
input.setAttribute("autocomplete", "off");
|
|
@@ -9504,7 +9643,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9504
9643
|
cleanup.push(
|
|
9505
9644
|
() => input.removeEventListener("focus", focusHandler),
|
|
9506
9645
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9507
|
-
() => document.removeEventListener("keydown", escHandler)
|
|
9646
|
+
() => document.removeEventListener("keydown", escHandler),
|
|
9647
|
+
() => window.removeEventListener("resize", repositionHandler),
|
|
9648
|
+
() => window.removeEventListener("scroll", repositionHandler, true),
|
|
9649
|
+
() => popup.remove()
|
|
9508
9650
|
);
|
|
9509
9651
|
this.instances.set(input, { cleanup, open, close });
|
|
9510
9652
|
},
|
|
@@ -10280,1072 +10422,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
10280
10422
|
window.VanduoSpotlight = Spotlight;
|
|
10281
10423
|
})();
|
|
10282
10424
|
|
|
10283
|
-
// js/components/music-player.js
|
|
10284
|
-
(function() {
|
|
10285
|
-
"use strict";
|
|
10286
|
-
function shuffleArray(arr) {
|
|
10287
|
-
const shuffled = arr.slice();
|
|
10288
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
10289
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
10290
|
-
const tmp = shuffled[i];
|
|
10291
|
-
shuffled[i] = shuffled[j];
|
|
10292
|
-
shuffled[j] = tmp;
|
|
10293
|
-
}
|
|
10294
|
-
return shuffled;
|
|
10295
|
-
}
|
|
10296
|
-
function formatTime(seconds) {
|
|
10297
|
-
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
10298
|
-
const m = Math.floor(seconds / 60);
|
|
10299
|
-
const s = Math.floor(seconds % 60);
|
|
10300
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
10301
|
-
}
|
|
10302
|
-
function persistStorageKey(id) {
|
|
10303
|
-
return "vanduo:music-player:" + (id && id.trim() ? id.trim() : "default") + ":pos";
|
|
10304
|
-
}
|
|
10305
|
-
function updateRangeFill(input) {
|
|
10306
|
-
const min = parseFloat(input.min) || 0;
|
|
10307
|
-
const max = parseFloat(input.max) || 1;
|
|
10308
|
-
const val = parseFloat(input.value) || 0;
|
|
10309
|
-
const pct = (val - min) / (max - min) * 100;
|
|
10310
|
-
input.style.setProperty("--vd-fill", pct + "%");
|
|
10311
|
-
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%)";
|
|
10312
|
-
}
|
|
10313
|
-
function icon(name) {
|
|
10314
|
-
const el = document.createElement("i");
|
|
10315
|
-
el.className = "ph ph-" + name;
|
|
10316
|
-
el.setAttribute("aria-hidden", "true");
|
|
10317
|
-
return el;
|
|
10318
|
-
}
|
|
10319
|
-
const MusicPlayer = {
|
|
10320
|
-
/** @type {Map<HTMLElement, Object>} */
|
|
10321
|
-
instances: /* @__PURE__ */ new Map(),
|
|
10322
|
-
/**
|
|
10323
|
-
* Default options.
|
|
10324
|
-
*/
|
|
10325
|
-
defaults: {
|
|
10326
|
-
tracks: [],
|
|
10327
|
-
volume: 0.5,
|
|
10328
|
-
shuffle: false,
|
|
10329
|
-
showProgress: false,
|
|
10330
|
-
showPlaylist: false,
|
|
10331
|
-
autoAdvance: true,
|
|
10332
|
-
glass: false,
|
|
10333
|
-
detachable: false,
|
|
10334
|
-
/** @type {null|string} */
|
|
10335
|
-
floatingPosition: null,
|
|
10336
|
-
draggable: false,
|
|
10337
|
-
minimizable: false,
|
|
10338
|
-
startMinimized: false,
|
|
10339
|
-
persistPosition: false,
|
|
10340
|
-
persistKey: ""
|
|
10341
|
-
},
|
|
10342
|
-
/**
|
|
10343
|
-
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
10344
|
-
* Options can be provided via data-music-player-options (JSON string).
|
|
10345
|
-
*/
|
|
10346
|
-
init: function(root) {
|
|
10347
|
-
window.Vanduo.queryAll(root, ".vd-music-player, [data-music-player]").forEach((el) => {
|
|
10348
|
-
if (this.instances.has(el)) return;
|
|
10349
|
-
let opts = {};
|
|
10350
|
-
const attr = el.getAttribute("data-music-player-options");
|
|
10351
|
-
if (attr) {
|
|
10352
|
-
try {
|
|
10353
|
-
opts = JSON.parse(attr);
|
|
10354
|
-
} catch (_) {
|
|
10355
|
-
}
|
|
10356
|
-
}
|
|
10357
|
-
this.initPlayer(el, opts);
|
|
10358
|
-
});
|
|
10359
|
-
},
|
|
10360
|
-
/**
|
|
10361
|
-
* Initialize a single player element.
|
|
10362
|
-
* @param {HTMLElement} container
|
|
10363
|
-
* @param {Object} [options]
|
|
10364
|
-
*/
|
|
10365
|
-
initPlayer: function(container, options) {
|
|
10366
|
-
const opts = Object.assign({}, this.defaults, options || {});
|
|
10367
|
-
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
10368
|
-
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
10369
|
-
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
10370
|
-
const state = {
|
|
10371
|
-
tracks: trackList,
|
|
10372
|
-
originalTracks: tracks.slice(),
|
|
10373
|
-
currentIndex: 0,
|
|
10374
|
-
isPlaying: false,
|
|
10375
|
-
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
10376
|
-
shuffle: opts.shuffle,
|
|
10377
|
-
showProgress: opts.showProgress,
|
|
10378
|
-
showPlaylist: opts.showPlaylist,
|
|
10379
|
-
autoAdvance: opts.autoAdvance,
|
|
10380
|
-
audio: null,
|
|
10381
|
-
glass: Boolean(opts.glass),
|
|
10382
|
-
detachable: Boolean(opts.detachable),
|
|
10383
|
-
floatingPosition: opts.floatingPosition || "bottom-right",
|
|
10384
|
-
draggable: Boolean(opts.draggable) && Boolean(opts.detachable),
|
|
10385
|
-
minimizable: Boolean(opts.minimizable),
|
|
10386
|
-
startMinimized: Boolean(opts.startMinimized),
|
|
10387
|
-
persistPosition: Boolean(opts.persistPosition),
|
|
10388
|
-
persistKey: typeof opts.persistKey === "string" ? opts.persistKey : "",
|
|
10389
|
-
isDetached: false,
|
|
10390
|
-
isMinimized: false,
|
|
10391
|
-
_startMinimizeApplied: false
|
|
10392
|
-
};
|
|
10393
|
-
const audio = new Audio();
|
|
10394
|
-
audio.volume = state.volume;
|
|
10395
|
-
audio.preload = "metadata";
|
|
10396
|
-
state.audio = audio;
|
|
10397
|
-
this._buildDOM(container, state);
|
|
10398
|
-
const refs = {
|
|
10399
|
-
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
10400
|
-
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
10401
|
-
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
10402
|
-
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
10403
|
-
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
10404
|
-
btnDetach: container.querySelector(".vd-music-player-btn-detach"),
|
|
10405
|
-
btnAttach: container.querySelector(".vd-music-player-btn-attach"),
|
|
10406
|
-
btnMinimize: container.querySelector(".vd-music-player-btn-minimize"),
|
|
10407
|
-
dragHandle: container.querySelector(".vd-music-player-drag-handle"),
|
|
10408
|
-
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
10409
|
-
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
10410
|
-
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
10411
|
-
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
10412
|
-
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
10413
|
-
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
10414
|
-
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
10415
|
-
};
|
|
10416
|
-
const renderPlayIcon = () => {
|
|
10417
|
-
const btn = refs.btnPlay;
|
|
10418
|
-
if (!btn) return;
|
|
10419
|
-
btn.innerHTML = "";
|
|
10420
|
-
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
10421
|
-
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
10422
|
-
btn.classList.toggle("is-active", state.isPlaying);
|
|
10423
|
-
};
|
|
10424
|
-
const renderTrackName = () => {
|
|
10425
|
-
const el = refs.trackName;
|
|
10426
|
-
if (!el) return;
|
|
10427
|
-
const track = state.tracks[state.currentIndex];
|
|
10428
|
-
if (track) {
|
|
10429
|
-
el.textContent = track.name || "Unknown Track";
|
|
10430
|
-
el.classList.remove("is-idle");
|
|
10431
|
-
} else {
|
|
10432
|
-
el.textContent = "No tracks loaded";
|
|
10433
|
-
el.classList.add("is-idle");
|
|
10434
|
-
}
|
|
10435
|
-
};
|
|
10436
|
-
const renderVolumeIcon = () => {
|
|
10437
|
-
const el = refs.volumeIcon;
|
|
10438
|
-
if (!el) return;
|
|
10439
|
-
el.innerHTML = "";
|
|
10440
|
-
const v = state.volume;
|
|
10441
|
-
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
10442
|
-
el.appendChild(icon(name));
|
|
10443
|
-
};
|
|
10444
|
-
const renderShuffleBtn = () => {
|
|
10445
|
-
const btn = refs.btnShuffle;
|
|
10446
|
-
if (!btn) return;
|
|
10447
|
-
btn.classList.toggle("is-active", state.shuffle);
|
|
10448
|
-
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10449
|
-
};
|
|
10450
|
-
const renderPlaylistItems = () => {
|
|
10451
|
-
const panel = refs.playlistPanel;
|
|
10452
|
-
if (!panel) return;
|
|
10453
|
-
panel.innerHTML = "";
|
|
10454
|
-
state.tracks.forEach((track, i) => {
|
|
10455
|
-
const item = document.createElement("button");
|
|
10456
|
-
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
10457
|
-
item.type = "button";
|
|
10458
|
-
item.setAttribute("data-index", String(i));
|
|
10459
|
-
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
10460
|
-
const num = document.createElement("span");
|
|
10461
|
-
num.className = "vd-music-player-playlist-num";
|
|
10462
|
-
num.textContent = String(i + 1);
|
|
10463
|
-
const name = document.createElement("span");
|
|
10464
|
-
name.className = "vd-music-player-playlist-name";
|
|
10465
|
-
name.textContent = track.name || "Track " + (i + 1);
|
|
10466
|
-
item.appendChild(num);
|
|
10467
|
-
item.appendChild(name);
|
|
10468
|
-
panel.appendChild(item);
|
|
10469
|
-
});
|
|
10470
|
-
};
|
|
10471
|
-
const renderProgress = () => {
|
|
10472
|
-
const bar = refs.progressBar;
|
|
10473
|
-
if (!bar || !audio.duration) return;
|
|
10474
|
-
const pct = audio.currentTime / audio.duration * 100;
|
|
10475
|
-
bar.value = String(pct);
|
|
10476
|
-
updateRangeFill(bar);
|
|
10477
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
10478
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10479
|
-
};
|
|
10480
|
-
const loadTrack = (index, autoPlay) => {
|
|
10481
|
-
const track = state.tracks[index];
|
|
10482
|
-
if (!track) return;
|
|
10483
|
-
state.currentIndex = index;
|
|
10484
|
-
audio.src = track.url;
|
|
10485
|
-
renderTrackName();
|
|
10486
|
-
renderPlaylistItems();
|
|
10487
|
-
if (refs.progressBar) {
|
|
10488
|
-
refs.progressBar.value = "0";
|
|
10489
|
-
updateRangeFill(refs.progressBar);
|
|
10490
|
-
}
|
|
10491
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
10492
|
-
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
10493
|
-
container.dispatchEvent(
|
|
10494
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
10495
|
-
bubbles: true,
|
|
10496
|
-
detail: { index, name: track.name, url: track.url }
|
|
10497
|
-
})
|
|
10498
|
-
);
|
|
10499
|
-
if (autoPlay) {
|
|
10500
|
-
audio.play().catch(() => {
|
|
10501
|
-
});
|
|
10502
|
-
}
|
|
10503
|
-
};
|
|
10504
|
-
const cleanupFunctions = [];
|
|
10505
|
-
const onPlay = () => {
|
|
10506
|
-
state.isPlaying = true;
|
|
10507
|
-
renderPlayIcon();
|
|
10508
|
-
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
10509
|
-
};
|
|
10510
|
-
const onPause = () => {
|
|
10511
|
-
state.isPlaying = false;
|
|
10512
|
-
renderPlayIcon();
|
|
10513
|
-
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
10514
|
-
};
|
|
10515
|
-
const onEnded = () => {
|
|
10516
|
-
if (state.autoAdvance && state.tracks.length > 1) {
|
|
10517
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10518
|
-
loadTrack(next, true);
|
|
10519
|
-
} else {
|
|
10520
|
-
state.isPlaying = false;
|
|
10521
|
-
renderPlayIcon();
|
|
10522
|
-
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
10523
|
-
}
|
|
10524
|
-
};
|
|
10525
|
-
const onTimeUpdate = () => {
|
|
10526
|
-
if (state.showProgress) renderProgress();
|
|
10527
|
-
};
|
|
10528
|
-
const onLoadedMetadata = () => {
|
|
10529
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10530
|
-
if (refs.progressBar) {
|
|
10531
|
-
refs.progressBar.max = "100";
|
|
10532
|
-
updateRangeFill(refs.progressBar);
|
|
10533
|
-
}
|
|
10534
|
-
};
|
|
10535
|
-
audio.addEventListener("play", onPlay);
|
|
10536
|
-
audio.addEventListener("pause", onPause);
|
|
10537
|
-
audio.addEventListener("ended", onEnded);
|
|
10538
|
-
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
10539
|
-
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
10540
|
-
cleanupFunctions.push(() => {
|
|
10541
|
-
audio.removeEventListener("play", onPlay);
|
|
10542
|
-
audio.removeEventListener("pause", onPause);
|
|
10543
|
-
audio.removeEventListener("ended", onEnded);
|
|
10544
|
-
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
10545
|
-
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
10546
|
-
audio.pause();
|
|
10547
|
-
audio.src = "";
|
|
10548
|
-
});
|
|
10549
|
-
if (refs.btnPlay) {
|
|
10550
|
-
const handler = () => {
|
|
10551
|
-
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
10552
|
-
if (state.isPlaying) {
|
|
10553
|
-
audio.pause();
|
|
10554
|
-
} else {
|
|
10555
|
-
audio.play().catch(() => {
|
|
10556
|
-
});
|
|
10557
|
-
}
|
|
10558
|
-
};
|
|
10559
|
-
refs.btnPlay.addEventListener("click", handler);
|
|
10560
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
10561
|
-
const keyHandler = (e) => {
|
|
10562
|
-
if (e.key === " " || e.key === "Enter") {
|
|
10563
|
-
e.preventDefault();
|
|
10564
|
-
handler();
|
|
10565
|
-
}
|
|
10566
|
-
};
|
|
10567
|
-
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
10568
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
10569
|
-
}
|
|
10570
|
-
if (refs.btnPrev) {
|
|
10571
|
-
const handler = () => {
|
|
10572
|
-
if (!state.tracks.length) return;
|
|
10573
|
-
if (audio.currentTime > 3) {
|
|
10574
|
-
audio.currentTime = 0;
|
|
10575
|
-
} else {
|
|
10576
|
-
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
10577
|
-
loadTrack(prev, state.isPlaying);
|
|
10578
|
-
}
|
|
10579
|
-
};
|
|
10580
|
-
refs.btnPrev.addEventListener("click", handler);
|
|
10581
|
-
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
10582
|
-
}
|
|
10583
|
-
if (refs.btnNext) {
|
|
10584
|
-
const handler = () => {
|
|
10585
|
-
if (!state.tracks.length) return;
|
|
10586
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10587
|
-
loadTrack(next, state.isPlaying);
|
|
10588
|
-
};
|
|
10589
|
-
refs.btnNext.addEventListener("click", handler);
|
|
10590
|
-
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
10591
|
-
}
|
|
10592
|
-
if (refs.btnShuffle) {
|
|
10593
|
-
const handler = () => {
|
|
10594
|
-
state.shuffle = !state.shuffle;
|
|
10595
|
-
if (state.shuffle) {
|
|
10596
|
-
const current = state.tracks[state.currentIndex];
|
|
10597
|
-
state.tracks = shuffleArray(state.tracks);
|
|
10598
|
-
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
10599
|
-
if (newIdx > 0) {
|
|
10600
|
-
state.tracks.splice(newIdx, 1);
|
|
10601
|
-
state.tracks.unshift(current);
|
|
10602
|
-
}
|
|
10603
|
-
state.currentIndex = 0;
|
|
10604
|
-
} else {
|
|
10605
|
-
const current = state.tracks[state.currentIndex];
|
|
10606
|
-
state.tracks = state.originalTracks.slice();
|
|
10607
|
-
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
10608
|
-
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
10609
|
-
}
|
|
10610
|
-
renderShuffleBtn();
|
|
10611
|
-
renderPlaylistItems();
|
|
10612
|
-
};
|
|
10613
|
-
refs.btnShuffle.addEventListener("click", handler);
|
|
10614
|
-
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
10615
|
-
}
|
|
10616
|
-
if (refs.btnPlaylist) {
|
|
10617
|
-
const handler = () => {
|
|
10618
|
-
const panel = refs.playlistPanel;
|
|
10619
|
-
if (!panel) return;
|
|
10620
|
-
const isOpen = panel.classList.toggle("is-open");
|
|
10621
|
-
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
10622
|
-
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
10623
|
-
};
|
|
10624
|
-
refs.btnPlaylist.addEventListener("click", handler);
|
|
10625
|
-
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
10626
|
-
}
|
|
10627
|
-
if (refs.volumeSlider) {
|
|
10628
|
-
const handler = (e) => {
|
|
10629
|
-
const v = parseFloat(e.target.value);
|
|
10630
|
-
state.volume = v;
|
|
10631
|
-
audio.volume = v;
|
|
10632
|
-
renderVolumeIcon();
|
|
10633
|
-
updateRangeFill(refs.volumeSlider);
|
|
10634
|
-
container.dispatchEvent(
|
|
10635
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10636
|
-
);
|
|
10637
|
-
};
|
|
10638
|
-
refs.volumeSlider.addEventListener("input", handler);
|
|
10639
|
-
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
10640
|
-
updateRangeFill(refs.volumeSlider);
|
|
10641
|
-
}
|
|
10642
|
-
if (refs.progressBar) {
|
|
10643
|
-
const handler = (e) => {
|
|
10644
|
-
if (!audio.duration) return;
|
|
10645
|
-
const pct = parseFloat(e.target.value);
|
|
10646
|
-
audio.currentTime = pct / 100 * audio.duration;
|
|
10647
|
-
updateRangeFill(refs.progressBar);
|
|
10648
|
-
};
|
|
10649
|
-
refs.progressBar.addEventListener("input", handler);
|
|
10650
|
-
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
10651
|
-
}
|
|
10652
|
-
if (refs.playlistPanel) {
|
|
10653
|
-
const panelHandler = (e) => {
|
|
10654
|
-
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
10655
|
-
if (!item) return;
|
|
10656
|
-
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
10657
|
-
if (!isNaN(idx)) loadTrack(idx, true);
|
|
10658
|
-
};
|
|
10659
|
-
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
10660
|
-
cleanupFunctions.push(
|
|
10661
|
-
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
10662
|
-
);
|
|
10663
|
-
}
|
|
10664
|
-
if (refs.btnDetach) {
|
|
10665
|
-
const h = () => {
|
|
10666
|
-
this.detach(container);
|
|
10667
|
-
};
|
|
10668
|
-
refs.btnDetach.addEventListener("click", h);
|
|
10669
|
-
cleanupFunctions.push(() => refs.btnDetach.removeEventListener("click", h));
|
|
10670
|
-
}
|
|
10671
|
-
if (refs.btnAttach) {
|
|
10672
|
-
const h = () => {
|
|
10673
|
-
this.attach(container);
|
|
10674
|
-
};
|
|
10675
|
-
refs.btnAttach.addEventListener("click", h);
|
|
10676
|
-
cleanupFunctions.push(() => refs.btnAttach.removeEventListener("click", h));
|
|
10677
|
-
}
|
|
10678
|
-
if (refs.btnMinimize) {
|
|
10679
|
-
const h = () => {
|
|
10680
|
-
this.toggleMinimize(container);
|
|
10681
|
-
};
|
|
10682
|
-
refs.btnMinimize.addEventListener("click", h);
|
|
10683
|
-
cleanupFunctions.push(() => refs.btnMinimize.removeEventListener("click", h));
|
|
10684
|
-
}
|
|
10685
|
-
renderPlayIcon();
|
|
10686
|
-
renderTrackName();
|
|
10687
|
-
renderVolumeIcon();
|
|
10688
|
-
if (opts.showPlaylist) renderPlaylistItems();
|
|
10689
|
-
this.instances.set(container, {
|
|
10690
|
-
state,
|
|
10691
|
-
audio,
|
|
10692
|
-
refs,
|
|
10693
|
-
cleanup: cleanupFunctions,
|
|
10694
|
-
ui: { restore: null, unbindDrag: null }
|
|
10695
|
-
});
|
|
10696
|
-
container.setAttribute("data-music-player-initialized", "true");
|
|
10697
|
-
},
|
|
10698
|
-
/* ─── DOM builder ─────────────────────────────────────── */
|
|
10699
|
-
/**
|
|
10700
|
-
* Build the inner DOM structure inside container.
|
|
10701
|
-
* Pre-existing inner content is replaced only if it has no
|
|
10702
|
-
* recognised child elements (allows server-rendered markup).
|
|
10703
|
-
* @param {HTMLElement} container
|
|
10704
|
-
* @param {Object} state
|
|
10705
|
-
*/
|
|
10706
|
-
_buildDOM: function(container, state) {
|
|
10707
|
-
if (container.querySelector(".vd-music-player-controls")) return;
|
|
10708
|
-
container.setAttribute("role", "region");
|
|
10709
|
-
container.setAttribute("aria-label", "Music Player");
|
|
10710
|
-
if (state.showProgress) container.classList.add("has-progress");
|
|
10711
|
-
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
10712
|
-
if (state.glass) container.classList.add("vd-music-player-glass");
|
|
10713
|
-
if (state.draggable) container.classList.add("vd-music-player-draggable");
|
|
10714
|
-
if (state.detachable || state.minimizable) {
|
|
10715
|
-
const tb = document.createElement("div");
|
|
10716
|
-
tb.className = "vd-music-player-toolbar";
|
|
10717
|
-
tb.setAttribute("role", "toolbar");
|
|
10718
|
-
tb.setAttribute("aria-label", "Player window");
|
|
10719
|
-
if (state.draggable) {
|
|
10720
|
-
const h = document.createElement("button");
|
|
10721
|
-
h.type = "button";
|
|
10722
|
-
h.className = "vd-music-player-drag-handle";
|
|
10723
|
-
h.setAttribute("aria-label", "Drag to move player");
|
|
10724
|
-
h.appendChild(icon("dots-six-vertical"));
|
|
10725
|
-
tb.appendChild(h);
|
|
10726
|
-
}
|
|
10727
|
-
const tSp = document.createElement("span");
|
|
10728
|
-
tSp.className = "vd-music-player-toolbar-spacer";
|
|
10729
|
-
tSp.setAttribute("aria-hidden", "true");
|
|
10730
|
-
tb.appendChild(tSp);
|
|
10731
|
-
if (state.minimizable) {
|
|
10732
|
-
const bMin = document.createElement("button");
|
|
10733
|
-
bMin.type = "button";
|
|
10734
|
-
bMin.className = "vd-music-player-btn vd-music-player-btn-minimize";
|
|
10735
|
-
bMin.setAttribute("aria-label", "Minimize player");
|
|
10736
|
-
bMin.setAttribute("aria-expanded", "true");
|
|
10737
|
-
bMin.appendChild(icon("minus"));
|
|
10738
|
-
tb.appendChild(bMin);
|
|
10739
|
-
}
|
|
10740
|
-
if (state.detachable) {
|
|
10741
|
-
const bOut = document.createElement("button");
|
|
10742
|
-
bOut.type = "button";
|
|
10743
|
-
bOut.className = "vd-music-player-btn vd-music-player-btn-detach";
|
|
10744
|
-
bOut.setAttribute("aria-label", "Detach player");
|
|
10745
|
-
bOut.appendChild(icon("arrows-out"));
|
|
10746
|
-
tb.appendChild(bOut);
|
|
10747
|
-
const bIn = document.createElement("button");
|
|
10748
|
-
bIn.type = "button";
|
|
10749
|
-
bIn.className = "vd-music-player-btn vd-music-player-btn-attach";
|
|
10750
|
-
bIn.setAttribute("aria-label", "Attach player");
|
|
10751
|
-
bIn.appendChild(icon("arrows-in"));
|
|
10752
|
-
tb.appendChild(bIn);
|
|
10753
|
-
}
|
|
10754
|
-
container.classList.add("vd-music-player-has-chrome");
|
|
10755
|
-
container.appendChild(tb);
|
|
10756
|
-
}
|
|
10757
|
-
const info = document.createElement("div");
|
|
10758
|
-
info.className = "vd-music-player-info";
|
|
10759
|
-
const iconWrap = document.createElement("span");
|
|
10760
|
-
iconWrap.className = "vd-music-player-icon";
|
|
10761
|
-
iconWrap.setAttribute("aria-hidden", "true");
|
|
10762
|
-
iconWrap.appendChild(icon("music-note"));
|
|
10763
|
-
const trackName = document.createElement("span");
|
|
10764
|
-
trackName.className = "vd-music-player-track-name";
|
|
10765
|
-
trackName.setAttribute("aria-live", "polite");
|
|
10766
|
-
trackName.setAttribute("aria-atomic", "true");
|
|
10767
|
-
info.appendChild(iconWrap);
|
|
10768
|
-
info.appendChild(trackName);
|
|
10769
|
-
container.appendChild(info);
|
|
10770
|
-
const controls = document.createElement("div");
|
|
10771
|
-
controls.className = "vd-music-player-controls";
|
|
10772
|
-
controls.setAttribute("role", "group");
|
|
10773
|
-
controls.setAttribute("aria-label", "Playback controls");
|
|
10774
|
-
const btnPrev = document.createElement("button");
|
|
10775
|
-
btnPrev.type = "button";
|
|
10776
|
-
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
10777
|
-
btnPrev.setAttribute("aria-label", "Previous track");
|
|
10778
|
-
btnPrev.appendChild(icon("skip-back"));
|
|
10779
|
-
const btnPlay = document.createElement("button");
|
|
10780
|
-
btnPlay.type = "button";
|
|
10781
|
-
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
10782
|
-
btnPlay.setAttribute("aria-label", "Play");
|
|
10783
|
-
btnPlay.appendChild(icon("play"));
|
|
10784
|
-
const btnNext = document.createElement("button");
|
|
10785
|
-
btnNext.type = "button";
|
|
10786
|
-
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
10787
|
-
btnNext.setAttribute("aria-label", "Next track");
|
|
10788
|
-
btnNext.appendChild(icon("skip-forward"));
|
|
10789
|
-
controls.appendChild(btnPrev);
|
|
10790
|
-
controls.appendChild(btnPlay);
|
|
10791
|
-
controls.appendChild(btnNext);
|
|
10792
|
-
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
10793
|
-
const btnShuffle = document.createElement("button");
|
|
10794
|
-
btnShuffle.type = "button";
|
|
10795
|
-
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
10796
|
-
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
10797
|
-
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10798
|
-
btnShuffle.appendChild(icon("shuffle"));
|
|
10799
|
-
controls.appendChild(btnShuffle);
|
|
10800
|
-
}
|
|
10801
|
-
const spacer = document.createElement("span");
|
|
10802
|
-
spacer.className = "vd-music-player-spacer";
|
|
10803
|
-
spacer.setAttribute("aria-hidden", "true");
|
|
10804
|
-
controls.appendChild(spacer);
|
|
10805
|
-
const volumeWrap = document.createElement("div");
|
|
10806
|
-
volumeWrap.className = "vd-music-player-volume";
|
|
10807
|
-
const volumeIcon = document.createElement("span");
|
|
10808
|
-
volumeIcon.className = "vd-music-player-volume-icon";
|
|
10809
|
-
volumeIcon.setAttribute("aria-hidden", "true");
|
|
10810
|
-
const volumeSlider = document.createElement("input");
|
|
10811
|
-
volumeSlider.type = "range";
|
|
10812
|
-
volumeSlider.className = "vd-music-player-volume-slider";
|
|
10813
|
-
volumeSlider.min = "0";
|
|
10814
|
-
volumeSlider.max = "1";
|
|
10815
|
-
volumeSlider.step = "0.01";
|
|
10816
|
-
volumeSlider.value = String(state.volume);
|
|
10817
|
-
volumeSlider.setAttribute("aria-label", "Volume");
|
|
10818
|
-
volumeWrap.appendChild(volumeIcon);
|
|
10819
|
-
volumeWrap.appendChild(volumeSlider);
|
|
10820
|
-
controls.appendChild(volumeWrap);
|
|
10821
|
-
if (state.showPlaylist) {
|
|
10822
|
-
const btnPlaylist = document.createElement("button");
|
|
10823
|
-
btnPlaylist.type = "button";
|
|
10824
|
-
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
10825
|
-
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
10826
|
-
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
10827
|
-
btnPlaylist.appendChild(icon("playlist"));
|
|
10828
|
-
controls.appendChild(btnPlaylist);
|
|
10829
|
-
}
|
|
10830
|
-
container.appendChild(controls);
|
|
10831
|
-
if (state.showProgress) {
|
|
10832
|
-
const progressRow = document.createElement("div");
|
|
10833
|
-
progressRow.className = "vd-music-player-progress";
|
|
10834
|
-
const timeElapsed = document.createElement("span");
|
|
10835
|
-
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
10836
|
-
timeElapsed.textContent = "0:00";
|
|
10837
|
-
timeElapsed.setAttribute("aria-hidden", "true");
|
|
10838
|
-
const progressBar = document.createElement("input");
|
|
10839
|
-
progressBar.type = "range";
|
|
10840
|
-
progressBar.className = "vd-music-player-progress-bar";
|
|
10841
|
-
progressBar.min = "0";
|
|
10842
|
-
progressBar.max = "100";
|
|
10843
|
-
progressBar.step = "0.1";
|
|
10844
|
-
progressBar.value = "0";
|
|
10845
|
-
progressBar.setAttribute("aria-label", "Seek");
|
|
10846
|
-
const timeDuration = document.createElement("span");
|
|
10847
|
-
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
10848
|
-
timeDuration.textContent = "0:00";
|
|
10849
|
-
timeDuration.setAttribute("aria-hidden", "true");
|
|
10850
|
-
progressRow.appendChild(timeElapsed);
|
|
10851
|
-
progressRow.appendChild(progressBar);
|
|
10852
|
-
progressRow.appendChild(timeDuration);
|
|
10853
|
-
container.appendChild(progressRow);
|
|
10854
|
-
}
|
|
10855
|
-
if (state.showPlaylist) {
|
|
10856
|
-
const playlist = document.createElement("div");
|
|
10857
|
-
playlist.className = "vd-music-player-playlist";
|
|
10858
|
-
playlist.setAttribute("aria-label", "Playlist");
|
|
10859
|
-
container.appendChild(playlist);
|
|
10860
|
-
}
|
|
10861
|
-
},
|
|
10862
|
-
/* ─── Public API ──────────────────────────────────────── */
|
|
10863
|
-
/**
|
|
10864
|
-
* @param {HTMLElement} container
|
|
10865
|
-
*/
|
|
10866
|
-
play: function(container) {
|
|
10867
|
-
const inst = this.instances.get(container);
|
|
10868
|
-
if (!inst) return;
|
|
10869
|
-
if (!inst.audio.src && inst.state.tracks.length) {
|
|
10870
|
-
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
10871
|
-
}
|
|
10872
|
-
inst.audio.play().catch(() => {
|
|
10873
|
-
});
|
|
10874
|
-
},
|
|
10875
|
-
/**
|
|
10876
|
-
* @param {HTMLElement} container
|
|
10877
|
-
*/
|
|
10878
|
-
pause: function(container) {
|
|
10879
|
-
const inst = this.instances.get(container);
|
|
10880
|
-
if (inst) inst.audio.pause();
|
|
10881
|
-
},
|
|
10882
|
-
/**
|
|
10883
|
-
* @param {HTMLElement} container
|
|
10884
|
-
*/
|
|
10885
|
-
toggle: function(container) {
|
|
10886
|
-
const inst = this.instances.get(container);
|
|
10887
|
-
if (!inst) return;
|
|
10888
|
-
if (inst.state.isPlaying) {
|
|
10889
|
-
this.pause(container);
|
|
10890
|
-
} else {
|
|
10891
|
-
this.play(container);
|
|
10892
|
-
}
|
|
10893
|
-
},
|
|
10894
|
-
/**
|
|
10895
|
-
* @param {HTMLElement} container
|
|
10896
|
-
*/
|
|
10897
|
-
next: function(container) {
|
|
10898
|
-
const inst = this.instances.get(container);
|
|
10899
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10900
|
-
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
10901
|
-
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
10902
|
-
},
|
|
10903
|
-
/**
|
|
10904
|
-
* @param {HTMLElement} container
|
|
10905
|
-
*/
|
|
10906
|
-
previous: function(container) {
|
|
10907
|
-
const inst = this.instances.get(container);
|
|
10908
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10909
|
-
const len = inst.state.tracks.length;
|
|
10910
|
-
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
10911
|
-
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
10912
|
-
},
|
|
10913
|
-
/**
|
|
10914
|
-
* @param {HTMLElement} container
|
|
10915
|
-
* @param {number} value - 0 to 1
|
|
10916
|
-
*/
|
|
10917
|
-
setVolume: function(container, value) {
|
|
10918
|
-
const inst = this.instances.get(container);
|
|
10919
|
-
if (!inst) return;
|
|
10920
|
-
const v = Math.max(0, Math.min(1, value));
|
|
10921
|
-
inst.state.volume = v;
|
|
10922
|
-
inst.audio.volume = v;
|
|
10923
|
-
if (inst.refs.volumeSlider) {
|
|
10924
|
-
inst.refs.volumeSlider.value = String(v);
|
|
10925
|
-
updateRangeFill(inst.refs.volumeSlider);
|
|
10926
|
-
}
|
|
10927
|
-
container.dispatchEvent(
|
|
10928
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10929
|
-
);
|
|
10930
|
-
},
|
|
10931
|
-
/**
|
|
10932
|
-
* @param {HTMLElement} container
|
|
10933
|
-
* @param {number} index - Track index
|
|
10934
|
-
*/
|
|
10935
|
-
setTrack: function(container, index) {
|
|
10936
|
-
const inst = this.instances.get(container);
|
|
10937
|
-
if (!inst) return;
|
|
10938
|
-
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
10939
|
-
},
|
|
10940
|
-
/**
|
|
10941
|
-
* Shuffle or un-shuffle the track list.
|
|
10942
|
-
* @param {HTMLElement} container
|
|
10943
|
-
*/
|
|
10944
|
-
shuffle: function(container) {
|
|
10945
|
-
const inst = this.instances.get(container);
|
|
10946
|
-
if (!inst || !inst.refs.btnShuffle) return;
|
|
10947
|
-
inst.refs.btnShuffle.click();
|
|
10948
|
-
},
|
|
10949
|
-
/**
|
|
10950
|
-
* Float the player above the page. Requires { detachable: true } at init.
|
|
10951
|
-
* @param {HTMLElement} container
|
|
10952
|
-
* @param {string} [position] 'bottom-left' or 'bottom-right'
|
|
10953
|
-
*/
|
|
10954
|
-
detach: function(container, position) {
|
|
10955
|
-
const inst = this.instances.get(container);
|
|
10956
|
-
if (!inst || !inst.state.detachable || inst.state.isDetached) return;
|
|
10957
|
-
const s = inst.state;
|
|
10958
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
10959
|
-
s.isDetached = true;
|
|
10960
|
-
inst.ui.restore = {
|
|
10961
|
-
parent: container.parentNode,
|
|
10962
|
-
next: container.nextSibling
|
|
10963
|
-
};
|
|
10964
|
-
document.body.appendChild(container);
|
|
10965
|
-
container.classList.add("vd-music-player-floating", "vd-music-player-detached");
|
|
10966
|
-
const pos = position != null && position !== void 0 ? position : s.floatingPosition;
|
|
10967
|
-
this._setCornerPosition(
|
|
10968
|
-
container,
|
|
10969
|
-
pos === "bottom-left" || pos === "bottom-right" ? pos : "bottom-right"
|
|
10970
|
-
);
|
|
10971
|
-
this._loadPersistedPosition(container, inst);
|
|
10972
|
-
if (s.startMinimized && !s._startMinimizeApplied) {
|
|
10973
|
-
s._startMinimizeApplied = true;
|
|
10974
|
-
this.minimize(container);
|
|
10975
|
-
}
|
|
10976
|
-
this._bindFloatingDrag(inst);
|
|
10977
|
-
container.dispatchEvent(new CustomEvent("musicplayer:detach", { bubbles: true }));
|
|
10978
|
-
},
|
|
10979
|
-
/**
|
|
10980
|
-
* Return a detached player to its original place in the document.
|
|
10981
|
-
* @param {HTMLElement} container
|
|
10982
|
-
*/
|
|
10983
|
-
attach: function(container) {
|
|
10984
|
-
const inst = this.instances.get(container);
|
|
10985
|
-
if (!inst || !inst.state.isDetached) return;
|
|
10986
|
-
this._unbindFloatingDrag(inst);
|
|
10987
|
-
inst.state.isDetached = false;
|
|
10988
|
-
const r = inst.ui && inst.ui.restore;
|
|
10989
|
-
container.classList.remove(
|
|
10990
|
-
"vd-music-player-floating",
|
|
10991
|
-
"vd-music-player-detached",
|
|
10992
|
-
"vd-music-player-floating-bottom-left",
|
|
10993
|
-
"vd-music-player-floating-bottom-right",
|
|
10994
|
-
"is-position-custom"
|
|
10995
|
-
);
|
|
10996
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
10997
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
10998
|
-
if (r && r.parent && r.parent.isConnected) {
|
|
10999
|
-
r.parent.insertBefore(container, r.next);
|
|
11000
|
-
}
|
|
11001
|
-
if (inst.ui) {
|
|
11002
|
-
inst.ui.restore = null;
|
|
11003
|
-
inst.ui.unbindDrag = null;
|
|
11004
|
-
}
|
|
11005
|
-
container.dispatchEvent(new CustomEvent("musicplayer:attach", { bubbles: true }));
|
|
11006
|
-
},
|
|
11007
|
-
/**
|
|
11008
|
-
* Collapse to essential controls. Requires { minimizable: true } at init.
|
|
11009
|
-
* @param {HTMLElement} container
|
|
11010
|
-
*/
|
|
11011
|
-
minimize: function(container) {
|
|
11012
|
-
const inst = this.instances.get(container);
|
|
11013
|
-
if (!inst || !inst.state.minimizable || inst.state.isMinimized) return;
|
|
11014
|
-
const s = inst.state;
|
|
11015
|
-
s.isMinimized = true;
|
|
11016
|
-
container.classList.add("vd-music-player-minimized");
|
|
11017
|
-
this._setMinimizeButtonState(inst, true);
|
|
11018
|
-
if (inst.refs.playlistPanel && inst.refs.playlistPanel.classList.contains("is-open") && inst.refs.btnPlaylist) {
|
|
11019
|
-
inst.refs.playlistPanel.classList.remove("is-open");
|
|
11020
|
-
inst.refs.btnPlaylist.classList.remove("is-active");
|
|
11021
|
-
inst.refs.btnPlaylist.setAttribute("aria-expanded", "false");
|
|
11022
|
-
}
|
|
11023
|
-
container.dispatchEvent(new CustomEvent("musicplayer:minimize", { bubbles: true }));
|
|
11024
|
-
},
|
|
11025
|
-
/**
|
|
11026
|
-
* Restore from minimized state.
|
|
11027
|
-
* @param {HTMLElement} container
|
|
11028
|
-
*/
|
|
11029
|
-
expand: function(container) {
|
|
11030
|
-
const inst = this.instances.get(container);
|
|
11031
|
-
if (!inst || !inst.state.minimizable || !inst.state.isMinimized) return;
|
|
11032
|
-
inst.state.isMinimized = false;
|
|
11033
|
-
container.classList.remove("vd-music-player-minimized");
|
|
11034
|
-
this._setMinimizeButtonState(inst, false);
|
|
11035
|
-
container.dispatchEvent(new CustomEvent("musicplayer:expand", { bubbles: true }));
|
|
11036
|
-
},
|
|
11037
|
-
/**
|
|
11038
|
-
* Toggle minimize / expand.
|
|
11039
|
-
* @param {HTMLElement} container
|
|
11040
|
-
*/
|
|
11041
|
-
toggleMinimize: function(container) {
|
|
11042
|
-
const inst = this.instances.get(container);
|
|
11043
|
-
if (!inst || !inst.state.minimizable) return;
|
|
11044
|
-
if (inst.state.isMinimized) {
|
|
11045
|
-
this.expand(container);
|
|
11046
|
-
} else {
|
|
11047
|
-
this.minimize(container);
|
|
11048
|
-
}
|
|
11049
|
-
},
|
|
11050
|
-
/**
|
|
11051
|
-
* Set floating corner or pixel position (detached only).
|
|
11052
|
-
* @param {HTMLElement} container
|
|
11053
|
-
* @param {string|{x:number,y:number}} position 'bottom-left' | 'bottom-right' | { x, y } viewport
|
|
11054
|
-
*/
|
|
11055
|
-
setPosition: function(container, position) {
|
|
11056
|
-
const inst = this.instances.get(container);
|
|
11057
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11058
|
-
if (typeof position === "string") {
|
|
11059
|
-
this._setCornerPosition(
|
|
11060
|
-
container,
|
|
11061
|
-
position === "bottom-left" || position === "bottom-right" ? position : "bottom-right"
|
|
11062
|
-
);
|
|
11063
|
-
} else if (position && typeof position.x === "number" && typeof position.y === "number") {
|
|
11064
|
-
this._setCustomPositionFromRect(container, position.x, position.y);
|
|
11065
|
-
}
|
|
11066
|
-
if (inst.state.persistPosition) {
|
|
11067
|
-
const r = container.getBoundingClientRect();
|
|
11068
|
-
this._savePositionPixels(inst, r.left, r.top);
|
|
11069
|
-
}
|
|
11070
|
-
},
|
|
11071
|
-
/**
|
|
11072
|
-
* @param {Object} inst
|
|
11073
|
-
* @param {boolean} minimized
|
|
11074
|
-
*/
|
|
11075
|
-
_setMinimizeButtonState: function(inst, minimized) {
|
|
11076
|
-
const b = inst.refs && inst.refs.btnMinimize;
|
|
11077
|
-
if (!b) return;
|
|
11078
|
-
b.innerHTML = "";
|
|
11079
|
-
b.appendChild(icon(minimized ? "plus" : "minus"));
|
|
11080
|
-
b.setAttribute("aria-label", minimized ? "Expand player" : "Minimize player");
|
|
11081
|
-
b.setAttribute("aria-expanded", minimized ? "false" : "true");
|
|
11082
|
-
},
|
|
11083
|
-
/**
|
|
11084
|
-
* @param {HTMLElement} container
|
|
11085
|
-
* @param {string} which 'bottom-left' | 'bottom-right'
|
|
11086
|
-
*/
|
|
11087
|
-
_setCornerPosition: function(container, which) {
|
|
11088
|
-
container.classList.remove("is-position-custom", "vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11089
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11090
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11091
|
-
if (which === "bottom-left") {
|
|
11092
|
-
container.classList.add("vd-music-player-floating-bottom-left");
|
|
11093
|
-
} else {
|
|
11094
|
-
container.classList.add("vd-music-player-floating-bottom-right");
|
|
11095
|
-
}
|
|
11096
|
-
},
|
|
11097
|
-
/**
|
|
11098
|
-
* @param {HTMLElement} container
|
|
11099
|
-
* @param {number} left
|
|
11100
|
-
* @param {number} top
|
|
11101
|
-
*/
|
|
11102
|
-
_setCustomPositionFromRect: function(container, left, top) {
|
|
11103
|
-
container.classList.remove("vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11104
|
-
container.classList.add("is-position-custom");
|
|
11105
|
-
container.style.setProperty("--vd-music-player-floating-left", left + "px");
|
|
11106
|
-
container.style.setProperty("--vd-music-player-floating-top", top + "px");
|
|
11107
|
-
},
|
|
11108
|
-
/**
|
|
11109
|
-
* @param {HTMLElement} container
|
|
11110
|
-
* @param {Object} inst
|
|
11111
|
-
*/
|
|
11112
|
-
_loadPersistedPosition: function(container, inst) {
|
|
11113
|
-
if (!inst.state.persistPosition) return;
|
|
11114
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11115
|
-
let raw = null;
|
|
11116
|
-
if (typeof window.safeStorageGet === "function") {
|
|
11117
|
-
raw = window.safeStorageGet(key, null);
|
|
11118
|
-
} else {
|
|
11119
|
-
try {
|
|
11120
|
-
raw = localStorage.getItem(key);
|
|
11121
|
-
} catch (_e) {
|
|
11122
|
-
}
|
|
11123
|
-
}
|
|
11124
|
-
if (!raw) return;
|
|
11125
|
-
try {
|
|
11126
|
-
const o = JSON.parse(raw);
|
|
11127
|
-
if (o && typeof o.x === "number" && typeof o.y === "number") {
|
|
11128
|
-
this._setCustomPositionFromRect(container, o.x, o.y);
|
|
11129
|
-
}
|
|
11130
|
-
} catch (_err) {
|
|
11131
|
-
}
|
|
11132
|
-
},
|
|
11133
|
-
/**
|
|
11134
|
-
* @param {Object} inst
|
|
11135
|
-
* @param {number} x
|
|
11136
|
-
* @param {number} y
|
|
11137
|
-
*/
|
|
11138
|
-
_savePositionPixels: function(inst, x, y) {
|
|
11139
|
-
if (!inst.state.persistPosition) return;
|
|
11140
|
-
const container = this._containerOf(inst);
|
|
11141
|
-
if (!container) return;
|
|
11142
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11143
|
-
const val = JSON.stringify({ x, y });
|
|
11144
|
-
if (typeof window.safeStorageSet === "function") {
|
|
11145
|
-
window.safeStorageSet(key, val);
|
|
11146
|
-
} else {
|
|
11147
|
-
try {
|
|
11148
|
-
localStorage.setItem(key, val);
|
|
11149
|
-
} catch (_e) {
|
|
11150
|
-
}
|
|
11151
|
-
}
|
|
11152
|
-
},
|
|
11153
|
-
/**
|
|
11154
|
-
* @param {Object} inst
|
|
11155
|
-
* @param {HTMLElement} container
|
|
11156
|
-
* @returns {string}
|
|
11157
|
-
*/
|
|
11158
|
-
_persistKeyForInstance: function(inst, container) {
|
|
11159
|
-
const pk = inst.state.persistKey;
|
|
11160
|
-
if (pk && String(pk).trim()) return persistStorageKey(String(pk).trim());
|
|
11161
|
-
return persistStorageKey(container.id || "");
|
|
11162
|
-
},
|
|
11163
|
-
/**
|
|
11164
|
-
* @param {Object} inst
|
|
11165
|
-
*/
|
|
11166
|
-
_unbindFloatingDrag: function(inst) {
|
|
11167
|
-
if (inst.ui && typeof inst.ui.unbindDrag === "function") {
|
|
11168
|
-
inst.ui.unbindDrag();
|
|
11169
|
-
inst.ui.unbindDrag = null;
|
|
11170
|
-
}
|
|
11171
|
-
},
|
|
11172
|
-
/**
|
|
11173
|
-
* Free-form pointer drag on the handle. Vanduo's `draggable` component uses HTML5
|
|
11174
|
-
* drag/drop for list reordering; floating players use pointer events on the handle instead.
|
|
11175
|
-
* @param {Object} inst
|
|
11176
|
-
*/
|
|
11177
|
-
_bindFloatingDrag: function(inst) {
|
|
11178
|
-
this._unbindFloatingDrag(inst);
|
|
11179
|
-
const h = inst.refs && inst.refs.dragHandle;
|
|
11180
|
-
if (!h || !inst.state || !inst.state.draggable) return;
|
|
11181
|
-
const self = this;
|
|
11182
|
-
const container = this._containerOf(inst);
|
|
11183
|
-
if (!container) return;
|
|
11184
|
-
let startX = 0;
|
|
11185
|
-
let startY = 0;
|
|
11186
|
-
let origL = 0;
|
|
11187
|
-
let origT = 0;
|
|
11188
|
-
let activeDrag = false;
|
|
11189
|
-
const onDown = function(e) {
|
|
11190
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
11191
|
-
e.preventDefault();
|
|
11192
|
-
activeDrag = true;
|
|
11193
|
-
const r = container.getBoundingClientRect();
|
|
11194
|
-
origL = r.left;
|
|
11195
|
-
origT = r.top;
|
|
11196
|
-
startX = e.clientX;
|
|
11197
|
-
startY = e.clientY;
|
|
11198
|
-
self._setCustomPositionFromRect(container, origL, origT);
|
|
11199
|
-
try {
|
|
11200
|
-
h.setPointerCapture(e.pointerId);
|
|
11201
|
-
} catch (_err) {
|
|
11202
|
-
}
|
|
11203
|
-
};
|
|
11204
|
-
const onMove = function(e) {
|
|
11205
|
-
if (!activeDrag) return;
|
|
11206
|
-
const dx = e.clientX - startX;
|
|
11207
|
-
const dy = e.clientY - startY;
|
|
11208
|
-
let nl = origL + dx;
|
|
11209
|
-
let nt = origT + dy;
|
|
11210
|
-
const r = container.getBoundingClientRect();
|
|
11211
|
-
const w = r.width;
|
|
11212
|
-
const ph = r.height;
|
|
11213
|
-
const vw = window.innerWidth;
|
|
11214
|
-
const vh = window.innerHeight;
|
|
11215
|
-
const pad = 8;
|
|
11216
|
-
nl = Math.max(pad, Math.min(nl, vw - w - pad));
|
|
11217
|
-
nt = Math.max(pad, Math.min(nt, vh - ph - pad));
|
|
11218
|
-
self._setCustomPositionFromRect(container, nl, nt);
|
|
11219
|
-
};
|
|
11220
|
-
const onUp = function(e) {
|
|
11221
|
-
if (!activeDrag) return;
|
|
11222
|
-
activeDrag = false;
|
|
11223
|
-
if (typeof h.hasPointerCapture === "function" && h.hasPointerCapture(e.pointerId)) {
|
|
11224
|
-
try {
|
|
11225
|
-
h.releasePointerCapture(e.pointerId);
|
|
11226
|
-
} catch (_e) {
|
|
11227
|
-
}
|
|
11228
|
-
}
|
|
11229
|
-
if (inst.state.persistPosition) {
|
|
11230
|
-
const r = container.getBoundingClientRect();
|
|
11231
|
-
self._savePositionPixels(inst, r.left, r.top);
|
|
11232
|
-
}
|
|
11233
|
-
};
|
|
11234
|
-
h.addEventListener("pointerdown", onDown);
|
|
11235
|
-
h.addEventListener("pointermove", onMove);
|
|
11236
|
-
h.addEventListener("pointerup", onUp);
|
|
11237
|
-
h.addEventListener("pointercancel", onUp);
|
|
11238
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11239
|
-
inst.ui.unbindDrag = function() {
|
|
11240
|
-
h.removeEventListener("pointerdown", onDown);
|
|
11241
|
-
h.removeEventListener("pointermove", onMove);
|
|
11242
|
-
h.removeEventListener("pointerup", onUp);
|
|
11243
|
-
h.removeEventListener("pointercancel", onUp);
|
|
11244
|
-
};
|
|
11245
|
-
},
|
|
11246
|
-
/**
|
|
11247
|
-
* Return a shallow copy of the current player state.
|
|
11248
|
-
* @param {HTMLElement} container
|
|
11249
|
-
* @returns {Object|null}
|
|
11250
|
-
*/
|
|
11251
|
-
getState: function(container) {
|
|
11252
|
-
const inst = this.instances.get(container);
|
|
11253
|
-
if (!inst) return null;
|
|
11254
|
-
const s = inst.state;
|
|
11255
|
-
return {
|
|
11256
|
-
isPlaying: s.isPlaying,
|
|
11257
|
-
currentIndex: s.currentIndex,
|
|
11258
|
-
currentTrack: s.tracks[s.currentIndex] || null,
|
|
11259
|
-
volume: s.volume,
|
|
11260
|
-
shuffle: s.shuffle,
|
|
11261
|
-
tracks: s.tracks.slice(),
|
|
11262
|
-
isDetached: Boolean(s.isDetached),
|
|
11263
|
-
isMinimized: Boolean(s.isMinimized)
|
|
11264
|
-
};
|
|
11265
|
-
},
|
|
11266
|
-
/**
|
|
11267
|
-
* Stop playback, clean up listeners, remove instance.
|
|
11268
|
-
* @param {HTMLElement} container
|
|
11269
|
-
*/
|
|
11270
|
-
destroy: function(container) {
|
|
11271
|
-
const inst = this.instances.get(container);
|
|
11272
|
-
if (!inst) return;
|
|
11273
|
-
this._unbindFloatingDrag(inst);
|
|
11274
|
-
if (inst.state && inst.state.isDetached) {
|
|
11275
|
-
try {
|
|
11276
|
-
this.attach(container);
|
|
11277
|
-
} catch (_e) {
|
|
11278
|
-
}
|
|
11279
|
-
}
|
|
11280
|
-
inst.cleanup.forEach((fn) => fn());
|
|
11281
|
-
this.instances.delete(container);
|
|
11282
|
-
container.removeAttribute("data-music-player-initialized");
|
|
11283
|
-
},
|
|
11284
|
-
/**
|
|
11285
|
-
* Destroy all instances.
|
|
11286
|
-
*/
|
|
11287
|
-
destroyAll: function() {
|
|
11288
|
-
this.instances.forEach((_, container) => this.destroy(container));
|
|
11289
|
-
},
|
|
11290
|
-
/* ─── Internal helpers ────────────────────────────────── */
|
|
11291
|
-
/**
|
|
11292
|
-
* Load track by index on an already-initialised instance object.
|
|
11293
|
-
* @param {Object} inst
|
|
11294
|
-
* @param {number} index
|
|
11295
|
-
* @param {boolean} autoPlay
|
|
11296
|
-
*/
|
|
11297
|
-
_loadTrack: function(inst, index, autoPlay) {
|
|
11298
|
-
const track = inst.state.tracks[index];
|
|
11299
|
-
if (!track) return;
|
|
11300
|
-
const container = this._containerOf(inst);
|
|
11301
|
-
inst.state.currentIndex = index;
|
|
11302
|
-
inst.audio.src = track.url;
|
|
11303
|
-
if (inst.refs.trackName) {
|
|
11304
|
-
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
11305
|
-
inst.refs.trackName.classList.remove("is-idle");
|
|
11306
|
-
}
|
|
11307
|
-
if (inst.refs.playlistPanel) {
|
|
11308
|
-
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
11309
|
-
const active = i === index;
|
|
11310
|
-
item.classList.toggle("is-active", active);
|
|
11311
|
-
item.setAttribute("aria-current", active ? "true" : "false");
|
|
11312
|
-
});
|
|
11313
|
-
}
|
|
11314
|
-
if (inst.refs.progressBar) {
|
|
11315
|
-
inst.refs.progressBar.value = "0";
|
|
11316
|
-
updateRangeFill(inst.refs.progressBar);
|
|
11317
|
-
}
|
|
11318
|
-
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
11319
|
-
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
11320
|
-
if (container) {
|
|
11321
|
-
container.dispatchEvent(
|
|
11322
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
11323
|
-
bubbles: true,
|
|
11324
|
-
detail: { index, name: track.name, url: track.url }
|
|
11325
|
-
})
|
|
11326
|
-
);
|
|
11327
|
-
}
|
|
11328
|
-
if (autoPlay) inst.audio.play().catch(() => {
|
|
11329
|
-
});
|
|
11330
|
-
},
|
|
11331
|
-
/**
|
|
11332
|
-
* Reverse-lookup the container element for a given instance object.
|
|
11333
|
-
* @param {Object} inst
|
|
11334
|
-
* @returns {HTMLElement|null}
|
|
11335
|
-
*/
|
|
11336
|
-
_containerOf: function(inst) {
|
|
11337
|
-
for (const [container, i] of this.instances) {
|
|
11338
|
-
if (i === inst) return container;
|
|
11339
|
-
}
|
|
11340
|
-
return null;
|
|
11341
|
-
}
|
|
11342
|
-
};
|
|
11343
|
-
if (typeof window.Vanduo !== "undefined") {
|
|
11344
|
-
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
11345
|
-
}
|
|
11346
|
-
window.VanduoMusicPlayer = MusicPlayer;
|
|
11347
|
-
})();
|
|
11348
|
-
|
|
11349
10425
|
// js/index.js
|
|
11350
10426
|
var Vanduo = window.Vanduo;
|
|
11351
10427
|
var index_default = Vanduo;
|