vidply 1.0.3 → 1.0.5

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,437 +1,611 @@
1
- /**
2
- * VidPly Playlist Manager
3
- * Manages playlists for audio and video content
4
- */
5
-
6
- import { DOMUtils } from '../utils/DOMUtils.js';
7
- import { createIconElement } from '../icons/Icons.js';
8
-
9
- export class PlaylistManager {
10
- constructor(player, options = {}) {
11
- this.player = player;
12
- this.tracks = [];
13
- this.currentIndex = -1;
14
-
15
- // Options
16
- this.options = {
17
- autoAdvance: options.autoAdvance !== false, // Default true
18
- loop: options.loop || false,
19
- showPanel: options.showPanel !== false, // Default true
20
- ...options
21
- };
22
-
23
- // UI elements
24
- this.container = null;
25
- this.playlistPanel = null;
26
- this.trackInfoElement = null;
27
-
28
- // Bind methods
29
- this.handleTrackEnd = this.handleTrackEnd.bind(this);
30
- this.handleTrackError = this.handleTrackError.bind(this);
31
-
32
- // Initialize
33
- this.init();
34
- }
35
-
36
- init() {
37
- // Listen for track end
38
- this.player.on('ended', this.handleTrackEnd);
39
- this.player.on('error', this.handleTrackError);
40
-
41
- // Create UI if needed
42
- if (this.options.showPanel) {
43
- this.createUI();
44
- }
45
- }
46
-
47
- /**
48
- * Load a playlist
49
- * @param {Array} tracks - Array of track objects
50
- */
51
- loadPlaylist(tracks) {
52
- this.tracks = tracks;
53
- this.currentIndex = -1;
54
-
55
- // Add playlist class to container
56
- if (this.container) {
57
- this.container.classList.add('vidply-has-playlist');
58
- }
59
-
60
- // Update UI
61
- if (this.playlistPanel) {
62
- this.renderPlaylist();
63
- }
64
-
65
- // Auto-play first track
66
- if (tracks.length > 0) {
67
- this.play(0);
68
- }
69
- }
70
-
71
- /**
72
- * Play a specific track
73
- * @param {number} index - Track index
74
- */
75
- play(index) {
76
- if (index < 0 || index >= this.tracks.length) {
77
- console.warn('VidPly Playlist: Invalid track index', index);
78
- return;
79
- }
80
-
81
- const track = this.tracks[index];
82
-
83
- // Update current index
84
- this.currentIndex = index;
85
-
86
- // Load track into player
87
- this.player.load({
88
- src: track.src,
89
- type: track.type,
90
- poster: track.poster,
91
- tracks: track.tracks || []
92
- });
93
-
94
- // Update UI
95
- this.updateTrackInfo(track);
96
- this.updatePlaylistUI();
97
-
98
- // Emit event
99
- this.player.emit('playlisttrackchange', {
100
- index: index,
101
- item: track,
102
- total: this.tracks.length
103
- });
104
-
105
- // Auto-play
106
- setTimeout(() => {
107
- this.player.play();
108
- }, 100);
109
- }
110
-
111
- /**
112
- * Play next track
113
- */
114
- next() {
115
- let nextIndex = this.currentIndex + 1;
116
-
117
- if (nextIndex >= this.tracks.length) {
118
- if (this.options.loop) {
119
- nextIndex = 0;
120
- } else {
121
- return;
122
- }
123
- }
124
-
125
- this.play(nextIndex);
126
- }
127
-
128
- /**
129
- * Play previous track
130
- */
131
- previous() {
132
- let prevIndex = this.currentIndex - 1;
133
-
134
- if (prevIndex < 0) {
135
- if (this.options.loop) {
136
- prevIndex = this.tracks.length - 1;
137
- } else {
138
- return;
139
- }
140
- }
141
-
142
- this.play(prevIndex);
143
- }
144
-
145
- /**
146
- * Handle track end
147
- */
148
- handleTrackEnd() {
149
- if (this.options.autoAdvance) {
150
- this.next();
151
- }
152
- }
153
-
154
- /**
155
- * Handle track error
156
- */
157
- handleTrackError(e) {
158
- console.error('VidPly Playlist: Track error', e);
159
-
160
- // Try next track
161
- if (this.options.autoAdvance) {
162
- setTimeout(() => {
163
- this.next();
164
- }, 1000);
165
- }
166
- }
167
-
168
- /**
169
- * Create playlist UI
170
- */
171
- createUI() {
172
- // Find player container
173
- this.container = this.player.container;
174
-
175
- if (!this.container) {
176
- console.warn('VidPly Playlist: No container found');
177
- return;
178
- }
179
-
180
- // Create track info element (shows current track)
181
- this.trackInfoElement = DOMUtils.createElement('div', {
182
- className: 'vidply-track-info'
183
- });
184
- this.trackInfoElement.style.display = 'none';
185
-
186
- this.container.appendChild(this.trackInfoElement);
187
-
188
- // Create playlist panel
189
- this.playlistPanel = DOMUtils.createElement('div', {
190
- className: 'vidply-playlist-panel'
191
- });
192
- this.playlistPanel.style.display = 'none';
193
-
194
- this.container.appendChild(this.playlistPanel);
195
- }
196
-
197
- /**
198
- * Update track info display
199
- */
200
- updateTrackInfo(track) {
201
- if (!this.trackInfoElement) return;
202
-
203
- const trackNumber = this.currentIndex + 1;
204
- const totalTracks = this.tracks.length;
205
-
206
- this.trackInfoElement.innerHTML = `
207
- <div class="vidply-track-number">Track ${trackNumber} of ${totalTracks}</div>
208
- <div class="vidply-track-title">${DOMUtils.escapeHTML(track.title || 'Untitled')}</div>
209
- ${track.artist ? `<div class="vidply-track-artist">${DOMUtils.escapeHTML(track.artist)}</div>` : ''}
210
- `;
211
-
212
- this.trackInfoElement.style.display = 'block';
213
- }
214
-
215
- /**
216
- * Render playlist
217
- */
218
- renderPlaylist() {
219
- if (!this.playlistPanel) return;
220
-
221
- // Clear existing
222
- this.playlistPanel.innerHTML = '';
223
-
224
- // Create header
225
- const header = DOMUtils.createElement('div', {
226
- className: 'vidply-playlist-header'
227
- });
228
- header.textContent = `Playlist (${this.tracks.length})`;
229
- this.playlistPanel.appendChild(header);
230
-
231
- // Create list
232
- const list = DOMUtils.createElement('div', {
233
- className: 'vidply-playlist-list'
234
- });
235
-
236
- this.tracks.forEach((track, index) => {
237
- const item = this.createPlaylistItem(track, index);
238
- list.appendChild(item);
239
- });
240
-
241
- this.playlistPanel.appendChild(list);
242
- this.playlistPanel.style.display = 'block';
243
- }
244
-
245
- /**
246
- * Create playlist item element
247
- */
248
- createPlaylistItem(track, index) {
249
- const item = DOMUtils.createElement('div', {
250
- className: 'vidply-playlist-item',
251
- role: 'button',
252
- tabIndex: 0,
253
- 'aria-label': `Play ${track.title || 'Track ' + (index + 1)}`
254
- });
255
-
256
- // Add active class if current
257
- if (index === this.currentIndex) {
258
- item.classList.add('vidply-playlist-item-active');
259
- }
260
-
261
- // Thumbnail or icon
262
- const thumbnail = DOMUtils.createElement('div', {
263
- className: 'vidply-playlist-thumbnail'
264
- });
265
-
266
- if (track.poster) {
267
- thumbnail.style.backgroundImage = `url(${track.poster})`;
268
- } else {
269
- // Show music/speaker icon for audio tracks
270
- const icon = createIconElement('music');
271
- icon.classList.add('vidply-playlist-thumbnail-icon');
272
- thumbnail.appendChild(icon);
273
- }
274
-
275
- item.appendChild(thumbnail);
276
-
277
- // Info
278
- const info = DOMUtils.createElement('div', {
279
- className: 'vidply-playlist-item-info'
280
- });
281
-
282
- const title = DOMUtils.createElement('div', {
283
- className: 'vidply-playlist-item-title'
284
- });
285
- title.textContent = track.title || `Track ${index + 1}`;
286
- info.appendChild(title);
287
-
288
- if (track.artist) {
289
- const artist = DOMUtils.createElement('div', {
290
- className: 'vidply-playlist-item-artist'
291
- });
292
- artist.textContent = track.artist;
293
- info.appendChild(artist);
294
- }
295
-
296
- item.appendChild(info);
297
-
298
- // Play icon
299
- const playIcon = createIconElement('play');
300
- playIcon.classList.add('vidply-playlist-item-icon');
301
- item.appendChild(playIcon);
302
-
303
- // Click handler
304
- item.addEventListener('click', () => {
305
- this.play(index);
306
- });
307
-
308
- // Keyboard handler
309
- item.addEventListener('keydown', (e) => {
310
- if (e.key === 'Enter' || e.key === ' ') {
311
- e.preventDefault();
312
- this.play(index);
313
- }
314
- });
315
-
316
- return item;
317
- }
318
-
319
- /**
320
- * Update playlist UI (highlight current track)
321
- */
322
- updatePlaylistUI() {
323
- if (!this.playlistPanel) return;
324
-
325
- const items = this.playlistPanel.querySelectorAll('.vidply-playlist-item');
326
-
327
- items.forEach((item, index) => {
328
- if (index === this.currentIndex) {
329
- item.classList.add('vidply-playlist-item-active');
330
-
331
- // Scroll into view
332
- item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
333
- } else {
334
- item.classList.remove('vidply-playlist-item-active');
335
- }
336
- });
337
- }
338
-
339
- /**
340
- * Get current track
341
- */
342
- getCurrentTrack() {
343
- return this.tracks[this.currentIndex] || null;
344
- }
345
-
346
- /**
347
- * Get playlist info
348
- */
349
- getPlaylistInfo() {
350
- return {
351
- currentIndex: this.currentIndex,
352
- totalTracks: this.tracks.length,
353
- currentTrack: this.getCurrentTrack(),
354
- hasNext: this.currentIndex < this.tracks.length - 1,
355
- hasPrevious: this.currentIndex > 0
356
- };
357
- }
358
-
359
- /**
360
- * Add track to playlist
361
- */
362
- addTrack(track) {
363
- this.tracks.push(track);
364
-
365
- if (this.playlistPanel) {
366
- this.renderPlaylist();
367
- }
368
- }
369
-
370
- /**
371
- * Remove track from playlist
372
- */
373
- removeTrack(index) {
374
- if (index < 0 || index >= this.tracks.length) return;
375
-
376
- this.tracks.splice(index, 1);
377
-
378
- // Adjust current index if needed
379
- if (index < this.currentIndex) {
380
- this.currentIndex--;
381
- } else if (index === this.currentIndex) {
382
- // Current track was removed, play next or stop
383
- if (this.currentIndex >= this.tracks.length) {
384
- this.currentIndex = this.tracks.length - 1;
385
- }
386
-
387
- if (this.currentIndex >= 0) {
388
- this.play(this.currentIndex);
389
- }
390
- }
391
-
392
- if (this.playlistPanel) {
393
- this.renderPlaylist();
394
- }
395
- }
396
-
397
- /**
398
- * Clear playlist
399
- */
400
- clear() {
401
- this.tracks = [];
402
- this.currentIndex = -1;
403
-
404
- if (this.playlistPanel) {
405
- this.playlistPanel.innerHTML = '';
406
- this.playlistPanel.style.display = 'none';
407
- }
408
-
409
- if (this.trackInfoElement) {
410
- this.trackInfoElement.innerHTML = '';
411
- this.trackInfoElement.style.display = 'none';
412
- }
413
- }
414
-
415
- /**
416
- * Destroy playlist manager
417
- */
418
- destroy() {
419
- // Remove event listeners
420
- this.player.off('ended', this.handleTrackEnd);
421
- this.player.off('error', this.handleTrackError);
422
-
423
- // Remove UI
424
- if (this.trackInfoElement) {
425
- this.trackInfoElement.remove();
426
- }
427
-
428
- if (this.playlistPanel) {
429
- this.playlistPanel.remove();
430
- }
431
-
432
- // Clear data
433
- this.clear();
434
- }
435
- }
436
-
437
- export default PlaylistManager;
1
+ /**
2
+ * VidPly Playlist Manager
3
+ * Manages playlists for audio and video content
4
+ */
5
+
6
+ import { DOMUtils } from '../utils/DOMUtils.js';
7
+ import { createIconElement } from '../icons/Icons.js';
8
+
9
+ export class PlaylistManager {
10
+ constructor(player, options = {}) {
11
+ this.player = player;
12
+ this.tracks = [];
13
+ this.currentIndex = -1;
14
+
15
+ // Options
16
+ this.options = {
17
+ autoAdvance: options.autoAdvance !== false, // Default true
18
+ loop: options.loop || false,
19
+ showPanel: options.showPanel !== false, // Default true
20
+ ...options
21
+ };
22
+
23
+ // UI elements
24
+ this.container = null;
25
+ this.playlistPanel = null;
26
+ this.trackInfoElement = null;
27
+
28
+ // Bind methods
29
+ this.handleTrackEnd = this.handleTrackEnd.bind(this);
30
+ this.handleTrackError = this.handleTrackError.bind(this);
31
+
32
+ // Register this playlist manager with the player
33
+ this.player.playlistManager = this;
34
+
35
+ // Initialize
36
+ this.init();
37
+
38
+ // Update controls to add playlist buttons
39
+ this.updatePlayerControls();
40
+ }
41
+
42
+ init() {
43
+ // Listen for track end
44
+ this.player.on('ended', this.handleTrackEnd);
45
+ this.player.on('error', this.handleTrackError);
46
+
47
+ // Create UI if needed
48
+ if (this.options.showPanel) {
49
+ this.createUI();
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Update player controls to add playlist navigation buttons
55
+ */
56
+ updatePlayerControls() {
57
+ if (!this.player.controlBar) return;
58
+
59
+ const controlBar = this.player.controlBar;
60
+
61
+ // Clear existing controls content (except the element itself)
62
+ controlBar.element.innerHTML = '';
63
+
64
+ // Recreate controls with playlist buttons now available
65
+ controlBar.createControls();
66
+
67
+ // Reattach events for the new controls
68
+ controlBar.attachEvents();
69
+ controlBar.setupAutoHide();
70
+ }
71
+
72
+ /**
73
+ * Load a playlist
74
+ * @param {Array} tracks - Array of track objects
75
+ */
76
+ loadPlaylist(tracks) {
77
+ this.tracks = tracks;
78
+ this.currentIndex = -1;
79
+
80
+ // Add playlist class to container
81
+ if (this.container) {
82
+ this.container.classList.add('vidply-has-playlist');
83
+ }
84
+
85
+ // Update UI
86
+ if (this.playlistPanel) {
87
+ this.renderPlaylist();
88
+ }
89
+
90
+ // Auto-play first track
91
+ if (tracks.length > 0) {
92
+ this.play(0);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Play a specific track
98
+ * @param {number} index - Track index
99
+ * @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
100
+ */
101
+ play(index, userInitiated = false) {
102
+ if (index < 0 || index >= this.tracks.length) {
103
+ console.warn('VidPly Playlist: Invalid track index', index);
104
+ return;
105
+ }
106
+
107
+ const track = this.tracks[index];
108
+
109
+ // Update current index
110
+ this.currentIndex = index;
111
+
112
+ // Load track into player
113
+ this.player.load({
114
+ src: track.src,
115
+ type: track.type,
116
+ poster: track.poster,
117
+ tracks: track.tracks || []
118
+ });
119
+
120
+ // Update UI
121
+ this.updateTrackInfo(track);
122
+ this.updatePlaylistUI();
123
+
124
+ // Emit event
125
+ this.player.emit('playlisttrackchange', {
126
+ index: index,
127
+ item: track,
128
+ total: this.tracks.length
129
+ });
130
+
131
+ // Return focus to player for keyboard navigation (only on user action)
132
+ if (userInitiated && this.player.container) {
133
+ this.player.container.focus();
134
+ }
135
+
136
+ // Auto-play
137
+ setTimeout(() => {
138
+ this.player.play();
139
+ }, 100);
140
+ }
141
+
142
+ /**
143
+ * Play next track
144
+ */
145
+ next() {
146
+ let nextIndex = this.currentIndex + 1;
147
+
148
+ if (nextIndex >= this.tracks.length) {
149
+ if (this.options.loop) {
150
+ nextIndex = 0;
151
+ } else {
152
+ return;
153
+ }
154
+ }
155
+
156
+ this.play(nextIndex);
157
+ }
158
+
159
+ /**
160
+ * Play previous track
161
+ */
162
+ previous() {
163
+ let prevIndex = this.currentIndex - 1;
164
+
165
+ if (prevIndex < 0) {
166
+ if (this.options.loop) {
167
+ prevIndex = this.tracks.length - 1;
168
+ } else {
169
+ return;
170
+ }
171
+ }
172
+
173
+ this.play(prevIndex);
174
+ }
175
+
176
+ /**
177
+ * Handle track end
178
+ */
179
+ handleTrackEnd() {
180
+ if (this.options.autoAdvance) {
181
+ this.next();
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Handle track error
187
+ */
188
+ handleTrackError(e) {
189
+ console.error('VidPly Playlist: Track error', e);
190
+
191
+ // Try next track
192
+ if (this.options.autoAdvance) {
193
+ setTimeout(() => {
194
+ this.next();
195
+ }, 1000);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Create playlist UI
201
+ */
202
+ createUI() {
203
+ // Find player container
204
+ this.container = this.player.container;
205
+
206
+ if (!this.container) {
207
+ console.warn('VidPly Playlist: No container found');
208
+ return;
209
+ }
210
+
211
+ // Create track info element (shows current track)
212
+ this.trackInfoElement = DOMUtils.createElement('div', {
213
+ className: 'vidply-track-info',
214
+ role: 'status',
215
+ 'aria-live': 'polite',
216
+ 'aria-atomic': 'true'
217
+ });
218
+ this.trackInfoElement.style.display = 'none';
219
+
220
+ this.container.appendChild(this.trackInfoElement);
221
+
222
+ // Create playlist panel with proper landmark
223
+ this.playlistPanel = DOMUtils.createElement('div', {
224
+ className: 'vidply-playlist-panel',
225
+ role: 'region',
226
+ 'aria-label': 'Media playlist'
227
+ });
228
+ this.playlistPanel.style.display = 'none';
229
+
230
+ this.container.appendChild(this.playlistPanel);
231
+ }
232
+
233
+ /**
234
+ * Update track info display
235
+ */
236
+ updateTrackInfo(track) {
237
+ if (!this.trackInfoElement) return;
238
+
239
+ const trackNumber = this.currentIndex + 1;
240
+ const totalTracks = this.tracks.length;
241
+ const trackTitle = track.title || 'Untitled';
242
+ const trackArtist = track.artist || '';
243
+
244
+ // Screen reader announcement
245
+ const announcement = `Now playing: Track ${trackNumber} of ${totalTracks}. ${trackTitle}${trackArtist ? ' by ' + trackArtist : ''}`;
246
+
247
+ this.trackInfoElement.innerHTML = `
248
+ <span class="vidply-sr-only">${DOMUtils.escapeHTML(announcement)}</span>
249
+ <div class="vidply-track-number" aria-hidden="true">Track ${trackNumber} of ${totalTracks}</div>
250
+ <div class="vidply-track-title" aria-hidden="true">${DOMUtils.escapeHTML(trackTitle)}</div>
251
+ ${trackArtist ? `<div class="vidply-track-artist" aria-hidden="true">${DOMUtils.escapeHTML(trackArtist)}</div>` : ''}
252
+ `;
253
+
254
+ this.trackInfoElement.style.display = 'block';
255
+ }
256
+
257
+ /**
258
+ * Render playlist
259
+ */
260
+ renderPlaylist() {
261
+ if (!this.playlistPanel) return;
262
+
263
+ // Clear existing
264
+ this.playlistPanel.innerHTML = '';
265
+
266
+ // Create header
267
+ const header = DOMUtils.createElement('h2', {
268
+ className: 'vidply-playlist-header',
269
+ id: 'vidply-playlist-heading'
270
+ });
271
+ header.textContent = `Playlist (${this.tracks.length})`;
272
+ this.playlistPanel.appendChild(header);
273
+
274
+ // Add keyboard instructions (visually hidden)
275
+ const instructions = DOMUtils.createElement('div', {
276
+ className: 'vidply-sr-only',
277
+ 'aria-hidden': 'false'
278
+ });
279
+ instructions.textContent = 'Use arrow keys to navigate between tracks. Press Enter or Space to play a track. Press Home or End to jump to first or last track.';
280
+ this.playlistPanel.appendChild(instructions);
281
+
282
+ // Create list (proper ul element)
283
+ const list = DOMUtils.createElement('ul', {
284
+ className: 'vidply-playlist-list',
285
+ 'aria-labelledby': 'vidply-playlist-heading',
286
+ 'aria-describedby': 'vidply-playlist-instructions'
287
+ });
288
+
289
+ // Add list description
290
+ const listDescription = DOMUtils.createElement('div', {
291
+ className: 'vidply-sr-only',
292
+ id: 'vidply-playlist-instructions'
293
+ });
294
+ listDescription.textContent = `Playlist with ${this.tracks.length} ${this.tracks.length === 1 ? 'track' : 'tracks'}`;
295
+ this.playlistPanel.appendChild(listDescription);
296
+
297
+ this.tracks.forEach((track, index) => {
298
+ const item = this.createPlaylistItem(track, index);
299
+ list.appendChild(item);
300
+ });
301
+
302
+ this.playlistPanel.appendChild(list);
303
+ this.playlistPanel.style.display = 'block';
304
+ }
305
+
306
+ /**
307
+ * Create playlist item element
308
+ */
309
+ createPlaylistItem(track, index) {
310
+ const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
311
+ const trackTitle = track.title || `Track ${index + 1}`;
312
+ const trackArtist = track.artist ? ` by ${track.artist}` : '';
313
+ const isActive = index === this.currentIndex;
314
+ const statusText = isActive ? 'Currently playing' : 'Not playing';
315
+ const actionText = isActive ? 'Press Enter to restart' : 'Press Enter to play';
316
+
317
+ const item = DOMUtils.createElement('li', {
318
+ className: 'vidply-playlist-item',
319
+ tabIndex: index === 0 ? 0 : -1, // Only first item is in tab order initially
320
+ 'aria-label': `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`,
321
+ 'aria-posinset': index + 1,
322
+ 'aria-setsize': this.tracks.length,
323
+ 'data-playlist-index': index
324
+ });
325
+
326
+ // Add active class if current
327
+ if (isActive) {
328
+ item.classList.add('vidply-playlist-item-active');
329
+ item.setAttribute('aria-current', 'true');
330
+ item.setAttribute('tabIndex', '0'); // Active item should always be tabbable
331
+ }
332
+
333
+ // Add screen reader only position info
334
+ const positionInfo = DOMUtils.createElement('span', {
335
+ className: 'vidply-sr-only'
336
+ });
337
+ positionInfo.textContent = `${trackPosition}: `;
338
+ item.appendChild(positionInfo);
339
+
340
+ // Thumbnail or icon
341
+ const thumbnail = DOMUtils.createElement('div', {
342
+ className: 'vidply-playlist-thumbnail',
343
+ 'aria-hidden': 'true'
344
+ });
345
+
346
+ if (track.poster) {
347
+ thumbnail.style.backgroundImage = `url(${track.poster})`;
348
+ thumbnail.setAttribute('role', 'img');
349
+ thumbnail.setAttribute('aria-label', `${trackTitle} thumbnail`);
350
+ } else {
351
+ // Show music/speaker icon for audio tracks
352
+ const icon = createIconElement('music');
353
+ icon.classList.add('vidply-playlist-thumbnail-icon');
354
+ thumbnail.appendChild(icon);
355
+ }
356
+
357
+ item.appendChild(thumbnail);
358
+
359
+ // Info
360
+ const info = DOMUtils.createElement('div', {
361
+ className: 'vidply-playlist-item-info',
362
+ 'aria-hidden': 'true'
363
+ });
364
+
365
+ const title = DOMUtils.createElement('div', {
366
+ className: 'vidply-playlist-item-title'
367
+ });
368
+ title.textContent = trackTitle;
369
+ info.appendChild(title);
370
+
371
+ if (track.artist) {
372
+ const artist = DOMUtils.createElement('div', {
373
+ className: 'vidply-playlist-item-artist'
374
+ });
375
+ artist.textContent = track.artist;
376
+ info.appendChild(artist);
377
+ }
378
+
379
+ item.appendChild(info);
380
+
381
+ // Status indicator for screen readers
382
+ if (isActive) {
383
+ const statusIndicator = DOMUtils.createElement('span', {
384
+ className: 'vidply-sr-only'
385
+ });
386
+ statusIndicator.textContent = ' (Currently playing)';
387
+ item.appendChild(statusIndicator);
388
+ }
389
+
390
+ // Play icon
391
+ const playIcon = createIconElement('play');
392
+ playIcon.classList.add('vidply-playlist-item-icon');
393
+ playIcon.setAttribute('aria-hidden', 'true');
394
+ item.appendChild(playIcon);
395
+
396
+ // Click handler
397
+ item.addEventListener('click', () => {
398
+ this.play(index, true); // User-initiated
399
+ });
400
+
401
+ // Keyboard handler
402
+ item.addEventListener('keydown', (e) => {
403
+ this.handlePlaylistItemKeydown(e, index);
404
+ });
405
+
406
+ return item;
407
+ }
408
+
409
+ /**
410
+ * Handle keyboard navigation in playlist items
411
+ */
412
+ handlePlaylistItemKeydown(e, index) {
413
+ const items = Array.from(this.playlistPanel.querySelectorAll('.vidply-playlist-item'));
414
+ let newIndex = -1;
415
+
416
+ switch(e.key) {
417
+ case 'Enter':
418
+ case ' ':
419
+ e.preventDefault();
420
+ this.play(index, true); // User-initiated
421
+ break;
422
+
423
+ case 'ArrowDown':
424
+ e.preventDefault();
425
+ // Move to next item
426
+ if (index < items.length - 1) {
427
+ newIndex = index + 1;
428
+ }
429
+ break;
430
+
431
+ case 'ArrowUp':
432
+ e.preventDefault();
433
+ // Move to previous item
434
+ if (index > 0) {
435
+ newIndex = index - 1;
436
+ }
437
+ break;
438
+
439
+ case 'Home':
440
+ e.preventDefault();
441
+ // Move to first item
442
+ newIndex = 0;
443
+ break;
444
+
445
+ case 'End':
446
+ e.preventDefault();
447
+ // Move to last item
448
+ newIndex = items.length - 1;
449
+ break;
450
+ }
451
+
452
+ // Update tab indices for roving tabindex pattern
453
+ if (newIndex !== -1 && newIndex !== index) {
454
+ items[index].setAttribute('tabIndex', '-1');
455
+ items[newIndex].setAttribute('tabIndex', '0');
456
+ items[newIndex].focus();
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Update playlist UI (highlight current track)
462
+ */
463
+ updatePlaylistUI() {
464
+ if (!this.playlistPanel) return;
465
+
466
+ const items = this.playlistPanel.querySelectorAll('.vidply-playlist-item');
467
+
468
+ items.forEach((item, index) => {
469
+ const track = this.tracks[index];
470
+ const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
471
+ const trackTitle = track.title || `Track ${index + 1}`;
472
+ const trackArtist = track.artist ? ` by ${track.artist}` : '';
473
+
474
+ if (index === this.currentIndex) {
475
+ item.classList.add('vidply-playlist-item-active');
476
+ item.setAttribute('aria-current', 'true');
477
+ item.setAttribute('tabIndex', '0'); // Active item should be tabbable
478
+
479
+ const statusText = 'Currently playing';
480
+ const actionText = 'Press Enter to restart';
481
+ item.setAttribute('aria-label', `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
482
+
483
+ // Scroll into view
484
+ item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
485
+ } else {
486
+ item.classList.remove('vidply-playlist-item-active');
487
+ item.removeAttribute('aria-current');
488
+ item.setAttribute('tabIndex', '-1'); // Remove from tab order (use arrow keys)
489
+
490
+ const statusText = 'Not playing';
491
+ const actionText = 'Press Enter to play';
492
+ item.setAttribute('aria-label', `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
493
+ }
494
+ });
495
+ }
496
+
497
+ /**
498
+ * Get current track
499
+ */
500
+ getCurrentTrack() {
501
+ return this.tracks[this.currentIndex] || null;
502
+ }
503
+
504
+ /**
505
+ * Get playlist info
506
+ */
507
+ getPlaylistInfo() {
508
+ return {
509
+ currentIndex: this.currentIndex,
510
+ totalTracks: this.tracks.length,
511
+ currentTrack: this.getCurrentTrack(),
512
+ hasNext: this.hasNext(),
513
+ hasPrevious: this.hasPrevious()
514
+ };
515
+ }
516
+
517
+ /**
518
+ * Check if there is a next track
519
+ */
520
+ hasNext() {
521
+ if (this.options.loop) return true;
522
+ return this.currentIndex < this.tracks.length - 1;
523
+ }
524
+
525
+ /**
526
+ * Check if there is a previous track
527
+ */
528
+ hasPrevious() {
529
+ if (this.options.loop) return true;
530
+ return this.currentIndex > 0;
531
+ }
532
+
533
+ /**
534
+ * Add track to playlist
535
+ */
536
+ addTrack(track) {
537
+ this.tracks.push(track);
538
+
539
+ if (this.playlistPanel) {
540
+ this.renderPlaylist();
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Remove track from playlist
546
+ */
547
+ removeTrack(index) {
548
+ if (index < 0 || index >= this.tracks.length) return;
549
+
550
+ this.tracks.splice(index, 1);
551
+
552
+ // Adjust current index if needed
553
+ if (index < this.currentIndex) {
554
+ this.currentIndex--;
555
+ } else if (index === this.currentIndex) {
556
+ // Current track was removed, play next or stop
557
+ if (this.currentIndex >= this.tracks.length) {
558
+ this.currentIndex = this.tracks.length - 1;
559
+ }
560
+
561
+ if (this.currentIndex >= 0) {
562
+ this.play(this.currentIndex);
563
+ }
564
+ }
565
+
566
+ if (this.playlistPanel) {
567
+ this.renderPlaylist();
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Clear playlist
573
+ */
574
+ clear() {
575
+ this.tracks = [];
576
+ this.currentIndex = -1;
577
+
578
+ if (this.playlistPanel) {
579
+ this.playlistPanel.innerHTML = '';
580
+ this.playlistPanel.style.display = 'none';
581
+ }
582
+
583
+ if (this.trackInfoElement) {
584
+ this.trackInfoElement.innerHTML = '';
585
+ this.trackInfoElement.style.display = 'none';
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Destroy playlist manager
591
+ */
592
+ destroy() {
593
+ // Remove event listeners
594
+ this.player.off('ended', this.handleTrackEnd);
595
+ this.player.off('error', this.handleTrackError);
596
+
597
+ // Remove UI
598
+ if (this.trackInfoElement) {
599
+ this.trackInfoElement.remove();
600
+ }
601
+
602
+ if (this.playlistPanel) {
603
+ this.playlistPanel.remove();
604
+ }
605
+
606
+ // Clear data
607
+ this.clear();
608
+ }
609
+ }
610
+
611
+ export default PlaylistManager;