@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.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
|
(() => {
|
|
3
3
|
// js/utils/lifecycle.js
|
|
4
4
|
(function() {
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
// js/vanduo.js
|
|
177
177
|
(function() {
|
|
178
178
|
"use strict";
|
|
179
|
-
const VANDUO_VERSION = true ? "1.4.
|
|
179
|
+
const VANDUO_VERSION = true ? "1.4.4" : "0.0.0-dev";
|
|
180
180
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
181
181
|
const Vanduo2 = {
|
|
182
182
|
version: VANDUO_VERSION,
|
|
@@ -461,9 +461,10 @@
|
|
|
461
461
|
init: function(root) {
|
|
462
462
|
const snippets = this.queryWithin(root, ".vd-code-snippet");
|
|
463
463
|
snippets.forEach((snippet) => {
|
|
464
|
-
if (
|
|
465
|
-
|
|
464
|
+
if (snippet.dataset.initialized === "true") {
|
|
465
|
+
return;
|
|
466
466
|
}
|
|
467
|
+
this.initSnippet(snippet);
|
|
467
468
|
});
|
|
468
469
|
},
|
|
469
470
|
/**
|
|
@@ -471,7 +472,9 @@
|
|
|
471
472
|
* @param {HTMLElement} snippet - Code snippet container element
|
|
472
473
|
*/
|
|
473
474
|
initSnippet: function(snippet) {
|
|
474
|
-
snippet.dataset.initialized
|
|
475
|
+
if (snippet.dataset.initialized === "true") {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
475
478
|
snippet._codeSnippetCleanup = [];
|
|
476
479
|
const toggle = snippet.querySelector(".vd-code-snippet-toggle");
|
|
477
480
|
const content = snippet.querySelector(".vd-code-snippet-content");
|
|
@@ -499,6 +502,7 @@
|
|
|
499
502
|
lineNumberPanes.forEach((pane) => {
|
|
500
503
|
this.addLineNumbers(pane);
|
|
501
504
|
});
|
|
505
|
+
snippet.dataset.initialized = "true";
|
|
502
506
|
},
|
|
503
507
|
/**
|
|
504
508
|
* Initialize collapsible functionality
|
|
@@ -508,13 +512,14 @@
|
|
|
508
512
|
*/
|
|
509
513
|
initCollapsible: function(snippet, toggle, content) {
|
|
510
514
|
const isExpanded = snippet.dataset.expanded === "true";
|
|
511
|
-
toggle.setAttribute("aria-expanded", isExpanded);
|
|
512
|
-
content.dataset.visible = isExpanded;
|
|
515
|
+
toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
|
|
516
|
+
content.dataset.visible = isExpanded ? "true" : "false";
|
|
513
517
|
this.addListener(snippet, toggle, "click", () => {
|
|
514
518
|
const expanded = snippet.dataset.expanded === "true";
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
519
|
+
const nextExpanded = !expanded;
|
|
520
|
+
snippet.dataset.expanded = nextExpanded ? "true" : "false";
|
|
521
|
+
toggle.setAttribute("aria-expanded", nextExpanded ? "true" : "false");
|
|
522
|
+
content.dataset.visible = nextExpanded ? "true" : "false";
|
|
518
523
|
if (!expanded) {
|
|
519
524
|
const extractPanes = content.querySelectorAll("[data-extract]:not([data-extracted])");
|
|
520
525
|
extractPanes.forEach((pane) => {
|
|
@@ -849,12 +854,38 @@
|
|
|
849
854
|
* @returns {string} HTML with syntax highlighting spans
|
|
850
855
|
*/
|
|
851
856
|
highlightHtml: function(html) {
|
|
852
|
-
html = html.replace(/(<\/?)([\w-]+)/g, '$1<span class="code-tag">$2</span>');
|
|
853
|
-
html = html.replace(/([\w-]+)(=)(["'])/g, '<span class="code-attr">$1</span>$2$3');
|
|
854
|
-
html = html.replace(/([\w-]+)(=)("|')/g, '<span class="code-attr">$1</span>$2$3');
|
|
855
|
-
html = html.replace(/(["'])([^"']*)(["'])/g, '$1<span class="code-string">$2</span>$3');
|
|
856
|
-
html = html.replace(/("|')([^&]*)("|')/g, '$1<span class="code-string">$2</span>$3');
|
|
857
857
|
html = html.replace(/(<!--)(.*?)(-->)/g, '<span class="code-comment">$1$2$3</span>');
|
|
858
|
+
html = html.replace(/<(?!--)([\s\S]*?)>/g, function(match, inner) {
|
|
859
|
+
const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
|
|
860
|
+
if (closingMatch) {
|
|
861
|
+
return "<" + closingMatch[1] + '<span class="code-tag">' + closingMatch[2] + "</span>>";
|
|
862
|
+
}
|
|
863
|
+
const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
|
|
864
|
+
const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : "";
|
|
865
|
+
const tagSource = selfClosingMatch ? inner.slice(0, inner.length - selfClosingMatch[0].length) : inner;
|
|
866
|
+
const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
|
|
867
|
+
if (!openMatch) {
|
|
868
|
+
return match;
|
|
869
|
+
}
|
|
870
|
+
const tagName = openMatch[1];
|
|
871
|
+
let attrSource = openMatch[2];
|
|
872
|
+
attrSource = attrSource.replace(
|
|
873
|
+
/([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
|
|
874
|
+
function(attrMatch, attrName, separator, quote, value) {
|
|
875
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + quote + '<span class="code-string">' + value + "</span>" + quote;
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
attrSource = attrSource.replace(
|
|
879
|
+
/([\w:-]+)(\s*=\s*)("|')([\s\S]*?)("|')/g,
|
|
880
|
+
function(attrMatch, attrName, separator, openQuote, value, closeQuote) {
|
|
881
|
+
if (openQuote !== closeQuote) {
|
|
882
|
+
return attrMatch;
|
|
883
|
+
}
|
|
884
|
+
return '<span class="code-attr">' + attrName + "</span>" + separator + openQuote + '<span class="code-string">' + value + "</span>" + closeQuote;
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
return '<<span class="code-tag">' + tagName + "</span>" + attrSource + selfClosingSuffix + ">";
|
|
888
|
+
});
|
|
858
889
|
return html;
|
|
859
890
|
},
|
|
860
891
|
/**
|
|
@@ -876,16 +907,26 @@
|
|
|
876
907
|
* @returns {string} JS with syntax highlighting spans
|
|
877
908
|
*/
|
|
878
909
|
highlightJs: function(js) {
|
|
910
|
+
const protectedTokens = [];
|
|
911
|
+
const protectToken = (value, className) => {
|
|
912
|
+
const marker = String.fromCharCode(57344 + protectedTokens.length);
|
|
913
|
+
protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + "</span>" });
|
|
914
|
+
return marker;
|
|
915
|
+
};
|
|
916
|
+
js = js.replace(
|
|
917
|
+
/\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
|
|
918
|
+
(match) => protectToken(match, match.startsWith("/") ? "code-comment" : "code-string")
|
|
919
|
+
);
|
|
879
920
|
const keywords = ["const", "let", "var", "function", "return", "if", "else", "for", "while", "switch", "case", "break", "continue", "new", "this", "class", "extends", "import", "export", "default", "async", "await", "try", "catch", "throw", "typeof", "instanceof"];
|
|
880
921
|
keywords.forEach((kw) => {
|
|
881
922
|
const regex = new RegExp(`\\b(${kw})\\b`, "g");
|
|
882
923
|
js = js.replace(regex, '<span class="code-keyword">$1</span>');
|
|
883
924
|
});
|
|
884
|
-
js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
|
|
885
925
|
js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
|
|
886
926
|
js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
|
|
887
|
-
|
|
888
|
-
|
|
927
|
+
protectedTokens.forEach((token) => {
|
|
928
|
+
js = js.replace(token.marker, token.html);
|
|
929
|
+
});
|
|
889
930
|
return js;
|
|
890
931
|
},
|
|
891
932
|
/**
|
|
@@ -5009,21 +5050,45 @@
|
|
|
5009
5050
|
// js/components/theme-switcher.js
|
|
5010
5051
|
(function() {
|
|
5011
5052
|
"use strict";
|
|
5053
|
+
const THEME_MODES = ["system", "light", "dark"];
|
|
5054
|
+
const THEME_ICON_CLASSES = {
|
|
5055
|
+
system: "ph ph-desktop",
|
|
5056
|
+
light: "ph ph-sun",
|
|
5057
|
+
dark: "ph ph-moon"
|
|
5058
|
+
};
|
|
5059
|
+
const THEME_LABELS = {
|
|
5060
|
+
system: "Theme: System",
|
|
5061
|
+
light: "Theme: Light",
|
|
5062
|
+
dark: "Theme: Dark"
|
|
5063
|
+
};
|
|
5064
|
+
const THEME_OPTION_TOOLTIPS = {
|
|
5065
|
+
system: "Use system preference",
|
|
5066
|
+
light: "Light theme",
|
|
5067
|
+
dark: "Dark theme"
|
|
5068
|
+
};
|
|
5012
5069
|
const ThemeSwitcher = {
|
|
5013
5070
|
isInitialized: false,
|
|
5014
5071
|
_mediaQuery: null,
|
|
5015
5072
|
_onMediaChange: null,
|
|
5073
|
+
menuInstances: /* @__PURE__ */ new Map(),
|
|
5016
5074
|
getToggles: function(root) {
|
|
5075
|
+
const scope = root || document;
|
|
5076
|
+
const toggles = window.Vanduo && typeof window.Vanduo.queryAll === "function" ? window.Vanduo.queryAll(scope, '[data-toggle="theme"]') : Array.from(scope.querySelectorAll('[data-toggle="theme"]'));
|
|
5077
|
+
return toggles.filter(function(toggle) {
|
|
5078
|
+
return !toggle.closest('.vd-theme-switcher[data-theme-ui="menu"]');
|
|
5079
|
+
});
|
|
5080
|
+
},
|
|
5081
|
+
getMenuSwitchers: function(root) {
|
|
5082
|
+
const scope = root || document;
|
|
5017
5083
|
if (window.Vanduo && typeof window.Vanduo.queryAll === "function") {
|
|
5018
|
-
return window.Vanduo.queryAll(
|
|
5084
|
+
return window.Vanduo.queryAll(scope, '.vd-theme-switcher[data-theme-ui="menu"]');
|
|
5019
5085
|
}
|
|
5020
|
-
return Array.from(
|
|
5086
|
+
return Array.from(scope.querySelectorAll('.vd-theme-switcher[data-theme-ui="menu"]'));
|
|
5021
5087
|
},
|
|
5022
5088
|
init: function(root) {
|
|
5023
5089
|
this.STORAGE_KEY = "vanduo-theme-preference";
|
|
5024
5090
|
this.state = {
|
|
5025
5091
|
preference: this.getPreference()
|
|
5026
|
-
// 'light', 'dark', or 'system'
|
|
5027
5092
|
};
|
|
5028
5093
|
if (this.isInitialized) {
|
|
5029
5094
|
this.applyTheme();
|
|
@@ -5040,6 +5105,9 @@
|
|
|
5040
5105
|
return this.getStorageValue(this.STORAGE_KEY, "system");
|
|
5041
5106
|
},
|
|
5042
5107
|
setPreference: function(pref) {
|
|
5108
|
+
if (!THEME_MODES.includes(pref)) {
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5043
5111
|
this.state.preference = pref;
|
|
5044
5112
|
this.setStorageValue(this.STORAGE_KEY, pref);
|
|
5045
5113
|
this.applyTheme();
|
|
@@ -5093,8 +5161,8 @@
|
|
|
5093
5161
|
};
|
|
5094
5162
|
this._mediaQuery.addEventListener("change", this._onMediaChange);
|
|
5095
5163
|
},
|
|
5096
|
-
// Helper to facilitate UI creation if needed, though often UI is in HTML
|
|
5097
5164
|
renderUI: function(root) {
|
|
5165
|
+
this.renderMenuSwitchers(root);
|
|
5098
5166
|
const toggles = this.getToggles(root);
|
|
5099
5167
|
toggles.forEach((toggle) => {
|
|
5100
5168
|
if (toggle.getAttribute("data-theme-initialized") === "true") {
|
|
@@ -5112,7 +5180,7 @@
|
|
|
5112
5180
|
toggle._themeToggleHandler = onChange;
|
|
5113
5181
|
} else {
|
|
5114
5182
|
const onClick = () => {
|
|
5115
|
-
const modes =
|
|
5183
|
+
const modes = THEME_MODES;
|
|
5116
5184
|
const nextIndex = (modes.indexOf(this.state.preference) + 1) % modes.length;
|
|
5117
5185
|
this.setPreference(modes[nextIndex]);
|
|
5118
5186
|
};
|
|
@@ -5121,6 +5189,188 @@
|
|
|
5121
5189
|
}
|
|
5122
5190
|
toggle.setAttribute("data-theme-initialized", "true");
|
|
5123
5191
|
});
|
|
5192
|
+
this.updateUI(root);
|
|
5193
|
+
},
|
|
5194
|
+
renderMenuSwitchers: function(root) {
|
|
5195
|
+
const switchers = this.getMenuSwitchers(root);
|
|
5196
|
+
switchers.forEach((switcher) => {
|
|
5197
|
+
if (switcher.getAttribute("data-theme-menu-initialized") === "true") {
|
|
5198
|
+
return;
|
|
5199
|
+
}
|
|
5200
|
+
const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
|
|
5201
|
+
const menu = switcher.querySelector(".vd-theme-switcher-menu");
|
|
5202
|
+
if (!toggle || !menu) {
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
const options = menu.querySelectorAll("[data-theme-value]");
|
|
5206
|
+
const cleanupFunctions = [];
|
|
5207
|
+
toggle.setAttribute("aria-haspopup", "true");
|
|
5208
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
5209
|
+
menu.setAttribute("aria-hidden", "true");
|
|
5210
|
+
const toggleClickHandler = (e) => {
|
|
5211
|
+
e.preventDefault();
|
|
5212
|
+
e.stopPropagation();
|
|
5213
|
+
this.toggleMenu(switcher, toggle, menu);
|
|
5214
|
+
};
|
|
5215
|
+
toggle.addEventListener("click", toggleClickHandler);
|
|
5216
|
+
cleanupFunctions.push(() => toggle.removeEventListener("click", toggleClickHandler));
|
|
5217
|
+
options.forEach((option) => {
|
|
5218
|
+
const optionClickHandler = (e) => {
|
|
5219
|
+
e.preventDefault();
|
|
5220
|
+
e.stopPropagation();
|
|
5221
|
+
const value = option.getAttribute("data-theme-value");
|
|
5222
|
+
if (value) {
|
|
5223
|
+
this.setPreference(value);
|
|
5224
|
+
}
|
|
5225
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5226
|
+
};
|
|
5227
|
+
option.addEventListener("click", optionClickHandler);
|
|
5228
|
+
cleanupFunctions.push(() => option.removeEventListener("click", optionClickHandler));
|
|
5229
|
+
const optionKeydownHandler = (e) => {
|
|
5230
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
5231
|
+
e.preventDefault();
|
|
5232
|
+
optionClickHandler(e);
|
|
5233
|
+
}
|
|
5234
|
+
};
|
|
5235
|
+
option.addEventListener("keydown", optionKeydownHandler);
|
|
5236
|
+
cleanupFunctions.push(() => option.removeEventListener("keydown", optionKeydownHandler));
|
|
5237
|
+
});
|
|
5238
|
+
const toggleKeydownHandler = (e) => {
|
|
5239
|
+
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
5240
|
+
e.preventDefault();
|
|
5241
|
+
if (!menu.classList.contains("is-open")) {
|
|
5242
|
+
this.openMenu(switcher, toggle, menu);
|
|
5243
|
+
}
|
|
5244
|
+
} else if (e.key === "Escape" && menu.classList.contains("is-open")) {
|
|
5245
|
+
e.preventDefault();
|
|
5246
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5247
|
+
}
|
|
5248
|
+
};
|
|
5249
|
+
toggle.addEventListener("keydown", toggleKeydownHandler);
|
|
5250
|
+
cleanupFunctions.push(() => toggle.removeEventListener("keydown", toggleKeydownHandler));
|
|
5251
|
+
const menuKeydownHandler = (e) => {
|
|
5252
|
+
this.handleMenuKeydown(e, switcher, toggle, menu, options);
|
|
5253
|
+
};
|
|
5254
|
+
menu.addEventListener("keydown", menuKeydownHandler);
|
|
5255
|
+
cleanupFunctions.push(() => menu.removeEventListener("keydown", menuKeydownHandler));
|
|
5256
|
+
const documentClickHandler = (e) => {
|
|
5257
|
+
if (!switcher.contains(e.target) && menu.classList.contains("is-open")) {
|
|
5258
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5259
|
+
}
|
|
5260
|
+
};
|
|
5261
|
+
document.addEventListener("click", documentClickHandler);
|
|
5262
|
+
cleanupFunctions.push(() => document.removeEventListener("click", documentClickHandler));
|
|
5263
|
+
this.menuInstances.set(switcher, { toggle, menu, cleanup: cleanupFunctions });
|
|
5264
|
+
switcher.setAttribute("data-theme-menu-initialized", "true");
|
|
5265
|
+
this.initMenuTooltips(switcher);
|
|
5266
|
+
});
|
|
5267
|
+
},
|
|
5268
|
+
initMenuTooltips: function(switcher) {
|
|
5269
|
+
const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
|
|
5270
|
+
if (tooltips && typeof tooltips.init === "function") {
|
|
5271
|
+
tooltips.init(switcher);
|
|
5272
|
+
}
|
|
5273
|
+
},
|
|
5274
|
+
closeOtherMenus: function(exceptMenu) {
|
|
5275
|
+
this.menuInstances.forEach((instance, switcher) => {
|
|
5276
|
+
if (instance.menu !== exceptMenu && instance.menu.classList.contains("is-open")) {
|
|
5277
|
+
this.closeMenu(switcher, instance.toggle, instance.menu);
|
|
5278
|
+
}
|
|
5279
|
+
});
|
|
5280
|
+
},
|
|
5281
|
+
toggleMenu: function(switcher, toggle, menu) {
|
|
5282
|
+
if (menu.classList.contains("is-open")) {
|
|
5283
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5284
|
+
} else {
|
|
5285
|
+
this.openMenu(switcher, toggle, menu);
|
|
5286
|
+
}
|
|
5287
|
+
},
|
|
5288
|
+
openMenu: function(switcher, toggle, menu) {
|
|
5289
|
+
this.closeOtherMenus(menu);
|
|
5290
|
+
switcher.classList.add("is-open");
|
|
5291
|
+
menu.classList.add("is-open");
|
|
5292
|
+
toggle.setAttribute("aria-expanded", "true");
|
|
5293
|
+
menu.setAttribute("aria-hidden", "false");
|
|
5294
|
+
const activeOption = menu.querySelector("[data-theme-value].is-active") || menu.querySelector("[data-theme-value]");
|
|
5295
|
+
if (activeOption) {
|
|
5296
|
+
setTimeout(() => activeOption.focus(), 0);
|
|
5297
|
+
}
|
|
5298
|
+
},
|
|
5299
|
+
closeMenu: function(switcher, toggle, menu) {
|
|
5300
|
+
switcher.classList.remove("is-open");
|
|
5301
|
+
menu.classList.remove("is-open");
|
|
5302
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
5303
|
+
menu.setAttribute("aria-hidden", "true");
|
|
5304
|
+
},
|
|
5305
|
+
handleMenuKeydown: function(e, switcher, toggle, menu, options) {
|
|
5306
|
+
const items = Array.from(options);
|
|
5307
|
+
const currentIndex = items.indexOf(document.activeElement);
|
|
5308
|
+
if (e.key === "Escape") {
|
|
5309
|
+
e.preventDefault();
|
|
5310
|
+
this.closeMenu(switcher, toggle, menu);
|
|
5311
|
+
toggle.focus();
|
|
5312
|
+
return;
|
|
5313
|
+
}
|
|
5314
|
+
if (e.key === "ArrowDown") {
|
|
5315
|
+
e.preventDefault();
|
|
5316
|
+
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
5317
|
+
items[nextIndex].focus();
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5320
|
+
if (e.key === "ArrowUp") {
|
|
5321
|
+
e.preventDefault();
|
|
5322
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
5323
|
+
items[prevIndex].focus();
|
|
5324
|
+
}
|
|
5325
|
+
},
|
|
5326
|
+
updateMenuSwitcher: function(switcher) {
|
|
5327
|
+
const toggle = switcher.querySelector(".vd-theme-switcher-toggle");
|
|
5328
|
+
const menu = switcher.querySelector(".vd-theme-switcher-menu");
|
|
5329
|
+
if (!toggle || !menu) {
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5332
|
+
const pref = this.state.preference;
|
|
5333
|
+
const icon = toggle.querySelector("[data-theme-icon]");
|
|
5334
|
+
const label = THEME_LABELS[pref] || THEME_LABELS.system;
|
|
5335
|
+
if (icon) {
|
|
5336
|
+
icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
|
|
5337
|
+
}
|
|
5338
|
+
toggle.setAttribute("aria-label", label);
|
|
5339
|
+
if (toggle.hasAttribute("data-tooltip")) {
|
|
5340
|
+
toggle.setAttribute("data-tooltip", label);
|
|
5341
|
+
this.refreshTooltipContent(toggle, label);
|
|
5342
|
+
}
|
|
5343
|
+
menu.querySelectorAll("[data-theme-value]").forEach((option) => {
|
|
5344
|
+
const value = option.getAttribute("data-theme-value");
|
|
5345
|
+
const isActive = value === pref;
|
|
5346
|
+
option.classList.toggle("is-active", isActive);
|
|
5347
|
+
option.setAttribute("aria-checked", isActive ? "true" : "false");
|
|
5348
|
+
const tooltipText = THEME_OPTION_TOOLTIPS[value];
|
|
5349
|
+
if (tooltipText && option.hasAttribute("data-tooltip")) {
|
|
5350
|
+
option.setAttribute("data-tooltip", tooltipText);
|
|
5351
|
+
this.refreshTooltipContent(option, tooltipText);
|
|
5352
|
+
}
|
|
5353
|
+
});
|
|
5354
|
+
},
|
|
5355
|
+
refreshTooltipContent: function(element, text) {
|
|
5356
|
+
const tooltips = window.Vanduo && typeof window.Vanduo.getComponent === "function" ? window.Vanduo.getComponent("tooltips") : null;
|
|
5357
|
+
if (!tooltips || typeof tooltips.update !== "function") {
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
tooltips.update(element, text);
|
|
5361
|
+
},
|
|
5362
|
+
updateCycleToggle: function(toggle) {
|
|
5363
|
+
const pref = this.state.preference;
|
|
5364
|
+
const icon = toggle.querySelector("[data-theme-icon]");
|
|
5365
|
+
const label = THEME_LABELS[pref] || THEME_LABELS.system;
|
|
5366
|
+
if (icon) {
|
|
5367
|
+
icon.className = THEME_ICON_CLASSES[pref] || THEME_ICON_CLASSES.system;
|
|
5368
|
+
}
|
|
5369
|
+
toggle.setAttribute("aria-label", label);
|
|
5370
|
+
if (toggle.hasAttribute("data-tooltip")) {
|
|
5371
|
+
toggle.setAttribute("data-tooltip", label);
|
|
5372
|
+
this.refreshTooltipContent(toggle, label);
|
|
5373
|
+
}
|
|
5124
5374
|
},
|
|
5125
5375
|
updateUI: function(root) {
|
|
5126
5376
|
const toggles = this.getToggles(root);
|
|
@@ -5132,11 +5382,26 @@
|
|
|
5132
5382
|
if (span) {
|
|
5133
5383
|
span.textContent = this.state.preference.charAt(0).toUpperCase() + this.state.preference.slice(1);
|
|
5134
5384
|
}
|
|
5385
|
+
if (toggle.querySelector("[data-theme-icon]")) {
|
|
5386
|
+
this.updateCycleToggle(toggle);
|
|
5387
|
+
}
|
|
5135
5388
|
}
|
|
5136
5389
|
});
|
|
5390
|
+
this.getMenuSwitchers(root).forEach((switcher) => {
|
|
5391
|
+
this.updateMenuSwitcher(switcher);
|
|
5392
|
+
});
|
|
5137
5393
|
},
|
|
5138
5394
|
destroyAll: function(root) {
|
|
5139
5395
|
const scope = root || document;
|
|
5396
|
+
this.getMenuSwitchers(scope).forEach((switcher) => {
|
|
5397
|
+
const instance = this.menuInstances.get(switcher);
|
|
5398
|
+
if (instance) {
|
|
5399
|
+
instance.cleanup.forEach((fn) => fn());
|
|
5400
|
+
this.closeMenu(switcher, instance.toggle, instance.menu);
|
|
5401
|
+
this.menuInstances.delete(switcher);
|
|
5402
|
+
}
|
|
5403
|
+
switcher.removeAttribute("data-theme-menu-initialized");
|
|
5404
|
+
});
|
|
5140
5405
|
const toggles = this.getToggles(scope).filter(function(toggle) {
|
|
5141
5406
|
return toggle.getAttribute("data-theme-initialized") === "true";
|
|
5142
5407
|
});
|
|
@@ -8431,11 +8696,6 @@
|
|
|
8431
8696
|
// js/components/suggest.js
|
|
8432
8697
|
(function() {
|
|
8433
8698
|
"use strict";
|
|
8434
|
-
function _escapeHtml(text) {
|
|
8435
|
-
const div = document.createElement("div");
|
|
8436
|
-
div.textContent = text;
|
|
8437
|
-
return div.innerHTML;
|
|
8438
|
-
}
|
|
8439
8699
|
function _isSafeUrl(url, allowlist) {
|
|
8440
8700
|
try {
|
|
8441
8701
|
const resolved = new URL(url, window.location.href);
|
|
@@ -8506,9 +8766,24 @@
|
|
|
8506
8766
|
li.id = listId + "-item-" + i;
|
|
8507
8767
|
const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
|
|
8508
8768
|
if (query) {
|
|
8509
|
-
const
|
|
8510
|
-
const
|
|
8511
|
-
|
|
8769
|
+
const lowerText = text.toLowerCase();
|
|
8770
|
+
const lowerQuery = query.toLowerCase();
|
|
8771
|
+
let start = 0;
|
|
8772
|
+
let matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8773
|
+
while (matchIndex !== -1) {
|
|
8774
|
+
if (matchIndex > start) {
|
|
8775
|
+
li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
|
|
8776
|
+
}
|
|
8777
|
+
const matchSpan = document.createElement("span");
|
|
8778
|
+
matchSpan.className = "vd-suggest-match";
|
|
8779
|
+
matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
|
|
8780
|
+
li.appendChild(matchSpan);
|
|
8781
|
+
start = matchIndex + query.length;
|
|
8782
|
+
matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
8783
|
+
}
|
|
8784
|
+
if (start < text.length) {
|
|
8785
|
+
li.appendChild(document.createTextNode(text.slice(start)));
|
|
8786
|
+
}
|
|
8512
8787
|
} else {
|
|
8513
8788
|
li.textContent = text;
|
|
8514
8789
|
}
|
|
@@ -8978,6 +9253,7 @@
|
|
|
8978
9253
|
let viewMode = "days";
|
|
8979
9254
|
let focusedDate = null;
|
|
8980
9255
|
let skipNextFocusOpen = false;
|
|
9256
|
+
let ignoreOutsideUntil = 0;
|
|
8981
9257
|
const isDisabled = (d) => {
|
|
8982
9258
|
if (minDate) {
|
|
8983
9259
|
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
@@ -9343,7 +9619,11 @@
|
|
|
9343
9619
|
const repositionHandler = () => {
|
|
9344
9620
|
positionPopup();
|
|
9345
9621
|
};
|
|
9622
|
+
const markIgnoreOutside = () => {
|
|
9623
|
+
ignoreOutsideUntil = Date.now() + 100;
|
|
9624
|
+
};
|
|
9346
9625
|
const open = () => {
|
|
9626
|
+
markIgnoreOutside();
|
|
9347
9627
|
viewMode = "days";
|
|
9348
9628
|
if (selectedDate) {
|
|
9349
9629
|
viewYear = selectedDate.getFullYear();
|
|
@@ -9377,8 +9657,22 @@
|
|
|
9377
9657
|
}
|
|
9378
9658
|
open();
|
|
9379
9659
|
};
|
|
9660
|
+
const clickHandler = () => {
|
|
9661
|
+
if (!popup.classList.contains("is-open")) open();
|
|
9662
|
+
};
|
|
9663
|
+
const isOpenAnchorTarget = (target) => {
|
|
9664
|
+
if (!target || !(target instanceof Node)) return false;
|
|
9665
|
+
if (target === input || input.contains(target) || popup.contains(target)) return true;
|
|
9666
|
+
const inputId = input.id;
|
|
9667
|
+
if (inputId) {
|
|
9668
|
+
const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
|
|
9669
|
+
if (label && (target === label || label.contains(target))) return true;
|
|
9670
|
+
}
|
|
9671
|
+
return false;
|
|
9672
|
+
};
|
|
9380
9673
|
const outsideHandler = (e) => {
|
|
9381
|
-
if (
|
|
9674
|
+
if (Date.now() < ignoreOutsideUntil) return;
|
|
9675
|
+
if (!isOpenAnchorTarget(e.target)) close();
|
|
9382
9676
|
};
|
|
9383
9677
|
const escHandler = (e) => {
|
|
9384
9678
|
if (e.key === "Escape" && popup.classList.contains("is-open")) {
|
|
@@ -9388,6 +9682,7 @@
|
|
|
9388
9682
|
}
|
|
9389
9683
|
};
|
|
9390
9684
|
input.addEventListener("focus", focusHandler);
|
|
9685
|
+
input.addEventListener("click", clickHandler);
|
|
9391
9686
|
document.addEventListener("click", outsideHandler, true);
|
|
9392
9687
|
document.addEventListener("keydown", escHandler);
|
|
9393
9688
|
popup.addEventListener("keydown", handleGridKeydown);
|
|
@@ -9398,6 +9693,7 @@
|
|
|
9398
9693
|
input.setAttribute("autocomplete", "off");
|
|
9399
9694
|
cleanup.push(
|
|
9400
9695
|
() => input.removeEventListener("focus", focusHandler),
|
|
9696
|
+
() => input.removeEventListener("click", clickHandler),
|
|
9401
9697
|
() => document.removeEventListener("click", outsideHandler, true),
|
|
9402
9698
|
() => document.removeEventListener("keydown", escHandler),
|
|
9403
9699
|
() => popup.removeEventListener("keydown", handleGridKeydown),
|
|
@@ -10330,1072 +10626,6 @@
|
|
|
10330
10626
|
window.VanduoSpotlight = Spotlight;
|
|
10331
10627
|
})();
|
|
10332
10628
|
|
|
10333
|
-
// js/components/music-player.js
|
|
10334
|
-
(function() {
|
|
10335
|
-
"use strict";
|
|
10336
|
-
function shuffleArray(arr) {
|
|
10337
|
-
const shuffled = arr.slice();
|
|
10338
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
10339
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
10340
|
-
const tmp = shuffled[i];
|
|
10341
|
-
shuffled[i] = shuffled[j];
|
|
10342
|
-
shuffled[j] = tmp;
|
|
10343
|
-
}
|
|
10344
|
-
return shuffled;
|
|
10345
|
-
}
|
|
10346
|
-
function formatTime(seconds) {
|
|
10347
|
-
if (!isFinite(seconds) || seconds < 0) return "0:00";
|
|
10348
|
-
const m = Math.floor(seconds / 60);
|
|
10349
|
-
const s = Math.floor(seconds % 60);
|
|
10350
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
10351
|
-
}
|
|
10352
|
-
function persistStorageKey(id) {
|
|
10353
|
-
return "vanduo:music-player:" + (id && id.trim() ? id.trim() : "default") + ":pos";
|
|
10354
|
-
}
|
|
10355
|
-
function updateRangeFill(input) {
|
|
10356
|
-
const min = parseFloat(input.min) || 0;
|
|
10357
|
-
const max = parseFloat(input.max) || 1;
|
|
10358
|
-
const val = parseFloat(input.value) || 0;
|
|
10359
|
-
const pct = (val - min) / (max - min) * 100;
|
|
10360
|
-
input.style.setProperty("--vd-fill", pct + "%");
|
|
10361
|
-
input.style.backgroundImage = "linear-gradient(to right, var(--vd-music-player-track-fill, currentColor) 0%, var(--vd-music-player-track-fill, currentColor) " + pct + "%, var(--vd-music-player-track-bg, #ccc) " + pct + "%, var(--vd-music-player-track-bg, #ccc) 100%)";
|
|
10362
|
-
}
|
|
10363
|
-
function icon(name) {
|
|
10364
|
-
const el = document.createElement("i");
|
|
10365
|
-
el.className = "ph ph-" + name;
|
|
10366
|
-
el.setAttribute("aria-hidden", "true");
|
|
10367
|
-
return el;
|
|
10368
|
-
}
|
|
10369
|
-
const MusicPlayer = {
|
|
10370
|
-
/** @type {Map<HTMLElement, Object>} */
|
|
10371
|
-
instances: /* @__PURE__ */ new Map(),
|
|
10372
|
-
/**
|
|
10373
|
-
* Default options.
|
|
10374
|
-
*/
|
|
10375
|
-
defaults: {
|
|
10376
|
-
tracks: [],
|
|
10377
|
-
volume: 0.5,
|
|
10378
|
-
shuffle: false,
|
|
10379
|
-
showProgress: false,
|
|
10380
|
-
showPlaylist: false,
|
|
10381
|
-
autoAdvance: true,
|
|
10382
|
-
glass: false,
|
|
10383
|
-
detachable: false,
|
|
10384
|
-
/** @type {null|string} */
|
|
10385
|
-
floatingPosition: null,
|
|
10386
|
-
draggable: false,
|
|
10387
|
-
minimizable: false,
|
|
10388
|
-
startMinimized: false,
|
|
10389
|
-
persistPosition: false,
|
|
10390
|
-
persistKey: ""
|
|
10391
|
-
},
|
|
10392
|
-
/**
|
|
10393
|
-
* Auto-initialize all .vd-music-player / [data-music-player] elements.
|
|
10394
|
-
* Options can be provided via data-music-player-options (JSON string).
|
|
10395
|
-
*/
|
|
10396
|
-
init: function(root) {
|
|
10397
|
-
window.Vanduo.queryAll(root, ".vd-music-player, [data-music-player]").forEach((el) => {
|
|
10398
|
-
if (this.instances.has(el)) return;
|
|
10399
|
-
let opts = {};
|
|
10400
|
-
const attr = el.getAttribute("data-music-player-options");
|
|
10401
|
-
if (attr) {
|
|
10402
|
-
try {
|
|
10403
|
-
opts = JSON.parse(attr);
|
|
10404
|
-
} catch (_) {
|
|
10405
|
-
}
|
|
10406
|
-
}
|
|
10407
|
-
this.initPlayer(el, opts);
|
|
10408
|
-
});
|
|
10409
|
-
},
|
|
10410
|
-
/**
|
|
10411
|
-
* Initialize a single player element.
|
|
10412
|
-
* @param {HTMLElement} container
|
|
10413
|
-
* @param {Object} [options]
|
|
10414
|
-
*/
|
|
10415
|
-
initPlayer: function(container, options) {
|
|
10416
|
-
const opts = Object.assign({}, this.defaults, options || {});
|
|
10417
|
-
const rawTracks = Array.isArray(opts.tracks) ? opts.tracks : [];
|
|
10418
|
-
const tracks = rawTracks.filter((t) => t && typeof t.url === "string" && t.url.trim());
|
|
10419
|
-
const trackList = opts.shuffle ? shuffleArray(tracks) : tracks.slice();
|
|
10420
|
-
const state = {
|
|
10421
|
-
tracks: trackList,
|
|
10422
|
-
originalTracks: tracks.slice(),
|
|
10423
|
-
currentIndex: 0,
|
|
10424
|
-
isPlaying: false,
|
|
10425
|
-
volume: Math.max(0, Math.min(1, opts.volume)),
|
|
10426
|
-
shuffle: opts.shuffle,
|
|
10427
|
-
showProgress: opts.showProgress,
|
|
10428
|
-
showPlaylist: opts.showPlaylist,
|
|
10429
|
-
autoAdvance: opts.autoAdvance,
|
|
10430
|
-
audio: null,
|
|
10431
|
-
glass: Boolean(opts.glass),
|
|
10432
|
-
detachable: Boolean(opts.detachable),
|
|
10433
|
-
floatingPosition: opts.floatingPosition || "bottom-right",
|
|
10434
|
-
draggable: Boolean(opts.draggable) && Boolean(opts.detachable),
|
|
10435
|
-
minimizable: Boolean(opts.minimizable),
|
|
10436
|
-
startMinimized: Boolean(opts.startMinimized),
|
|
10437
|
-
persistPosition: Boolean(opts.persistPosition),
|
|
10438
|
-
persistKey: typeof opts.persistKey === "string" ? opts.persistKey : "",
|
|
10439
|
-
isDetached: false,
|
|
10440
|
-
isMinimized: false,
|
|
10441
|
-
_startMinimizeApplied: false
|
|
10442
|
-
};
|
|
10443
|
-
const audio = new Audio();
|
|
10444
|
-
audio.volume = state.volume;
|
|
10445
|
-
audio.preload = "metadata";
|
|
10446
|
-
state.audio = audio;
|
|
10447
|
-
this._buildDOM(container, state);
|
|
10448
|
-
const refs = {
|
|
10449
|
-
btnPlay: container.querySelector(".vd-music-player-btn-play"),
|
|
10450
|
-
btnPrev: container.querySelector(".vd-music-player-btn-prev"),
|
|
10451
|
-
btnNext: container.querySelector(".vd-music-player-btn-next"),
|
|
10452
|
-
btnShuffle: container.querySelector(".vd-music-player-btn-shuffle"),
|
|
10453
|
-
btnPlaylist: container.querySelector(".vd-music-player-btn-playlist"),
|
|
10454
|
-
btnDetach: container.querySelector(".vd-music-player-btn-detach"),
|
|
10455
|
-
btnAttach: container.querySelector(".vd-music-player-btn-attach"),
|
|
10456
|
-
btnMinimize: container.querySelector(".vd-music-player-btn-minimize"),
|
|
10457
|
-
dragHandle: container.querySelector(".vd-music-player-drag-handle"),
|
|
10458
|
-
trackName: container.querySelector(".vd-music-player-track-name"),
|
|
10459
|
-
volumeSlider: container.querySelector(".vd-music-player-volume-slider"),
|
|
10460
|
-
volumeIcon: container.querySelector(".vd-music-player-volume-icon"),
|
|
10461
|
-
progressBar: container.querySelector(".vd-music-player-progress-bar"),
|
|
10462
|
-
timeElapsed: container.querySelector(".vd-music-player-time-elapsed"),
|
|
10463
|
-
timeDuration: container.querySelector(".vd-music-player-time-duration"),
|
|
10464
|
-
playlistPanel: container.querySelector(".vd-music-player-playlist")
|
|
10465
|
-
};
|
|
10466
|
-
const renderPlayIcon = () => {
|
|
10467
|
-
const btn = refs.btnPlay;
|
|
10468
|
-
if (!btn) return;
|
|
10469
|
-
btn.innerHTML = "";
|
|
10470
|
-
btn.appendChild(icon(state.isPlaying ? "pause" : "play"));
|
|
10471
|
-
btn.setAttribute("aria-label", state.isPlaying ? "Pause" : "Play");
|
|
10472
|
-
btn.classList.toggle("is-active", state.isPlaying);
|
|
10473
|
-
};
|
|
10474
|
-
const renderTrackName = () => {
|
|
10475
|
-
const el = refs.trackName;
|
|
10476
|
-
if (!el) return;
|
|
10477
|
-
const track = state.tracks[state.currentIndex];
|
|
10478
|
-
if (track) {
|
|
10479
|
-
el.textContent = track.name || "Unknown Track";
|
|
10480
|
-
el.classList.remove("is-idle");
|
|
10481
|
-
} else {
|
|
10482
|
-
el.textContent = "No tracks loaded";
|
|
10483
|
-
el.classList.add("is-idle");
|
|
10484
|
-
}
|
|
10485
|
-
};
|
|
10486
|
-
const renderVolumeIcon = () => {
|
|
10487
|
-
const el = refs.volumeIcon;
|
|
10488
|
-
if (!el) return;
|
|
10489
|
-
el.innerHTML = "";
|
|
10490
|
-
const v = state.volume;
|
|
10491
|
-
const name = v === 0 ? "speaker-none" : v < 0.5 ? "speaker-low" : "speaker-high";
|
|
10492
|
-
el.appendChild(icon(name));
|
|
10493
|
-
};
|
|
10494
|
-
const renderShuffleBtn = () => {
|
|
10495
|
-
const btn = refs.btnShuffle;
|
|
10496
|
-
if (!btn) return;
|
|
10497
|
-
btn.classList.toggle("is-active", state.shuffle);
|
|
10498
|
-
btn.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10499
|
-
};
|
|
10500
|
-
const renderPlaylistItems = () => {
|
|
10501
|
-
const panel = refs.playlistPanel;
|
|
10502
|
-
if (!panel) return;
|
|
10503
|
-
panel.innerHTML = "";
|
|
10504
|
-
state.tracks.forEach((track, i) => {
|
|
10505
|
-
const item = document.createElement("button");
|
|
10506
|
-
item.className = "vd-music-player-playlist-item" + (i === state.currentIndex ? " is-active" : "");
|
|
10507
|
-
item.type = "button";
|
|
10508
|
-
item.setAttribute("data-index", String(i));
|
|
10509
|
-
item.setAttribute("aria-current", i === state.currentIndex ? "true" : "false");
|
|
10510
|
-
const num = document.createElement("span");
|
|
10511
|
-
num.className = "vd-music-player-playlist-num";
|
|
10512
|
-
num.textContent = String(i + 1);
|
|
10513
|
-
const name = document.createElement("span");
|
|
10514
|
-
name.className = "vd-music-player-playlist-name";
|
|
10515
|
-
name.textContent = track.name || "Track " + (i + 1);
|
|
10516
|
-
item.appendChild(num);
|
|
10517
|
-
item.appendChild(name);
|
|
10518
|
-
panel.appendChild(item);
|
|
10519
|
-
});
|
|
10520
|
-
};
|
|
10521
|
-
const renderProgress = () => {
|
|
10522
|
-
const bar = refs.progressBar;
|
|
10523
|
-
if (!bar || !audio.duration) return;
|
|
10524
|
-
const pct = audio.currentTime / audio.duration * 100;
|
|
10525
|
-
bar.value = String(pct);
|
|
10526
|
-
updateRangeFill(bar);
|
|
10527
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = formatTime(audio.currentTime);
|
|
10528
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10529
|
-
};
|
|
10530
|
-
const loadTrack = (index, autoPlay) => {
|
|
10531
|
-
const track = state.tracks[index];
|
|
10532
|
-
if (!track) return;
|
|
10533
|
-
state.currentIndex = index;
|
|
10534
|
-
audio.src = track.url;
|
|
10535
|
-
renderTrackName();
|
|
10536
|
-
renderPlaylistItems();
|
|
10537
|
-
if (refs.progressBar) {
|
|
10538
|
-
refs.progressBar.value = "0";
|
|
10539
|
-
updateRangeFill(refs.progressBar);
|
|
10540
|
-
}
|
|
10541
|
-
if (refs.timeElapsed) refs.timeElapsed.textContent = "0:00";
|
|
10542
|
-
if (refs.timeDuration) refs.timeDuration.textContent = "0:00";
|
|
10543
|
-
container.dispatchEvent(
|
|
10544
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
10545
|
-
bubbles: true,
|
|
10546
|
-
detail: { index, name: track.name, url: track.url }
|
|
10547
|
-
})
|
|
10548
|
-
);
|
|
10549
|
-
if (autoPlay) {
|
|
10550
|
-
audio.play().catch(() => {
|
|
10551
|
-
});
|
|
10552
|
-
}
|
|
10553
|
-
};
|
|
10554
|
-
const cleanupFunctions = [];
|
|
10555
|
-
const onPlay = () => {
|
|
10556
|
-
state.isPlaying = true;
|
|
10557
|
-
renderPlayIcon();
|
|
10558
|
-
container.dispatchEvent(new CustomEvent("musicplayer:play", { bubbles: true }));
|
|
10559
|
-
};
|
|
10560
|
-
const onPause = () => {
|
|
10561
|
-
state.isPlaying = false;
|
|
10562
|
-
renderPlayIcon();
|
|
10563
|
-
container.dispatchEvent(new CustomEvent("musicplayer:pause", { bubbles: true }));
|
|
10564
|
-
};
|
|
10565
|
-
const onEnded = () => {
|
|
10566
|
-
if (state.autoAdvance && state.tracks.length > 1) {
|
|
10567
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10568
|
-
loadTrack(next, true);
|
|
10569
|
-
} else {
|
|
10570
|
-
state.isPlaying = false;
|
|
10571
|
-
renderPlayIcon();
|
|
10572
|
-
container.dispatchEvent(new CustomEvent("musicplayer:ended", { bubbles: true }));
|
|
10573
|
-
}
|
|
10574
|
-
};
|
|
10575
|
-
const onTimeUpdate = () => {
|
|
10576
|
-
if (state.showProgress) renderProgress();
|
|
10577
|
-
};
|
|
10578
|
-
const onLoadedMetadata = () => {
|
|
10579
|
-
if (refs.timeDuration) refs.timeDuration.textContent = formatTime(audio.duration);
|
|
10580
|
-
if (refs.progressBar) {
|
|
10581
|
-
refs.progressBar.max = "100";
|
|
10582
|
-
updateRangeFill(refs.progressBar);
|
|
10583
|
-
}
|
|
10584
|
-
};
|
|
10585
|
-
audio.addEventListener("play", onPlay);
|
|
10586
|
-
audio.addEventListener("pause", onPause);
|
|
10587
|
-
audio.addEventListener("ended", onEnded);
|
|
10588
|
-
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
10589
|
-
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
10590
|
-
cleanupFunctions.push(() => {
|
|
10591
|
-
audio.removeEventListener("play", onPlay);
|
|
10592
|
-
audio.removeEventListener("pause", onPause);
|
|
10593
|
-
audio.removeEventListener("ended", onEnded);
|
|
10594
|
-
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
10595
|
-
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
10596
|
-
audio.pause();
|
|
10597
|
-
audio.src = "";
|
|
10598
|
-
});
|
|
10599
|
-
if (refs.btnPlay) {
|
|
10600
|
-
const handler = () => {
|
|
10601
|
-
if (!audio.src && state.tracks.length) loadTrack(state.currentIndex, false);
|
|
10602
|
-
if (state.isPlaying) {
|
|
10603
|
-
audio.pause();
|
|
10604
|
-
} else {
|
|
10605
|
-
audio.play().catch(() => {
|
|
10606
|
-
});
|
|
10607
|
-
}
|
|
10608
|
-
};
|
|
10609
|
-
refs.btnPlay.addEventListener("click", handler);
|
|
10610
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("click", handler));
|
|
10611
|
-
const keyHandler = (e) => {
|
|
10612
|
-
if (e.key === " " || e.key === "Enter") {
|
|
10613
|
-
e.preventDefault();
|
|
10614
|
-
handler();
|
|
10615
|
-
}
|
|
10616
|
-
};
|
|
10617
|
-
refs.btnPlay.addEventListener("keydown", keyHandler);
|
|
10618
|
-
cleanupFunctions.push(() => refs.btnPlay.removeEventListener("keydown", keyHandler));
|
|
10619
|
-
}
|
|
10620
|
-
if (refs.btnPrev) {
|
|
10621
|
-
const handler = () => {
|
|
10622
|
-
if (!state.tracks.length) return;
|
|
10623
|
-
if (audio.currentTime > 3) {
|
|
10624
|
-
audio.currentTime = 0;
|
|
10625
|
-
} else {
|
|
10626
|
-
const prev = state.currentIndex === 0 ? state.tracks.length - 1 : state.currentIndex - 1;
|
|
10627
|
-
loadTrack(prev, state.isPlaying);
|
|
10628
|
-
}
|
|
10629
|
-
};
|
|
10630
|
-
refs.btnPrev.addEventListener("click", handler);
|
|
10631
|
-
cleanupFunctions.push(() => refs.btnPrev.removeEventListener("click", handler));
|
|
10632
|
-
}
|
|
10633
|
-
if (refs.btnNext) {
|
|
10634
|
-
const handler = () => {
|
|
10635
|
-
if (!state.tracks.length) return;
|
|
10636
|
-
const next = (state.currentIndex + 1) % state.tracks.length;
|
|
10637
|
-
loadTrack(next, state.isPlaying);
|
|
10638
|
-
};
|
|
10639
|
-
refs.btnNext.addEventListener("click", handler);
|
|
10640
|
-
cleanupFunctions.push(() => refs.btnNext.removeEventListener("click", handler));
|
|
10641
|
-
}
|
|
10642
|
-
if (refs.btnShuffle) {
|
|
10643
|
-
const handler = () => {
|
|
10644
|
-
state.shuffle = !state.shuffle;
|
|
10645
|
-
if (state.shuffle) {
|
|
10646
|
-
const current = state.tracks[state.currentIndex];
|
|
10647
|
-
state.tracks = shuffleArray(state.tracks);
|
|
10648
|
-
const newIdx = state.tracks.findIndex((t) => t === current);
|
|
10649
|
-
if (newIdx > 0) {
|
|
10650
|
-
state.tracks.splice(newIdx, 1);
|
|
10651
|
-
state.tracks.unshift(current);
|
|
10652
|
-
}
|
|
10653
|
-
state.currentIndex = 0;
|
|
10654
|
-
} else {
|
|
10655
|
-
const current = state.tracks[state.currentIndex];
|
|
10656
|
-
state.tracks = state.originalTracks.slice();
|
|
10657
|
-
state.currentIndex = state.tracks.findIndex((t) => t === current);
|
|
10658
|
-
if (state.currentIndex < 0) state.currentIndex = 0;
|
|
10659
|
-
}
|
|
10660
|
-
renderShuffleBtn();
|
|
10661
|
-
renderPlaylistItems();
|
|
10662
|
-
};
|
|
10663
|
-
refs.btnShuffle.addEventListener("click", handler);
|
|
10664
|
-
cleanupFunctions.push(() => refs.btnShuffle.removeEventListener("click", handler));
|
|
10665
|
-
}
|
|
10666
|
-
if (refs.btnPlaylist) {
|
|
10667
|
-
const handler = () => {
|
|
10668
|
-
const panel = refs.playlistPanel;
|
|
10669
|
-
if (!panel) return;
|
|
10670
|
-
const isOpen = panel.classList.toggle("is-open");
|
|
10671
|
-
refs.btnPlaylist.classList.toggle("is-active", isOpen);
|
|
10672
|
-
refs.btnPlaylist.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
10673
|
-
};
|
|
10674
|
-
refs.btnPlaylist.addEventListener("click", handler);
|
|
10675
|
-
cleanupFunctions.push(() => refs.btnPlaylist.removeEventListener("click", handler));
|
|
10676
|
-
}
|
|
10677
|
-
if (refs.volumeSlider) {
|
|
10678
|
-
const handler = (e) => {
|
|
10679
|
-
const v = parseFloat(e.target.value);
|
|
10680
|
-
state.volume = v;
|
|
10681
|
-
audio.volume = v;
|
|
10682
|
-
renderVolumeIcon();
|
|
10683
|
-
updateRangeFill(refs.volumeSlider);
|
|
10684
|
-
container.dispatchEvent(
|
|
10685
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10686
|
-
);
|
|
10687
|
-
};
|
|
10688
|
-
refs.volumeSlider.addEventListener("input", handler);
|
|
10689
|
-
cleanupFunctions.push(() => refs.volumeSlider.removeEventListener("input", handler));
|
|
10690
|
-
updateRangeFill(refs.volumeSlider);
|
|
10691
|
-
}
|
|
10692
|
-
if (refs.progressBar) {
|
|
10693
|
-
const handler = (e) => {
|
|
10694
|
-
if (!audio.duration) return;
|
|
10695
|
-
const pct = parseFloat(e.target.value);
|
|
10696
|
-
audio.currentTime = pct / 100 * audio.duration;
|
|
10697
|
-
updateRangeFill(refs.progressBar);
|
|
10698
|
-
};
|
|
10699
|
-
refs.progressBar.addEventListener("input", handler);
|
|
10700
|
-
cleanupFunctions.push(() => refs.progressBar.removeEventListener("input", handler));
|
|
10701
|
-
}
|
|
10702
|
-
if (refs.playlistPanel) {
|
|
10703
|
-
const panelHandler = (e) => {
|
|
10704
|
-
const item = e.target.closest(".vd-music-player-playlist-item");
|
|
10705
|
-
if (!item) return;
|
|
10706
|
-
const idx = parseInt(item.getAttribute("data-index"), 10);
|
|
10707
|
-
if (!isNaN(idx)) loadTrack(idx, true);
|
|
10708
|
-
};
|
|
10709
|
-
refs.playlistPanel.addEventListener("click", panelHandler);
|
|
10710
|
-
cleanupFunctions.push(
|
|
10711
|
-
() => refs.playlistPanel.removeEventListener("click", panelHandler)
|
|
10712
|
-
);
|
|
10713
|
-
}
|
|
10714
|
-
if (refs.btnDetach) {
|
|
10715
|
-
const h = () => {
|
|
10716
|
-
this.detach(container);
|
|
10717
|
-
};
|
|
10718
|
-
refs.btnDetach.addEventListener("click", h);
|
|
10719
|
-
cleanupFunctions.push(() => refs.btnDetach.removeEventListener("click", h));
|
|
10720
|
-
}
|
|
10721
|
-
if (refs.btnAttach) {
|
|
10722
|
-
const h = () => {
|
|
10723
|
-
this.attach(container);
|
|
10724
|
-
};
|
|
10725
|
-
refs.btnAttach.addEventListener("click", h);
|
|
10726
|
-
cleanupFunctions.push(() => refs.btnAttach.removeEventListener("click", h));
|
|
10727
|
-
}
|
|
10728
|
-
if (refs.btnMinimize) {
|
|
10729
|
-
const h = () => {
|
|
10730
|
-
this.toggleMinimize(container);
|
|
10731
|
-
};
|
|
10732
|
-
refs.btnMinimize.addEventListener("click", h);
|
|
10733
|
-
cleanupFunctions.push(() => refs.btnMinimize.removeEventListener("click", h));
|
|
10734
|
-
}
|
|
10735
|
-
renderPlayIcon();
|
|
10736
|
-
renderTrackName();
|
|
10737
|
-
renderVolumeIcon();
|
|
10738
|
-
if (opts.showPlaylist) renderPlaylistItems();
|
|
10739
|
-
this.instances.set(container, {
|
|
10740
|
-
state,
|
|
10741
|
-
audio,
|
|
10742
|
-
refs,
|
|
10743
|
-
cleanup: cleanupFunctions,
|
|
10744
|
-
ui: { restore: null, unbindDrag: null }
|
|
10745
|
-
});
|
|
10746
|
-
container.setAttribute("data-music-player-initialized", "true");
|
|
10747
|
-
},
|
|
10748
|
-
/* ─── DOM builder ─────────────────────────────────────── */
|
|
10749
|
-
/**
|
|
10750
|
-
* Build the inner DOM structure inside container.
|
|
10751
|
-
* Pre-existing inner content is replaced only if it has no
|
|
10752
|
-
* recognised child elements (allows server-rendered markup).
|
|
10753
|
-
* @param {HTMLElement} container
|
|
10754
|
-
* @param {Object} state
|
|
10755
|
-
*/
|
|
10756
|
-
_buildDOM: function(container, state) {
|
|
10757
|
-
if (container.querySelector(".vd-music-player-controls")) return;
|
|
10758
|
-
container.setAttribute("role", "region");
|
|
10759
|
-
container.setAttribute("aria-label", "Music Player");
|
|
10760
|
-
if (state.showProgress) container.classList.add("has-progress");
|
|
10761
|
-
if (state.showPlaylist) container.classList.add("has-playlist");
|
|
10762
|
-
if (state.glass) container.classList.add("vd-music-player-glass");
|
|
10763
|
-
if (state.draggable) container.classList.add("vd-music-player-draggable");
|
|
10764
|
-
if (state.detachable || state.minimizable) {
|
|
10765
|
-
const tb = document.createElement("div");
|
|
10766
|
-
tb.className = "vd-music-player-toolbar";
|
|
10767
|
-
tb.setAttribute("role", "toolbar");
|
|
10768
|
-
tb.setAttribute("aria-label", "Player window");
|
|
10769
|
-
if (state.draggable) {
|
|
10770
|
-
const h = document.createElement("button");
|
|
10771
|
-
h.type = "button";
|
|
10772
|
-
h.className = "vd-music-player-drag-handle";
|
|
10773
|
-
h.setAttribute("aria-label", "Drag to move player");
|
|
10774
|
-
h.appendChild(icon("dots-six-vertical"));
|
|
10775
|
-
tb.appendChild(h);
|
|
10776
|
-
}
|
|
10777
|
-
const tSp = document.createElement("span");
|
|
10778
|
-
tSp.className = "vd-music-player-toolbar-spacer";
|
|
10779
|
-
tSp.setAttribute("aria-hidden", "true");
|
|
10780
|
-
tb.appendChild(tSp);
|
|
10781
|
-
if (state.minimizable) {
|
|
10782
|
-
const bMin = document.createElement("button");
|
|
10783
|
-
bMin.type = "button";
|
|
10784
|
-
bMin.className = "vd-music-player-btn vd-music-player-btn-minimize";
|
|
10785
|
-
bMin.setAttribute("aria-label", "Minimize player");
|
|
10786
|
-
bMin.setAttribute("aria-expanded", "true");
|
|
10787
|
-
bMin.appendChild(icon("minus"));
|
|
10788
|
-
tb.appendChild(bMin);
|
|
10789
|
-
}
|
|
10790
|
-
if (state.detachable) {
|
|
10791
|
-
const bOut = document.createElement("button");
|
|
10792
|
-
bOut.type = "button";
|
|
10793
|
-
bOut.className = "vd-music-player-btn vd-music-player-btn-detach";
|
|
10794
|
-
bOut.setAttribute("aria-label", "Detach player");
|
|
10795
|
-
bOut.appendChild(icon("arrows-out"));
|
|
10796
|
-
tb.appendChild(bOut);
|
|
10797
|
-
const bIn = document.createElement("button");
|
|
10798
|
-
bIn.type = "button";
|
|
10799
|
-
bIn.className = "vd-music-player-btn vd-music-player-btn-attach";
|
|
10800
|
-
bIn.setAttribute("aria-label", "Attach player");
|
|
10801
|
-
bIn.appendChild(icon("arrows-in"));
|
|
10802
|
-
tb.appendChild(bIn);
|
|
10803
|
-
}
|
|
10804
|
-
container.classList.add("vd-music-player-has-chrome");
|
|
10805
|
-
container.appendChild(tb);
|
|
10806
|
-
}
|
|
10807
|
-
const info = document.createElement("div");
|
|
10808
|
-
info.className = "vd-music-player-info";
|
|
10809
|
-
const iconWrap = document.createElement("span");
|
|
10810
|
-
iconWrap.className = "vd-music-player-icon";
|
|
10811
|
-
iconWrap.setAttribute("aria-hidden", "true");
|
|
10812
|
-
iconWrap.appendChild(icon("music-note"));
|
|
10813
|
-
const trackName = document.createElement("span");
|
|
10814
|
-
trackName.className = "vd-music-player-track-name";
|
|
10815
|
-
trackName.setAttribute("aria-live", "polite");
|
|
10816
|
-
trackName.setAttribute("aria-atomic", "true");
|
|
10817
|
-
info.appendChild(iconWrap);
|
|
10818
|
-
info.appendChild(trackName);
|
|
10819
|
-
container.appendChild(info);
|
|
10820
|
-
const controls = document.createElement("div");
|
|
10821
|
-
controls.className = "vd-music-player-controls";
|
|
10822
|
-
controls.setAttribute("role", "group");
|
|
10823
|
-
controls.setAttribute("aria-label", "Playback controls");
|
|
10824
|
-
const btnPrev = document.createElement("button");
|
|
10825
|
-
btnPrev.type = "button";
|
|
10826
|
-
btnPrev.className = "vd-music-player-btn vd-music-player-btn-prev";
|
|
10827
|
-
btnPrev.setAttribute("aria-label", "Previous track");
|
|
10828
|
-
btnPrev.appendChild(icon("skip-back"));
|
|
10829
|
-
const btnPlay = document.createElement("button");
|
|
10830
|
-
btnPlay.type = "button";
|
|
10831
|
-
btnPlay.className = "vd-music-player-btn vd-music-player-btn-play";
|
|
10832
|
-
btnPlay.setAttribute("aria-label", "Play");
|
|
10833
|
-
btnPlay.appendChild(icon("play"));
|
|
10834
|
-
const btnNext = document.createElement("button");
|
|
10835
|
-
btnNext.type = "button";
|
|
10836
|
-
btnNext.className = "vd-music-player-btn vd-music-player-btn-next";
|
|
10837
|
-
btnNext.setAttribute("aria-label", "Next track");
|
|
10838
|
-
btnNext.appendChild(icon("skip-forward"));
|
|
10839
|
-
controls.appendChild(btnPrev);
|
|
10840
|
-
controls.appendChild(btnPlay);
|
|
10841
|
-
controls.appendChild(btnNext);
|
|
10842
|
-
if (state.showPlaylist || state.shuffle !== void 0) {
|
|
10843
|
-
const btnShuffle = document.createElement("button");
|
|
10844
|
-
btnShuffle.type = "button";
|
|
10845
|
-
btnShuffle.className = "vd-music-player-btn vd-music-player-btn-shuffle";
|
|
10846
|
-
btnShuffle.setAttribute("aria-label", "Shuffle");
|
|
10847
|
-
btnShuffle.setAttribute("aria-pressed", state.shuffle ? "true" : "false");
|
|
10848
|
-
btnShuffle.appendChild(icon("shuffle"));
|
|
10849
|
-
controls.appendChild(btnShuffle);
|
|
10850
|
-
}
|
|
10851
|
-
const spacer = document.createElement("span");
|
|
10852
|
-
spacer.className = "vd-music-player-spacer";
|
|
10853
|
-
spacer.setAttribute("aria-hidden", "true");
|
|
10854
|
-
controls.appendChild(spacer);
|
|
10855
|
-
const volumeWrap = document.createElement("div");
|
|
10856
|
-
volumeWrap.className = "vd-music-player-volume";
|
|
10857
|
-
const volumeIcon = document.createElement("span");
|
|
10858
|
-
volumeIcon.className = "vd-music-player-volume-icon";
|
|
10859
|
-
volumeIcon.setAttribute("aria-hidden", "true");
|
|
10860
|
-
const volumeSlider = document.createElement("input");
|
|
10861
|
-
volumeSlider.type = "range";
|
|
10862
|
-
volumeSlider.className = "vd-music-player-volume-slider";
|
|
10863
|
-
volumeSlider.min = "0";
|
|
10864
|
-
volumeSlider.max = "1";
|
|
10865
|
-
volumeSlider.step = "0.01";
|
|
10866
|
-
volumeSlider.value = String(state.volume);
|
|
10867
|
-
volumeSlider.setAttribute("aria-label", "Volume");
|
|
10868
|
-
volumeWrap.appendChild(volumeIcon);
|
|
10869
|
-
volumeWrap.appendChild(volumeSlider);
|
|
10870
|
-
controls.appendChild(volumeWrap);
|
|
10871
|
-
if (state.showPlaylist) {
|
|
10872
|
-
const btnPlaylist = document.createElement("button");
|
|
10873
|
-
btnPlaylist.type = "button";
|
|
10874
|
-
btnPlaylist.className = "vd-music-player-btn vd-music-player-btn-playlist";
|
|
10875
|
-
btnPlaylist.setAttribute("aria-label", "Show playlist");
|
|
10876
|
-
btnPlaylist.setAttribute("aria-expanded", "false");
|
|
10877
|
-
btnPlaylist.appendChild(icon("playlist"));
|
|
10878
|
-
controls.appendChild(btnPlaylist);
|
|
10879
|
-
}
|
|
10880
|
-
container.appendChild(controls);
|
|
10881
|
-
if (state.showProgress) {
|
|
10882
|
-
const progressRow = document.createElement("div");
|
|
10883
|
-
progressRow.className = "vd-music-player-progress";
|
|
10884
|
-
const timeElapsed = document.createElement("span");
|
|
10885
|
-
timeElapsed.className = "vd-music-player-time vd-music-player-time-elapsed";
|
|
10886
|
-
timeElapsed.textContent = "0:00";
|
|
10887
|
-
timeElapsed.setAttribute("aria-hidden", "true");
|
|
10888
|
-
const progressBar = document.createElement("input");
|
|
10889
|
-
progressBar.type = "range";
|
|
10890
|
-
progressBar.className = "vd-music-player-progress-bar";
|
|
10891
|
-
progressBar.min = "0";
|
|
10892
|
-
progressBar.max = "100";
|
|
10893
|
-
progressBar.step = "0.1";
|
|
10894
|
-
progressBar.value = "0";
|
|
10895
|
-
progressBar.setAttribute("aria-label", "Seek");
|
|
10896
|
-
const timeDuration = document.createElement("span");
|
|
10897
|
-
timeDuration.className = "vd-music-player-time vd-music-player-time-duration";
|
|
10898
|
-
timeDuration.textContent = "0:00";
|
|
10899
|
-
timeDuration.setAttribute("aria-hidden", "true");
|
|
10900
|
-
progressRow.appendChild(timeElapsed);
|
|
10901
|
-
progressRow.appendChild(progressBar);
|
|
10902
|
-
progressRow.appendChild(timeDuration);
|
|
10903
|
-
container.appendChild(progressRow);
|
|
10904
|
-
}
|
|
10905
|
-
if (state.showPlaylist) {
|
|
10906
|
-
const playlist = document.createElement("div");
|
|
10907
|
-
playlist.className = "vd-music-player-playlist";
|
|
10908
|
-
playlist.setAttribute("aria-label", "Playlist");
|
|
10909
|
-
container.appendChild(playlist);
|
|
10910
|
-
}
|
|
10911
|
-
},
|
|
10912
|
-
/* ─── Public API ──────────────────────────────────────── */
|
|
10913
|
-
/**
|
|
10914
|
-
* @param {HTMLElement} container
|
|
10915
|
-
*/
|
|
10916
|
-
play: function(container) {
|
|
10917
|
-
const inst = this.instances.get(container);
|
|
10918
|
-
if (!inst) return;
|
|
10919
|
-
if (!inst.audio.src && inst.state.tracks.length) {
|
|
10920
|
-
inst.audio.src = inst.state.tracks[inst.state.currentIndex].url;
|
|
10921
|
-
}
|
|
10922
|
-
inst.audio.play().catch(() => {
|
|
10923
|
-
});
|
|
10924
|
-
},
|
|
10925
|
-
/**
|
|
10926
|
-
* @param {HTMLElement} container
|
|
10927
|
-
*/
|
|
10928
|
-
pause: function(container) {
|
|
10929
|
-
const inst = this.instances.get(container);
|
|
10930
|
-
if (inst) inst.audio.pause();
|
|
10931
|
-
},
|
|
10932
|
-
/**
|
|
10933
|
-
* @param {HTMLElement} container
|
|
10934
|
-
*/
|
|
10935
|
-
toggle: function(container) {
|
|
10936
|
-
const inst = this.instances.get(container);
|
|
10937
|
-
if (!inst) return;
|
|
10938
|
-
if (inst.state.isPlaying) {
|
|
10939
|
-
this.pause(container);
|
|
10940
|
-
} else {
|
|
10941
|
-
this.play(container);
|
|
10942
|
-
}
|
|
10943
|
-
},
|
|
10944
|
-
/**
|
|
10945
|
-
* @param {HTMLElement} container
|
|
10946
|
-
*/
|
|
10947
|
-
next: function(container) {
|
|
10948
|
-
const inst = this.instances.get(container);
|
|
10949
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10950
|
-
const next = (inst.state.currentIndex + 1) % inst.state.tracks.length;
|
|
10951
|
-
this._loadTrack(inst, next, inst.state.isPlaying);
|
|
10952
|
-
},
|
|
10953
|
-
/**
|
|
10954
|
-
* @param {HTMLElement} container
|
|
10955
|
-
*/
|
|
10956
|
-
previous: function(container) {
|
|
10957
|
-
const inst = this.instances.get(container);
|
|
10958
|
-
if (!inst || !inst.state.tracks.length) return;
|
|
10959
|
-
const len = inst.state.tracks.length;
|
|
10960
|
-
const prev = (inst.state.currentIndex - 1 + len) % len;
|
|
10961
|
-
this._loadTrack(inst, prev, inst.state.isPlaying);
|
|
10962
|
-
},
|
|
10963
|
-
/**
|
|
10964
|
-
* @param {HTMLElement} container
|
|
10965
|
-
* @param {number} value - 0 to 1
|
|
10966
|
-
*/
|
|
10967
|
-
setVolume: function(container, value) {
|
|
10968
|
-
const inst = this.instances.get(container);
|
|
10969
|
-
if (!inst) return;
|
|
10970
|
-
const v = Math.max(0, Math.min(1, value));
|
|
10971
|
-
inst.state.volume = v;
|
|
10972
|
-
inst.audio.volume = v;
|
|
10973
|
-
if (inst.refs.volumeSlider) {
|
|
10974
|
-
inst.refs.volumeSlider.value = String(v);
|
|
10975
|
-
updateRangeFill(inst.refs.volumeSlider);
|
|
10976
|
-
}
|
|
10977
|
-
container.dispatchEvent(
|
|
10978
|
-
new CustomEvent("musicplayer:volumechange", { bubbles: true, detail: { volume: v } })
|
|
10979
|
-
);
|
|
10980
|
-
},
|
|
10981
|
-
/**
|
|
10982
|
-
* @param {HTMLElement} container
|
|
10983
|
-
* @param {number} index - Track index
|
|
10984
|
-
*/
|
|
10985
|
-
setTrack: function(container, index) {
|
|
10986
|
-
const inst = this.instances.get(container);
|
|
10987
|
-
if (!inst) return;
|
|
10988
|
-
this._loadTrack(inst, index, inst.state.isPlaying);
|
|
10989
|
-
},
|
|
10990
|
-
/**
|
|
10991
|
-
* Shuffle or un-shuffle the track list.
|
|
10992
|
-
* @param {HTMLElement} container
|
|
10993
|
-
*/
|
|
10994
|
-
shuffle: function(container) {
|
|
10995
|
-
const inst = this.instances.get(container);
|
|
10996
|
-
if (!inst || !inst.refs.btnShuffle) return;
|
|
10997
|
-
inst.refs.btnShuffle.click();
|
|
10998
|
-
},
|
|
10999
|
-
/**
|
|
11000
|
-
* Float the player above the page. Requires { detachable: true } at init.
|
|
11001
|
-
* @param {HTMLElement} container
|
|
11002
|
-
* @param {string} [position] 'bottom-left' or 'bottom-right'
|
|
11003
|
-
*/
|
|
11004
|
-
detach: function(container, position) {
|
|
11005
|
-
const inst = this.instances.get(container);
|
|
11006
|
-
if (!inst || !inst.state.detachable || inst.state.isDetached) return;
|
|
11007
|
-
const s = inst.state;
|
|
11008
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11009
|
-
s.isDetached = true;
|
|
11010
|
-
inst.ui.restore = {
|
|
11011
|
-
parent: container.parentNode,
|
|
11012
|
-
next: container.nextSibling
|
|
11013
|
-
};
|
|
11014
|
-
document.body.appendChild(container);
|
|
11015
|
-
container.classList.add("vd-music-player-floating", "vd-music-player-detached");
|
|
11016
|
-
const pos = position != null && position !== void 0 ? position : s.floatingPosition;
|
|
11017
|
-
this._setCornerPosition(
|
|
11018
|
-
container,
|
|
11019
|
-
pos === "bottom-left" || pos === "bottom-right" ? pos : "bottom-right"
|
|
11020
|
-
);
|
|
11021
|
-
this._loadPersistedPosition(container, inst);
|
|
11022
|
-
if (s.startMinimized && !s._startMinimizeApplied) {
|
|
11023
|
-
s._startMinimizeApplied = true;
|
|
11024
|
-
this.minimize(container);
|
|
11025
|
-
}
|
|
11026
|
-
this._bindFloatingDrag(inst);
|
|
11027
|
-
container.dispatchEvent(new CustomEvent("musicplayer:detach", { bubbles: true }));
|
|
11028
|
-
},
|
|
11029
|
-
/**
|
|
11030
|
-
* Return a detached player to its original place in the document.
|
|
11031
|
-
* @param {HTMLElement} container
|
|
11032
|
-
*/
|
|
11033
|
-
attach: function(container) {
|
|
11034
|
-
const inst = this.instances.get(container);
|
|
11035
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11036
|
-
this._unbindFloatingDrag(inst);
|
|
11037
|
-
inst.state.isDetached = false;
|
|
11038
|
-
const r = inst.ui && inst.ui.restore;
|
|
11039
|
-
container.classList.remove(
|
|
11040
|
-
"vd-music-player-floating",
|
|
11041
|
-
"vd-music-player-detached",
|
|
11042
|
-
"vd-music-player-floating-bottom-left",
|
|
11043
|
-
"vd-music-player-floating-bottom-right",
|
|
11044
|
-
"is-position-custom"
|
|
11045
|
-
);
|
|
11046
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11047
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11048
|
-
if (r && r.parent && r.parent.isConnected) {
|
|
11049
|
-
r.parent.insertBefore(container, r.next);
|
|
11050
|
-
}
|
|
11051
|
-
if (inst.ui) {
|
|
11052
|
-
inst.ui.restore = null;
|
|
11053
|
-
inst.ui.unbindDrag = null;
|
|
11054
|
-
}
|
|
11055
|
-
container.dispatchEvent(new CustomEvent("musicplayer:attach", { bubbles: true }));
|
|
11056
|
-
},
|
|
11057
|
-
/**
|
|
11058
|
-
* Collapse to essential controls. Requires { minimizable: true } at init.
|
|
11059
|
-
* @param {HTMLElement} container
|
|
11060
|
-
*/
|
|
11061
|
-
minimize: function(container) {
|
|
11062
|
-
const inst = this.instances.get(container);
|
|
11063
|
-
if (!inst || !inst.state.minimizable || inst.state.isMinimized) return;
|
|
11064
|
-
const s = inst.state;
|
|
11065
|
-
s.isMinimized = true;
|
|
11066
|
-
container.classList.add("vd-music-player-minimized");
|
|
11067
|
-
this._setMinimizeButtonState(inst, true);
|
|
11068
|
-
if (inst.refs.playlistPanel && inst.refs.playlistPanel.classList.contains("is-open") && inst.refs.btnPlaylist) {
|
|
11069
|
-
inst.refs.playlistPanel.classList.remove("is-open");
|
|
11070
|
-
inst.refs.btnPlaylist.classList.remove("is-active");
|
|
11071
|
-
inst.refs.btnPlaylist.setAttribute("aria-expanded", "false");
|
|
11072
|
-
}
|
|
11073
|
-
container.dispatchEvent(new CustomEvent("musicplayer:minimize", { bubbles: true }));
|
|
11074
|
-
},
|
|
11075
|
-
/**
|
|
11076
|
-
* Restore from minimized state.
|
|
11077
|
-
* @param {HTMLElement} container
|
|
11078
|
-
*/
|
|
11079
|
-
expand: function(container) {
|
|
11080
|
-
const inst = this.instances.get(container);
|
|
11081
|
-
if (!inst || !inst.state.minimizable || !inst.state.isMinimized) return;
|
|
11082
|
-
inst.state.isMinimized = false;
|
|
11083
|
-
container.classList.remove("vd-music-player-minimized");
|
|
11084
|
-
this._setMinimizeButtonState(inst, false);
|
|
11085
|
-
container.dispatchEvent(new CustomEvent("musicplayer:expand", { bubbles: true }));
|
|
11086
|
-
},
|
|
11087
|
-
/**
|
|
11088
|
-
* Toggle minimize / expand.
|
|
11089
|
-
* @param {HTMLElement} container
|
|
11090
|
-
*/
|
|
11091
|
-
toggleMinimize: function(container) {
|
|
11092
|
-
const inst = this.instances.get(container);
|
|
11093
|
-
if (!inst || !inst.state.minimizable) return;
|
|
11094
|
-
if (inst.state.isMinimized) {
|
|
11095
|
-
this.expand(container);
|
|
11096
|
-
} else {
|
|
11097
|
-
this.minimize(container);
|
|
11098
|
-
}
|
|
11099
|
-
},
|
|
11100
|
-
/**
|
|
11101
|
-
* Set floating corner or pixel position (detached only).
|
|
11102
|
-
* @param {HTMLElement} container
|
|
11103
|
-
* @param {string|{x:number,y:number}} position 'bottom-left' | 'bottom-right' | { x, y } viewport
|
|
11104
|
-
*/
|
|
11105
|
-
setPosition: function(container, position) {
|
|
11106
|
-
const inst = this.instances.get(container);
|
|
11107
|
-
if (!inst || !inst.state.isDetached) return;
|
|
11108
|
-
if (typeof position === "string") {
|
|
11109
|
-
this._setCornerPosition(
|
|
11110
|
-
container,
|
|
11111
|
-
position === "bottom-left" || position === "bottom-right" ? position : "bottom-right"
|
|
11112
|
-
);
|
|
11113
|
-
} else if (position && typeof position.x === "number" && typeof position.y === "number") {
|
|
11114
|
-
this._setCustomPositionFromRect(container, position.x, position.y);
|
|
11115
|
-
}
|
|
11116
|
-
if (inst.state.persistPosition) {
|
|
11117
|
-
const r = container.getBoundingClientRect();
|
|
11118
|
-
this._savePositionPixels(inst, r.left, r.top);
|
|
11119
|
-
}
|
|
11120
|
-
},
|
|
11121
|
-
/**
|
|
11122
|
-
* @param {Object} inst
|
|
11123
|
-
* @param {boolean} minimized
|
|
11124
|
-
*/
|
|
11125
|
-
_setMinimizeButtonState: function(inst, minimized) {
|
|
11126
|
-
const b = inst.refs && inst.refs.btnMinimize;
|
|
11127
|
-
if (!b) return;
|
|
11128
|
-
b.innerHTML = "";
|
|
11129
|
-
b.appendChild(icon(minimized ? "plus" : "minus"));
|
|
11130
|
-
b.setAttribute("aria-label", minimized ? "Expand player" : "Minimize player");
|
|
11131
|
-
b.setAttribute("aria-expanded", minimized ? "false" : "true");
|
|
11132
|
-
},
|
|
11133
|
-
/**
|
|
11134
|
-
* @param {HTMLElement} container
|
|
11135
|
-
* @param {string} which 'bottom-left' | 'bottom-right'
|
|
11136
|
-
*/
|
|
11137
|
-
_setCornerPosition: function(container, which) {
|
|
11138
|
-
container.classList.remove("is-position-custom", "vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11139
|
-
container.style.removeProperty("--vd-music-player-floating-top");
|
|
11140
|
-
container.style.removeProperty("--vd-music-player-floating-left");
|
|
11141
|
-
if (which === "bottom-left") {
|
|
11142
|
-
container.classList.add("vd-music-player-floating-bottom-left");
|
|
11143
|
-
} else {
|
|
11144
|
-
container.classList.add("vd-music-player-floating-bottom-right");
|
|
11145
|
-
}
|
|
11146
|
-
},
|
|
11147
|
-
/**
|
|
11148
|
-
* @param {HTMLElement} container
|
|
11149
|
-
* @param {number} left
|
|
11150
|
-
* @param {number} top
|
|
11151
|
-
*/
|
|
11152
|
-
_setCustomPositionFromRect: function(container, left, top) {
|
|
11153
|
-
container.classList.remove("vd-music-player-floating-bottom-left", "vd-music-player-floating-bottom-right");
|
|
11154
|
-
container.classList.add("is-position-custom");
|
|
11155
|
-
container.style.setProperty("--vd-music-player-floating-left", left + "px");
|
|
11156
|
-
container.style.setProperty("--vd-music-player-floating-top", top + "px");
|
|
11157
|
-
},
|
|
11158
|
-
/**
|
|
11159
|
-
* @param {HTMLElement} container
|
|
11160
|
-
* @param {Object} inst
|
|
11161
|
-
*/
|
|
11162
|
-
_loadPersistedPosition: function(container, inst) {
|
|
11163
|
-
if (!inst.state.persistPosition) return;
|
|
11164
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11165
|
-
let raw = null;
|
|
11166
|
-
if (typeof window.safeStorageGet === "function") {
|
|
11167
|
-
raw = window.safeStorageGet(key, null);
|
|
11168
|
-
} else {
|
|
11169
|
-
try {
|
|
11170
|
-
raw = localStorage.getItem(key);
|
|
11171
|
-
} catch (_e) {
|
|
11172
|
-
}
|
|
11173
|
-
}
|
|
11174
|
-
if (!raw) return;
|
|
11175
|
-
try {
|
|
11176
|
-
const o = JSON.parse(raw);
|
|
11177
|
-
if (o && typeof o.x === "number" && typeof o.y === "number") {
|
|
11178
|
-
this._setCustomPositionFromRect(container, o.x, o.y);
|
|
11179
|
-
}
|
|
11180
|
-
} catch (_err) {
|
|
11181
|
-
}
|
|
11182
|
-
},
|
|
11183
|
-
/**
|
|
11184
|
-
* @param {Object} inst
|
|
11185
|
-
* @param {number} x
|
|
11186
|
-
* @param {number} y
|
|
11187
|
-
*/
|
|
11188
|
-
_savePositionPixels: function(inst, x, y) {
|
|
11189
|
-
if (!inst.state.persistPosition) return;
|
|
11190
|
-
const container = this._containerOf(inst);
|
|
11191
|
-
if (!container) return;
|
|
11192
|
-
const key = this._persistKeyForInstance(inst, container);
|
|
11193
|
-
const val = JSON.stringify({ x, y });
|
|
11194
|
-
if (typeof window.safeStorageSet === "function") {
|
|
11195
|
-
window.safeStorageSet(key, val);
|
|
11196
|
-
} else {
|
|
11197
|
-
try {
|
|
11198
|
-
localStorage.setItem(key, val);
|
|
11199
|
-
} catch (_e) {
|
|
11200
|
-
}
|
|
11201
|
-
}
|
|
11202
|
-
},
|
|
11203
|
-
/**
|
|
11204
|
-
* @param {Object} inst
|
|
11205
|
-
* @param {HTMLElement} container
|
|
11206
|
-
* @returns {string}
|
|
11207
|
-
*/
|
|
11208
|
-
_persistKeyForInstance: function(inst, container) {
|
|
11209
|
-
const pk = inst.state.persistKey;
|
|
11210
|
-
if (pk && String(pk).trim()) return persistStorageKey(String(pk).trim());
|
|
11211
|
-
return persistStorageKey(container.id || "");
|
|
11212
|
-
},
|
|
11213
|
-
/**
|
|
11214
|
-
* @param {Object} inst
|
|
11215
|
-
*/
|
|
11216
|
-
_unbindFloatingDrag: function(inst) {
|
|
11217
|
-
if (inst.ui && typeof inst.ui.unbindDrag === "function") {
|
|
11218
|
-
inst.ui.unbindDrag();
|
|
11219
|
-
inst.ui.unbindDrag = null;
|
|
11220
|
-
}
|
|
11221
|
-
},
|
|
11222
|
-
/**
|
|
11223
|
-
* Free-form pointer drag on the handle. Vanduo's `draggable` component uses HTML5
|
|
11224
|
-
* drag/drop for list reordering; floating players use pointer events on the handle instead.
|
|
11225
|
-
* @param {Object} inst
|
|
11226
|
-
*/
|
|
11227
|
-
_bindFloatingDrag: function(inst) {
|
|
11228
|
-
this._unbindFloatingDrag(inst);
|
|
11229
|
-
const h = inst.refs && inst.refs.dragHandle;
|
|
11230
|
-
if (!h || !inst.state || !inst.state.draggable) return;
|
|
11231
|
-
const self = this;
|
|
11232
|
-
const container = this._containerOf(inst);
|
|
11233
|
-
if (!container) return;
|
|
11234
|
-
let startX = 0;
|
|
11235
|
-
let startY = 0;
|
|
11236
|
-
let origL = 0;
|
|
11237
|
-
let origT = 0;
|
|
11238
|
-
let activeDrag = false;
|
|
11239
|
-
const onDown = function(e) {
|
|
11240
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
11241
|
-
e.preventDefault();
|
|
11242
|
-
activeDrag = true;
|
|
11243
|
-
const r = container.getBoundingClientRect();
|
|
11244
|
-
origL = r.left;
|
|
11245
|
-
origT = r.top;
|
|
11246
|
-
startX = e.clientX;
|
|
11247
|
-
startY = e.clientY;
|
|
11248
|
-
self._setCustomPositionFromRect(container, origL, origT);
|
|
11249
|
-
try {
|
|
11250
|
-
h.setPointerCapture(e.pointerId);
|
|
11251
|
-
} catch (_err) {
|
|
11252
|
-
}
|
|
11253
|
-
};
|
|
11254
|
-
const onMove = function(e) {
|
|
11255
|
-
if (!activeDrag) return;
|
|
11256
|
-
const dx = e.clientX - startX;
|
|
11257
|
-
const dy = e.clientY - startY;
|
|
11258
|
-
let nl = origL + dx;
|
|
11259
|
-
let nt = origT + dy;
|
|
11260
|
-
const r = container.getBoundingClientRect();
|
|
11261
|
-
const w = r.width;
|
|
11262
|
-
const ph = r.height;
|
|
11263
|
-
const vw = window.innerWidth;
|
|
11264
|
-
const vh = window.innerHeight;
|
|
11265
|
-
const pad = 8;
|
|
11266
|
-
nl = Math.max(pad, Math.min(nl, vw - w - pad));
|
|
11267
|
-
nt = Math.max(pad, Math.min(nt, vh - ph - pad));
|
|
11268
|
-
self._setCustomPositionFromRect(container, nl, nt);
|
|
11269
|
-
};
|
|
11270
|
-
const onUp = function(e) {
|
|
11271
|
-
if (!activeDrag) return;
|
|
11272
|
-
activeDrag = false;
|
|
11273
|
-
if (typeof h.hasPointerCapture === "function" && h.hasPointerCapture(e.pointerId)) {
|
|
11274
|
-
try {
|
|
11275
|
-
h.releasePointerCapture(e.pointerId);
|
|
11276
|
-
} catch (_e) {
|
|
11277
|
-
}
|
|
11278
|
-
}
|
|
11279
|
-
if (inst.state.persistPosition) {
|
|
11280
|
-
const r = container.getBoundingClientRect();
|
|
11281
|
-
self._savePositionPixels(inst, r.left, r.top);
|
|
11282
|
-
}
|
|
11283
|
-
};
|
|
11284
|
-
h.addEventListener("pointerdown", onDown);
|
|
11285
|
-
h.addEventListener("pointermove", onMove);
|
|
11286
|
-
h.addEventListener("pointerup", onUp);
|
|
11287
|
-
h.addEventListener("pointercancel", onUp);
|
|
11288
|
-
inst.ui = inst.ui || { restore: null, unbindDrag: null };
|
|
11289
|
-
inst.ui.unbindDrag = function() {
|
|
11290
|
-
h.removeEventListener("pointerdown", onDown);
|
|
11291
|
-
h.removeEventListener("pointermove", onMove);
|
|
11292
|
-
h.removeEventListener("pointerup", onUp);
|
|
11293
|
-
h.removeEventListener("pointercancel", onUp);
|
|
11294
|
-
};
|
|
11295
|
-
},
|
|
11296
|
-
/**
|
|
11297
|
-
* Return a shallow copy of the current player state.
|
|
11298
|
-
* @param {HTMLElement} container
|
|
11299
|
-
* @returns {Object|null}
|
|
11300
|
-
*/
|
|
11301
|
-
getState: function(container) {
|
|
11302
|
-
const inst = this.instances.get(container);
|
|
11303
|
-
if (!inst) return null;
|
|
11304
|
-
const s = inst.state;
|
|
11305
|
-
return {
|
|
11306
|
-
isPlaying: s.isPlaying,
|
|
11307
|
-
currentIndex: s.currentIndex,
|
|
11308
|
-
currentTrack: s.tracks[s.currentIndex] || null,
|
|
11309
|
-
volume: s.volume,
|
|
11310
|
-
shuffle: s.shuffle,
|
|
11311
|
-
tracks: s.tracks.slice(),
|
|
11312
|
-
isDetached: Boolean(s.isDetached),
|
|
11313
|
-
isMinimized: Boolean(s.isMinimized)
|
|
11314
|
-
};
|
|
11315
|
-
},
|
|
11316
|
-
/**
|
|
11317
|
-
* Stop playback, clean up listeners, remove instance.
|
|
11318
|
-
* @param {HTMLElement} container
|
|
11319
|
-
*/
|
|
11320
|
-
destroy: function(container) {
|
|
11321
|
-
const inst = this.instances.get(container);
|
|
11322
|
-
if (!inst) return;
|
|
11323
|
-
this._unbindFloatingDrag(inst);
|
|
11324
|
-
if (inst.state && inst.state.isDetached) {
|
|
11325
|
-
try {
|
|
11326
|
-
this.attach(container);
|
|
11327
|
-
} catch (_e) {
|
|
11328
|
-
}
|
|
11329
|
-
}
|
|
11330
|
-
inst.cleanup.forEach((fn) => fn());
|
|
11331
|
-
this.instances.delete(container);
|
|
11332
|
-
container.removeAttribute("data-music-player-initialized");
|
|
11333
|
-
},
|
|
11334
|
-
/**
|
|
11335
|
-
* Destroy all instances.
|
|
11336
|
-
*/
|
|
11337
|
-
destroyAll: function() {
|
|
11338
|
-
this.instances.forEach((_, container) => this.destroy(container));
|
|
11339
|
-
},
|
|
11340
|
-
/* ─── Internal helpers ────────────────────────────────── */
|
|
11341
|
-
/**
|
|
11342
|
-
* Load track by index on an already-initialised instance object.
|
|
11343
|
-
* @param {Object} inst
|
|
11344
|
-
* @param {number} index
|
|
11345
|
-
* @param {boolean} autoPlay
|
|
11346
|
-
*/
|
|
11347
|
-
_loadTrack: function(inst, index, autoPlay) {
|
|
11348
|
-
const track = inst.state.tracks[index];
|
|
11349
|
-
if (!track) return;
|
|
11350
|
-
const container = this._containerOf(inst);
|
|
11351
|
-
inst.state.currentIndex = index;
|
|
11352
|
-
inst.audio.src = track.url;
|
|
11353
|
-
if (inst.refs.trackName) {
|
|
11354
|
-
inst.refs.trackName.textContent = track.name || "Unknown Track";
|
|
11355
|
-
inst.refs.trackName.classList.remove("is-idle");
|
|
11356
|
-
}
|
|
11357
|
-
if (inst.refs.playlistPanel) {
|
|
11358
|
-
inst.refs.playlistPanel.querySelectorAll(".vd-music-player-playlist-item").forEach((item, i) => {
|
|
11359
|
-
const active = i === index;
|
|
11360
|
-
item.classList.toggle("is-active", active);
|
|
11361
|
-
item.setAttribute("aria-current", active ? "true" : "false");
|
|
11362
|
-
});
|
|
11363
|
-
}
|
|
11364
|
-
if (inst.refs.progressBar) {
|
|
11365
|
-
inst.refs.progressBar.value = "0";
|
|
11366
|
-
updateRangeFill(inst.refs.progressBar);
|
|
11367
|
-
}
|
|
11368
|
-
if (inst.refs.timeElapsed) inst.refs.timeElapsed.textContent = "0:00";
|
|
11369
|
-
if (inst.refs.timeDuration) inst.refs.timeDuration.textContent = "0:00";
|
|
11370
|
-
if (container) {
|
|
11371
|
-
container.dispatchEvent(
|
|
11372
|
-
new CustomEvent("musicplayer:trackchange", {
|
|
11373
|
-
bubbles: true,
|
|
11374
|
-
detail: { index, name: track.name, url: track.url }
|
|
11375
|
-
})
|
|
11376
|
-
);
|
|
11377
|
-
}
|
|
11378
|
-
if (autoPlay) inst.audio.play().catch(() => {
|
|
11379
|
-
});
|
|
11380
|
-
},
|
|
11381
|
-
/**
|
|
11382
|
-
* Reverse-lookup the container element for a given instance object.
|
|
11383
|
-
* @param {Object} inst
|
|
11384
|
-
* @returns {HTMLElement|null}
|
|
11385
|
-
*/
|
|
11386
|
-
_containerOf: function(inst) {
|
|
11387
|
-
for (const [container, i] of this.instances) {
|
|
11388
|
-
if (i === inst) return container;
|
|
11389
|
-
}
|
|
11390
|
-
return null;
|
|
11391
|
-
}
|
|
11392
|
-
};
|
|
11393
|
-
if (typeof window.Vanduo !== "undefined") {
|
|
11394
|
-
window.Vanduo.register("musicPlayer", MusicPlayer);
|
|
11395
|
-
}
|
|
11396
|
-
window.VanduoMusicPlayer = MusicPlayer;
|
|
11397
|
-
})();
|
|
11398
|
-
|
|
11399
10629
|
// js/index.js
|
|
11400
10630
|
var Vanduo = window.Vanduo;
|
|
11401
10631
|
var index_default = Vanduo;
|