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