@vanduo-oss/framework 1.4.2 → 1.4.4
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 +15 -3
- package/css/components/theme-switcher.css +129 -0
- package/css/vanduo.css +1 -1
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +328 -1098
- 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 +117 -690
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +328 -1098
- 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 +328 -1098
- 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 +87 -24
- package/js/components/datepicker.js +27 -1
- package/js/components/suggest.js +18 -15
- package/js/components/theme-switcher.js +290 -29
- package/js/index.js +0 -1
- package/package.json +5 -5
- 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.4 | Built: 2026-06-07T19:14:20.877Z | git:781df0d | 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.4" : "0.0.0-dev";
|
|
205
205
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
206
206
|
const Vanduo2 = {
|
|
207
207
|
version: VANDUO_VERSION,
|
|
@@ -486,9 +486,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
486
486
|
init: function(root) {
|
|
487
487
|
const snippets = this.queryWithin(root, ".vd-code-snippet");
|
|
488
488
|
snippets.forEach((snippet) => {
|
|
489
|
-
if (
|
|
490
|
-
|
|
489
|
+
if (snippet.dataset.initialized === "true") {
|
|
490
|
+
return;
|
|
491
491
|
}
|
|
492
|
+
this.initSnippet(snippet);
|
|
492
493
|
});
|
|
493
494
|
},
|
|
494
495
|
/**
|
|
@@ -496,7 +497,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
496
497
|
* @param {HTMLElement} snippet - Code snippet container element
|
|
497
498
|
*/
|
|
498
499
|
initSnippet: function(snippet) {
|
|
499
|
-
snippet.dataset.initialized
|
|
500
|
+
if (snippet.dataset.initialized === "true") {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
500
503
|
snippet._codeSnippetCleanup = [];
|
|
501
504
|
const toggle = snippet.querySelector(".vd-code-snippet-toggle");
|
|
502
505
|
const content = snippet.querySelector(".vd-code-snippet-content");
|
|
@@ -524,6 +527,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
524
527
|
lineNumberPanes.forEach((pane) => {
|
|
525
528
|
this.addLineNumbers(pane);
|
|
526
529
|
});
|
|
530
|
+
snippet.dataset.initialized = "true";
|
|
527
531
|
},
|
|
528
532
|
/**
|
|
529
533
|
* Initialize collapsible functionality
|
|
@@ -533,13 +537,14 @@ module.exports = __toCommonJS(index_exports);
|
|
|
533
537
|
*/
|
|
534
538
|
initCollapsible: function(snippet, toggle, content) {
|
|
535
539
|
const isExpanded = snippet.dataset.expanded === "true";
|
|
536
|
-
toggle.setAttribute("aria-expanded", isExpanded);
|
|
537
|
-
content.dataset.visible = isExpanded;
|
|
540
|
+
toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
|
|
541
|
+
content.dataset.visible = isExpanded ? "true" : "false";
|
|
538
542
|
this.addListener(snippet, toggle, "click", () => {
|
|
539
543
|
const expanded = snippet.dataset.expanded === "true";
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
544
|
+
const nextExpanded = !expanded;
|
|
545
|
+
snippet.dataset.expanded = nextExpanded ? "true" : "false";
|
|
546
|
+
toggle.setAttribute("aria-expanded", nextExpanded ? "true" : "false");
|
|
547
|
+
content.dataset.visible = nextExpanded ? "true" : "false";
|
|
543
548
|
if (!expanded) {
|
|
544
549
|
const extractPanes = content.querySelectorAll("[data-extract]:not([data-extracted])");
|
|
545
550
|
extractPanes.forEach((pane) => {
|
|
@@ -874,12 +879,38 @@ module.exports = __toCommonJS(index_exports);
|
|
|
874
879
|
* @returns {string} HTML with syntax highlighting spans
|
|
875
880
|
*/
|
|
876
881
|
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
882
|
html = html.replace(/(<!--)(.*?)(-->)/g, '<span class="code-comment">$1$2$3</span>');
|
|
883
|
+
html = html.replace(/<(?!--)([\s\S]*?)>/g, function(match, inner) {
|
|
884
|
+
const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
|
|
885
|
+
if (closingMatch) {
|
|
886
|
+
return "<" + closingMatch[1] + '<span class="code-tag">' + closingMatch[2] + "</span>>";
|
|
887
|
+
}
|
|
888
|
+
const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
|
|
889
|
+
const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : "";
|
|
890
|
+
const tagSource = selfClosingMatch ? inner.slice(0, inner.length - selfClosingMatch[0].length) : inner;
|
|
891
|
+
const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
|
|
892
|
+
if (!openMatch) {
|
|
893
|
+
return match;
|
|
894
|
+
}
|
|
895
|
+
const tagName = openMatch[1];
|
|
896
|
+
let attrSource = openMatch[2];
|
|
897
|
+
attrSource = attrSource.replace(
|
|
898
|
+
/([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
|
|
899
|
+
function(attrMatch, attrName, separator, quote, value) {
|
|
900
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + quote + '<span class="code-string">' + value + "</span>" + quote;
|
|
901
|
+
}
|
|
902
|
+
);
|
|
903
|
+
attrSource = attrSource.replace(
|
|
904
|
+
/([\w:-]+)(\s*=\s*)("|')([\s\S]*?)("|')/g,
|
|
905
|
+
function(attrMatch, attrName, separator, openQuote, value, closeQuote) {
|
|
906
|
+
if (openQuote !== closeQuote) {
|
|
907
|
+
return attrMatch;
|
|
908
|
+
}
|
|
909
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + openQuote + '<span class="code-string">' + value + "</span>" + closeQuote;
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
return '<<span class="code-tag">' + tagName + "</span>" + attrSource + selfClosingSuffix + ">";
|
|
913
|
+
});
|
|
883
914
|
return html;
|
|
884
915
|
},
|
|
885
916
|
/**
|
|
@@ -901,16 +932,26 @@ module.exports = __toCommonJS(index_exports);
|
|
|
901
932
|
* @returns {string} JS with syntax highlighting spans
|
|
902
933
|
*/
|
|
903
934
|
highlightJs: function(js) {
|
|
935
|
+
const protectedTokens = [];
|
|
936
|
+
const protectToken = (value, className) => {
|
|
937
|
+
const marker = String.fromCharCode(57344 + protectedTokens.length);
|
|
938
|
+
protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + "</span>" });
|
|
939
|
+
return marker;
|
|
940
|
+
};
|
|
941
|
+
js = js.replace(
|
|
942
|
+
/\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
|
|
943
|
+
(match) => protectToken(match, match.startsWith("/") ? "code-comment" : "code-string")
|
|
944
|
+
);
|
|
904
945
|
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
946
|
keywords.forEach((kw) => {
|
|
906
947
|
const regex = new RegExp(`\\b(${kw})\\b`, "g");
|
|
907
948
|
js = js.replace(regex, '<span class="code-keyword">$1</span>');
|
|
908
949
|
});
|
|
909
|
-
js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
|
|
910
950
|
js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
|
|
911
951
|
js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
|
|
912
|
-
|
|
913
|
-
|
|
952
|
+
protectedTokens.forEach((token) => {
|
|
953
|
+
js = js.replace(token.marker, token.html);
|
|
954
|
+
});
|
|
914
955
|
return js;
|
|
915
956
|
},
|
|
916
957
|
/**
|
|
@@ -5034,21 +5075,45 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5034
5075
|
// js/components/theme-switcher.js
|
|
5035
5076
|
(function() {
|
|
5036
5077
|
"use strict";
|
|
5078
|
+
const THEME_MODES = ["system", "light", "dark"];
|
|
5079
|
+
const THEME_ICON_CLASSES = {
|
|
5080
|
+
system: "ph ph-desktop",
|
|
5081
|
+
light: "ph ph-sun",
|
|
5082
|
+
dark: "ph ph-moon"
|
|
5083
|
+
};
|
|
5084
|
+
const THEME_LABELS = {
|
|
5085
|
+
system: "Theme: System",
|
|
5086
|
+
light: "Theme: Light",
|
|
5087
|
+
dark: "Theme: Dark"
|
|
5088
|
+
};
|
|
5089
|
+
const THEME_OPTION_TOOLTIPS = {
|
|
5090
|
+
system: "Use system preference",
|
|
5091
|
+
light: "Light theme",
|
|
5092
|
+
dark: "Dark theme"
|
|
5093
|
+
};
|
|
5037
5094
|
const ThemeSwitcher = {
|
|
5038
5095
|
isInitialized: false,
|
|
5039
5096
|
_mediaQuery: null,
|
|
5040
5097
|
_onMediaChange: null,
|
|
5098
|
+
menuInstances: /* @__PURE__ */ new Map(),
|
|
5041
5099
|
getToggles: function(root) {
|
|
5100
|
+
const scope = root || document;
|
|
5101
|
+
const toggles = window.Vanduo && typeof window.Vanduo.queryAll === "function" ? window.Vanduo.queryAll(scope, '[data-toggle="theme"]') : Array.from(scope.querySelectorAll('[data-toggle="theme"]'));
|
|
5102
|
+
return toggles.filter(function(toggle) {
|
|
5103
|
+
return !toggle.closest('.vd-theme-switcher[data-theme-ui="menu"]');
|
|
5104
|
+
});
|
|
5105
|
+
},
|
|
5106
|
+
getMenuSwitchers: function(root) {
|
|
5107
|
+
const scope = root || document;
|
|
5042
5108
|
if (window.Vanduo && typeof window.Vanduo.queryAll === "function") {
|
|
5043
|
-
return window.Vanduo.queryAll(
|
|
5109
|
+
return window.Vanduo.queryAll(scope, '.vd-theme-switcher[data-theme-ui="menu"]');
|
|
5044
5110
|
}
|
|
5045
|
-
return Array.from(
|
|
5111
|
+
return Array.from(scope.querySelectorAll('.vd-theme-switcher[data-theme-ui="menu"]'));
|
|
5046
5112
|
},
|
|
5047
5113
|
init: function(root) {
|
|
5048
5114
|
this.STORAGE_KEY = "vanduo-theme-preference";
|
|
5049
5115
|
this.state = {
|
|
5050
5116
|
preference: this.getPreference()
|
|
5051
|
-
// 'light', 'dark', or 'system'
|
|
5052
5117
|
};
|
|
5053
5118
|
if (this.isInitialized) {
|
|
5054
5119
|
this.applyTheme();
|
|
@@ -5065,6 +5130,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5065
5130
|
return this.getStorageValue(this.STORAGE_KEY, "system");
|
|
5066
5131
|
},
|
|
5067
5132
|
setPreference: function(pref) {
|
|
5133
|
+
if (!THEME_MODES.includes(pref)) {
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
5068
5136
|
this.state.preference = pref;
|
|
5069
5137
|
this.setStorageValue(this.STORAGE_KEY, pref);
|
|
5070
5138
|
this.applyTheme();
|
|
@@ -5118,8 +5186,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5118
5186
|
};
|
|
5119
5187
|
this._mediaQuery.addEventListener("change", this._onMediaChange);
|
|
5120
5188
|
},
|
|
5121
|
-
// Helper to facilitate UI creation if needed, though often UI is in HTML
|
|
5122
5189
|
renderUI: function(root) {
|
|
5190
|
+
this.renderMenuSwitchers(root);
|
|
5123
5191
|
const toggles = this.getToggles(root);
|
|
5124
5192
|
toggles.forEach((toggle) => {
|
|
5125
5193
|
if (toggle.getAttribute("data-theme-initialized") === "true") {
|
|
@@ -5137,7 +5205,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5137
5205
|
toggle._themeToggleHandler = onChange;
|
|
5138
5206
|
} else {
|
|
5139
5207
|
const onClick = () => {
|
|
5140
|
-
const modes =
|
|
5208
|
+
const modes = THEME_MODES;
|
|
5141
5209
|
const nextIndex = (modes.indexOf(this.state.preference) + 1) % modes.length;
|
|
5142
5210
|
this.setPreference(modes[nextIndex]);
|
|
5143
5211
|
};
|
|
@@ -5146,6 +5214,188 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5146
5214
|
}
|
|
5147
5215
|
toggle.setAttribute("data-theme-initialized", "true");
|
|
5148
5216
|
});
|
|
5217
|
+
this.updateUI(root);
|
|
5218
|
+
},
|
|
5219
|
+
renderMenuSwitchers: function(root) {
|
|
5220
|
+
const switchers = this.getMenuSwitchers(root);
|
|
5221
|
+
switchers.forEach((switcher) => {
|
|
5222
|
+
if (switcher.getAttribute("data-theme-menu-initialized") === "true") {
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
|
|
5226
|
+
const menu = switcher.querySelector(".vd-theme-switcher-menu");
|
|
5227
|
+
if (!toggle || !menu) {
|
|
5228
|
+
return;
|
|
5229
|
+
}
|
|
5230
|
+
const options = menu.querySelectorAll("[data-theme-value]");
|
|
5231
|
+
const cleanupFunctions = [];
|
|
5232
|
+
toggle.setAttribute("aria-haspopup", "true");
|
|
5233
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
5234
|
+
menu.setAttribute("aria-hidden", "true");
|
|
5235
|
+
const toggleClickHandler = (e) => {
|
|
5236
|
+
e.preventDefault();
|
|
5237
|
+
e.stopPropagation();
|
|
5238
|
+
this.toggleMenu(switcher, toggle, menu);
|
|
5239
|
+
};
|
|
5240
|
+
toggle.addEventListener("click", toggleClickHandler);
|
|
5241
|
+
cleanupFunctions.push(() => toggle.removeEventListener("click", toggleClickHandler));
|
|
5242
|
+
options.forEach((option) => {
|
|
5243
|
+
const optionClickHandler = (e) => {
|
|
5244
|
+
e.preventDefault();
|
|
5245
|
+
e.stopPropagation();
|
|
5246
|
+
const value = option.getAttribute("data-theme-value");
|
|
5247
|
+
if (value) {
|
|
5248
|
+
this.setPreference(value);
|
|
5249
|
+
}
|
|
5250
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5251
|
+
};
|
|
5252
|
+
option.addEventListener("click", optionClickHandler);
|
|
5253
|
+
cleanupFunctions.push(() => option.removeEventListener("click", optionClickHandler));
|
|
5254
|
+
const optionKeydownHandler = (e) => {
|
|
5255
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
5256
|
+
e.preventDefault();
|
|
5257
|
+
optionClickHandler(e);
|
|
5258
|
+
}
|
|
5259
|
+
};
|
|
5260
|
+
option.addEventListener("keydown", optionKeydownHandler);
|
|
5261
|
+
cleanupFunctions.push(() => option.removeEventListener("keydown", optionKeydownHandler));
|
|
5262
|
+
});
|
|
5263
|
+
const toggleKeydownHandler = (e) => {
|
|
5264
|
+
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
5265
|
+
e.preventDefault();
|
|
5266
|
+
if (!menu.classList.contains("is-open")) {
|
|
5267
|
+
this.openMenu(switcher, toggle, menu);
|
|
5268
|
+
}
|
|
5269
|
+
} else if (e.key === "Escape" && menu.classList.contains("is-open")) {
|
|
5270
|
+
e.preventDefault();
|
|
5271
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5272
|
+
}
|
|
5273
|
+
};
|
|
5274
|
+
toggle.addEventListener("keydown", toggleKeydownHandler);
|
|
5275
|
+
cleanupFunctions.push(() => toggle.removeEventListener("keydown", toggleKeydownHandler));
|
|
5276
|
+
const menuKeydownHandler = (e) => {
|
|
5277
|
+
this.handleMenuKeydown(e, switcher, toggle, menu, options);
|
|
5278
|
+
};
|
|
5279
|
+
menu.addEventListener("keydown", menuKeydownHandler);
|
|
5280
|
+
cleanupFunctions.push(() => menu.removeEventListener("keydown", menuKeydownHandler));
|
|
5281
|
+
const documentClickHandler = (e) => {
|
|
5282
|
+
if (!switcher.contains(e.target) && menu.classList.contains("is-open")) {
|
|
5283
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5284
|
+
}
|
|
5285
|
+
};
|
|
5286
|
+
document.addEventListener("click", documentClickHandler);
|
|
5287
|
+
cleanupFunctions.push(() => document.removeEventListener("click", documentClickHandler));
|
|
5288
|
+
this.menuInstances.set(switcher, { toggle, menu, cleanup: cleanupFunctions });
|
|
5289
|
+
switcher.setAttribute("data-theme-menu-initialized", "true");
|
|
5290
|
+
this.initMenuTooltips(switcher);
|
|
5291
|
+
});
|
|
5292
|
+
},
|
|
5293
|
+
initMenuTooltips: function(switcher) {
|
|
5294
|
+
const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
|
|
5295
|
+
if (tooltips && typeof tooltips.init === "function") {
|
|
5296
|
+
tooltips.init(switcher);
|
|
5297
|
+
}
|
|
5298
|
+
},
|
|
5299
|
+
closeOtherMenus: function(exceptMenu) {
|
|
5300
|
+
this.menuInstances.forEach((instance, switcher) => {
|
|
5301
|
+
if (instance.menu !== exceptMenu && instance.menu.classList.contains("is-open")) {
|
|
5302
|
+
this.closeMenu(switcher, instance.toggle, instance.menu);
|
|
5303
|
+
}
|
|
5304
|
+
});
|
|
5305
|
+
},
|
|
5306
|
+
toggleMenu: function(switcher, toggle, menu) {
|
|
5307
|
+
if (menu.classList.contains("is-open")) {
|
|
5308
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5309
|
+
} else {
|
|
5310
|
+
this.openMenu(switcher, toggle, menu);
|
|
5311
|
+
}
|
|
5312
|
+
},
|
|
5313
|
+
openMenu: function(switcher, toggle, menu) {
|
|
5314
|
+
this.closeOtherMenus(menu);
|
|
5315
|
+
switcher.classList.add("is-open");
|
|
5316
|
+
menu.classList.add("is-open");
|
|
5317
|
+
toggle.setAttribute("aria-expanded", "true");
|
|
5318
|
+
menu.setAttribute("aria-hidden", "false");
|
|
5319
|
+
const activeOption = menu.querySelector("[data-theme-value].is-active") || menu.querySelector("[data-theme-value]");
|
|
5320
|
+
if (activeOption) {
|
|
5321
|
+
setTimeout(() => activeOption.focus(), 0);
|
|
5322
|
+
}
|
|
5323
|
+
},
|
|
5324
|
+
closeMenu: function(switcher, toggle, menu) {
|
|
5325
|
+
switcher.classList.remove("is-open");
|
|
5326
|
+
menu.classList.remove("is-open");
|
|
5327
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
5328
|
+
menu.setAttribute("aria-hidden", "true");
|
|
5329
|
+
},
|
|
5330
|
+
handleMenuKeydown: function(e, switcher, toggle, menu, options) {
|
|
5331
|
+
const items = Array.from(options);
|
|
5332
|
+
const currentIndex = items.indexOf(document.activeElement);
|
|
5333
|
+
if (e.key === "Escape") {
|
|
5334
|
+
e.preventDefault();
|
|
5335
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5336
|
+
toggle.focus();
|
|
5337
|
+
return;
|
|
5338
|
+
}
|
|
5339
|
+
if (e.key === "ArrowDown") {
|
|
5340
|
+
e.preventDefault();
|
|
5341
|
+
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
5342
|
+
items[nextIndex].focus();
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
if (e.key === "ArrowUp") {
|
|
5346
|
+
e.preventDefault();
|
|
5347
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
5348
|
+
items[prevIndex].focus();
|
|
5349
|
+
}
|
|
5350
|
+
},
|
|
5351
|
+
updateMenuSwitcher: function(switcher) {
|
|
5352
|
+
const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
|
|
5353
|
+
const menu = switcher.querySelector(".vd-theme-switcher-menu");
|
|
5354
|
+
if (!toggle || !menu) {
|
|
5355
|
+
return;
|
|
5356
|
+
}
|
|
5357
|
+
const pref = this.state.preference;
|
|
5358
|
+
const icon = toggle.querySelector("[data-theme-icon]");
|
|
5359
|
+
const label = THEME_LABELS[pref] || THEME_LABELS.system;
|
|
5360
|
+
if (icon) {
|
|
5361
|
+
icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
|
|
5362
|
+
}
|
|
5363
|
+
toggle.setAttribute("aria-label", label);
|
|
5364
|
+
if (toggle.hasAttribute("data-tooltip")) {
|
|
5365
|
+
toggle.setAttribute("data-tooltip", label);
|
|
5366
|
+
this.refreshTooltipContent(toggle, label);
|
|
5367
|
+
}
|
|
5368
|
+
menu.querySelectorAll("[data-theme-value]").forEach((option) => {
|
|
5369
|
+
const value = option.getAttribute("data-theme-value");
|
|
5370
|
+
const isActive = value === pref;
|
|
5371
|
+
option.classList.toggle("is-active", isActive);
|
|
5372
|
+
option.setAttribute("aria-checked", isActive ? "true" : "false");
|
|
5373
|
+
const tooltipText = THEME_OPTION_TOOLTIPS[value];
|
|
5374
|
+
if (tooltipText && option.hasAttribute("data-tooltip")) {
|
|
5375
|
+
option.setAttribute("data-tooltip", tooltipText);
|
|
5376
|
+
this.refreshTooltipContent(option, tooltipText);
|
|
5377
|
+
}
|
|
5378
|
+
});
|
|
5379
|
+
},
|
|
5380
|
+
refreshTooltipContent: function(element, text) {
|
|
5381
|
+
const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
|
|
5382
|
+
if (!tooltips || typeof tooltips.update !== "function") {
|
|
5383
|
+
return;
|
|
5384
|
+
}
|
|
5385
|
+
tooltips.update(element, text);
|
|
5386
|
+
},
|
|
5387
|
+
updateCycleToggle: function(toggle) {
|
|
5388
|
+
const pref = this.state.preference;
|
|
5389
|
+
const icon = toggle.querySelector("[data-theme-icon]");
|
|
5390
|
+
const label = THEME_LABELS[pref] || THEME_LABELS.system;
|
|
5391
|
+
if (icon) {
|
|
5392
|
+
icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
|
|
5393
|
+
}
|
|
5394
|
+
toggle.setAttribute("aria-label", label);
|
|
5395
|
+
if (toggle.hasAttribute("data-tooltip")) {
|
|
5396
|
+
toggle.setAttribute("data-tooltip", label);
|
|
5397
|
+
this.refreshTooltipContent(toggle, label);
|
|
5398
|
+
}
|
|
5149
5399
|
},
|
|
5150
5400
|
updateUI: function(root) {
|
|
5151
5401
|
const toggles = this.getToggles(root);
|
|
@@ -5157,11 +5407,26 @@ module.exports = __toCommonJS(index_exports);
|
|
|
5157
5407
|
if (span) {
|
|
5158
5408
|
span.textContent = this.state.preference.charAt(0).toUpperCase() + this.state.preference.slice(1);
|
|
5159
5409
|
}
|
|
5410
|
+
if (toggle.querySelector("[data-theme-icon]")) {
|
|
5411
|
+
this.updateCycleToggle(toggle);
|
|
5412
|
+
}
|
|
5160
5413
|
}
|
|
5161
5414
|
});
|
|
5415
|
+
this.getMenuSwitchers(root).forEach((switcher) => {
|
|
5416
|
+
this.updateMenuSwitcher(switcher);
|
|
5417
|
+
});
|
|
5162
5418
|
},
|
|
5163
5419
|
destroyAll: function(root) {
|
|
5164
5420
|
const scope = root || document;
|
|
5421
|
+
this.getMenuSwitchers(scope).forEach((switcher) => {
|
|
5422
|
+
const instance = this.menuInstances.get(switcher);
|
|
5423
|
+
if (instance) {
|
|
5424
|
+
instance.cleanup.forEach((fn) => fn());
|
|
5425
|
+
this.closeMenu(switcher, instance.toggle, instance.menu);
|
|
5426
|
+
this.menuInstances.delete(switcher);
|
|
5427
|
+
}
|
|
5428
|
+
switcher.removeAttribute("data-theme-menu-initialized");
|
|
5429
|
+
});
|
|
5165
5430
|
const toggles = this.getToggles(scope).filter(function(toggle) {
|
|
5166
5431
|
return toggle.getAttribute("data-theme-initialized") === "true";
|
|
5167
5432
|
});
|
|
@@ -8456,11 +8721,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8456
8721
|
// js/components/suggest.js
|
|
8457
8722
|
(function() {
|
|
8458
8723
|
"use strict";
|
|
8459
|
-
function _escapeHtml(text) {
|
|
8460
|
-
const div = document.createElement("div");
|
|
8461
|
-
div.textContent = text;
|
|
8462
|
-
return div.innerHTML;
|
|
8463
|
-
}
|
|
8464
8724
|
function _isSafeUrl(url, allowlist) {
|
|
8465
8725
|
try {
|
|
8466
8726
|
const resolved = new URL(url, window.location.href);
|
|
@@ -8531,9 +8791,24 @@ module.exports = __toCommonJS(index_exports);
|
|
|
8531
8791
|
li.id = listId + "-item-" + i;
|
|
8532
8792
|
const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
|
|
8533
8793
|
if (query) {
|
|
8534
|
-
const
|
|
8535
|
-
const
|
|
8536
|
-
|
|
8794
|
+
const lowerText = text.toLowerCase();
|
|
8795
|
+
const lowerQuery = query.toLowerCase();
|
|
8796
|
+
let start = 0;
|
|
8797
|
+
let matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8798
|
+
while (matchIndex !== -1) {
|
|
8799
|
+
if (matchIndex > start) {
|
|
8800
|
+
li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
|
|
8801
|
+
}
|
|
8802
|
+
const matchSpan = document.createElement("span");
|
|
8803
|
+
matchSpan.className = "vd-suggest-match";
|
|
8804
|
+
matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
|
|
8805
|
+
li.appendChild(matchSpan);
|
|
8806
|
+
start = matchIndex + query.length;
|
|
8807
|
+
matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8808
|
+
}
|
|
8809
|
+
if (start < text.length) {
|
|
8810
|
+
li.appendChild(document.createTextNode(text.slice(start)));
|
|
8811
|
+
}
|
|
8537
8812
|
} else {
|
|
8538
8813
|
li.textContent = text;
|
|
8539
8814
|
}
|
|
@@ -9003,6 +9278,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9003
9278
|
let viewMode = "days";
|
|
9004
9279
|
let focusedDate = null;
|
|
9005
9280
|
let skipNextFocusOpen = false;
|
|
9281
|
+
let ignoreOutsideUntil = 0;
|
|
9006
9282
|
const isDisabled = (d) => {
|
|
9007
9283
|
if (minDate) {
|
|
9008
9284
|
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
@@ -9368,7 +9644,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9368
9644
|
const repositionHandler = () => {
|
|
9369
9645
|
positionPopup();
|
|
9370
9646
|
};
|
|
9647
|
+
const markIgnoreOutside = () => {
|
|
9648
|
+
ignoreOutsideUntil = Date.now() + 100;
|
|
9649
|
+
};
|
|
9371
9650
|
const open = () => {
|
|
9651
|
+
markIgnoreOutside();
|
|
9372
9652
|
viewMode = "days";
|
|
9373
9653
|
if (selectedDate) {
|
|
9374
9654
|
viewYear = selectedDate.getFullYear();
|
|
@@ -9402,8 +9682,22 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9402
9682
|
}
|
|
9403
9683
|
open();
|
|
9404
9684
|
};
|
|
9685
|
+
const clickHandler = () => {
|
|
9686
|
+
if (!popup.classList.contains("is-open")) open();
|
|
9687
|
+
};
|
|
9688
|
+
const isOpenAnchorTarget = (target) => {
|
|
9689
|
+
if (!target || !(target instanceof Node)) return false;
|
|
9690
|
+
if (target === input || input.contains(target) || popup.contains(target)) return true;
|
|
9691
|
+
const inputId = input.id;
|
|
9692
|
+
if (inputId) {
|
|
9693
|
+
const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
|
|
9694
|
+
if (label && (target === label || label.contains(target))) return true;
|
|
9695
|
+
}
|
|
9696
|
+
return false;
|
|
9697
|
+
};
|
|
9405
9698
|
const outsideHandler = (e) => {
|
|
9406
|
-
if (
|
|
9699
|
+
if (Date.now() < ignoreOutsideUntil) return;
|
|
9700
|
+
if (!isOpenAnchorTarget(e.target)) close();
|
|
9407
9701
|
};
|
|
9408
9702
|
const escHandler = (e) => {
|
|
9409
9703
|
if (e.key === "Escape" && popup.classList.contains("is-open")) {
|
|
@@ -9413,6 +9707,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9413
9707
|
}
|
|
9414
9708
|
};
|
|
9415
9709
|
input.addEventListener("focus", focusHandler);
|
|
9710
|
+
input.addEventListener("click", clickHandler);
|
|
9416
9711
|
document.addEventListener("click", outsideHandler, true);
|
|
9417
9712
|
document.addEventListener("keydown", escHandler);
|
|
9418
9713
|
popup.addEventListener("keydown", handleGridKeydown);
|
|
@@ -9423,6 +9718,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
9423
9718
|
input.setAttribute("autocomplete", "off");
|
|
9424
9719
|
cleanup.push(
|
|
9425
9720
|
() => input.removeEventListener("focus", focusHandler),
|
|
9721
|
+
() => input.removeEventListener("click", clickHandler),
|
|
9426
9722
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9427
9723
|
() => document.removeEventListener("keydown", escHandler),
|
|
9428
9724
|
() => popup.removeEventListener("keydown", handleGridKeydown),
|
|
@@ -10355,1072 +10651,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
10355
10651
|
window.VanduoSpotlight = Spotlight;
|
|
10356
10652
|
})();
|
|
10357
10653
|
|
|
10358
|
-
// js/components/music-player.js
|
|
10359
|
-
(function() {
|
|
10360
|
-
"use strict";
|
|
10361
|
-
function shuffleArray(arr) {
|
|
10362
|
-
const shuffled = arr.slice();
|
|
10363
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
10364
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
10365
|
-
const tmp = shuffled[i];
|
|
10366
|
-
shuffled[i] = shuffled[j];
|
|
10367
|
-
shuffled[j] = tmp;
|
|
10368
|
-
}
|
|
10369
|
-
return shuffled;
|
|
10370
|
-
}
|
|
10371
|
-
function formatTime(seconds) {
|
|
10372
|
-
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
10373
|
-
const m = Math.floor(seconds / 60);
|
|
10374
|
-
const s = Math.floor(seconds % 60);
|
|
10375
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
10376
|
-
}
|
|
10377
|
-
function persistStorageKey(id) {
|
|
10378
|
-
return "vanduo:music-player:" + (id && id.trim() ? id.trim() : "default") + ":pos";
|
|
10379
|
-
}
|
|
10380
|
-
function updateRangeFill(input) {
|
|
10381
|
-
const min = parseFloat(input.min) || 0;
|
|
10382
|
-
const max = parseFloat(input.max) || 1;
|
|
10383
|
-
const val = parseFloat(input.value) || 0;
|
|
10384
|
-
const pct = (val - min) / (max - min) * 100;
|
|
10385
|
-
input.style.setProperty("--vd-fill", pct + "%");
|
|
10386
|
-
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%)";
|
|
10387
|
-
}
|
|
10388
|
-
function icon(name) {
|
|
10389
|
-
const el = document.createElement("i");
|
|
10390
|
-
el.className = "ph ph-" + name;
|
|
10391
|
-
el.setAttribute("aria-hidden", "true");
|
|
10392
|
-
return el;
|
|
10393
|
-
}
|
|
10394
|
-
const MusicPlayer = {
|
|
10395
|
-
/** @type {Map<HTMLElement, Object>} */
|
|
10396
|
-
instances: /* @__PURE__ */ new Map(),
|
|
10397
|
-
/**
|
|
10398
|
-
* Default options.
|
|
10399
|
-
*/
|
|
10400
|
-
defaults: {
|
|
10401
|
-
tracks: [],
|
|
10402
|
-
volume: 0.5,
|
|
10403
|
-
shuffle: false,
|
|
10404
|
-
showProgress: false,
|
|
10405
|
-
showPlaylist: false,
|
|
10406
|
-
autoAdvance: true,
|
|
10407
|
-
glass: false,
|
|
10408
|
-
detachable: false,
|
|
10409
|
-
/** @type {null|string} */
|
|
10410
|
-
floatingPosition: null,
|
|
10411
|
-
draggable: false,
|
|
10412
|
-
minimizable: false,
|
|
10413
|
-
startMinimized: false,
|
|
10414
|
-
persistPosition: false,
|
|
10415
|
-
persistKey: ""
|
|
10416
|
-
},
|
|
10417
|
-
/**
|
|
10418
|
-
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
10419
|
-
* Options can be provided via data-music-player-options (JSON string).
|
|
10420
|
-
*/
|
|
10421
|
-
init: function(root) {
|
|
10422
|
-
window.Vanduo.queryAll(root, ".vd-music-player, [data-music-player]").forEach((el) => {
|
|
10423
|
-
if (this.instances.has(el)) return;
|
|
10424
|
-
let opts = {};
|
|
10425
|
-
const attr = el.getAttribute("data-music-player-options");
|
|
10426
|
-
if (attr) {
|
|
10427
|
-
try {
|
|
10428
|
-
opts = JSON.parse(attr);
|
|
10429
|
-
} catch (_) {
|
|
10430
|
-
}
|
|
10431
|
-
}
|
|
10432
|
-
this.initPlayer(el, opts);
|
|
10433
|
-
});
|
|
10434
|
-
},
|
|
10435
|
-
/**
|
|
10436
|
-
* Initialize a single player element.
|
|
10437
|
-
* @param {HTMLElement} container
|
|
10438
|
-
* @param {Object} [options]
|
|
10439
|
-
*/
|
|
10440
|
-
initPlayer: function(container, options) {
|
|
10441
|
-
const opts = Object.assign({}, this.defaults, options || {});
|
|
10442
|
-
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
10443
|
-
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
10444
|
-
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
10445
|
-
const state = {
|
|
10446
|
-
tracks: trackList,
|
|
10447
|
-
originalTracks: tracks.slice(),
|
|
10448
|
-
currentIndex: 0,
|
|
10449
|
-
isPlaying: false,
|
|
10450
|
-
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
10451
|
-
shuffle: opts.shuffle,
|
|
10452
|
-
showProgress: opts.showProgress,
|
|
10453
|
-
showPlaylist: opts.showPlaylist,
|
|
10454
|
-
autoAdvance: opts.autoAdvance,
|
|
10455
|
-
audio: null,
|
|
10456
|
-
glass: Boolean(opts.glass),
|
|
10457
|
-
detachable: Boolean(opts.detachable),
|
|
10458
|
-
floatingPosition: opts.floatingPosition || "bottom-right",
|
|
10459
|
-
draggable: Boolean(opts.draggable) && Boolean(opts.detachable),
|
|
10460
|
-
minimizable: Boolean(opts.minimizable),
|
|
10461
|
-
startMinimized: Boolean(opts.startMinimized),
|
|
10462
|
-
persistPosition: Boolean(opts.persistPosition),
|
|
10463
|
-
persistKey: typeof opts.persistKey === "string" ? opts.persistKey : "",
|
|
10464
|
-
isDetached: false,
|
|
10465
|
-
isMinimized: false,
|
|
10466
|
-
_startMinimizeApplied: false
|
|
10467
|
-
};
|
|
10468
|
-
const audio = new Audio();
|
|
10469
|
-
audio.volume = state.volume;
|
|
10470
|
-
audio.preload = "metadata";
|
|
10471
|
-
state.audio = audio;
|
|
10472
|
-
this._buildDOM(container, state);
|
|
10473
|
-
const refs = {
|
|
10474
|
-
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
10475
|
-
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
10476
|
-
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
10477
|
-
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
10478
|
-
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
10479
|
-
btnDetach: container.querySelector(".vd-music-player-btn-detach"),
|
|
10480
|
-
btnAttach: container.querySelector(".vd-music-player-btn-attach"),
|
|
10481
|
-
btnMinimize: container.querySelector(".vd-music-player-btn-minimize"),
|
|
10482
|
-
dragHandle: container.querySelector(".vd-music-player-drag-handle"),
|
|
10483
|
-
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
10484
|
-
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
10485
|
-
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
10486
|
-
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
10487
|
-
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
10488
|
-
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
10489
|
-
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
10490
|
-
};
|
|
10491
|
-
const renderPlayIcon = () => {
|
|
10492
|
-
const btn = refs.btnPlay;
|
|
10493
|
-
if (!btn) return;
|
|
10494
|
-
btn.innerHTML = "";
|
|
10495
|
-
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
10496
|
-
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
10497
|
-
btn.classList.toggle("is-active", state.isPlaying);
|
|
10498
|
-
};
|
|
10499
|
-
const renderTrackName = () => {
|
|
10500
|
-
const el = refs.trackName;
|
|
10501
|
-
if (!el) return;
|
|
10502
|
-
const track = state.tracks[state.currentIndex];
|
|
10503
|
-
if (track) {
|
|
10504
|
-
el.textContent = track.name || "Unknown Track";
|
|
10505
|
-
el.classList.remove("is-idle");
|
|
10506
|
-
} else {
|
|
10507
|
-
el.textContent = "No tracks loaded";
|
|
10508
|
-
el.classList.add("is-idle");
|
|
10509
|
-
}
|
|
10510
|
-
};
|
|
10511
|
-
const renderVolumeIcon = () => {
|
|
10512
|
-
const el = refs.volumeIcon;
|
|
10513
|
-
if (!el) return;
|
|
10514
|
-
el.innerHTML = "";
|
|
10515
|
-
const v = state.volume;
|
|
10516
|
-
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
10517
|
-
el.appendChild(icon(name));
|
|
10518
|
-
};
|
|
10519
|
-
const renderShuffleBtn = () => {
|
|
10520
|
-
const btn = refs.btnShuffle;
|
|
10521
|
-
if (!btn) return;
|
|
10522
|
-
btn.classList.toggle("is-active", state.shuffle);
|
|
10523
|
-
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10524
|
-
};
|
|
10525
|
-
const renderPlaylistItems = () => {
|
|
10526
|
-
const panel = refs.playlistPanel;
|
|
10527
|
-
if (!panel) return;
|
|
10528
|
-
panel.innerHTML = "";
|
|
10529
|
-
state.tracks.forEach((track, i) => {
|
|
10530
|
-
const item = document.createElement("button");
|
|
10531
|
-
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
10532
|
-
item.type = "button";
|
|
10533
|
-
item.setAttribute("data-index", String(i));
|
|
10534
|
-
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
10535
|
-
const num = document.createElement("span");
|
|
10536
|
-
num.className = "vd-music-player-playlist-num";
|
|
10537
|
-
num.textContent = String(i + 1);
|
|
10538
|
-
const name = document.createElement("span");
|
|
10539
|
-
name.className = "vd-music-player-playlist-name";
|
|
10540
|
-
name.textContent = track.name || "Track " + (i + 1);
|
|
10541
|
-
item.appendChild(num);
|
|
10542
|
-
item.appendChild(name);
|
|
10543
|
-
panel.appendChild(item);
|
|
10544
|
-
});
|
|
10545
|
-
};
|
|
10546
|
-
const renderProgress = () => {
|
|
10547
|
-
const bar = refs.progressBar;
|
|
10548
|
-
if (!bar || !audio.duration) return;
|
|
10549
|
-
const pct = audio.currentTime / audio.duration * 100;
|
|
10550
|
-
bar.value = String(pct);
|
|
10551
|
-
updateRangeFill(bar);
|
|
10552
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
10553
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10554
|
-
};
|
|
10555
|
-
const loadTrack = (index, autoPlay) => {
|
|
10556
|
-
const track = state.tracks[index];
|
|
10557
|
-
if (!track) return;
|
|
10558
|
-
state.currentIndex = index;
|
|
10559
|
-
audio.src = track.url;
|
|
10560
|
-
renderTrackName();
|
|
10561
|
-
renderPlaylistItems();
|
|
10562
|
-
if (refs.progressBar) {
|
|
10563
|
-
refs.progressBar.value = "0";
|
|
10564
|
-
updateRangeFill(refs.progressBar);
|
|
10565
|
-
}
|
|
10566
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
10567
|
-
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
10568
|
-
container.dispatchEvent(
|
|
10569
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
10570
|
-
bubbles: true,
|
|
10571
|
-
detail: { index, name: track.name, url: track.url }
|
|
10572
|
-
})
|
|
10573
|
-
);
|
|
10574
|
-
if (autoPlay) {
|
|
10575
|
-
audio.play().catch(() => {
|
|
10576
|
-
});
|
|
10577
|
-
}
|
|
10578
|
-
};
|
|
10579
|
-
const cleanupFunctions = [];
|
|
10580
|
-
const onPlay = () => {
|
|
10581
|
-
state.isPlaying = true;
|
|
10582
|
-
renderPlayIcon();
|
|
10583
|
-
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
10584
|
-
};
|
|
10585
|
-
const onPause = () => {
|
|
10586
|
-
state.isPlaying = false;
|
|
10587
|
-
renderPlayIcon();
|
|
10588
|
-
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
10589
|
-
};
|
|
10590
|
-
const onEnded = () => {
|
|
10591
|
-
if (state.autoAdvance && state.tracks.length > 1) {
|
|
10592
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10593
|
-
loadTrack(next, true);
|
|
10594
|
-
} else {
|
|
10595
|
-
state.isPlaying = false;
|
|
10596
|
-
renderPlayIcon();
|
|
10597
|
-
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
10598
|
-
}
|
|
10599
|
-
};
|
|
10600
|
-
const onTimeUpdate = () => {
|
|
10601
|
-
if (state.showProgress) renderProgress();
|
|
10602
|
-
};
|
|
10603
|
-
const onLoadedMetadata = () => {
|
|
10604
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10605
|
-
if (refs.progressBar) {
|
|
10606
|
-
refs.progressBar.max = "100";
|
|
10607
|
-
updateRangeFill(refs.progressBar);
|
|
10608
|
-
}
|
|
10609
|
-
};
|
|
10610
|
-
audio.addEventListener("play", onPlay);
|
|
10611
|
-
audio.addEventListener("pause", onPause);
|
|
10612
|
-
audio.addEventListener("ended", onEnded);
|
|
10613
|
-
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
10614
|
-
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
10615
|
-
cleanupFunctions.push(() => {
|
|
10616
|
-
audio.removeEventListener("play", onPlay);
|
|
10617
|
-
audio.removeEventListener("pause", onPause);
|
|
10618
|
-
audio.removeEventListener("ended", onEnded);
|
|
10619
|
-
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
10620
|
-
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
10621
|
-
audio.pause();
|
|
10622
|
-
audio.src = "";
|
|
10623
|
-
});
|
|
10624
|
-
if (refs.btnPlay) {
|
|
10625
|
-
const handler = () => {
|
|
10626
|
-
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
10627
|
-
if (state.isPlaying) {
|
|
10628
|
-
audio.pause();
|
|
10629
|
-
} else {
|
|
10630
|
-
audio.play().catch(() => {
|
|
10631
|
-
});
|
|
10632
|
-
}
|
|
10633
|
-
};
|
|
10634
|
-
refs.btnPlay.addEventListener("click", handler);
|
|
10635
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
10636
|
-
const keyHandler = (e) => {
|
|
10637
|
-
if (e.key === " " || e.key === "Enter") {
|
|
10638
|
-
e.preventDefault();
|
|
10639
|
-
handler();
|
|
10640
|
-
}
|
|
10641
|
-
};
|
|
10642
|
-
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
10643
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
10644
|
-
}
|
|
10645
|
-
if (refs.btnPrev) {
|
|
10646
|
-
const handler = () => {
|
|
10647
|
-
if (!state.tracks.length) return;
|
|
10648
|
-
if (audio.currentTime > 3) {
|
|
10649
|
-
audio.currentTime = 0;
|
|
10650
|
-
} else {
|
|
10651
|
-
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
10652
|
-
loadTrack(prev, state.isPlaying);
|
|
10653
|
-
}
|
|
10654
|
-
};
|
|
10655
|
-
refs.btnPrev.addEventListener("click", handler);
|
|
10656
|
-
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
10657
|
-
}
|
|
10658
|
-
if (refs.btnNext) {
|
|
10659
|
-
const handler = () => {
|
|
10660
|
-
if (!state.tracks.length) return;
|
|
10661
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10662
|
-
loadTrack(next, state.isPlaying);
|
|
10663
|
-
};
|
|
10664
|
-
refs.btnNext.addEventListener("click", handler);
|
|
10665
|
-
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
10666
|
-
}
|
|
10667
|
-
if (refs.btnShuffle) {
|
|
10668
|
-
const handler = () => {
|
|
10669
|
-
state.shuffle = !state.shuffle;
|
|
10670
|
-
if (state.shuffle) {
|
|
10671
|
-
const current = state.tracks[state.currentIndex];
|
|
10672
|
-
state.tracks = shuffleArray(state.tracks);
|
|
10673
|
-
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
10674
|
-
if (newIdx > 0) {
|
|
10675
|
-
state.tracks.splice(newIdx, 1);
|
|
10676
|
-
state.tracks.unshift(current);
|
|
10677
|
-
}
|
|
10678
|
-
state.currentIndex = 0;
|
|
10679
|
-
} else {
|
|
10680
|
-
const current = state.tracks[state.currentIndex];
|
|
10681
|
-
state.tracks = state.originalTracks.slice();
|
|
10682
|
-
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
10683
|
-
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
10684
|
-
}
|
|
10685
|
-
renderShuffleBtn();
|
|
10686
|
-
renderPlaylistItems();
|
|
10687
|
-
};
|
|
10688
|
-
refs.btnShuffle.addEventListener("click", handler);
|
|
10689
|
-
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
10690
|
-
}
|
|
10691
|
-
if (refs.btnPlaylist) {
|
|
10692
|
-
const handler = () => {
|
|
10693
|
-
const panel = refs.playlistPanel;
|
|
10694
|
-
if (!panel) return;
|
|
10695
|
-
const isOpen = panel.classList.toggle("is-open");
|
|
10696
|
-
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
10697
|
-
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
10698
|
-
};
|
|
10699
|
-
refs.btnPlaylist.addEventListener("click", handler);
|
|
10700
|
-
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
10701
|
-
}
|
|
10702
|
-
if (refs.volumeSlider) {
|
|
10703
|
-
const handler = (e) => {
|
|
10704
|
-
const v = parseFloat(e.target.value);
|
|
10705
|
-
state.volume = v;
|
|
10706
|
-
audio.volume = v;
|
|
10707
|
-
renderVolumeIcon();
|
|
10708
|
-
updateRangeFill(refs.volumeSlider);
|
|
10709
|
-
container.dispatchEvent(
|
|
10710
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10711
|
-
);
|
|
10712
|
-
};
|
|
10713
|
-
refs.volumeSlider.addEventListener("input", handler);
|
|
10714
|
-
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
10715
|
-
updateRangeFill(refs.volumeSlider);
|
|
10716
|
-
}
|
|
10717
|
-
if (refs.progressBar) {
|
|
10718
|
-
const handler = (e) => {
|
|
10719
|
-
if (!audio.duration) return;
|
|
10720
|
-
const pct = parseFloat(e.target.value);
|
|
10721
|
-
audio.currentTime = pct / 100 * audio.duration;
|
|
10722
|
-
updateRangeFill(refs.progressBar);
|
|
10723
|
-
};
|
|
10724
|
-
refs.progressBar.addEventListener("input", handler);
|
|
10725
|
-
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
10726
|
-
}
|
|
10727
|
-
if (refs.playlistPanel) {
|
|
10728
|
-
const panelHandler = (e) => {
|
|
10729
|
-
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
10730
|
-
if (!item) return;
|
|
10731
|
-
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
10732
|
-
if (!isNaN(idx)) loadTrack(idx, true);
|
|
10733
|
-
};
|
|
10734
|
-
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
10735
|
-
cleanupFunctions.push(
|
|
10736
|
-
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
10737
|
-
);
|
|
10738
|
-
}
|
|
10739
|
-
if (refs.btnDetach) {
|
|
10740
|
-
const h = () => {
|
|
10741
|
-
this.detach(container);
|
|
10742
|
-
};
|
|
10743
|
-
refs.btnDetach.addEventListener("click", h);
|
|
10744
|
-
cleanupFunctions.push(() => refs.btnDetach.removeEventListener("click", h));
|
|
10745
|
-
}
|
|
10746
|
-
if (refs.btnAttach) {
|
|
10747
|
-
const h = () => {
|
|
10748
|
-
this.attach(container);
|
|
10749
|
-
};
|
|
10750
|
-
refs.btnAttach.addEventListener("click", h);
|
|
10751
|
-
cleanupFunctions.push(() => refs.btnAttach.removeEventListener("click", h));
|
|
10752
|
-
}
|
|
10753
|
-
if (refs.btnMinimize) {
|
|
10754
|
-
const h = () => {
|
|
10755
|
-
this.toggleMinimize(container);
|
|
10756
|
-
};
|
|
10757
|
-
refs.btnMinimize.addEventListener("click", h);
|
|
10758
|
-
cleanupFunctions.push(() => refs.btnMinimize.removeEventListener("click", h));
|
|
10759
|
-
}
|
|
10760
|
-
renderPlayIcon();
|
|
10761
|
-
renderTrackName();
|
|
10762
|
-
renderVolumeIcon();
|
|
10763
|
-
if (opts.showPlaylist) renderPlaylistItems();
|
|
10764
|
-
this.instances.set(container, {
|
|
10765
|
-
state,
|
|
10766
|
-
audio,
|
|
10767
|
-
refs,
|
|
10768
|
-
cleanup: cleanupFunctions,
|
|
10769
|
-
ui: { restore: null, unbindDrag: null }
|
|
10770
|
-
});
|
|
10771
|
-
container.setAttribute("data-music-player-initialized", "true");
|
|
10772
|
-
},
|
|
10773
|
-
/* ─── DOM builder ─────────────────────────────────────── */
|
|
10774
|
-
/**
|
|
10775
|
-
* Build the inner DOM structure inside container.
|
|
10776
|
-
* Pre-existing inner content is replaced only if it has no
|
|
10777
|
-
* recognised child elements (allows server-rendered markup).
|
|
10778
|
-
* @param {HTMLElement} container
|
|
10779
|
-
* @param {Object} state
|
|
10780
|
-
*/
|
|
10781
|
-
_buildDOM: function(container, state) {
|
|
10782
|
-
if (container.querySelector(".vd-music-player-controls")) return;
|
|
10783
|
-
container.setAttribute("role", "region");
|
|
10784
|
-
container.setAttribute("aria-label", "Music Player");
|
|
10785
|
-
if (state.showProgress) container.classList.add("has-progress");
|
|
10786
|
-
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
10787
|
-
if (state.glass) container.classList.add("vd-music-player-glass");
|
|
10788
|
-
if (state.draggable) container.classList.add("vd-music-player-draggable");
|
|
10789
|
-
if (state.detachable || state.minimizable) {
|
|
10790
|
-
const tb = document.createElement("div");
|
|
10791
|
-
tb.className = "vd-music-player-toolbar";
|
|
10792
|
-
tb.setAttribute("role", "toolbar");
|
|
10793
|
-
tb.setAttribute("aria-label", "Player window");
|
|
10794
|
-
if (state.draggable) {
|
|
10795
|
-
const h = document.createElement("button");
|
|
10796
|
-
h.type = "button";
|
|
10797
|
-
h.className = "vd-music-player-drag-handle";
|
|
10798
|
-
h.setAttribute("aria-label", "Drag to move player");
|
|
10799
|
-
h.appendChild(icon("dots-six-vertical"));
|
|
10800
|
-
tb.appendChild(h);
|
|
10801
|
-
}
|
|
10802
|
-
const tSp = document.createElement("span");
|
|
10803
|
-
tSp.className = "vd-music-player-toolbar-spacer";
|
|
10804
|
-
tSp.setAttribute("aria-hidden", "true");
|
|
10805
|
-
tb.appendChild(tSp);
|
|
10806
|
-
if (state.minimizable) {
|
|
10807
|
-
const bMin = document.createElement("button");
|
|
10808
|
-
bMin.type = "button";
|
|
10809
|
-
bMin.className = "vd-music-player-btn vd-music-player-btn-minimize";
|
|
10810
|
-
bMin.setAttribute("aria-label", "Minimize player");
|
|
10811
|
-
bMin.setAttribute("aria-expanded", "true");
|
|
10812
|
-
bMin.appendChild(icon("minus"));
|
|
10813
|
-
tb.appendChild(bMin);
|
|
10814
|
-
}
|
|
10815
|
-
if (state.detachable) {
|
|
10816
|
-
const bOut = document.createElement("button");
|
|
10817
|
-
bOut.type = "button";
|
|
10818
|
-
bOut.className = "vd-music-player-btn vd-music-player-btn-detach";
|
|
10819
|
-
bOut.setAttribute("aria-label", "Detach player");
|
|
10820
|
-
bOut.appendChild(icon("arrows-out"));
|
|
10821
|
-
tb.appendChild(bOut);
|
|
10822
|
-
const bIn = document.createElement("button");
|
|
10823
|
-
bIn.type = "button";
|
|
10824
|
-
bIn.className = "vd-music-player-btn vd-music-player-btn-attach";
|
|
10825
|
-
bIn.setAttribute("aria-label", "Attach player");
|
|
10826
|
-
bIn.appendChild(icon("arrows-in"));
|
|
10827
|
-
tb.appendChild(bIn);
|
|
10828
|
-
}
|
|
10829
|
-
container.classList.add("vd-music-player-has-chrome");
|
|
10830
|
-
container.appendChild(tb);
|
|
10831
|
-
}
|
|
10832
|
-
const info = document.createElement("div");
|
|
10833
|
-
info.className = "vd-music-player-info";
|
|
10834
|
-
const iconWrap = document.createElement("span");
|
|
10835
|
-
iconWrap.className = "vd-music-player-icon";
|
|
10836
|
-
iconWrap.setAttribute("aria-hidden", "true");
|
|
10837
|
-
iconWrap.appendChild(icon("music-note"));
|
|
10838
|
-
const trackName = document.createElement("span");
|
|
10839
|
-
trackName.className = "vd-music-player-track-name";
|
|
10840
|
-
trackName.setAttribute("aria-live", "polite");
|
|
10841
|
-
trackName.setAttribute("aria-atomic", "true");
|
|
10842
|
-
info.appendChild(iconWrap);
|
|
10843
|
-
info.appendChild(trackName);
|
|
10844
|
-
container.appendChild(info);
|
|
10845
|
-
const controls = document.createElement("div");
|
|
10846
|
-
controls.className = "vd-music-player-controls";
|
|
10847
|
-
controls.setAttribute("role", "group");
|
|
10848
|
-
controls.setAttribute("aria-label", "Playback controls");
|
|
10849
|
-
const btnPrev = document.createElement("button");
|
|
10850
|
-
btnPrev.type = "button";
|
|
10851
|
-
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
10852
|
-
btnPrev.setAttribute("aria-label", "Previous track");
|
|
10853
|
-
btnPrev.appendChild(icon("skip-back"));
|
|
10854
|
-
const btnPlay = document.createElement("button");
|
|
10855
|
-
btnPlay.type = "button";
|
|
10856
|
-
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
10857
|
-
btnPlay.setAttribute("aria-label", "Play");
|
|
10858
|
-
btnPlay.appendChild(icon("play"));
|
|
10859
|
-
const btnNext = document.createElement("button");
|
|
10860
|
-
btnNext.type = "button";
|
|
10861
|
-
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
10862
|
-
btnNext.setAttribute("aria-label", "Next track");
|
|
10863
|
-
btnNext.appendChild(icon("skip-forward"));
|
|
10864
|
-
controls.appendChild(btnPrev);
|
|
10865
|
-
controls.appendChild(btnPlay);
|
|
10866
|
-
controls.appendChild(btnNext);
|
|
10867
|
-
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
10868
|
-
const btnShuffle = document.createElement("button");
|
|
10869
|
-
btnShuffle.type = "button";
|
|
10870
|
-
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
10871
|
-
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
10872
|
-
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10873
|
-
btnShuffle.appendChild(icon("shuffle"));
|
|
10874
|
-
controls.appendChild(btnShuffle);
|
|
10875
|
-
}
|
|
10876
|
-
const spacer = document.createElement("span");
|
|
10877
|
-
spacer.className = "vd-music-player-spacer";
|
|
10878
|
-
spacer.setAttribute("aria-hidden", "true");
|
|
10879
|
-
controls.appendChild(spacer);
|
|
10880
|
-
const volumeWrap = document.createElement("div");
|
|
10881
|
-
volumeWrap.className = "vd-music-player-volume";
|
|
10882
|
-
const volumeIcon = document.createElement("span");
|
|
10883
|
-
volumeIcon.className = "vd-music-player-volume-icon";
|
|
10884
|
-
volumeIcon.setAttribute("aria-hidden", "true");
|
|
10885
|
-
const volumeSlider = document.createElement("input");
|
|
10886
|
-
volumeSlider.type = "range";
|
|
10887
|
-
volumeSlider.className = "vd-music-player-volume-slider";
|
|
10888
|
-
volumeSlider.min = "0";
|
|
10889
|
-
volumeSlider.max = "1";
|
|
10890
|
-
volumeSlider.step = "0.01";
|
|
10891
|
-
volumeSlider.value = String(state.volume);
|
|
10892
|
-
volumeSlider.setAttribute("aria-label", "Volume");
|
|
10893
|
-
volumeWrap.appendChild(volumeIcon);
|
|
10894
|
-
volumeWrap.appendChild(volumeSlider);
|
|
10895
|
-
controls.appendChild(volumeWrap);
|
|
10896
|
-
if (state.showPlaylist) {
|
|
10897
|
-
const btnPlaylist = document.createElement("button");
|
|
10898
|
-
btnPlaylist.type = "button";
|
|
10899
|
-
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
10900
|
-
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
10901
|
-
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
10902
|
-
btnPlaylist.appendChild(icon("playlist"));
|
|
10903
|
-
controls.appendChild(btnPlaylist);
|
|
10904
|
-
}
|
|
10905
|
-
container.appendChild(controls);
|
|
10906
|
-
if (state.showProgress) {
|
|
10907
|
-
const progressRow = document.createElement("div");
|
|
10908
|
-
progressRow.className = "vd-music-player-progress";
|
|
10909
|
-
const timeElapsed = document.createElement("span");
|
|
10910
|
-
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
10911
|
-
timeElapsed.textContent = "0:00";
|
|
10912
|
-
timeElapsed.setAttribute("aria-hidden", "true");
|
|
10913
|
-
const progressBar = document.createElement("input");
|
|
10914
|
-
progressBar.type = "range";
|
|
10915
|
-
progressBar.className = "vd-music-player-progress-bar";
|
|
10916
|
-
progressBar.min = "0";
|
|
10917
|
-
progressBar.max = "100";
|
|
10918
|
-
progressBar.step = "0.1";
|
|
10919
|
-
progressBar.value = "0";
|
|
10920
|
-
progressBar.setAttribute("aria-label", "Seek");
|
|
10921
|
-
const timeDuration = document.createElement("span");
|
|
10922
|
-
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
10923
|
-
timeDuration.textContent = "0:00";
|
|
10924
|
-
timeDuration.setAttribute("aria-hidden", "true");
|
|
10925
|
-
progressRow.appendChild(timeElapsed);
|
|
10926
|
-
progressRow.appendChild(progressBar);
|
|
10927
|
-
progressRow.appendChild(timeDuration);
|
|
10928
|
-
container.appendChild(progressRow);
|
|
10929
|
-
}
|
|
10930
|
-
if (state.showPlaylist) {
|
|
10931
|
-
const playlist = document.createElement("div");
|
|
10932
|
-
playlist.className = "vd-music-player-playlist";
|
|
10933
|
-
playlist.setAttribute("aria-label", "Playlist");
|
|
10934
|
-
container.appendChild(playlist);
|
|
10935
|
-
}
|
|
10936
|
-
},
|
|
10937
|
-
/* ─── Public API ──────────────────────────────────────── */
|
|
10938
|
-
/**
|
|
10939
|
-
* @param {HTMLElement} container
|
|
10940
|
-
*/
|
|
10941
|
-
play: function(container) {
|
|
10942
|
-
const inst = this.instances.get(container);
|
|
10943
|
-
if (!inst) return;
|
|
10944
|
-
if (!inst.audio.src && inst.state.tracks.length) {
|
|
10945
|
-
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
10946
|
-
}
|
|
10947
|
-
inst.audio.play().catch(() => {
|
|
10948
|
-
});
|
|
10949
|
-
},
|
|
10950
|
-
/**
|
|
10951
|
-
* @param {HTMLElement} container
|
|
10952
|
-
*/
|
|
10953
|
-
pause: function(container) {
|
|
10954
|
-
const inst = this.instances.get(container);
|
|
10955
|
-
if (inst) inst.audio.pause();
|
|
10956
|
-
},
|
|
10957
|
-
/**
|
|
10958
|
-
* @param {HTMLElement} container
|
|
10959
|
-
*/
|
|
10960
|
-
toggle: function(container) {
|
|
10961
|
-
const inst = this.instances.get(container);
|
|
10962
|
-
if (!inst) return;
|
|
10963
|
-
if (inst.state.isPlaying) {
|
|
10964
|
-
this.pause(container);
|
|
10965
|
-
} else {
|
|
10966
|
-
this.play(container);
|
|
10967
|
-
}
|
|
10968
|
-
},
|
|
10969
|
-
/**
|
|
10970
|
-
* @param {HTMLElement} container
|
|
10971
|
-
*/
|
|
10972
|
-
next: function(container) {
|
|
10973
|
-
const inst = this.instances.get(container);
|
|
10974
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10975
|
-
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
10976
|
-
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
10977
|
-
},
|
|
10978
|
-
/**
|
|
10979
|
-
* @param {HTMLElement} container
|
|
10980
|
-
*/
|
|
10981
|
-
previous: function(container) {
|
|
10982
|
-
const inst = this.instances.get(container);
|
|
10983
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10984
|
-
const len = inst.state.tracks.length;
|
|
10985
|
-
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
10986
|
-
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
10987
|
-
},
|
|
10988
|
-
/**
|
|
10989
|
-
* @param {HTMLElement} container
|
|
10990
|
-
* @param {number} value - 0 to 1
|
|
10991
|
-
*/
|
|
10992
|
-
setVolume: function(container, value) {
|
|
10993
|
-
const inst = this.instances.get(container);
|
|
10994
|
-
if (!inst) return;
|
|
10995
|
-
const v = Math.max(0, Math.min(1, value));
|
|
10996
|
-
inst.state.volume = v;
|
|
10997
|
-
inst.audio.volume = v;
|
|
10998
|
-
if (inst.refs.volumeSlider) {
|
|
10999
|
-
inst.refs.volumeSlider.value = String(v);
|
|
11000
|
-
updateRangeFill(inst.refs.volumeSlider);
|
|
11001
|
-
}
|
|
11002
|
-
container.dispatchEvent(
|
|
11003
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
11004
|
-
);
|
|
11005
|
-
},
|
|
11006
|
-
/**
|
|
11007
|
-
* @param {HTMLElement} container
|
|
11008
|
-
* @param {number} index - Track index
|
|
11009
|
-
*/
|
|
11010
|
-
setTrack: function(container, index) {
|
|
11011
|
-
const inst = this.instances.get(container);
|
|
11012
|
-
if (!inst) return;
|
|
11013
|
-
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
11014
|
-
},
|
|
11015
|
-
/**
|
|
11016
|
-
* Shuffle or un-shuffle the track list.
|
|
11017
|
-
* @param {HTMLElement} container
|
|
11018
|
-
*/
|
|
11019
|
-
shuffle: function(container) {
|
|
11020
|
-
const inst = this.instances.get(container);
|
|
11021
|
-
if (!inst || !inst.refs.btnShuffle) return;
|
|
11022
|
-
inst.refs.btnShuffle.click();
|
|
11023
|
-
},
|
|
11024
|
-
/**
|
|
11025
|
-
* Float the player above the page. Requires { detachable: true } at init.
|
|
11026
|
-
* @param {HTMLElement} container
|
|
11027
|
-
* @param {string} [position] 'bottom-left' or 'bottom-right'
|
|
11028
|
-
*/
|
|
11029
|
-
detach: function(container, position) {
|
|
11030
|
-
const inst = this.instances.get(container);
|
|
11031
|
-
if (!inst || !inst.state.detachable || inst.state.isDetached) return;
|
|
11032
|
-
const s = inst.state;
|
|
11033
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11034
|
-
s.isDetached = true;
|
|
11035
|
-
inst.ui.restore = {
|
|
11036
|
-
parent: container.parentNode,
|
|
11037
|
-
next: container.nextSibling
|
|
11038
|
-
};
|
|
11039
|
-
document.body.appendChild(container);
|
|
11040
|
-
container.classList.add("vd-music-player-floating", "vd-music-player-detached");
|
|
11041
|
-
const pos = position != null && position !== void 0 ? position : s.floatingPosition;
|
|
11042
|
-
this._setCornerPosition(
|
|
11043
|
-
container,
|
|
11044
|
-
pos === "bottom-left" || pos === "bottom-right" ? pos : "bottom-right"
|
|
11045
|
-
);
|
|
11046
|
-
this._loadPersistedPosition(container, inst);
|
|
11047
|
-
if (s.startMinimized && !s._startMinimizeApplied) {
|
|
11048
|
-
s._startMinimizeApplied = true;
|
|
11049
|
-
this.minimize(container);
|
|
11050
|
-
}
|
|
11051
|
-
this._bindFloatingDrag(inst);
|
|
11052
|
-
container.dispatchEvent(new CustomEvent("musicplayer:detach", { bubbles: true }));
|
|
11053
|
-
},
|
|
11054
|
-
/**
|
|
11055
|
-
* Return a detached player to its original place in the document.
|
|
11056
|
-
* @param {HTMLElement} container
|
|
11057
|
-
*/
|
|
11058
|
-
attach: function(container) {
|
|
11059
|
-
const inst = this.instances.get(container);
|
|
11060
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11061
|
-
this._unbindFloatingDrag(inst);
|
|
11062
|
-
inst.state.isDetached = false;
|
|
11063
|
-
const r = inst.ui && inst.ui.restore;
|
|
11064
|
-
container.classList.remove(
|
|
11065
|
-
"vd-music-player-floating",
|
|
11066
|
-
"vd-music-player-detached",
|
|
11067
|
-
"vd-music-player-floating-bottom-left",
|
|
11068
|
-
"vd-music-player-floating-bottom-right",
|
|
11069
|
-
"is-position-custom"
|
|
11070
|
-
);
|
|
11071
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11072
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11073
|
-
if (r && r.parent && r.parent.isConnected) {
|
|
11074
|
-
r.parent.insertBefore(container, r.next);
|
|
11075
|
-
}
|
|
11076
|
-
if (inst.ui) {
|
|
11077
|
-
inst.ui.restore = null;
|
|
11078
|
-
inst.ui.unbindDrag = null;
|
|
11079
|
-
}
|
|
11080
|
-
container.dispatchEvent(new CustomEvent("musicplayer:attach", { bubbles: true }));
|
|
11081
|
-
},
|
|
11082
|
-
/**
|
|
11083
|
-
* Collapse to essential controls. Requires { minimizable: true } at init.
|
|
11084
|
-
* @param {HTMLElement} container
|
|
11085
|
-
*/
|
|
11086
|
-
minimize: function(container) {
|
|
11087
|
-
const inst = this.instances.get(container);
|
|
11088
|
-
if (!inst || !inst.state.minimizable || inst.state.isMinimized) return;
|
|
11089
|
-
const s = inst.state;
|
|
11090
|
-
s.isMinimized = true;
|
|
11091
|
-
container.classList.add("vd-music-player-minimized");
|
|
11092
|
-
this._setMinimizeButtonState(inst, true);
|
|
11093
|
-
if (inst.refs.playlistPanel && inst.refs.playlistPanel.classList.contains("is-open") && inst.refs.btnPlaylist) {
|
|
11094
|
-
inst.refs.playlistPanel.classList.remove("is-open");
|
|
11095
|
-
inst.refs.btnPlaylist.classList.remove("is-active");
|
|
11096
|
-
inst.refs.btnPlaylist.setAttribute("aria-expanded", "false");
|
|
11097
|
-
}
|
|
11098
|
-
container.dispatchEvent(new CustomEvent("musicplayer:minimize", { bubbles: true }));
|
|
11099
|
-
},
|
|
11100
|
-
/**
|
|
11101
|
-
* Restore from minimized state.
|
|
11102
|
-
* @param {HTMLElement} container
|
|
11103
|
-
*/
|
|
11104
|
-
expand: function(container) {
|
|
11105
|
-
const inst = this.instances.get(container);
|
|
11106
|
-
if (!inst || !inst.state.minimizable || !inst.state.isMinimized) return;
|
|
11107
|
-
inst.state.isMinimized = false;
|
|
11108
|
-
container.classList.remove("vd-music-player-minimized");
|
|
11109
|
-
this._setMinimizeButtonState(inst, false);
|
|
11110
|
-
container.dispatchEvent(new CustomEvent("musicplayer:expand", { bubbles: true }));
|
|
11111
|
-
},
|
|
11112
|
-
/**
|
|
11113
|
-
* Toggle minimize / expand.
|
|
11114
|
-
* @param {HTMLElement} container
|
|
11115
|
-
*/
|
|
11116
|
-
toggleMinimize: function(container) {
|
|
11117
|
-
const inst = this.instances.get(container);
|
|
11118
|
-
if (!inst || !inst.state.minimizable) return;
|
|
11119
|
-
if (inst.state.isMinimized) {
|
|
11120
|
-
this.expand(container);
|
|
11121
|
-
} else {
|
|
11122
|
-
this.minimize(container);
|
|
11123
|
-
}
|
|
11124
|
-
},
|
|
11125
|
-
/**
|
|
11126
|
-
* Set floating corner or pixel position (detached only).
|
|
11127
|
-
* @param {HTMLElement} container
|
|
11128
|
-
* @param {string|{x:number,y:number}} position 'bottom-left' | 'bottom-right' | { x, y } viewport
|
|
11129
|
-
*/
|
|
11130
|
-
setPosition: function(container, position) {
|
|
11131
|
-
const inst = this.instances.get(container);
|
|
11132
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11133
|
-
if (typeof position === "string") {
|
|
11134
|
-
this._setCornerPosition(
|
|
11135
|
-
container,
|
|
11136
|
-
position === "bottom-left" || position === "bottom-right" ? position : "bottom-right"
|
|
11137
|
-
);
|
|
11138
|
-
} else if (position && typeof position.x === "number" && typeof position.y === "number") {
|
|
11139
|
-
this._setCustomPositionFromRect(container, position.x, position.y);
|
|
11140
|
-
}
|
|
11141
|
-
if (inst.state.persistPosition) {
|
|
11142
|
-
const r = container.getBoundingClientRect();
|
|
11143
|
-
this._savePositionPixels(inst, r.left, r.top);
|
|
11144
|
-
}
|
|
11145
|
-
},
|
|
11146
|
-
/**
|
|
11147
|
-
* @param {Object} inst
|
|
11148
|
-
* @param {boolean} minimized
|
|
11149
|
-
*/
|
|
11150
|
-
_setMinimizeButtonState: function(inst, minimized) {
|
|
11151
|
-
const b = inst.refs && inst.refs.btnMinimize;
|
|
11152
|
-
if (!b) return;
|
|
11153
|
-
b.innerHTML = "";
|
|
11154
|
-
b.appendChild(icon(minimized ? "plus" : "minus"));
|
|
11155
|
-
b.setAttribute("aria-label", minimized ? "Expand player" : "Minimize player");
|
|
11156
|
-
b.setAttribute("aria-expanded", minimized ? "false" : "true");
|
|
11157
|
-
},
|
|
11158
|
-
/**
|
|
11159
|
-
* @param {HTMLElement} container
|
|
11160
|
-
* @param {string} which 'bottom-left' | 'bottom-right'
|
|
11161
|
-
*/
|
|
11162
|
-
_setCornerPosition: function(container, which) {
|
|
11163
|
-
container.classList.remove("is-position-custom", "vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11164
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11165
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11166
|
-
if (which === "bottom-left") {
|
|
11167
|
-
container.classList.add("vd-music-player-floating-bottom-left");
|
|
11168
|
-
} else {
|
|
11169
|
-
container.classList.add("vd-music-player-floating-bottom-right");
|
|
11170
|
-
}
|
|
11171
|
-
},
|
|
11172
|
-
/**
|
|
11173
|
-
* @param {HTMLElement} container
|
|
11174
|
-
* @param {number} left
|
|
11175
|
-
* @param {number} top
|
|
11176
|
-
*/
|
|
11177
|
-
_setCustomPositionFromRect: function(container, left, top) {
|
|
11178
|
-
container.classList.remove("vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11179
|
-
container.classList.add("is-position-custom");
|
|
11180
|
-
container.style.setProperty("--vd-music-player-floating-left", left + "px");
|
|
11181
|
-
container.style.setProperty("--vd-music-player-floating-top", top + "px");
|
|
11182
|
-
},
|
|
11183
|
-
/**
|
|
11184
|
-
* @param {HTMLElement} container
|
|
11185
|
-
* @param {Object} inst
|
|
11186
|
-
*/
|
|
11187
|
-
_loadPersistedPosition: function(container, inst) {
|
|
11188
|
-
if (!inst.state.persistPosition) return;
|
|
11189
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11190
|
-
let raw = null;
|
|
11191
|
-
if (typeof window.safeStorageGet === "function") {
|
|
11192
|
-
raw = window.safeStorageGet(key, null);
|
|
11193
|
-
} else {
|
|
11194
|
-
try {
|
|
11195
|
-
raw = localStorage.getItem(key);
|
|
11196
|
-
} catch (_e) {
|
|
11197
|
-
}
|
|
11198
|
-
}
|
|
11199
|
-
if (!raw) return;
|
|
11200
|
-
try {
|
|
11201
|
-
const o = JSON.parse(raw);
|
|
11202
|
-
if (o && typeof o.x === "number" && typeof o.y === "number") {
|
|
11203
|
-
this._setCustomPositionFromRect(container, o.x, o.y);
|
|
11204
|
-
}
|
|
11205
|
-
} catch (_err) {
|
|
11206
|
-
}
|
|
11207
|
-
},
|
|
11208
|
-
/**
|
|
11209
|
-
* @param {Object} inst
|
|
11210
|
-
* @param {number} x
|
|
11211
|
-
* @param {number} y
|
|
11212
|
-
*/
|
|
11213
|
-
_savePositionPixels: function(inst, x, y) {
|
|
11214
|
-
if (!inst.state.persistPosition) return;
|
|
11215
|
-
const container = this._containerOf(inst);
|
|
11216
|
-
if (!container) return;
|
|
11217
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11218
|
-
const val = JSON.stringify({ x, y });
|
|
11219
|
-
if (typeof window.safeStorageSet === "function") {
|
|
11220
|
-
window.safeStorageSet(key, val);
|
|
11221
|
-
} else {
|
|
11222
|
-
try {
|
|
11223
|
-
localStorage.setItem(key, val);
|
|
11224
|
-
} catch (_e) {
|
|
11225
|
-
}
|
|
11226
|
-
}
|
|
11227
|
-
},
|
|
11228
|
-
/**
|
|
11229
|
-
* @param {Object} inst
|
|
11230
|
-
* @param {HTMLElement} container
|
|
11231
|
-
* @returns {string}
|
|
11232
|
-
*/
|
|
11233
|
-
_persistKeyForInstance: function(inst, container) {
|
|
11234
|
-
const pk = inst.state.persistKey;
|
|
11235
|
-
if (pk && String(pk).trim()) return persistStorageKey(String(pk).trim());
|
|
11236
|
-
return persistStorageKey(container.id || "");
|
|
11237
|
-
},
|
|
11238
|
-
/**
|
|
11239
|
-
* @param {Object} inst
|
|
11240
|
-
*/
|
|
11241
|
-
_unbindFloatingDrag: function(inst) {
|
|
11242
|
-
if (inst.ui && typeof inst.ui.unbindDrag === "function") {
|
|
11243
|
-
inst.ui.unbindDrag();
|
|
11244
|
-
inst.ui.unbindDrag = null;
|
|
11245
|
-
}
|
|
11246
|
-
},
|
|
11247
|
-
/**
|
|
11248
|
-
* Free-form pointer drag on the handle. Vanduo's `draggable` component uses HTML5
|
|
11249
|
-
* drag/drop for list reordering; floating players use pointer events on the handle instead.
|
|
11250
|
-
* @param {Object} inst
|
|
11251
|
-
*/
|
|
11252
|
-
_bindFloatingDrag: function(inst) {
|
|
11253
|
-
this._unbindFloatingDrag(inst);
|
|
11254
|
-
const h = inst.refs && inst.refs.dragHandle;
|
|
11255
|
-
if (!h || !inst.state || !inst.state.draggable) return;
|
|
11256
|
-
const self = this;
|
|
11257
|
-
const container = this._containerOf(inst);
|
|
11258
|
-
if (!container) return;
|
|
11259
|
-
let startX = 0;
|
|
11260
|
-
let startY = 0;
|
|
11261
|
-
let origL = 0;
|
|
11262
|
-
let origT = 0;
|
|
11263
|
-
let activeDrag = false;
|
|
11264
|
-
const onDown = function(e) {
|
|
11265
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
11266
|
-
e.preventDefault();
|
|
11267
|
-
activeDrag = true;
|
|
11268
|
-
const r = container.getBoundingClientRect();
|
|
11269
|
-
origL = r.left;
|
|
11270
|
-
origT = r.top;
|
|
11271
|
-
startX = e.clientX;
|
|
11272
|
-
startY = e.clientY;
|
|
11273
|
-
self._setCustomPositionFromRect(container, origL, origT);
|
|
11274
|
-
try {
|
|
11275
|
-
h.setPointerCapture(e.pointerId);
|
|
11276
|
-
} catch (_err) {
|
|
11277
|
-
}
|
|
11278
|
-
};
|
|
11279
|
-
const onMove = function(e) {
|
|
11280
|
-
if (!activeDrag) return;
|
|
11281
|
-
const dx = e.clientX - startX;
|
|
11282
|
-
const dy = e.clientY - startY;
|
|
11283
|
-
let nl = origL + dx;
|
|
11284
|
-
let nt = origT + dy;
|
|
11285
|
-
const r = container.getBoundingClientRect();
|
|
11286
|
-
const w = r.width;
|
|
11287
|
-
const ph = r.height;
|
|
11288
|
-
const vw = window.innerWidth;
|
|
11289
|
-
const vh = window.innerHeight;
|
|
11290
|
-
const pad = 8;
|
|
11291
|
-
nl = Math.max(pad, Math.min(nl, vw - w - pad));
|
|
11292
|
-
nt = Math.max(pad, Math.min(nt, vh - ph - pad));
|
|
11293
|
-
self._setCustomPositionFromRect(container, nl, nt);
|
|
11294
|
-
};
|
|
11295
|
-
const onUp = function(e) {
|
|
11296
|
-
if (!activeDrag) return;
|
|
11297
|
-
activeDrag = false;
|
|
11298
|
-
if (typeof h.hasPointerCapture === "function" && h.hasPointerCapture(e.pointerId)) {
|
|
11299
|
-
try {
|
|
11300
|
-
h.releasePointerCapture(e.pointerId);
|
|
11301
|
-
} catch (_e) {
|
|
11302
|
-
}
|
|
11303
|
-
}
|
|
11304
|
-
if (inst.state.persistPosition) {
|
|
11305
|
-
const r = container.getBoundingClientRect();
|
|
11306
|
-
self._savePositionPixels(inst, r.left, r.top);
|
|
11307
|
-
}
|
|
11308
|
-
};
|
|
11309
|
-
h.addEventListener("pointerdown", onDown);
|
|
11310
|
-
h.addEventListener("pointermove", onMove);
|
|
11311
|
-
h.addEventListener("pointerup", onUp);
|
|
11312
|
-
h.addEventListener("pointercancel", onUp);
|
|
11313
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11314
|
-
inst.ui.unbindDrag = function() {
|
|
11315
|
-
h.removeEventListener("pointerdown", onDown);
|
|
11316
|
-
h.removeEventListener("pointermove", onMove);
|
|
11317
|
-
h.removeEventListener("pointerup", onUp);
|
|
11318
|
-
h.removeEventListener("pointercancel", onUp);
|
|
11319
|
-
};
|
|
11320
|
-
},
|
|
11321
|
-
/**
|
|
11322
|
-
* Return a shallow copy of the current player state.
|
|
11323
|
-
* @param {HTMLElement} container
|
|
11324
|
-
* @returns {Object|null}
|
|
11325
|
-
*/
|
|
11326
|
-
getState: function(container) {
|
|
11327
|
-
const inst = this.instances.get(container);
|
|
11328
|
-
if (!inst) return null;
|
|
11329
|
-
const s = inst.state;
|
|
11330
|
-
return {
|
|
11331
|
-
isPlaying: s.isPlaying,
|
|
11332
|
-
currentIndex: s.currentIndex,
|
|
11333
|
-
currentTrack: s.tracks[s.currentIndex] || null,
|
|
11334
|
-
volume: s.volume,
|
|
11335
|
-
shuffle: s.shuffle,
|
|
11336
|
-
tracks: s.tracks.slice(),
|
|
11337
|
-
isDetached: Boolean(s.isDetached),
|
|
11338
|
-
isMinimized: Boolean(s.isMinimized)
|
|
11339
|
-
};
|
|
11340
|
-
},
|
|
11341
|
-
/**
|
|
11342
|
-
* Stop playback, clean up listeners, remove instance.
|
|
11343
|
-
* @param {HTMLElement} container
|
|
11344
|
-
*/
|
|
11345
|
-
destroy: function(container) {
|
|
11346
|
-
const inst = this.instances.get(container);
|
|
11347
|
-
if (!inst) return;
|
|
11348
|
-
this._unbindFloatingDrag(inst);
|
|
11349
|
-
if (inst.state && inst.state.isDetached) {
|
|
11350
|
-
try {
|
|
11351
|
-
this.attach(container);
|
|
11352
|
-
} catch (_e) {
|
|
11353
|
-
}
|
|
11354
|
-
}
|
|
11355
|
-
inst.cleanup.forEach((fn) => fn());
|
|
11356
|
-
this.instances.delete(container);
|
|
11357
|
-
container.removeAttribute("data-music-player-initialized");
|
|
11358
|
-
},
|
|
11359
|
-
/**
|
|
11360
|
-
* Destroy all instances.
|
|
11361
|
-
*/
|
|
11362
|
-
destroyAll: function() {
|
|
11363
|
-
this.instances.forEach((_, container) => this.destroy(container));
|
|
11364
|
-
},
|
|
11365
|
-
/* ─── Internal helpers ────────────────────────────────── */
|
|
11366
|
-
/**
|
|
11367
|
-
* Load track by index on an already-initialised instance object.
|
|
11368
|
-
* @param {Object} inst
|
|
11369
|
-
* @param {number} index
|
|
11370
|
-
* @param {boolean} autoPlay
|
|
11371
|
-
*/
|
|
11372
|
-
_loadTrack: function(inst, index, autoPlay) {
|
|
11373
|
-
const track = inst.state.tracks[index];
|
|
11374
|
-
if (!track) return;
|
|
11375
|
-
const container = this._containerOf(inst);
|
|
11376
|
-
inst.state.currentIndex = index;
|
|
11377
|
-
inst.audio.src = track.url;
|
|
11378
|
-
if (inst.refs.trackName) {
|
|
11379
|
-
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
11380
|
-
inst.refs.trackName.classList.remove("is-idle");
|
|
11381
|
-
}
|
|
11382
|
-
if (inst.refs.playlistPanel) {
|
|
11383
|
-
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
11384
|
-
const active = i === index;
|
|
11385
|
-
item.classList.toggle("is-active", active);
|
|
11386
|
-
item.setAttribute("aria-current", active ? "true" : "false");
|
|
11387
|
-
});
|
|
11388
|
-
}
|
|
11389
|
-
if (inst.refs.progressBar) {
|
|
11390
|
-
inst.refs.progressBar.value = "0";
|
|
11391
|
-
updateRangeFill(inst.refs.progressBar);
|
|
11392
|
-
}
|
|
11393
|
-
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
11394
|
-
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
11395
|
-
if (container) {
|
|
11396
|
-
container.dispatchEvent(
|
|
11397
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
11398
|
-
bubbles: true,
|
|
11399
|
-
detail: { index, name: track.name, url: track.url }
|
|
11400
|
-
})
|
|
11401
|
-
);
|
|
11402
|
-
}
|
|
11403
|
-
if (autoPlay) inst.audio.play().catch(() => {
|
|
11404
|
-
});
|
|
11405
|
-
},
|
|
11406
|
-
/**
|
|
11407
|
-
* Reverse-lookup the container element for a given instance object.
|
|
11408
|
-
* @param {Object} inst
|
|
11409
|
-
* @returns {HTMLElement|null}
|
|
11410
|
-
*/
|
|
11411
|
-
_containerOf: function(inst) {
|
|
11412
|
-
for (const [container, i] of this.instances) {
|
|
11413
|
-
if (i === inst) return container;
|
|
11414
|
-
}
|
|
11415
|
-
return null;
|
|
11416
|
-
}
|
|
11417
|
-
};
|
|
11418
|
-
if (typeof window.Vanduo !== "undefined") {
|
|
11419
|
-
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
11420
|
-
}
|
|
11421
|
-
window.VanduoMusicPlayer = MusicPlayer;
|
|
11422
|
-
})();
|
|
11423
|
-
|
|
11424
10654
|
// js/index.js
|
|
11425
10655
|
var Vanduo = window.Vanduo;
|
|
11426
10656
|
var index_default = Vanduo;
|