@vanduo-oss/framework 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /*! Vanduo v1.4.2 | Built: 2026-05-23T19:59:36.731Z | git:adbe750 | development */
1
+ /*! Vanduo v1.4.3 | Built: 2026-05-28T13:14:25.371Z | git:02ef7f6 | development */
2
2
 
3
3
  // js/utils/lifecycle.js
4
4
  (function() {
@@ -176,7 +176,7 @@
176
176
  // js/vanduo.js
177
177
  (function() {
178
178
  "use strict";
179
- const VANDUO_VERSION = true ? "1.4.2" : "0.0.0-dev";
179
+ const VANDUO_VERSION = true ? "1.4.3" : "0.0.0-dev";
180
180
  const hasOwn = Object.prototype.hasOwnProperty;
181
181
  const Vanduo2 = {
182
182
  version: VANDUO_VERSION,
@@ -849,12 +849,38 @@
849
849
  * @returns {string} HTML with syntax highlighting spans
850
850
  */
851
851
  highlightHtml: function(html) {
852
- html = html.replace(/(&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
852
  html = html.replace(/(&lt;!--)(.*?)(--&gt;)/g, '<span class="code-comment">$1$2$3</span>');
853
+ html = html.replace(/&lt;(?!--)([\s\S]*?)&gt;/g, function(match, inner) {
854
+ const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
855
+ if (closingMatch) {
856
+ return "&lt;" + closingMatch[1] + '<span class="code-tag">' + closingMatch[2] + "</span>&gt;";
857
+ }
858
+ const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
859
+ const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : "";
860
+ const tagSource = selfClosingMatch ? inner.slice(0, inner.length - selfClosingMatch[0].length) : inner;
861
+ const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
862
+ if (!openMatch) {
863
+ return match;
864
+ }
865
+ const tagName = openMatch[1];
866
+ let attrSource = openMatch[2];
867
+ attrSource = attrSource.replace(
868
+ /([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
869
+ function(attrMatch, attrName, separator, quote, value) {
870
+ return '<span class="code-attr">' + attrName + "</span>" + separator + quote + '<span class="code-string">' + value + "</span>" + quote;
871
+ }
872
+ );
873
+ attrSource = attrSource.replace(
874
+ /([\w:-]+)(\s*=\s*)(&quot;|&#39;)([\s\S]*?)(&quot;|&#39;)/g,
875
+ function(attrMatch, attrName, separator, openQuote, value, closeQuote) {
876
+ if (openQuote !== closeQuote) {
877
+ return attrMatch;
878
+ }
879
+ return '<span class="code-attr">' + attrName + "</span>" + separator + openQuote + '<span class="code-string">' + value + "</span>" + closeQuote;
880
+ }
881
+ );
882
+ return '&lt;<span class="code-tag">' + tagName + "</span>" + attrSource + selfClosingSuffix + "&gt;";
883
+ });
858
884
  return html;
859
885
  },
860
886
  /**
@@ -876,16 +902,26 @@
876
902
  * @returns {string} JS with syntax highlighting spans
877
903
  */
878
904
  highlightJs: function(js) {
905
+ const protectedTokens = [];
906
+ const protectToken = (value, className) => {
907
+ const marker = String.fromCharCode(57344 + protectedTokens.length);
908
+ protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + "</span>" });
909
+ return marker;
910
+ };
911
+ js = js.replace(
912
+ /\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
913
+ (match) => protectToken(match, match.startsWith("/") ? "code-comment" : "code-string")
914
+ );
879
915
  const keywords = ["const", "let", "var", "function", "return", "if", "else", "for", "while", "switch", "case", "break", "continue", "new", "this", "class", "extends", "import", "export", "default", "async", "await", "try", "catch", "throw", "typeof", "instanceof"];
880
916
  keywords.forEach((kw) => {
881
917
  const regex = new RegExp(`\\b(${kw})\\b`, "g");
882
918
  js = js.replace(regex, '<span class="code-keyword">$1</span>');
883
919
  });
884
- js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
885
920
  js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
886
921
  js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
887
- js = js.replace(/(\/\/.*$)/gm, '<span class="code-comment">$1</span>');
888
- js = js.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
922
+ protectedTokens.forEach((token) => {
923
+ js = js.replace(token.marker, token.html);
924
+ });
889
925
  return js;
890
926
  },
891
927
  /**
@@ -8431,11 +8467,6 @@
8431
8467
  // js/components/suggest.js
8432
8468
  (function() {
8433
8469
  "use strict";
8434
- function _escapeHtml(text) {
8435
- const div = document.createElement("div");
8436
- div.textContent = text;
8437
- return div.innerHTML;
8438
- }
8439
8470
  function _isSafeUrl(url, allowlist) {
8440
8471
  try {
8441
8472
  const resolved = new URL(url, window.location.href);
@@ -8506,9 +8537,24 @@
8506
8537
  li.id = listId + "-item-" + i;
8507
8538
  const text = typeof item === "object" ? item.label || item.text || String(item) : String(item);
8508
8539
  if (query) {
8509
- const 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>');
8540
+ const lowerText = text.toLowerCase();
8541
+ const lowerQuery = query.toLowerCase();
8542
+ let start = 0;
8543
+ let matchIndex = lowerText.indexOf(lowerQuery, start);
8544
+ while (matchIndex !== -1) {
8545
+ if (matchIndex > start) {
8546
+ li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
8547
+ }
8548
+ const matchSpan = document.createElement("span");
8549
+ matchSpan.className = "vd-suggest-match";
8550
+ matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
8551
+ li.appendChild(matchSpan);
8552
+ start = matchIndex + query.length;
8553
+ matchIndex = lowerText.indexOf(lowerQuery, start);
8554
+ }
8555
+ if (start < text.length) {
8556
+ li.appendChild(document.createTextNode(text.slice(start)));
8557
+ }
8512
8558
  } else {
8513
8559
  li.textContent = text;
8514
8560
  }
@@ -8978,6 +9024,7 @@
8978
9024
  let viewMode = "days";
8979
9025
  let focusedDate = null;
8980
9026
  let skipNextFocusOpen = false;
9027
+ let ignoreOutsideUntil = 0;
8981
9028
  const isDisabled = (d) => {
8982
9029
  if (minDate) {
8983
9030
  const t = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
@@ -9343,7 +9390,11 @@
9343
9390
  const repositionHandler = () => {
9344
9391
  positionPopup();
9345
9392
  };
9393
+ const markIgnoreOutside = () => {
9394
+ ignoreOutsideUntil = Date.now() + 100;
9395
+ };
9346
9396
  const open = () => {
9397
+ markIgnoreOutside();
9347
9398
  viewMode = "days";
9348
9399
  if (selectedDate) {
9349
9400
  viewYear = selectedDate.getFullYear();
@@ -9377,8 +9428,22 @@
9377
9428
  }
9378
9429
  open();
9379
9430
  };
9431
+ const clickHandler = () => {
9432
+ if (!popup.classList.contains("is-open")) open();
9433
+ };
9434
+ const isOpenAnchorTarget = (target) => {
9435
+ if (!target || !(target instanceof Node)) return false;
9436
+ if (target === input || input.contains(target) || popup.contains(target)) return true;
9437
+ const inputId = input.id;
9438
+ if (inputId) {
9439
+ const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
9440
+ if (label && (target === label || label.contains(target))) return true;
9441
+ }
9442
+ return false;
9443
+ };
9380
9444
  const outsideHandler = (e) => {
9381
- if (!input.contains(e.target) && !popup.contains(e.target)) close();
9445
+ if (Date.now() < ignoreOutsideUntil) return;
9446
+ if (!isOpenAnchorTarget(e.target)) close();
9382
9447
  };
9383
9448
  const escHandler = (e) => {
9384
9449
  if (e.key === "Escape" && popup.classList.contains("is-open")) {
@@ -9388,6 +9453,7 @@
9388
9453
  }
9389
9454
  };
9390
9455
  input.addEventListener("focus", focusHandler);
9456
+ input.addEventListener("click", clickHandler);
9391
9457
  document.addEventListener("click", outsideHandler, true);
9392
9458
  document.addEventListener("keydown", escHandler);
9393
9459
  popup.addEventListener("keydown", handleGridKeydown);
@@ -9398,6 +9464,7 @@
9398
9464
  input.setAttribute("autocomplete", "off");
9399
9465
  cleanup.push(
9400
9466
  () => input.removeEventListener("focus", focusHandler),
9467
+ () => input.removeEventListener("click", clickHandler),
9401
9468
  () => document.removeEventListener("click", outsideHandler, true),
9402
9469
  () => document.removeEventListener("keydown", escHandler),
9403
9470
  () => popup.removeEventListener("keydown", handleGridKeydown),
@@ -10330,1072 +10397,6 @@
10330
10397
  window.VanduoSpotlight = Spotlight;
10331
10398
  })();
10332
10399
 
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
10400
  // js/index.js
11400
10401
  var Vanduo = window.Vanduo;
11401
10402
  var index_default = Vanduo;