@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.
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.4.2 | Built: 2026-05-23T19:59:36.731Z | git:adbe750 | development */
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.2" : "0.0.0-dev";
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 (!snippet.dataset.initialized) {
465
- this.initSnippet(snippet);
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 = "true";
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
- snippet.dataset.expanded = !expanded;
516
- toggle.setAttribute("aria-expanded", !expanded);
517
- content.dataset.visible = !expanded;
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(/(&lt;\/?)([\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-]+)(=)(&quot;|&#39;)/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(/(&quot;|&#39;)([^&]*)(&quot;|&#39;)/g, '$1<span class="code-string">$2</span>$3');
857
857
  html = html.replace(/(&lt;!--)(.*?)(--&gt;)/g, '<span class="code-comment">$1$2$3</span>');
858
+ html = html.replace(/&lt;(?!--)([\s\S]*?)&gt;/g, function(match, inner) {
859
+ const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
860
+ if (closingMatch) {
861
+ return "&lt;" + closingMatch[1] + '<span class="code-tag">' + closingMatch[2] + "</span>&gt;";
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*)(&quot;|&#39;)([\s\S]*?)(&quot;|&#39;)/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 '&lt;<span class="code-tag">' + tagName + "</span>" + attrSource + selfClosingSuffix + "&gt;";
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
- js = js.replace(/(\/\/.*$)/gm, '<span class="code-comment">$1</span>');
888
- js = js.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
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(root, '[data-toggle="theme"]');
5084
+ return window.Vanduo.queryAll(scope, '.vd-theme-switcher[data-theme-ui="menu"]');
5019
5085
  }
5020
- return Array.from(document.querySelectorAll('[data-toggle="theme"]'));
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 = ["system", "light", "dark"];
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 escaped = _escapeHtml(text);
8510
- const re = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
8511
- li.innerHTML = escaped.replace(re, '<span class="vd-suggest-match">$1</span>');
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 (!input.contains(e.target) && !popup.contains(e.target)) close();
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;