vidply 1.0.27 → 1.0.29

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.
Files changed (49) hide show
  1. package/dist/dev/{vidply.TranscriptManager-GZKY44ON.js → vidply.TranscriptManager-T677KF4N.js} +5 -6
  2. package/dist/dev/vidply.TranscriptManager-T677KF4N.js.map +7 -0
  3. package/dist/dev/{vidply.chunk-UH5MTGKF.js → vidply.chunk-GS2JX5RQ.js} +149 -100
  4. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +7 -0
  5. package/dist/dev/{vidply.de-THBIMP4S.js → vidply.de-SNL6AJ4D.js} +10 -2
  6. package/dist/dev/{vidply.de-THBIMP4S.js.map → vidply.de-SNL6AJ4D.js.map} +2 -2
  7. package/dist/dev/{vidply.es-6VWDNNNL.js → vidply.es-2QCQKZ4U.js} +10 -2
  8. package/dist/dev/{vidply.es-6VWDNNNL.js.map → vidply.es-2QCQKZ4U.js.map} +2 -2
  9. package/dist/dev/vidply.esm.js +1681 -317
  10. package/dist/dev/vidply.esm.js.map +4 -4
  11. package/dist/dev/{vidply.fr-WHTWCHWT.js → vidply.fr-FJAZRL4L.js} +10 -2
  12. package/dist/dev/{vidply.fr-WHTWCHWT.js.map → vidply.fr-FJAZRL4L.js.map} +2 -2
  13. package/dist/dev/{vidply.ja-BFQNPOFI.js → vidply.ja-2XQOW53T.js} +10 -2
  14. package/dist/dev/vidply.ja-2XQOW53T.js.map +7 -0
  15. package/dist/legacy/vidply.js +1829 -361
  16. package/dist/legacy/vidply.js.map +4 -4
  17. package/dist/legacy/vidply.min.js +1 -1
  18. package/dist/legacy/vidply.min.meta.json +103 -35
  19. package/dist/prod/vidply.TranscriptManager-WFZSW6NR.min.js +6 -0
  20. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +6 -0
  21. package/dist/prod/vidply.de-FR3XX54P.min.js +6 -0
  22. package/dist/prod/vidply.es-3IJCQLJ7.min.js +6 -0
  23. package/dist/prod/vidply.esm.min.js +8 -8
  24. package/dist/prod/vidply.fr-NC4VEAPH.min.js +6 -0
  25. package/dist/prod/vidply.ja-4ZC6ZQLV.min.js +6 -0
  26. package/dist/vidply.esm.min.meta.json +115 -47
  27. package/package.json +1 -1
  28. package/src/controls/ControlBar.js +3 -7
  29. package/src/controls/TranscriptManager.js +8 -8
  30. package/src/core/AudioDescriptionManager.js +701 -0
  31. package/src/core/Player.js +4776 -4921
  32. package/src/core/SignLanguageManager.js +1134 -0
  33. package/src/features/PlaylistManager.js +12 -12
  34. package/src/i18n/languages/de.js +9 -1
  35. package/src/i18n/languages/en.js +9 -1
  36. package/src/i18n/languages/es.js +9 -1
  37. package/src/i18n/languages/fr.js +9 -1
  38. package/src/i18n/languages/ja.js +9 -1
  39. package/src/utils/DOMUtils.js +153 -114
  40. package/src/utils/MenuFactory.js +374 -0
  41. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +0 -7
  42. package/dist/dev/vidply.chunk-UH5MTGKF.js.map +0 -7
  43. package/dist/dev/vidply.ja-BFQNPOFI.js.map +0 -7
  44. package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +0 -6
  45. package/dist/prod/vidply.chunk-MBUR3U5L.min.js +0 -6
  46. package/dist/prod/vidply.de-SWFW4HYT.min.js +0 -6
  47. package/dist/prod/vidply.es-7BJ2DJAY.min.js +0 -6
  48. package/dist/prod/vidply.fr-DPVR5DFY.min.js +0 -6
  49. package/dist/prod/vidply.ja-PEBVWKVH.min.js +0 -6
@@ -969,7 +969,7 @@ export class PlaylistManager {
969
969
  id: `${this.uniqueId}-keyboard-instructions`
970
970
  }
971
971
  });
972
- instructions.textContent = 'Playlist navigation: Use Up and Down arrow keys to move between tracks. Press Page Up or Page Down to skip 5 tracks. Press Home to go to first track, End to go to last track. Press Enter or Space to play the selected track.';
972
+ instructions.textContent = i18n.t('playlist.keyboardInstructions');
973
973
  this.playlistPanel.appendChild(instructions);
974
974
 
975
975
  // Create list (proper ul element)
@@ -1180,10 +1180,10 @@ export class PlaylistManager {
1180
1180
  newIndex = index + 1;
1181
1181
  } else {
1182
1182
  // At the end, announce boundary
1183
- announcement = `End of playlist. ${buttons.length} of ${buttons.length}.`;
1183
+ announcement = i18n.t('playlist.endOfPlaylist', { current: buttons.length, total: buttons.length });
1184
1184
  }
1185
1185
  break;
1186
-
1186
+
1187
1187
  case 'ArrowUp':
1188
1188
  e.preventDefault();
1189
1189
  e.stopPropagation();
@@ -1192,47 +1192,47 @@ export class PlaylistManager {
1192
1192
  newIndex = index - 1;
1193
1193
  } else {
1194
1194
  // At the beginning, announce boundary
1195
- announcement = 'Beginning of playlist. 1 of ' + buttons.length + '.';
1195
+ announcement = i18n.t('playlist.beginningOfPlaylist', { total: buttons.length });
1196
1196
  }
1197
1197
  break;
1198
-
1198
+
1199
1199
  case 'PageDown':
1200
1200
  e.preventDefault();
1201
1201
  e.stopPropagation();
1202
1202
  // Move 5 items down (or to end)
1203
1203
  newIndex = Math.min(index + 5, buttons.length - 1);
1204
1204
  if (newIndex === buttons.length - 1 && index !== newIndex) {
1205
- announcement = `Jumped to last track. ${newIndex + 1} of ${buttons.length}.`;
1205
+ announcement = i18n.t('playlist.jumpedToLastTrack', { current: newIndex + 1, total: buttons.length });
1206
1206
  }
1207
1207
  break;
1208
-
1208
+
1209
1209
  case 'PageUp':
1210
1210
  e.preventDefault();
1211
1211
  e.stopPropagation();
1212
1212
  // Move 5 items up (or to beginning)
1213
1213
  newIndex = Math.max(index - 5, 0);
1214
1214
  if (newIndex === 0 && index !== newIndex) {
1215
- announcement = `Jumped to first track. 1 of ${buttons.length}.`;
1215
+ announcement = i18n.t('playlist.jumpedToFirstTrack', { total: buttons.length });
1216
1216
  }
1217
1217
  break;
1218
-
1218
+
1219
1219
  case 'Home':
1220
1220
  e.preventDefault();
1221
1221
  e.stopPropagation();
1222
1222
  // Move to first item
1223
1223
  newIndex = 0;
1224
1224
  if (index !== 0) {
1225
- announcement = `First track. 1 of ${buttons.length}.`;
1225
+ announcement = i18n.t('playlist.firstTrack', { total: buttons.length });
1226
1226
  }
1227
1227
  break;
1228
-
1228
+
1229
1229
  case 'End':
1230
1230
  e.preventDefault();
1231
1231
  e.stopPropagation();
1232
1232
  // Move to last item
1233
1233
  newIndex = buttons.length - 1;
1234
1234
  if (index !== buttons.length - 1) {
1235
- announcement = `Last track. ${buttons.length} of ${buttons.length}.`;
1235
+ announcement = i18n.t('playlist.lastTrack', { current: buttons.length, total: buttons.length });
1236
1236
  }
1237
1237
  break;
1238
1238
  }
@@ -102,6 +102,7 @@ export const de = {
102
102
  },
103
103
  transcript: {
104
104
  title: 'Transkript',
105
+ ariaLabel: 'Video-Transkript',
105
106
  close: 'Transkript schließen',
106
107
  loading: 'Transkript wird geladen...',
107
108
  noTranscript: 'Kein Transkript für dieses Video verfügbar.',
@@ -168,7 +169,14 @@ export const de = {
168
169
  currentlyPlaying: 'Wird gerade abgespielt',
169
170
  notPlaying: 'Nicht aktiv',
170
171
  pressEnterPlay: 'Eingabetaste zum Abspielen',
171
- pressEnterRestart: 'Eingabetaste zum Neustart'
172
+ pressEnterRestart: 'Eingabetaste zum Neustart',
173
+ keyboardInstructions: 'Wiedergabelisten-Navigation: Verwenden Sie die Pfeiltasten nach oben und unten, um zwischen Titeln zu wechseln. Drücken Sie Bild auf oder Bild ab, um 5 Titel zu überspringen. Drücken Sie Pos1, um zum ersten Titel zu springen, Ende für den letzten Titel. Drücken Sie die Eingabetaste oder Leertaste, um den ausgewählten Titel abzuspielen.',
174
+ endOfPlaylist: 'Ende der Wiedergabeliste. {current} von {total}.',
175
+ beginningOfPlaylist: 'Anfang der Wiedergabeliste. 1 von {total}.',
176
+ jumpedToLastTrack: 'Zum letzten Titel gesprungen. {current} von {total}.',
177
+ jumpedToFirstTrack: 'Zum ersten Titel gesprungen. 1 von {total}.',
178
+ firstTrack: 'Erster Titel. 1 von {total}.',
179
+ lastTrack: 'Letzter Titel. {current} von {total}.'
172
180
  }
173
181
  };
174
182
 
@@ -102,6 +102,7 @@ export const en = {
102
102
  },
103
103
  transcript: {
104
104
  title: 'Transcript',
105
+ ariaLabel: 'Video Transcript',
105
106
  close: 'Close transcript',
106
107
  loading: 'Loading transcript...',
107
108
  noTranscript: 'No transcript available for this video.',
@@ -168,7 +169,14 @@ export const en = {
168
169
  currentlyPlaying: 'Currently playing',
169
170
  notPlaying: 'Not playing',
170
171
  pressEnterPlay: 'Press Enter to play',
171
- pressEnterRestart: 'Press Enter to restart'
172
+ pressEnterRestart: 'Press Enter to restart',
173
+ keyboardInstructions: 'Playlist navigation: Use Up and Down arrow keys to move between tracks. Press Page Up or Page Down to skip 5 tracks. Press Home to go to first track, End to go to last track. Press Enter or Space to play the selected track.',
174
+ endOfPlaylist: 'End of playlist. {current} of {total}.',
175
+ beginningOfPlaylist: 'Beginning of playlist. 1 of {total}.',
176
+ jumpedToLastTrack: 'Jumped to last track. {current} of {total}.',
177
+ jumpedToFirstTrack: 'Jumped to first track. 1 of {total}.',
178
+ firstTrack: 'First track. 1 of {total}.',
179
+ lastTrack: 'Last track. {current} of {total}.'
172
180
  }
173
181
  };
174
182
 
@@ -102,6 +102,7 @@ export const es = {
102
102
  },
103
103
  transcript: {
104
104
  title: 'Transcripción',
105
+ ariaLabel: 'Transcripción de video',
105
106
  close: 'Cerrar transcripción',
106
107
  loading: 'Cargando transcripción...',
107
108
  noTranscript: 'No hay transcripción disponible para este video.',
@@ -168,7 +169,14 @@ export const es = {
168
169
  currentlyPlaying: 'Reproduciendo actualmente',
169
170
  notPlaying: 'Sin reproducir',
170
171
  pressEnterPlay: 'Pulsa Enter para reproducir',
171
- pressEnterRestart: 'Pulsa Enter para reiniciar'
172
+ pressEnterRestart: 'Pulsa Enter para reiniciar',
173
+ keyboardInstructions: 'Navegación de lista de reproducción: Use las teclas de flecha arriba y abajo para moverse entre pistas. Pulse Retroceder página o Avanzar página para saltar 5 pistas. Pulse Inicio para ir a la primera pista, Fin para la última pista. Pulse Intro o Espacio para reproducir la pista seleccionada.',
174
+ endOfPlaylist: 'Fin de la lista de reproducción. {current} de {total}.',
175
+ beginningOfPlaylist: 'Inicio de la lista de reproducción. 1 de {total}.',
176
+ jumpedToLastTrack: 'Saltó a la última pista. {current} de {total}.',
177
+ jumpedToFirstTrack: 'Saltó a la primera pista. 1 de {total}.',
178
+ firstTrack: 'Primera pista. 1 de {total}.',
179
+ lastTrack: 'Última pista. {current} de {total}.'
172
180
  }
173
181
  };
174
182
 
@@ -102,6 +102,7 @@ export const fr = {
102
102
  },
103
103
  transcript: {
104
104
  title: 'Transcription',
105
+ ariaLabel: 'Transcription vidéo',
105
106
  close: 'Fermer la transcription',
106
107
  loading: 'Chargement de la transcription...',
107
108
  noTranscript: 'Aucune transcription disponible pour cette vidéo.',
@@ -168,7 +169,14 @@ export const fr = {
168
169
  currentlyPlaying: 'En cours de lecture',
169
170
  notPlaying: 'Non en lecture',
170
171
  pressEnterPlay: 'Appuyez sur Entrée pour lire',
171
- pressEnterRestart: 'Appuyez sur Entrée pour recommencer'
172
+ pressEnterRestart: 'Appuyez sur Entrée pour recommencer',
173
+ keyboardInstructions: 'Navigation de la liste de lecture : Utilisez les touches fléchées haut et bas pour naviguer entre les pistes. Appuyez sur Page précédente ou Page suivante pour sauter 5 pistes. Appuyez sur Début pour aller à la première piste, Fin pour la dernière piste. Appuyez sur Entrée ou Espace pour lire la piste sélectionnée.',
174
+ endOfPlaylist: 'Fin de la liste de lecture. {current} sur {total}.',
175
+ beginningOfPlaylist: 'Début de la liste de lecture. 1 sur {total}.',
176
+ jumpedToLastTrack: 'Sauté à la dernière piste. {current} sur {total}.',
177
+ jumpedToFirstTrack: 'Sauté à la première piste. 1 sur {total}.',
178
+ firstTrack: 'Première piste. 1 sur {total}.',
179
+ lastTrack: 'Dernière piste. {current} sur {total}.'
172
180
  }
173
181
  };
174
182
 
@@ -102,6 +102,7 @@ export const ja = {
102
102
  },
103
103
  transcript: {
104
104
  title: '文字起こし',
105
+ ariaLabel: 'ビデオ文字起こし',
105
106
  close: '文字起こしを閉じる',
106
107
  loading: '文字起こしを読み込み中...',
107
108
  noTranscript: 'このビデオの文字起こしはありません。',
@@ -168,7 +169,14 @@ export const ja = {
168
169
  currentlyPlaying: '再生中',
169
170
  notPlaying: '停止中',
170
171
  pressEnterPlay: 'Enterキーで再生',
171
- pressEnterRestart: 'Enterキーで最初から再生'
172
+ pressEnterRestart: 'Enterキーで最初から再生',
173
+ keyboardInstructions: 'プレイリストナビゲーション:上下の矢印キーでトラック間を移動します。Page UpまたはPage Downで5トラックをスキップします。Homeで最初のトラックへ、Endで最後のトラックへ移動します。EnterまたはSpaceで選択したトラックを再生します。',
174
+ endOfPlaylist: 'プレイリストの終わりです。{current}/{total}。',
175
+ beginningOfPlaylist: 'プレイリストの始めです。1/{total}。',
176
+ jumpedToLastTrack: '最後のトラックにジャンプしました。{current}/{total}。',
177
+ jumpedToFirstTrack: '最初のトラックにジャンプしました。1/{total}。',
178
+ firstTrack: '最初のトラックです。1/{total}。',
179
+ lastTrack: '最後のトラックです。{current}/{total}。'
172
180
  }
173
181
  };
174
182
 
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * DOM manipulation utilities
3
+ * Optimized for performance with CSS transitions
3
4
  */
4
5
 
5
6
  export const DOMUtils = {
7
+ /**
8
+ * Create an element with options
9
+ * @param {string} tag - HTML tag name
10
+ * @param {Object} options - Element options
11
+ * @returns {HTMLElement}
12
+ */
6
13
  createElement(tag, options = {}) {
7
14
  const element = document.createElement(tag);
8
15
 
@@ -11,9 +18,9 @@ export const DOMUtils = {
11
18
  }
12
19
 
13
20
  if (options.attributes) {
14
- Object.entries(options.attributes).forEach(([key, value]) => {
21
+ for (const [key, value] of Object.entries(options.attributes)) {
15
22
  element.setAttribute(key, value);
16
- });
23
+ }
17
24
  }
18
25
 
19
26
  if (options.innerHTML) {
@@ -29,193 +36,225 @@ export const DOMUtils = {
29
36
  }
30
37
 
31
38
  if (options.children) {
32
- options.children.forEach(child => {
39
+ for (const child of options.children) {
33
40
  if (child) element.appendChild(child);
34
- });
41
+ }
35
42
  }
36
43
 
37
44
  return element;
38
45
  },
39
46
 
40
- addClass(element, className) {
41
- if (element && className) {
42
- element.classList.add(className);
43
- }
44
- },
45
-
46
- removeClass(element, className) {
47
- if (element && className) {
48
- element.classList.remove(className);
49
- }
50
- },
51
-
52
- toggleClass(element, className) {
53
- if (element && className) {
54
- element.classList.toggle(className);
55
- }
56
- },
57
-
58
- hasClass(element, className) {
59
- return element && element.classList.contains(className);
60
- },
61
-
47
+ /**
48
+ * Show element (remove display:none)
49
+ * @param {HTMLElement} element
50
+ */
62
51
  show(element) {
63
- if (element) {
64
- element.style.display = '';
65
- }
52
+ element?.style && (element.style.display = '');
66
53
  },
67
54
 
55
+ /**
56
+ * Hide element
57
+ * @param {HTMLElement} element
58
+ */
68
59
  hide(element) {
69
- if (element) {
70
- element.style.display = 'none';
71
- }
60
+ element?.style && (element.style.display = 'none');
72
61
  },
73
62
 
74
- fadeIn(element, duration = 300) {
63
+ /**
64
+ * Fade in element using CSS transitions (GPU accelerated)
65
+ * @param {HTMLElement} element
66
+ * @param {number} duration - Duration in ms
67
+ * @param {Function} [onComplete] - Callback when complete
68
+ */
69
+ fadeIn(element, duration = 300, onComplete) {
75
70
  if (!element) return;
76
71
 
72
+ // Set up initial state
77
73
  element.style.opacity = '0';
78
74
  element.style.display = '';
75
+ element.style.transition = `opacity ${duration}ms ease`;
79
76
 
80
- let start = null;
81
- const animate = (timestamp) => {
82
- if (!start) start = timestamp;
83
- const progress = timestamp - start;
84
- const opacity = Math.min(progress / duration, 1);
85
-
86
- element.style.opacity = opacity;
87
-
88
- if (progress < duration) {
89
- requestAnimationFrame(animate);
90
- }
91
- };
77
+ // Force reflow to ensure transition works
78
+ element.offsetHeight;
79
+
80
+ // Trigger transition
81
+ element.style.opacity = '1';
92
82
 
93
- requestAnimationFrame(animate);
83
+ // Cleanup after transition
84
+ if (onComplete) {
85
+ const cleanup = () => {
86
+ element.removeEventListener('transitionend', cleanup);
87
+ onComplete();
88
+ };
89
+ element.addEventListener('transitionend', cleanup, { once: true });
90
+ // Fallback timeout in case transitionend doesn't fire
91
+ setTimeout(cleanup, duration + 50);
92
+ }
94
93
  },
95
94
 
96
- fadeOut(element, duration = 300) {
95
+ /**
96
+ * Fade out element using CSS transitions (GPU accelerated)
97
+ * @param {HTMLElement} element
98
+ * @param {number} duration - Duration in ms
99
+ * @param {Function} [onComplete] - Callback when complete
100
+ */
101
+ fadeOut(element, duration = 300, onComplete) {
97
102
  if (!element) return;
98
103
 
99
- const startOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
100
- let start = null;
101
-
102
- const animate = (timestamp) => {
103
- if (!start) start = timestamp;
104
- const progress = timestamp - start;
105
- const opacity = Math.max(startOpacity - (progress / duration), 0);
106
-
107
- element.style.opacity = opacity;
108
-
109
- if (progress < duration) {
110
- requestAnimationFrame(animate);
111
- } else {
112
- element.style.display = 'none';
113
- }
104
+ element.style.transition = `opacity ${duration}ms ease`;
105
+ element.style.opacity = '0';
106
+
107
+ const cleanup = () => {
108
+ element.removeEventListener('transitionend', cleanup);
109
+ element.style.display = 'none';
110
+ if (onComplete) onComplete();
114
111
  };
115
112
 
116
- requestAnimationFrame(animate);
113
+ element.addEventListener('transitionend', cleanup, { once: true });
114
+ // Fallback timeout in case transitionend doesn't fire
115
+ setTimeout(cleanup, duration + 50);
117
116
  },
118
117
 
118
+ /**
119
+ * Get element's offset position and dimensions
120
+ * @param {HTMLElement} element
121
+ * @returns {Object} { top, left, width, height }
122
+ */
119
123
  offset(element) {
120
- if (!element) return { top: 0, left: 0 };
124
+ if (!element) return { top: 0, left: 0, width: 0, height: 0 };
121
125
 
122
126
  const rect = element.getBoundingClientRect();
123
127
  return {
124
- top: rect.top + window.pageYOffset,
125
- left: rect.left + window.pageXOffset,
128
+ top: rect.top + window.scrollY,
129
+ left: rect.left + window.scrollX,
126
130
  width: rect.width,
127
131
  height: rect.height
128
132
  };
129
133
  },
130
134
 
135
+ /**
136
+ * Escape HTML special characters
137
+ * @param {string} str - String to escape
138
+ * @returns {string} Escaped string
139
+ */
131
140
  escapeHTML(str) {
132
- const div = document.createElement('div');
133
- div.textContent = str;
134
- return div.innerHTML;
141
+ const escapeMap = {
142
+ '&': '&amp;',
143
+ '<': '&lt;',
144
+ '>': '&gt;',
145
+ '"': '&quot;',
146
+ "'": '&#x27;'
147
+ };
148
+ return str.replace(/[&<>"']/g, char => escapeMap[char]);
135
149
  },
136
150
 
151
+ /**
152
+ * Basic HTML sanitization for VTT captions
153
+ * Allows safe formatting tags, removes dangerous content
154
+ * @param {string} html - HTML string to sanitize
155
+ * @returns {string} Sanitized HTML
156
+ */
137
157
  sanitizeHTML(html) {
138
- // Basic HTML sanitization - allow safe tags for VTT captions
139
- // Since we control the HTML (from VTT parsing), we can safely allow these tags
140
- const temp = document.createElement('div');
141
-
142
- // Strip out any potentially dangerous tags/attributes
143
- // Allow: strong, em, u, span, b, i with class and data-voice attributes
158
+ // Remove dangerous content
144
159
  const safeHtml = html
145
160
  .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
146
161
  .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
147
- .replace(/on\w+\s*=/gi, '') // Remove event handlers
148
- .replace(/javascript:/gi, ''); // Remove javascript: protocol
162
+ .replace(/on\w+\s*=/gi, '') // Event handlers
163
+ .replace(/javascript:/gi, ''); // javascript: protocol
149
164
 
165
+ // Use DOM parser for final sanitization
166
+ const temp = document.createElement('div');
150
167
  temp.innerHTML = safeHtml;
151
168
  return temp.innerHTML;
152
169
  },
153
170
 
154
171
  /**
155
- * Create a tooltip element that is aria-hidden (not read by screen readers)
172
+ * Create a tooltip element (aria-hidden)
156
173
  * @param {string} text - Tooltip text
157
- * @param {string} classPrefix - Class prefix for styling
158
- * @returns {HTMLElement} Tooltip element
174
+ * @param {string} classPrefix - Class prefix
175
+ * @returns {HTMLElement}
159
176
  */
160
177
  createTooltip(text, classPrefix = 'vidply') {
161
- const tooltip = this.createElement('span', {
178
+ return this.createElement('span', {
162
179
  className: `${classPrefix}-tooltip`,
163
180
  textContent: text,
164
- attributes: {
165
- 'aria-hidden': 'true'
166
- }
181
+ attributes: { 'aria-hidden': 'true' }
167
182
  });
168
- return tooltip;
169
183
  },
170
184
 
171
185
  /**
172
- * Attach a tooltip to an element
173
- * @param {HTMLElement} element - Element to attach tooltip to
186
+ * Attach a tooltip to an element with hover/focus behavior
187
+ * @param {HTMLElement} element - Target element
174
188
  * @param {string} text - Tooltip text
175
- * @param {string} classPrefix - Class prefix for styling
189
+ * @param {string} classPrefix - Class prefix
176
190
  */
177
191
  attachTooltip(element, text, classPrefix = 'vidply') {
178
192
  if (!element || !text) return;
179
193
 
180
- // Remove existing tooltip if any
181
- const existingTooltip = element.querySelector(`.${classPrefix}-tooltip`);
182
- if (existingTooltip) {
183
- existingTooltip.remove();
184
- }
194
+ // Remove existing tooltip
195
+ element.querySelector(`.${classPrefix}-tooltip`)?.remove();
185
196
 
186
197
  const tooltip = this.createTooltip(text, classPrefix);
187
198
  element.appendChild(tooltip);
188
199
 
189
- // Show tooltip on hover/focus
190
- const showTooltip = () => {
191
- tooltip.classList.add(`${classPrefix}-tooltip-visible`);
192
- };
193
-
194
- const hideTooltip = () => {
195
- tooltip.classList.remove(`${classPrefix}-tooltip-visible`);
196
- };
200
+ const visibleClass = `${classPrefix}-tooltip-visible`;
201
+ const show = () => tooltip.classList.add(visibleClass);
202
+ const hide = () => tooltip.classList.remove(visibleClass);
197
203
 
198
- element.addEventListener('mouseenter', showTooltip);
199
- element.addEventListener('mouseleave', hideTooltip);
200
- element.addEventListener('focus', showTooltip);
201
- element.addEventListener('blur', hideTooltip);
204
+ element.addEventListener('mouseenter', show);
205
+ element.addEventListener('mouseleave', hide);
206
+ element.addEventListener('focus', show);
207
+ element.addEventListener('blur', hide);
202
208
  },
203
209
 
204
210
  /**
205
- * Create visible button text that is hidden by CSS but visible when CSS is disabled
211
+ * Create button text element (visible when CSS disabled)
206
212
  * @param {string} text - Button text
207
- * @param {string} classPrefix - Class prefix for styling
208
- * @returns {HTMLElement} Button text element
213
+ * @param {string} classPrefix - Class prefix
214
+ * @returns {HTMLElement}
209
215
  */
210
216
  createButtonText(text, classPrefix = 'vidply') {
211
- const buttonText = this.createElement('span', {
217
+ return this.createElement('span', {
212
218
  className: `${classPrefix}-button-text`,
213
219
  textContent: text,
214
- attributes: {
215
- 'aria-hidden': 'true'
216
- }
220
+ attributes: { 'aria-hidden': 'true' }
217
221
  });
218
- return buttonText;
222
+ },
223
+
224
+ /**
225
+ * Add class to element (null-safe)
226
+ * @param {HTMLElement} element
227
+ * @param {string} className
228
+ */
229
+ addClass(element, className) {
230
+ element?.classList?.add(className);
231
+ },
232
+
233
+ /**
234
+ * Remove class from element (null-safe)
235
+ * @param {HTMLElement} element
236
+ * @param {string} className
237
+ */
238
+ removeClass(element, className) {
239
+ element?.classList?.remove(className);
240
+ },
241
+
242
+ /**
243
+ * Toggle class on element (null-safe)
244
+ * @param {HTMLElement} element
245
+ * @param {string} className
246
+ */
247
+ toggleClass(element, className) {
248
+ element?.classList?.toggle(className);
249
+ },
250
+
251
+ /**
252
+ * Check if element has class (null-safe)
253
+ * @param {HTMLElement} element
254
+ * @param {string} className
255
+ * @returns {boolean}
256
+ */
257
+ hasClass(element, className) {
258
+ return element?.classList?.contains(className) ?? false;
219
259
  }
220
260
  };
221
-