@arraypress/waveform-bar 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/js/dom.js CHANGED
@@ -40,6 +40,8 @@ export function buildBarHTML(config) {
40
40
  left += '</div>';
41
41
 
42
42
  // --- Centre zone: waveform + time ---
43
+ // In classic mode (`waveform: false`) the embedded player renders its own
44
+ // built-in 'seekbar' style into this same container (see _initPlayer).
43
45
  const centre = `<div class="wb-centre">
44
46
  <div class="wb-waveform-container"></div>
45
47
  <div class="wb-time"><span class="wb-time-current">0:00</span> / <span class="wb-time-total">0:00</span></div>
@@ -74,11 +76,22 @@ export function buildBarHTML(config) {
74
76
  right += '</div>';
75
77
  }
76
78
 
79
+ if (config.share) {
80
+ right += `<button class="wb-btn wb-btn-sm wb-share" aria-label="Share" title="Copy share link">${ICONS.share}</button>`;
81
+ }
82
+
77
83
  if (config.showQueue) {
78
84
  right += `<button class="wb-btn wb-btn-sm wb-queue-btn" aria-label="Queue" title="Queue">${ICONS.queue}</button>`;
79
85
  }
80
86
 
81
87
  right += '</div>';
82
88
 
83
- return `<div class="wb-inner">${left}${centre}${right}</div>`;
89
+ // Collapse-to-pill toggle. A direct child of .wb-inner (its own zone) so
90
+ // the collapsed-pill CSS can hide .wb-centre/.wb-right while keeping this
91
+ // button visible as the "expand" affordance.
92
+ const collapse = config.collapsible
93
+ ? `<button class="wb-btn wb-btn-sm wb-collapse" aria-label="Collapse" title="Collapse">${ICONS.collapse}</button>`
94
+ : '';
95
+
96
+ return `<div class="wb-inner">${left}${centre}${right}${collapse}</div>`;
84
97
  }
package/src/js/icons.js CHANGED
@@ -9,7 +9,10 @@ export const ICONS = {
9
9
  prev: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>',
10
10
  next: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>',
11
11
  queue: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>',
12
+ share: '<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>',
12
13
  music: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" opacity="0.5"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>',
14
+ collapse: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
15
+ expand: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>',
13
16
  volHigh: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>',
14
17
  volLow: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>',
15
18
  volMute: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>',
package/src/js/utils.js CHANGED
@@ -27,6 +27,30 @@ export function escapeHtml(str) {
27
27
  return d.innerHTML;
28
28
  }
29
29
 
30
+ /**
31
+ * Whether a URL is safe to navigate to (assign to `location.href`).
32
+ * Allows only `http`/`https` and relative URLs, rejecting `javascript:`,
33
+ * `data:`, `blob:`, `vbscript:` and other script-bearing schemes.
34
+ *
35
+ * TODO(harden): adopt `WaveformPlayer.utils.isSafeHref` once the peer dep
36
+ * is bumped to ^1.8.0 (which ships this helper). Inlined here so the
37
+ * open-redirect / XSS guard is fixed without a peer bump.
38
+ *
39
+ * @param {string} url
40
+ * @returns {boolean}
41
+ */
42
+ export function isSafeHref(url) {
43
+ if (typeof url !== 'string' || url === '') return false;
44
+ try {
45
+ // Resolve relative URLs against the current document; only the
46
+ // scheme matters for the safety decision.
47
+ const u = new URL(url, location.href);
48
+ return u.protocol === 'http:' || u.protocol === 'https:';
49
+ } catch (e) {
50
+ return false;
51
+ }
52
+ }
53
+
30
54
  /**
31
55
  * Format seconds to M:SS
32
56
  * @param {number} seconds
@@ -48,11 +72,32 @@ export function parseTrackFromElement(el) {
48
72
  const url = el.dataset.wbUrl || el.dataset.url;
49
73
  if (!url) return null;
50
74
 
75
+ // Parse + shape-coerce. JSON.parse only validates the syntax — a value
76
+ // like '"x"' or '[1,2]' parses cleanly but is the wrong shape and would
77
+ // strand the bar downstream (e.g. `.map()` on a non-array). Coerce each
78
+ // field to the shape the rest of the code expects.
51
79
  let meta = {};
52
- try { meta = JSON.parse(el.dataset.wbMeta || el.dataset.meta || '{}'); } catch (e) {}
80
+ try {
81
+ const parsed = JSON.parse(el.dataset.wbMeta || el.dataset.meta || '{}');
82
+ meta = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
83
+ } catch (e) {}
84
+
85
+ let markers = [];
86
+ try {
87
+ const parsed = JSON.parse(el.dataset.wbMarkers || el.dataset.markers || 'null');
88
+ markers = Array.isArray(parsed) ? parsed : [];
89
+ } catch (e) {}
90
+ // Coerce each marker time to a finite number; drop entries that aren't
91
+ // usable objects or whose time can't be parsed.
92
+ markers = markers
93
+ .map(m => (m && typeof m === 'object') ? {...m, time: Number(m.time)} : null)
94
+ .filter(m => m && Number.isFinite(m.time));
53
95
 
54
- let markers = null;
55
- try { markers = JSON.parse(el.dataset.wbMarkers || el.dataset.markers || 'null'); } catch (e) {}
96
+ let waveform = null;
97
+ try {
98
+ const parsed = JSON.parse(el.dataset.wbWaveform || el.dataset.waveform || 'null');
99
+ waveform = Array.isArray(parsed) ? parsed : null;
100
+ } catch (e) {}
56
101
 
57
102
  return {
58
103
  url,
@@ -65,7 +110,7 @@ export function parseTrackFromElement(el) {
65
110
  duration: el.dataset.wbDuration || el.dataset.duration || '',
66
111
  bpm: el.dataset.wbBpm || el.dataset.bpm || '',
67
112
  key: el.dataset.wbKey || el.dataset.key || '',
68
- waveform: el.dataset.wbWaveform || el.dataset.waveform || '',
113
+ waveform,
69
114
  markers,
70
115
  favorited: el.dataset.wbFavorited === 'true',
71
116
  inCart: el.dataset.wbInCart === 'true',