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.
- package/dist/dev/{vidply.TranscriptManager-GZKY44ON.js → vidply.TranscriptManager-T677KF4N.js} +5 -6
- package/dist/dev/vidply.TranscriptManager-T677KF4N.js.map +7 -0
- package/dist/dev/{vidply.chunk-UH5MTGKF.js → vidply.chunk-GS2JX5RQ.js} +149 -100
- package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +7 -0
- package/dist/dev/{vidply.de-THBIMP4S.js → vidply.de-SNL6AJ4D.js} +10 -2
- package/dist/dev/{vidply.de-THBIMP4S.js.map → vidply.de-SNL6AJ4D.js.map} +2 -2
- package/dist/dev/{vidply.es-6VWDNNNL.js → vidply.es-2QCQKZ4U.js} +10 -2
- package/dist/dev/{vidply.es-6VWDNNNL.js.map → vidply.es-2QCQKZ4U.js.map} +2 -2
- package/dist/dev/vidply.esm.js +1681 -317
- package/dist/dev/vidply.esm.js.map +4 -4
- package/dist/dev/{vidply.fr-WHTWCHWT.js → vidply.fr-FJAZRL4L.js} +10 -2
- package/dist/dev/{vidply.fr-WHTWCHWT.js.map → vidply.fr-FJAZRL4L.js.map} +2 -2
- package/dist/dev/{vidply.ja-BFQNPOFI.js → vidply.ja-2XQOW53T.js} +10 -2
- package/dist/dev/vidply.ja-2XQOW53T.js.map +7 -0
- package/dist/legacy/vidply.js +1829 -361
- package/dist/legacy/vidply.js.map +4 -4
- package/dist/legacy/vidply.min.js +1 -1
- package/dist/legacy/vidply.min.meta.json +103 -35
- package/dist/prod/vidply.TranscriptManager-WFZSW6NR.min.js +6 -0
- package/dist/prod/vidply.chunk-LGTJRPUL.min.js +6 -0
- package/dist/prod/vidply.de-FR3XX54P.min.js +6 -0
- package/dist/prod/vidply.es-3IJCQLJ7.min.js +6 -0
- package/dist/prod/vidply.esm.min.js +8 -8
- package/dist/prod/vidply.fr-NC4VEAPH.min.js +6 -0
- package/dist/prod/vidply.ja-4ZC6ZQLV.min.js +6 -0
- package/dist/vidply.esm.min.meta.json +115 -47
- package/package.json +1 -1
- package/src/controls/ControlBar.js +3 -7
- package/src/controls/TranscriptManager.js +8 -8
- package/src/core/AudioDescriptionManager.js +701 -0
- package/src/core/Player.js +4776 -4921
- package/src/core/SignLanguageManager.js +1134 -0
- package/src/features/PlaylistManager.js +12 -12
- package/src/i18n/languages/de.js +9 -1
- package/src/i18n/languages/en.js +9 -1
- package/src/i18n/languages/es.js +9 -1
- package/src/i18n/languages/fr.js +9 -1
- package/src/i18n/languages/ja.js +9 -1
- package/src/utils/DOMUtils.js +153 -114
- package/src/utils/MenuFactory.js +374 -0
- package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +0 -7
- package/dist/dev/vidply.chunk-UH5MTGKF.js.map +0 -7
- package/dist/dev/vidply.ja-BFQNPOFI.js.map +0 -7
- package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +0 -6
- package/dist/prod/vidply.chunk-MBUR3U5L.min.js +0 -6
- package/dist/prod/vidply.de-SWFW4HYT.min.js +0 -6
- package/dist/prod/vidply.es-7BJ2DJAY.min.js +0 -6
- package/dist/prod/vidply.fr-DPVR5DFY.min.js +0 -6
- 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 = '
|
|
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 =
|
|
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 = '
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1235
|
+
announcement = i18n.t('playlist.lastTrack', { current: buttons.length, total: buttons.length });
|
|
1236
1236
|
}
|
|
1237
1237
|
break;
|
|
1238
1238
|
}
|
package/src/i18n/languages/de.js
CHANGED
|
@@ -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
|
|
package/src/i18n/languages/en.js
CHANGED
|
@@ -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
|
|
package/src/i18n/languages/es.js
CHANGED
|
@@ -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
|
|
package/src/i18n/languages/fr.js
CHANGED
|
@@ -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
|
|
package/src/i18n/languages/ja.js
CHANGED
|
@@ -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
|
|
package/src/utils/DOMUtils.js
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
element.style.display = 'none';
|
|
71
|
-
}
|
|
60
|
+
element?.style && (element.style.display = 'none');
|
|
72
61
|
},
|
|
73
62
|
|
|
74
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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.
|
|
125
|
-
left: rect.left + window.
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
const escapeMap = {
|
|
142
|
+
'&': '&',
|
|
143
|
+
'<': '<',
|
|
144
|
+
'>': '>',
|
|
145
|
+
'"': '"',
|
|
146
|
+
"'": '''
|
|
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
|
-
//
|
|
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, '') //
|
|
148
|
-
.replace(/javascript:/gi, ''); //
|
|
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
|
|
172
|
+
* Create a tooltip element (aria-hidden)
|
|
156
173
|
* @param {string} text - Tooltip text
|
|
157
|
-
* @param {string} classPrefix - Class prefix
|
|
158
|
-
* @returns {HTMLElement}
|
|
174
|
+
* @param {string} classPrefix - Class prefix
|
|
175
|
+
* @returns {HTMLElement}
|
|
159
176
|
*/
|
|
160
177
|
createTooltip(text, classPrefix = 'vidply') {
|
|
161
|
-
|
|
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 -
|
|
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
|
|
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
|
|
181
|
-
|
|
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
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
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',
|
|
199
|
-
element.addEventListener('mouseleave',
|
|
200
|
-
element.addEventListener('focus',
|
|
201
|
-
element.addEventListener('blur',
|
|
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
|
|
211
|
+
* Create button text element (visible when CSS disabled)
|
|
206
212
|
* @param {string} text - Button text
|
|
207
|
-
* @param {string} classPrefix - Class prefix
|
|
208
|
-
* @returns {HTMLElement}
|
|
213
|
+
* @param {string} classPrefix - Class prefix
|
|
214
|
+
* @returns {HTMLElement}
|
|
209
215
|
*/
|
|
210
216
|
createButtonText(text, classPrefix = 'vidply') {
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|