vidply 1.0.28 → 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 (61) hide show
  1. package/dist/dev/{vidply.TranscriptManager-QSF2PWUN.js → vidply.TranscriptManager-T677KF4N.js} +4 -5
  2. package/dist/dev/{vidply.TranscriptManager-QSF2PWUN.js.map → vidply.TranscriptManager-T677KF4N.js.map} +2 -2
  3. package/dist/dev/{vidply.chunk-SRM7VNHG.js → vidply.chunk-GS2JX5RQ.js} +136 -95
  4. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +7 -0
  5. package/dist/dev/vidply.esm.js +1674 -310
  6. package/dist/dev/vidply.esm.js.map +4 -4
  7. package/dist/legacy/vidply.js +1776 -348
  8. package/dist/legacy/vidply.js.map +4 -4
  9. package/dist/legacy/vidply.min.js +1 -1
  10. package/dist/legacy/vidply.min.meta.json +92 -24
  11. package/dist/prod/vidply.TranscriptManager-WFZSW6NR.min.js +6 -0
  12. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +6 -0
  13. package/dist/prod/vidply.esm.min.js +8 -8
  14. package/dist/vidply.esm.min.meta.json +92 -24
  15. package/package.json +1 -1
  16. package/src/controls/ControlBar.js +3 -7
  17. package/src/controls/TranscriptManager.js +7 -7
  18. package/src/core/AudioDescriptionManager.js +701 -0
  19. package/src/core/Player.js +4776 -4921
  20. package/src/core/SignLanguageManager.js +1134 -0
  21. package/src/utils/DOMUtils.js +153 -114
  22. package/src/utils/MenuFactory.js +374 -0
  23. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js +0 -1744
  24. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +0 -7
  25. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js +0 -1744
  26. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js.map +0 -7
  27. package/dist/dev/vidply.chunk-5663PYKK.js +0 -1631
  28. package/dist/dev/vidply.chunk-5663PYKK.js.map +0 -7
  29. package/dist/dev/vidply.chunk-SRM7VNHG.js.map +0 -7
  30. package/dist/dev/vidply.chunk-UH5MTGKF.js +0 -1630
  31. package/dist/dev/vidply.chunk-UH5MTGKF.js.map +0 -7
  32. package/dist/dev/vidply.de-RXAJM5QE.js +0 -181
  33. package/dist/dev/vidply.de-RXAJM5QE.js.map +0 -7
  34. package/dist/dev/vidply.de-THBIMP4S.js +0 -180
  35. package/dist/dev/vidply.de-THBIMP4S.js.map +0 -7
  36. package/dist/dev/vidply.es-6VWDNNNL.js +0 -180
  37. package/dist/dev/vidply.es-6VWDNNNL.js.map +0 -7
  38. package/dist/dev/vidply.es-SADVLJTQ.js +0 -181
  39. package/dist/dev/vidply.es-SADVLJTQ.js.map +0 -7
  40. package/dist/dev/vidply.fr-V3VAYBBT.js +0 -181
  41. package/dist/dev/vidply.fr-V3VAYBBT.js.map +0 -7
  42. package/dist/dev/vidply.fr-WHTWCHWT.js +0 -180
  43. package/dist/dev/vidply.fr-WHTWCHWT.js.map +0 -7
  44. package/dist/dev/vidply.ja-BFQNPOFI.js +0 -180
  45. package/dist/dev/vidply.ja-BFQNPOFI.js.map +0 -7
  46. package/dist/dev/vidply.ja-KL2TLZGJ.js +0 -181
  47. package/dist/dev/vidply.ja-KL2TLZGJ.js.map +0 -7
  48. package/dist/prod/vidply.TranscriptManager-DZ2WZU3K.min.js +0 -6
  49. package/dist/prod/vidply.TranscriptManager-E5QHGFIR.min.js +0 -6
  50. package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +0 -6
  51. package/dist/prod/vidply.chunk-5DWTMWEO.min.js +0 -6
  52. package/dist/prod/vidply.chunk-IBNYTGGM.min.js +0 -6
  53. package/dist/prod/vidply.chunk-MBUR3U5L.min.js +0 -6
  54. package/dist/prod/vidply.de-HGJBCLLE.min.js +0 -6
  55. package/dist/prod/vidply.de-SWFW4HYT.min.js +0 -6
  56. package/dist/prod/vidply.es-7BJ2DJAY.min.js +0 -6
  57. package/dist/prod/vidply.es-CZEBXCZN.min.js +0 -6
  58. package/dist/prod/vidply.fr-DPVR5DFY.min.js +0 -6
  59. package/dist/prod/vidply.fr-HFOL7MWA.min.js +0 -6
  60. package/dist/prod/vidply.ja-PEBVWKVH.min.js +0 -6
  61. package/dist/prod/vidply.ja-QTVU5C25.min.js +0 -6
@@ -0,0 +1,1134 @@
1
+ /**
2
+ * Sign Language Video Manager
3
+ * Handles picture-in-picture sign language video overlay
4
+ */
5
+
6
+ import { DOMUtils } from '../utils/DOMUtils.js';
7
+ import { createIconElement } from '../icons/Icons.js';
8
+ import { i18n } from '../i18n/i18n.js';
9
+ import { DraggableResizable } from '../utils/DraggableResizable.js';
10
+ import { createMenuItem, attachMenuKeyboardNavigation, focusFirstMenuItem } from '../utils/MenuUtils.js';
11
+ import { createLabeledSelect, preventDragOnElement } from '../utils/FormUtils.js';
12
+
13
+ export class SignLanguageManager {
14
+ constructor(player) {
15
+ this.player = player;
16
+
17
+ // Sources
18
+ this.src = player.options.signLanguageSrc;
19
+ this.sources = player.options.signLanguageSources || {};
20
+ this.currentLanguage = null;
21
+ this.desiredPosition = player.options.signLanguagePosition || 'bottom-right';
22
+
23
+ // DOM elements
24
+ this.wrapper = null;
25
+ this.header = null;
26
+ this.video = null;
27
+ this.selector = null;
28
+ this.settingsButton = null;
29
+ this.settingsMenu = null;
30
+ this.resizeHandles = [];
31
+
32
+ // State
33
+ this.enabled = false;
34
+ this.settingsMenuVisible = false;
35
+ this.settingsMenuJustOpened = false;
36
+ this.documentClickHandlerAdded = false;
37
+
38
+ // Handlers
39
+ this.handlers = null;
40
+ this.settingsHandlers = null;
41
+ this.interactionHandlers = null;
42
+ this.draggable = null;
43
+ this.documentClickHandler = null;
44
+ this.settingsMenuKeyHandler = null;
45
+ this.customKeyHandler = null;
46
+
47
+ // Menu option references
48
+ this.dragOptionButton = null;
49
+ this.dragOptionText = null;
50
+ this.resizeOptionButton = null;
51
+ this.resizeOptionText = null;
52
+ }
53
+
54
+ /**
55
+ * Check if sign language is available
56
+ */
57
+ isAvailable() {
58
+ return Object.keys(this.sources).length > 0 || !!this.src;
59
+ }
60
+
61
+ /**
62
+ * Enable sign language video
63
+ */
64
+ enable() {
65
+ const hasMultipleSources = Object.keys(this.sources).length > 0;
66
+ const hasSingleSource = !!this.src;
67
+
68
+ if (!hasMultipleSources && !hasSingleSource) {
69
+ console.warn('No sign language video source provided');
70
+ return;
71
+ }
72
+
73
+ if (this.wrapper) {
74
+ // Already exists, just show it
75
+ this.wrapper.style.display = 'block';
76
+ this.enabled = true;
77
+ this.player.state.signLanguageEnabled = true;
78
+ this.player.emit('signlanguageenabled');
79
+
80
+ // Focus settings button
81
+ this.player.setManagedTimeout(() => {
82
+ if (this.settingsButton && document.contains(this.settingsButton)) {
83
+ this.settingsButton.focus({ preventScroll: true });
84
+ }
85
+ }, 150);
86
+ return;
87
+ }
88
+
89
+ // Determine initial language
90
+ let initialLang = null;
91
+ let initialSrc = null;
92
+
93
+ if (hasMultipleSources) {
94
+ initialLang = this._determineInitialLanguage();
95
+ initialSrc = this.sources[initialLang];
96
+ this.currentLanguage = initialLang;
97
+ } else {
98
+ initialSrc = this.src;
99
+ }
100
+
101
+ // Create UI
102
+ this._createWrapper();
103
+ this._createHeader(hasMultipleSources, initialLang);
104
+ this._createVideo(initialSrc);
105
+ this._createResizeHandles();
106
+
107
+ // Assemble
108
+ this.wrapper.appendChild(this.header);
109
+ this.wrapper.appendChild(this.video);
110
+ this.resizeHandles.forEach(handle => this.wrapper.appendChild(handle));
111
+
112
+ // Set initial size
113
+ this._applyInitialSize();
114
+
115
+ // Add to container
116
+ this.player.container.appendChild(this.wrapper);
117
+
118
+ // Position
119
+ requestAnimationFrame(() => {
120
+ this.constrainPosition();
121
+ });
122
+
123
+ // Sync with main video
124
+ this.video.currentTime = this.player.state.currentTime;
125
+ if (!this.player.state.paused) {
126
+ this.video.play();
127
+ }
128
+
129
+ // Setup interaction
130
+ this._setupInteraction();
131
+
132
+ // Setup event handlers
133
+ this._setupEventHandlers(hasMultipleSources);
134
+
135
+ this.enabled = true;
136
+ this.player.state.signLanguageEnabled = true;
137
+ this.player.emit('signlanguageenabled');
138
+
139
+ // Focus settings button
140
+ this.player.setManagedTimeout(() => {
141
+ if (this.settingsButton && document.contains(this.settingsButton)) {
142
+ this.settingsButton.focus({ preventScroll: true });
143
+ }
144
+ }, 150);
145
+ }
146
+
147
+ /**
148
+ * Disable sign language video
149
+ */
150
+ disable() {
151
+ if (this.settingsMenuVisible) {
152
+ this.hideSettingsMenu({ focusButton: false });
153
+ }
154
+
155
+ if (this.wrapper) {
156
+ this.wrapper.style.display = 'none';
157
+ }
158
+ this.enabled = false;
159
+ this.player.state.signLanguageEnabled = false;
160
+ this.player.emit('signlanguagedisabled');
161
+ }
162
+
163
+ /**
164
+ * Toggle sign language video
165
+ */
166
+ toggle() {
167
+ if (this.enabled) {
168
+ this.disable();
169
+ } else {
170
+ this.enable();
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Switch to a different sign language
176
+ */
177
+ switchLanguage(langCode) {
178
+ if (!this.sources[langCode] || !this.video) {
179
+ return;
180
+ }
181
+
182
+ const currentTime = this.video.currentTime;
183
+ const wasPlaying = !this.video.paused;
184
+
185
+ this.video.src = this.sources[langCode];
186
+ this.currentLanguage = langCode;
187
+
188
+ // Restore playback state
189
+ this.video.currentTime = currentTime;
190
+ if (wasPlaying) {
191
+ this.video.play().catch(() => {});
192
+ }
193
+
194
+ this.player.emit('signlanguagelanguagechanged', langCode);
195
+ }
196
+
197
+ /**
198
+ * Get language label
199
+ */
200
+ getLanguageLabel(langCode) {
201
+ const langNames = {
202
+ 'en': 'English',
203
+ 'de': 'Deutsch',
204
+ 'es': 'Español',
205
+ 'fr': 'Français',
206
+ 'it': 'Italiano',
207
+ 'ja': '日本語',
208
+ 'pt': 'Português',
209
+ 'ar': 'العربية',
210
+ 'hi': 'हिन्दी'
211
+ };
212
+ return langNames[langCode] || langCode.toUpperCase();
213
+ }
214
+
215
+ /**
216
+ * Determine initial sign language
217
+ */
218
+ _determineInitialLanguage() {
219
+ // Try caption language
220
+ if (this.player.captionManager && this.player.captionManager.currentTrack) {
221
+ const captionLang = this.player.captionManager.currentTrack.language?.toLowerCase().split('-')[0];
222
+ if (captionLang && this.sources[captionLang]) {
223
+ return captionLang;
224
+ }
225
+ }
226
+
227
+ // Try player language
228
+ if (this.player.options.language) {
229
+ const playerLang = this.player.options.language.toLowerCase().split('-')[0];
230
+ if (this.sources[playerLang]) {
231
+ return playerLang;
232
+ }
233
+ }
234
+
235
+ // First available
236
+ return Object.keys(this.sources)[0];
237
+ }
238
+
239
+ /**
240
+ * Create wrapper element
241
+ */
242
+ _createWrapper() {
243
+ this.wrapper = document.createElement('div');
244
+ this.wrapper.className = 'vidply-sign-language-wrapper';
245
+ this.wrapper.setAttribute('tabindex', '0');
246
+ this.wrapper.setAttribute('aria-label', i18n.t('player.signLanguageDragResize'));
247
+ }
248
+
249
+ /**
250
+ * Create header element
251
+ */
252
+ _createHeader(hasMultipleSources, initialLang) {
253
+ const classPrefix = this.player.options.classPrefix;
254
+
255
+ this.header = DOMUtils.createElement('div', {
256
+ className: `${classPrefix}-sign-language-header`,
257
+ attributes: { 'tabindex': '0' }
258
+ });
259
+
260
+ const headerLeft = DOMUtils.createElement('div', {
261
+ className: `${classPrefix}-sign-language-header-left`
262
+ });
263
+
264
+ const title = DOMUtils.createElement('h3', {
265
+ textContent: i18n.t('player.signLanguageVideo')
266
+ });
267
+
268
+ // Settings button
269
+ this._createSettingsButton(headerLeft);
270
+
271
+ // Language selector
272
+ if (hasMultipleSources) {
273
+ this._createLanguageSelector(headerLeft, initialLang);
274
+ }
275
+
276
+ headerLeft.appendChild(title);
277
+
278
+ // Close button
279
+ const closeButton = this._createCloseButton();
280
+
281
+ this.header.appendChild(headerLeft);
282
+ this.header.appendChild(closeButton);
283
+
284
+ // Initialize settings menu state
285
+ this.settingsMenuVisible = false;
286
+ this.settingsMenu = null;
287
+ this.settingsMenuJustOpened = false;
288
+ }
289
+
290
+ /**
291
+ * Create settings button
292
+ */
293
+ _createSettingsButton(container) {
294
+ const classPrefix = this.player.options.classPrefix;
295
+ const ariaLabel = i18n.t('player.signLanguageSettings');
296
+
297
+ this.settingsButton = DOMUtils.createElement('button', {
298
+ className: `${classPrefix}-sign-language-settings`,
299
+ attributes: {
300
+ 'type': 'button',
301
+ 'aria-label': ariaLabel,
302
+ 'aria-expanded': 'false'
303
+ }
304
+ });
305
+ this.settingsButton.appendChild(createIconElement('settings'));
306
+ DOMUtils.attachTooltip(this.settingsButton, ariaLabel, classPrefix);
307
+
308
+ this.settingsHandlers = {
309
+ click: (e) => {
310
+ e.preventDefault();
311
+ e.stopPropagation();
312
+ if (this.documentClickHandler) {
313
+ this.settingsMenuJustOpened = true;
314
+ setTimeout(() => { this.settingsMenuJustOpened = false; }, 100);
315
+ }
316
+ if (this.settingsMenuVisible) {
317
+ this.hideSettingsMenu();
318
+ } else {
319
+ this.showSettingsMenu();
320
+ }
321
+ },
322
+ keydown: (e) => {
323
+ if (e.key === 'd' || e.key === 'D') {
324
+ e.preventDefault();
325
+ e.stopPropagation();
326
+ this.toggleKeyboardDragMode();
327
+ } else if (e.key === 'r' || e.key === 'R') {
328
+ e.preventDefault();
329
+ e.stopPropagation();
330
+ this.toggleResizeMode();
331
+ } else if (e.key === 'Escape' && this.settingsMenuVisible) {
332
+ e.preventDefault();
333
+ e.stopPropagation();
334
+ this.hideSettingsMenu();
335
+ }
336
+ }
337
+ };
338
+
339
+ this.settingsButton.addEventListener('click', this.settingsHandlers.click);
340
+ this.settingsButton.addEventListener('keydown', this.settingsHandlers.keydown);
341
+ container.appendChild(this.settingsButton);
342
+ }
343
+
344
+ /**
345
+ * Create language selector
346
+ */
347
+ _createLanguageSelector(container, initialLang) {
348
+ const classPrefix = this.player.options.classPrefix;
349
+ const selectId = `${classPrefix}-sign-language-select-${Date.now()}`;
350
+
351
+ const options = Object.keys(this.sources).map(langCode => ({
352
+ value: langCode,
353
+ text: this.getLanguageLabel(langCode),
354
+ selected: langCode === initialLang
355
+ }));
356
+
357
+ const { label, select } = createLabeledSelect({
358
+ classPrefix,
359
+ labelClass: `${classPrefix}-sign-language-label`,
360
+ selectClass: `${classPrefix}-sign-language-select`,
361
+ labelText: 'settings.language',
362
+ selectId,
363
+ options,
364
+ onChange: (e) => {
365
+ e.stopPropagation();
366
+ this.switchLanguage(e.target.value);
367
+ }
368
+ });
369
+
370
+ this.selector = select;
371
+
372
+ const selectorWrapper = DOMUtils.createElement('div', {
373
+ className: `${classPrefix}-sign-language-selector-wrapper`
374
+ });
375
+ selectorWrapper.appendChild(label);
376
+ selectorWrapper.appendChild(this.selector);
377
+
378
+ preventDragOnElement(selectorWrapper);
379
+ container.appendChild(selectorWrapper);
380
+ }
381
+
382
+ /**
383
+ * Create close button
384
+ */
385
+ _createCloseButton() {
386
+ const classPrefix = this.player.options.classPrefix;
387
+ const ariaLabel = i18n.t('player.closeSignLanguage');
388
+
389
+ const closeButton = DOMUtils.createElement('button', {
390
+ className: `${classPrefix}-sign-language-close`,
391
+ attributes: {
392
+ 'type': 'button',
393
+ 'aria-label': ariaLabel
394
+ }
395
+ });
396
+ closeButton.appendChild(createIconElement('close'));
397
+ DOMUtils.attachTooltip(closeButton, ariaLabel, classPrefix);
398
+
399
+ closeButton.addEventListener('click', () => {
400
+ this.disable();
401
+ if (this.player.controlBar?.controls?.signLanguage) {
402
+ setTimeout(() => {
403
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
404
+ }, 0);
405
+ }
406
+ });
407
+
408
+ return closeButton;
409
+ }
410
+
411
+ /**
412
+ * Create video element
413
+ */
414
+ _createVideo(src) {
415
+ this.video = document.createElement('video');
416
+ this.video.className = 'vidply-sign-language-video';
417
+ this.video.src = src;
418
+ this.video.setAttribute('aria-label', i18n.t('player.signLanguage'));
419
+ this.video.muted = true;
420
+ this.video.setAttribute('playsinline', '');
421
+ }
422
+
423
+ /**
424
+ * Create resize handles
425
+ */
426
+ _createResizeHandles() {
427
+ const classPrefix = this.player.options.classPrefix;
428
+
429
+ this.resizeHandles = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'].map(dir => {
430
+ const handle = DOMUtils.createElement('div', {
431
+ className: `${classPrefix}-sign-resize-handle ${classPrefix}-sign-resize-${dir}`,
432
+ attributes: {
433
+ 'data-direction': dir,
434
+ 'data-vidply-managed-resize': 'true',
435
+ 'aria-hidden': 'true'
436
+ }
437
+ });
438
+ handle.style.display = 'none';
439
+ return handle;
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Apply initial size
445
+ */
446
+ _applyInitialSize() {
447
+ const saved = this.player.storage.getSignLanguagePreferences();
448
+ if (saved?.size?.width) {
449
+ this.wrapper.style.width = saved.size.width;
450
+ } else {
451
+ this.wrapper.style.width = '280px';
452
+ }
453
+ this.wrapper.style.height = 'auto';
454
+ }
455
+
456
+ /**
457
+ * Setup interaction (drag and resize)
458
+ */
459
+ _setupInteraction() {
460
+ const isMobile = window.innerWidth < 768;
461
+ const isFullscreen = this.player.state.fullscreen;
462
+
463
+ if (isMobile && !isFullscreen) {
464
+ if (this.draggable) {
465
+ this.draggable.destroy();
466
+ this.draggable = null;
467
+ }
468
+ return;
469
+ }
470
+
471
+ if (this.draggable) return;
472
+
473
+ const classPrefix = this.player.options.classPrefix;
474
+
475
+ this.draggable = new DraggableResizable(this.wrapper, {
476
+ dragHandle: this.header,
477
+ resizeHandles: this.resizeHandles,
478
+ constrainToViewport: true,
479
+ maintainAspectRatio: true,
480
+ minWidth: 150,
481
+ minHeight: 100,
482
+ classPrefix: `${classPrefix}-sign`,
483
+ keyboardDragKey: 'd',
484
+ keyboardResizeKey: 'r',
485
+ keyboardStep: 10,
486
+ keyboardStepLarge: 50,
487
+ pointerResizeIndicatorText: i18n.t('player.signLanguageResizeActive'),
488
+ onPointerResizeToggle: (enabled) => {
489
+ this.resizeHandles.forEach(handle => {
490
+ handle.style.display = enabled ? 'block' : 'none';
491
+ });
492
+ },
493
+ onDragStart: (e) => {
494
+ if (e.target.closest(`.${classPrefix}-sign-language-close`) ||
495
+ e.target.closest(`.${classPrefix}-sign-language-settings`) ||
496
+ e.target.closest(`.${classPrefix}-sign-language-select`) ||
497
+ e.target.closest(`.${classPrefix}-sign-language-label`) ||
498
+ e.target.closest(`.${classPrefix}-sign-language-settings-menu`)) {
499
+ return false;
500
+ }
501
+ return true;
502
+ }
503
+ });
504
+
505
+ this._setupCustomKeyHandler();
506
+
507
+ this.interactionHandlers = {
508
+ draggable: this.draggable,
509
+ customKeyHandler: this.customKeyHandler
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Setup custom keyboard handler
515
+ */
516
+ _setupCustomKeyHandler() {
517
+ this.customKeyHandler = (e) => {
518
+ const key = e.key.toLowerCase();
519
+
520
+ if (this.settingsMenuVisible) return;
521
+
522
+ if (key === 'home') {
523
+ e.preventDefault();
524
+ e.stopPropagation();
525
+ if (this.draggable) {
526
+ if (this.draggable.pointerResizeMode) {
527
+ this.draggable.disablePointerResizeMode();
528
+ }
529
+ this.draggable.manuallyPositioned = false;
530
+ this.constrainPosition();
531
+ }
532
+ return;
533
+ }
534
+
535
+ if (key === 'r') {
536
+ e.preventDefault();
537
+ e.stopPropagation();
538
+ if (this.toggleResizeMode()) {
539
+ this.wrapper.focus({ preventScroll: true });
540
+ }
541
+ return;
542
+ }
543
+
544
+ if (key === 'escape') {
545
+ e.preventDefault();
546
+ e.stopPropagation();
547
+ if (this.draggable?.pointerResizeMode) {
548
+ this.draggable.disablePointerResizeMode();
549
+ return;
550
+ }
551
+ if (this.draggable?.keyboardDragMode) {
552
+ this.draggable.disableKeyboardDragMode();
553
+ return;
554
+ }
555
+ this.disable();
556
+ if (this.player.controlBar?.controls?.signLanguage) {
557
+ setTimeout(() => {
558
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
559
+ }, 0);
560
+ }
561
+ }
562
+ };
563
+
564
+ this.wrapper.addEventListener('keydown', this.customKeyHandler);
565
+ }
566
+
567
+ /**
568
+ * Setup event handlers
569
+ */
570
+ _setupEventHandlers(hasMultipleSources) {
571
+ this.handlers = {
572
+ play: () => { if (this.video) this.video.play(); },
573
+ pause: () => { if (this.video) this.video.pause(); },
574
+ timeupdate: () => {
575
+ if (this.video && Math.abs(this.video.currentTime - this.player.state.currentTime) > 0.5) {
576
+ this.video.currentTime = this.player.state.currentTime;
577
+ }
578
+ },
579
+ ratechange: () => {
580
+ if (this.video) this.video.playbackRate = this.player.state.playbackSpeed;
581
+ }
582
+ };
583
+
584
+ this.player.on('play', this.handlers.play);
585
+ this.player.on('pause', this.handlers.pause);
586
+ this.player.on('timeupdate', this.handlers.timeupdate);
587
+ this.player.on('ratechange', this.handlers.ratechange);
588
+
589
+ if (hasMultipleSources) {
590
+ this.handlers.captionChange = () => {
591
+ if (this.player.captionManager?.currentTrack && this.selector) {
592
+ const captionLang = this.player.captionManager.currentTrack.language?.toLowerCase().split('-')[0];
593
+ if (captionLang && this.sources[captionLang] && this.currentLanguage !== captionLang) {
594
+ this.switchLanguage(captionLang);
595
+ this.selector.value = captionLang;
596
+ }
597
+ }
598
+ };
599
+ this.player.on('captionsenabled', this.handlers.captionChange);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Constrain position within video wrapper
605
+ */
606
+ constrainPosition() {
607
+ if (!this.wrapper || !this.player.videoWrapper) return;
608
+
609
+ if (this.draggable?.manuallyPositioned) return;
610
+
611
+ if (!this.wrapper.style.width) {
612
+ this.wrapper.style.width = '280px';
613
+ }
614
+
615
+ const videoWrapperRect = this.player.videoWrapper.getBoundingClientRect();
616
+ const containerRect = this.player.container.getBoundingClientRect();
617
+ const wrapperRect = this.wrapper.getBoundingClientRect();
618
+
619
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
620
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
621
+ const videoWrapperWidth = videoWrapperRect.width;
622
+ const videoWrapperHeight = videoWrapperRect.height;
623
+
624
+ let wrapperWidth = wrapperRect.width || 280;
625
+ let wrapperHeight = wrapperRect.height || ((280 * 9) / 16);
626
+
627
+ let left, top;
628
+ const margin = 16;
629
+ const controlsHeight = 95;
630
+
631
+ const position = this.desiredPosition || 'bottom-right';
632
+
633
+ switch (position) {
634
+ case 'bottom-right':
635
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
636
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
637
+ break;
638
+ case 'bottom-left':
639
+ left = videoWrapperLeft + margin;
640
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
641
+ break;
642
+ case 'top-right':
643
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
644
+ top = videoWrapperTop + margin;
645
+ break;
646
+ case 'top-left':
647
+ left = videoWrapperLeft + margin;
648
+ top = videoWrapperTop + margin;
649
+ break;
650
+ default:
651
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
652
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
653
+ }
654
+
655
+ left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperWidth - wrapperWidth));
656
+ top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight));
657
+
658
+ this.wrapper.style.left = `${left}px`;
659
+ this.wrapper.style.top = `${top}px`;
660
+ this.wrapper.style.right = 'auto';
661
+ this.wrapper.style.bottom = 'auto';
662
+ }
663
+
664
+ /**
665
+ * Show settings menu
666
+ */
667
+ showSettingsMenu() {
668
+ this.settingsMenuJustOpened = true;
669
+ setTimeout(() => { this.settingsMenuJustOpened = false; }, 350);
670
+
671
+ this._addDocumentClickHandler();
672
+
673
+ if (this.settingsMenu) {
674
+ this.settingsMenu.style.display = 'block';
675
+ this.settingsMenuVisible = true;
676
+ this.settingsButton?.setAttribute('aria-expanded', 'true');
677
+ this._attachMenuKeyboardNavigation();
678
+ this._positionSettingsMenu();
679
+ this._updateDragOptionState();
680
+ this._updateResizeOptionState();
681
+ focusFirstMenuItem(this.settingsMenu, `.${this.player.options.classPrefix}-sign-language-settings-item`);
682
+ return;
683
+ }
684
+
685
+ this._createSettingsMenu();
686
+ }
687
+
688
+ /**
689
+ * Hide settings menu
690
+ */
691
+ hideSettingsMenu({ focusButton = true } = {}) {
692
+ if (this.settingsMenu) {
693
+ this.settingsMenu.style.display = 'none';
694
+ this.settingsMenuVisible = false;
695
+ this.settingsMenuJustOpened = false;
696
+
697
+ if (this.settingsMenuKeyHandler) {
698
+ this.settingsMenu.removeEventListener('keydown', this.settingsMenuKeyHandler);
699
+ this.settingsMenuKeyHandler = null;
700
+ }
701
+
702
+ const classPrefix = this.player.options.classPrefix;
703
+ const menuItems = Array.from(this.settingsMenu.querySelectorAll(`.${classPrefix}-sign-language-settings-item`));
704
+ menuItems.forEach(item => item.setAttribute('tabindex', '-1'));
705
+
706
+ if (this.settingsButton) {
707
+ this.settingsButton.setAttribute('aria-expanded', 'false');
708
+ if (focusButton) {
709
+ this.settingsButton.focus({ preventScroll: true });
710
+ }
711
+ }
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Add document click handler
717
+ */
718
+ _addDocumentClickHandler() {
719
+ if (this.documentClickHandlerAdded) return;
720
+
721
+ this.documentClickHandler = (e) => {
722
+ if (this.settingsMenuJustOpened) return;
723
+
724
+ if (this.settingsButton &&
725
+ (this.settingsButton === e.target || this.settingsButton.contains(e.target))) {
726
+ return;
727
+ }
728
+
729
+ if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
730
+ return;
731
+ }
732
+
733
+ if (this.settingsMenuVisible) {
734
+ this.hideSettingsMenu();
735
+ }
736
+ };
737
+
738
+ setTimeout(() => {
739
+ document.addEventListener('mousedown', this.documentClickHandler, true);
740
+ this.documentClickHandlerAdded = true;
741
+ }, 300);
742
+ }
743
+
744
+ /**
745
+ * Create settings menu
746
+ */
747
+ _createSettingsMenu() {
748
+ const classPrefix = this.player.options.classPrefix;
749
+
750
+ this.settingsMenu = DOMUtils.createElement('div', {
751
+ className: `${classPrefix}-sign-language-settings-menu`,
752
+ attributes: { 'role': 'menu' }
753
+ });
754
+
755
+ // Drag option
756
+ const dragOption = createMenuItem({
757
+ classPrefix,
758
+ itemClass: `${classPrefix}-sign-language-settings-item`,
759
+ icon: 'move',
760
+ label: 'player.enableSignDragMode',
761
+ hasTextClass: true,
762
+ onClick: () => {
763
+ this.toggleKeyboardDragMode();
764
+ this.hideSettingsMenu();
765
+ }
766
+ });
767
+ dragOption.setAttribute('role', 'switch');
768
+ dragOption.setAttribute('aria-checked', 'false');
769
+ this._removeTooltipFromMenuItem(dragOption);
770
+ this.dragOptionButton = dragOption;
771
+ this.dragOptionText = dragOption.querySelector(`.${classPrefix}-settings-text`);
772
+ this._updateDragOptionState();
773
+
774
+ // Resize option
775
+ const resizeOption = createMenuItem({
776
+ classPrefix,
777
+ itemClass: `${classPrefix}-sign-language-settings-item`,
778
+ icon: 'resize',
779
+ label: 'player.enableSignResizeMode',
780
+ hasTextClass: true,
781
+ onClick: (event) => {
782
+ event.preventDefault();
783
+ event.stopPropagation();
784
+
785
+ const enabled = this.toggleResizeMode({ focus: false });
786
+
787
+ if (enabled) {
788
+ this.hideSettingsMenu({ focusButton: false });
789
+ setTimeout(() => {
790
+ if (this.wrapper) this.wrapper.focus({ preventScroll: true });
791
+ }, 20);
792
+ } else {
793
+ this.hideSettingsMenu({ focusButton: true });
794
+ }
795
+ }
796
+ });
797
+ resizeOption.setAttribute('role', 'switch');
798
+ resizeOption.setAttribute('aria-checked', 'false');
799
+ this._removeTooltipFromMenuItem(resizeOption);
800
+ this.resizeOptionButton = resizeOption;
801
+ this.resizeOptionText = resizeOption.querySelector(`.${classPrefix}-settings-text`);
802
+ this._updateResizeOptionState();
803
+
804
+ // Close option
805
+ const closeOption = createMenuItem({
806
+ classPrefix,
807
+ itemClass: `${classPrefix}-sign-language-settings-item`,
808
+ icon: 'close',
809
+ label: 'transcript.closeMenu',
810
+ onClick: () => this.hideSettingsMenu()
811
+ });
812
+ this._removeTooltipFromMenuItem(closeOption);
813
+
814
+ this.settingsMenu.appendChild(dragOption);
815
+ this.settingsMenu.appendChild(resizeOption);
816
+ this.settingsMenu.appendChild(closeOption);
817
+
818
+ // Position and show
819
+ this.settingsMenu.style.visibility = 'hidden';
820
+ this.settingsMenu.style.display = 'block';
821
+
822
+ if (this.settingsButton?.parentNode) {
823
+ this.settingsButton.insertAdjacentElement('afterend', this.settingsMenu);
824
+ } else if (this.wrapper) {
825
+ this.wrapper.appendChild(this.settingsMenu);
826
+ }
827
+
828
+ this._positionSettingsMenuImmediate();
829
+
830
+ requestAnimationFrame(() => {
831
+ if (this.settingsMenu) {
832
+ this.settingsMenu.style.visibility = 'visible';
833
+ }
834
+ });
835
+
836
+ this._attachMenuKeyboardNavigation();
837
+
838
+ this.settingsMenuVisible = true;
839
+ this.settingsButton?.setAttribute('aria-expanded', 'true');
840
+ this._updateDragOptionState();
841
+ this._updateResizeOptionState();
842
+
843
+ focusFirstMenuItem(this.settingsMenu, `.${classPrefix}-sign-language-settings-item`);
844
+ }
845
+
846
+ /**
847
+ * Remove tooltip from menu item
848
+ */
849
+ _removeTooltipFromMenuItem(item) {
850
+ const classPrefix = this.player.options.classPrefix;
851
+ const tooltip = item.querySelector(`.${classPrefix}-tooltip`);
852
+ if (tooltip) tooltip.remove();
853
+ const buttonText = item.querySelector(`.${classPrefix}-button-text`);
854
+ if (buttonText) buttonText.remove();
855
+ }
856
+
857
+ /**
858
+ * Attach menu keyboard navigation
859
+ */
860
+ _attachMenuKeyboardNavigation() {
861
+ if (this.settingsMenuKeyHandler) {
862
+ this.settingsMenu.removeEventListener('keydown', this.settingsMenuKeyHandler);
863
+ }
864
+
865
+ this.settingsMenuKeyHandler = attachMenuKeyboardNavigation(
866
+ this.settingsMenu,
867
+ this.settingsButton,
868
+ `.${this.player.options.classPrefix}-sign-language-settings-item`,
869
+ () => this.hideSettingsMenu({ focusButton: true })
870
+ );
871
+ }
872
+
873
+ /**
874
+ * Position settings menu immediately
875
+ */
876
+ _positionSettingsMenuImmediate() {
877
+ if (!this.settingsMenu || !this.settingsButton) return;
878
+
879
+ const buttonRect = this.settingsButton.getBoundingClientRect();
880
+ const menuRect = this.settingsMenu.getBoundingClientRect();
881
+ const viewportWidth = window.innerWidth;
882
+ const viewportHeight = window.innerHeight;
883
+
884
+ const parentContainer = this.settingsButton.parentElement;
885
+ if (!parentContainer) return;
886
+
887
+ const parentRect = parentContainer.getBoundingClientRect();
888
+
889
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2 - parentRect.left;
890
+ const buttonBottom = buttonRect.bottom - parentRect.top;
891
+ const buttonTop = buttonRect.top - parentRect.top;
892
+
893
+ const spaceAbove = buttonRect.top;
894
+ const spaceBelow = viewportHeight - buttonRect.bottom;
895
+
896
+ let menuTop = buttonBottom + 8;
897
+ let menuBottom = null;
898
+
899
+ if (spaceBelow < menuRect.height + 20 && spaceAbove > spaceBelow) {
900
+ menuTop = null;
901
+ const parentHeight = parentRect.bottom - parentRect.top;
902
+ menuBottom = parentHeight - buttonTop + 8;
903
+ this.settingsMenu.classList.add('vidply-menu-above');
904
+ } else {
905
+ this.settingsMenu.classList.remove('vidply-menu-above');
906
+ }
907
+
908
+ let menuLeft = buttonCenterX - menuRect.width / 2;
909
+ let menuRight = 'auto';
910
+ let transformX = 'translateX(0)';
911
+
912
+ const menuLeftAbsolute = buttonRect.left + buttonRect.width / 2 - menuRect.width / 2;
913
+ if (menuLeftAbsolute < 10) {
914
+ menuLeft = 0;
915
+ } else if (menuLeftAbsolute + menuRect.width > viewportWidth - 10) {
916
+ menuLeft = 'auto';
917
+ menuRight = 0;
918
+ } else {
919
+ menuLeft = buttonCenterX;
920
+ transformX = 'translateX(-50%)';
921
+ }
922
+
923
+ if (menuTop !== null) {
924
+ this.settingsMenu.style.top = `${menuTop}px`;
925
+ this.settingsMenu.style.bottom = 'auto';
926
+ } else if (menuBottom !== null) {
927
+ this.settingsMenu.style.top = 'auto';
928
+ this.settingsMenu.style.bottom = `${menuBottom}px`;
929
+ }
930
+
931
+ if (menuLeft !== 'auto') {
932
+ this.settingsMenu.style.left = `${menuLeft}px`;
933
+ this.settingsMenu.style.right = 'auto';
934
+ } else {
935
+ this.settingsMenu.style.left = 'auto';
936
+ this.settingsMenu.style.right = `${menuRight}px`;
937
+ }
938
+
939
+ this.settingsMenu.style.transform = transformX;
940
+ }
941
+
942
+ /**
943
+ * Position settings menu with RAF
944
+ */
945
+ _positionSettingsMenu() {
946
+ requestAnimationFrame(() => {
947
+ setTimeout(() => {
948
+ this._positionSettingsMenuImmediate();
949
+ }, 10);
950
+ });
951
+ }
952
+
953
+ /**
954
+ * Toggle keyboard drag mode
955
+ */
956
+ toggleKeyboardDragMode() {
957
+ if (this.draggable) {
958
+ const wasEnabled = this.draggable.keyboardDragMode;
959
+ this.draggable.toggleKeyboardDragMode();
960
+ const isEnabled = this.draggable.keyboardDragMode;
961
+ if (!wasEnabled && isEnabled) {
962
+ this._enableMoveMode();
963
+ }
964
+ this._updateDragOptionState();
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Enable move mode visual feedback
970
+ */
971
+ _enableMoveMode() {
972
+ this.wrapper.classList.add(`${this.player.options.classPrefix}-sign-move-mode`);
973
+ this._updateResizeOptionState();
974
+ setTimeout(() => {
975
+ this.wrapper.classList.remove(`${this.player.options.classPrefix}-sign-move-mode`);
976
+ }, 2000);
977
+ }
978
+
979
+ /**
980
+ * Toggle resize mode
981
+ */
982
+ toggleResizeMode({ focus = true } = {}) {
983
+ if (!this.draggable) return false;
984
+
985
+ if (this.draggable.pointerResizeMode) {
986
+ this.draggable.disablePointerResizeMode({ focus });
987
+ this._updateResizeOptionState();
988
+ return false;
989
+ }
990
+
991
+ this.draggable.enablePointerResizeMode({ focus });
992
+ this._updateResizeOptionState();
993
+ return true;
994
+ }
995
+
996
+ /**
997
+ * Update drag option state
998
+ */
999
+ _updateDragOptionState() {
1000
+ if (!this.dragOptionButton) return;
1001
+
1002
+ const isEnabled = !!(this.draggable?.keyboardDragMode);
1003
+ const text = isEnabled
1004
+ ? i18n.t('player.disableSignDragMode')
1005
+ : i18n.t('player.enableSignDragMode');
1006
+ const ariaLabel = isEnabled
1007
+ ? i18n.t('player.disableSignDragModeAria')
1008
+ : i18n.t('player.enableSignDragModeAria');
1009
+
1010
+ this.dragOptionButton.setAttribute('aria-checked', isEnabled ? 'true' : 'false');
1011
+ this.dragOptionButton.setAttribute('aria-label', ariaLabel);
1012
+
1013
+ if (this.dragOptionText) {
1014
+ this.dragOptionText.textContent = text;
1015
+ }
1016
+ }
1017
+
1018
+ /**
1019
+ * Update resize option state
1020
+ */
1021
+ _updateResizeOptionState() {
1022
+ if (!this.resizeOptionButton) return;
1023
+
1024
+ const isEnabled = !!(this.draggable?.pointerResizeMode);
1025
+ const text = isEnabled
1026
+ ? i18n.t('player.disableSignResizeMode')
1027
+ : i18n.t('player.enableSignResizeMode');
1028
+ const ariaLabel = isEnabled
1029
+ ? i18n.t('player.disableSignResizeModeAria')
1030
+ : i18n.t('player.enableSignResizeModeAria');
1031
+
1032
+ this.resizeOptionButton.setAttribute('aria-checked', isEnabled ? 'true' : 'false');
1033
+ this.resizeOptionButton.setAttribute('aria-label', ariaLabel);
1034
+
1035
+ if (this.resizeOptionText) {
1036
+ this.resizeOptionText.textContent = text;
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Save preferences
1042
+ */
1043
+ savePreferences() {
1044
+ if (!this.wrapper) return;
1045
+
1046
+ this.player.storage.saveSignLanguagePreferences({
1047
+ size: { width: this.wrapper.style.width }
1048
+ });
1049
+ }
1050
+
1051
+ /**
1052
+ * Update sources (called when playlist changes)
1053
+ */
1054
+ updateSources(signLanguageSrc, signLanguageSources) {
1055
+ this.src = signLanguageSrc || null;
1056
+ this.sources = signLanguageSources || {};
1057
+ // Reset state for new playlist item (cleanup is called separately before this)
1058
+ this.currentLanguage = null;
1059
+ }
1060
+
1061
+ /**
1062
+ * Cleanup
1063
+ */
1064
+ cleanup() {
1065
+ if (this.settingsMenuVisible) {
1066
+ this.hideSettingsMenu({ focusButton: false });
1067
+ }
1068
+
1069
+ // Remove document click handler
1070
+ if (this.documentClickHandler && this.documentClickHandlerAdded) {
1071
+ document.removeEventListener('mousedown', this.documentClickHandler, true);
1072
+ this.documentClickHandlerAdded = false;
1073
+ this.documentClickHandler = null;
1074
+ }
1075
+
1076
+ // Remove settings handlers
1077
+ if (this.settingsHandlers && this.settingsButton) {
1078
+ this.settingsButton.removeEventListener('click', this.settingsHandlers.click);
1079
+ this.settingsButton.removeEventListener('keydown', this.settingsHandlers.keydown);
1080
+ }
1081
+ this.settingsHandlers = null;
1082
+
1083
+ // Remove event handlers
1084
+ if (this.handlers) {
1085
+ this.player.off('play', this.handlers.play);
1086
+ this.player.off('pause', this.handlers.pause);
1087
+ this.player.off('timeupdate', this.handlers.timeupdate);
1088
+ this.player.off('ratechange', this.handlers.ratechange);
1089
+ if (this.handlers.captionChange) {
1090
+ this.player.off('captionsenabled', this.handlers.captionChange);
1091
+ }
1092
+ this.handlers = null;
1093
+ }
1094
+
1095
+ // Remove custom key handler
1096
+ if (this.wrapper && this.customKeyHandler) {
1097
+ this.wrapper.removeEventListener('keydown', this.customKeyHandler);
1098
+ }
1099
+
1100
+ // Destroy draggable
1101
+ if (this.draggable) {
1102
+ if (this.draggable.pointerResizeMode) {
1103
+ this.draggable.disablePointerResizeMode();
1104
+ }
1105
+ this.draggable.destroy();
1106
+ this.draggable = null;
1107
+ }
1108
+
1109
+ this.interactionHandlers = null;
1110
+
1111
+ // Remove video and wrapper
1112
+ if (this.wrapper?.parentNode) {
1113
+ if (this.video) {
1114
+ this.video.pause();
1115
+ this.video.src = '';
1116
+ }
1117
+ this.wrapper.parentNode.removeChild(this.wrapper);
1118
+ }
1119
+
1120
+ this.wrapper = null;
1121
+ this.video = null;
1122
+ this.settingsButton = null;
1123
+ this.settingsMenu = null;
1124
+ }
1125
+
1126
+ /**
1127
+ * Destroy
1128
+ */
1129
+ destroy() {
1130
+ this.cleanup();
1131
+ this.enabled = false;
1132
+ }
1133
+ }
1134
+