vidply 1.0.8 → 1.0.10
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/vidply.css +154 -52
- package/dist/vidply.esm.js +1867 -731
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +28 -10
- package/dist/vidply.js +1867 -731
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +27 -9
- package/package.json +1 -1
- package/src/controls/ControlBar.js +24 -14
- package/src/controls/TranscriptManager.js +658 -591
- package/src/core/Player.js +831 -331
- package/src/i18n/translations.js +55 -5
- package/src/icons/Icons.js +2 -2
- package/src/styles/vidply.css +154 -52
- package/src/utils/DraggableResizable.js +771 -0
|
@@ -8,6 +8,7 @@ import { TimeUtils } from '../utils/TimeUtils.js';
|
|
|
8
8
|
import { createIconElement } from '../icons/Icons.js';
|
|
9
9
|
import { i18n } from '../i18n/i18n.js';
|
|
10
10
|
import { StorageManager } from '../utils/StorageManager.js';
|
|
11
|
+
import { DraggableResizable } from '../utils/DraggableResizable.js';
|
|
11
12
|
|
|
12
13
|
export class TranscriptManager {
|
|
13
14
|
constructor(player) {
|
|
@@ -21,19 +22,8 @@ export class TranscriptManager {
|
|
|
21
22
|
// Storage manager
|
|
22
23
|
this.storage = new StorageManager('vidply');
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
this.
|
|
26
|
-
this.dragOffsetX = 0;
|
|
27
|
-
this.dragOffsetY = 0;
|
|
28
|
-
|
|
29
|
-
// Resizing state
|
|
30
|
-
this.isResizing = false;
|
|
31
|
-
this.resizeDirection = null;
|
|
32
|
-
this.resizeStartX = 0;
|
|
33
|
-
this.resizeStartY = 0;
|
|
34
|
-
this.resizeStartWidth = 0;
|
|
35
|
-
this.resizeStartHeight = 0;
|
|
36
|
-
this.resizeEnabled = false;
|
|
25
|
+
// Draggable/Resizable utility
|
|
26
|
+
this.draggableResizable = null;
|
|
37
27
|
|
|
38
28
|
// Settings menu state
|
|
39
29
|
this.settingsMenuVisible = false;
|
|
@@ -41,17 +31,31 @@ export class TranscriptManager {
|
|
|
41
31
|
this.settingsButton = null;
|
|
42
32
|
this.settingsMenuJustOpened = false;
|
|
43
33
|
|
|
44
|
-
//
|
|
45
|
-
this.
|
|
34
|
+
// Resize mode state
|
|
35
|
+
this.resizeOptionButton = null;
|
|
36
|
+
this.resizeOptionText = null;
|
|
37
|
+
this.resizeModeIndicator = null;
|
|
38
|
+
this.resizeModeIndicatorTimeout = null;
|
|
39
|
+
this.transcriptResizeHandles = [];
|
|
40
|
+
this.liveRegion = null;
|
|
46
41
|
|
|
47
42
|
// Style dialog state
|
|
48
43
|
this.styleDialog = null;
|
|
49
44
|
this.styleDialogVisible = false;
|
|
50
45
|
this.styleDialogJustOpened = false;
|
|
51
46
|
|
|
47
|
+
// Language selector state
|
|
48
|
+
this.languageSelector = null;
|
|
49
|
+
this.currentTranscriptLanguage = null;
|
|
50
|
+
this.availableTranscriptLanguages = [];
|
|
51
|
+
this.languageSelectorHandler = null;
|
|
52
|
+
|
|
52
53
|
// Load saved preferences from localStorage
|
|
53
54
|
const savedPreferences = this.storage.getTranscriptPreferences();
|
|
54
55
|
|
|
56
|
+
// Autoscroll state (default: true)
|
|
57
|
+
this.autoscrollEnabled = savedPreferences?.autoscroll !== undefined ? savedPreferences.autoscroll : true;
|
|
58
|
+
|
|
55
59
|
// Transcript styling options (with defaults, then player options, then saved preferences)
|
|
56
60
|
this.transcriptStyle = {
|
|
57
61
|
fontSize: savedPreferences?.fontSize || this.player.options.transcriptFontSize || '100%',
|
|
@@ -65,31 +69,33 @@ export class TranscriptManager {
|
|
|
65
69
|
this.handlers = {
|
|
66
70
|
timeupdate: () => this.updateActiveEntry(),
|
|
67
71
|
resize: null,
|
|
68
|
-
mousemove: null,
|
|
69
|
-
mouseup: null,
|
|
70
|
-
touchmove: null,
|
|
71
|
-
touchend: null,
|
|
72
|
-
mousedown: null,
|
|
73
|
-
touchstart: null,
|
|
74
|
-
keydown: null,
|
|
75
72
|
settingsClick: null,
|
|
76
73
|
settingsKeydown: null,
|
|
77
74
|
documentClick: null,
|
|
78
75
|
styleDialogKeydown: null
|
|
79
76
|
};
|
|
80
77
|
|
|
78
|
+
// Timeout management (for cleanup)
|
|
79
|
+
this.timeouts = new Set();
|
|
80
|
+
|
|
81
81
|
this.init();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
init() {
|
|
85
|
+
// Set up metadata handling immediately (independent of transcript display)
|
|
86
|
+
this.setupMetadataHandlingOnLoad();
|
|
87
|
+
|
|
85
88
|
// Listen for time updates to highlight active transcript entry
|
|
86
89
|
this.player.on('timeupdate', this.handlers.timeupdate);
|
|
87
90
|
|
|
88
91
|
// Reposition transcript when entering/exiting fullscreen
|
|
89
92
|
this.player.on('fullscreenchange', () => {
|
|
90
93
|
if (this.isVisible) {
|
|
91
|
-
//
|
|
92
|
-
|
|
94
|
+
// Only auto-position if user hasn't manually positioned it
|
|
95
|
+
if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
|
|
96
|
+
// Add a small delay to ensure DOM has updated after fullscreen transition
|
|
97
|
+
this.setManagedTimeout(() => this.positionTranscript(), 100);
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
});
|
|
95
101
|
}
|
|
@@ -112,11 +118,15 @@ export class TranscriptManager {
|
|
|
112
118
|
if (this.transcriptWindow) {
|
|
113
119
|
this.transcriptWindow.style.display = 'flex';
|
|
114
120
|
this.isVisible = true;
|
|
121
|
+
|
|
122
|
+
if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === 'function') {
|
|
123
|
+
this.player.controlBar.updateTranscriptButton();
|
|
124
|
+
}
|
|
115
125
|
|
|
116
|
-
// Focus the
|
|
117
|
-
|
|
118
|
-
if (this.
|
|
119
|
-
this.
|
|
126
|
+
// Focus the header for keyboard accessibility
|
|
127
|
+
this.setManagedTimeout(() => {
|
|
128
|
+
if (this.transcriptHeader) {
|
|
129
|
+
this.transcriptHeader.focus();
|
|
120
130
|
}
|
|
121
131
|
}, 150);
|
|
122
132
|
return;
|
|
@@ -129,13 +139,17 @@ export class TranscriptManager {
|
|
|
129
139
|
// Show the window
|
|
130
140
|
if (this.transcriptWindow) {
|
|
131
141
|
this.transcriptWindow.style.display = 'flex';
|
|
132
|
-
// Re-position after showing (in case window was resized while hidden)
|
|
133
|
-
setTimeout(() => this.positionTranscript(), 0);
|
|
134
142
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
// Only auto-position if user hasn't manually positioned it
|
|
144
|
+
// This prevents overwriting saved positions from localStorage
|
|
145
|
+
if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
|
|
146
|
+
this.setManagedTimeout(() => this.positionTranscript(), 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Focus the header for keyboard accessibility
|
|
150
|
+
this.setManagedTimeout(() => {
|
|
151
|
+
if (this.transcriptHeader) {
|
|
152
|
+
this.transcriptHeader.focus();
|
|
139
153
|
}
|
|
140
154
|
}, 150);
|
|
141
155
|
}
|
|
@@ -145,11 +159,29 @@ export class TranscriptManager {
|
|
|
145
159
|
/**
|
|
146
160
|
* Hide transcript window
|
|
147
161
|
*/
|
|
148
|
-
hideTranscript() {
|
|
162
|
+
hideTranscript({ focusButton = false } = {}) {
|
|
149
163
|
if (this.transcriptWindow) {
|
|
150
164
|
this.transcriptWindow.style.display = 'none';
|
|
151
165
|
this.isVisible = false;
|
|
152
166
|
}
|
|
167
|
+
if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
|
|
168
|
+
this.draggableResizable.disablePointerResizeMode();
|
|
169
|
+
this.updateResizeOptionState();
|
|
170
|
+
}
|
|
171
|
+
this.hideResizeModeIndicator();
|
|
172
|
+
this.announceLive('');
|
|
173
|
+
|
|
174
|
+
// Update transcript button state in control bar
|
|
175
|
+
if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === 'function') {
|
|
176
|
+
this.player.controlBar.updateTranscriptButton();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (focusButton) {
|
|
180
|
+
const transcriptButton = this.player.controlBar?.controls?.transcript;
|
|
181
|
+
if (transcriptButton && typeof transcriptButton.focus === 'function') {
|
|
182
|
+
transcriptButton.focus();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
153
185
|
}
|
|
154
186
|
|
|
155
187
|
/**
|
|
@@ -169,7 +201,6 @@ export class TranscriptManager {
|
|
|
169
201
|
this.transcriptHeader = DOMUtils.createElement('div', {
|
|
170
202
|
className: `${this.player.options.classPrefix}-transcript-header`,
|
|
171
203
|
attributes: {
|
|
172
|
-
'aria-label': 'Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.',
|
|
173
204
|
'tabindex': '0'
|
|
174
205
|
}
|
|
175
206
|
});
|
|
@@ -184,7 +215,7 @@ export class TranscriptManager {
|
|
|
184
215
|
className: `${this.player.options.classPrefix}-transcript-settings`,
|
|
185
216
|
attributes: {
|
|
186
217
|
'type': 'button',
|
|
187
|
-
'aria-label': i18n.t('transcript.
|
|
218
|
+
'aria-label': i18n.t('transcript.settingsMenu'),
|
|
188
219
|
'aria-expanded': 'false'
|
|
189
220
|
}
|
|
190
221
|
});
|
|
@@ -224,11 +255,52 @@ export class TranscriptManager {
|
|
|
224
255
|
this.settingsButton.addEventListener('keydown', this.handlers.settingsKeydown);
|
|
225
256
|
|
|
226
257
|
const title = DOMUtils.createElement('h3', {
|
|
227
|
-
textContent: i18n.t('transcript.title')
|
|
258
|
+
textContent: `${i18n.t('transcript.title')}. ${i18n.t('transcript.dragResizePrompt')}`
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Autoscroll checkbox
|
|
262
|
+
const autoscrollLabel = DOMUtils.createElement('label', {
|
|
263
|
+
className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
|
|
264
|
+
attributes: {
|
|
265
|
+
'title': i18n.t('transcript.autoscroll')
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.autoscrollCheckbox = DOMUtils.createElement('input', {
|
|
270
|
+
attributes: {
|
|
271
|
+
'type': 'checkbox',
|
|
272
|
+
'checked': this.autoscrollEnabled,
|
|
273
|
+
'aria-label': i18n.t('transcript.autoscroll')
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const autoscrollText = DOMUtils.createElement('span', {
|
|
278
|
+
textContent: i18n.t('transcript.autoscroll'),
|
|
279
|
+
className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
autoscrollLabel.appendChild(this.autoscrollCheckbox);
|
|
283
|
+
autoscrollLabel.appendChild(autoscrollText);
|
|
284
|
+
|
|
285
|
+
// Handle autoscroll checkbox change
|
|
286
|
+
this.autoscrollCheckbox.addEventListener('change', (e) => {
|
|
287
|
+
this.autoscrollEnabled = e.target.checked;
|
|
288
|
+
this.saveAutoscrollPreference();
|
|
228
289
|
});
|
|
229
290
|
|
|
291
|
+
this.transcriptHeader.appendChild(title);
|
|
230
292
|
this.headerLeft.appendChild(this.settingsButton);
|
|
231
|
-
this.headerLeft.appendChild(
|
|
293
|
+
this.headerLeft.appendChild(autoscrollLabel);
|
|
294
|
+
|
|
295
|
+
// Language selector (will be populated after tracks are loaded)
|
|
296
|
+
this.languageSelector = DOMUtils.createElement('select', {
|
|
297
|
+
className: `${this.player.options.classPrefix}-transcript-language-select`,
|
|
298
|
+
attributes: {
|
|
299
|
+
'aria-label': i18n.t('settings.language') || 'Language',
|
|
300
|
+
'style': 'display: none;' // Hidden until we detect multiple languages
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
this.headerLeft.appendChild(this.languageSelector);
|
|
232
304
|
|
|
233
305
|
const closeButton = DOMUtils.createElement('button', {
|
|
234
306
|
className: `${this.player.options.classPrefix}-transcript-close`,
|
|
@@ -238,7 +310,7 @@ export class TranscriptManager {
|
|
|
238
310
|
}
|
|
239
311
|
});
|
|
240
312
|
closeButton.appendChild(createIconElement('close'));
|
|
241
|
-
closeButton.addEventListener('click', () => this.hideTranscript());
|
|
313
|
+
closeButton.addEventListener('click', () => this.hideTranscript({ focusButton: true }));
|
|
242
314
|
|
|
243
315
|
this.transcriptHeader.appendChild(this.headerLeft);
|
|
244
316
|
this.transcriptHeader.appendChild(closeButton);
|
|
@@ -251,15 +323,30 @@ export class TranscriptManager {
|
|
|
251
323
|
this.transcriptWindow.appendChild(this.transcriptHeader);
|
|
252
324
|
this.transcriptWindow.appendChild(this.transcriptContent);
|
|
253
325
|
|
|
326
|
+
this.createResizeHandles();
|
|
327
|
+
|
|
328
|
+
// Live region for announcements (screen reader feedback)
|
|
329
|
+
this.liveRegion = DOMUtils.createElement('div', {
|
|
330
|
+
className: 'vidply-sr-only',
|
|
331
|
+
attributes: {
|
|
332
|
+
'aria-live': 'polite',
|
|
333
|
+
'aria-atomic': 'true'
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
this.transcriptWindow.appendChild(this.liveRegion);
|
|
337
|
+
|
|
254
338
|
// Append to player container
|
|
255
339
|
this.player.container.appendChild(this.transcriptWindow);
|
|
256
340
|
|
|
257
|
-
//
|
|
258
|
-
this.positionTranscript();
|
|
259
|
-
|
|
260
|
-
// Setup drag functionality
|
|
341
|
+
// Setup drag functionality FIRST (this will restore saved position if it exists)
|
|
261
342
|
this.setupDragAndDrop();
|
|
262
343
|
|
|
344
|
+
// Then position it next to the video wrapper ONLY if user hasn't manually positioned it
|
|
345
|
+
// This ensures we don't overwrite saved positions from localStorage
|
|
346
|
+
if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
|
|
347
|
+
this.positionTranscript();
|
|
348
|
+
}
|
|
349
|
+
|
|
263
350
|
// Setup document click handler to close settings menu and style dialog
|
|
264
351
|
// DON'T add it yet - it will be added when the menu is first opened
|
|
265
352
|
this.handlers.documentClick = (e) => {
|
|
@@ -297,21 +384,53 @@ export class TranscriptManager {
|
|
|
297
384
|
// Store flag to track if handler has been added
|
|
298
385
|
this.documentClickHandlerAdded = false;
|
|
299
386
|
|
|
300
|
-
// Re-position on window resize (debounced)
|
|
387
|
+
// Re-position on window resize (debounced) - but only if not manually positioned
|
|
301
388
|
let resizeTimeout;
|
|
302
389
|
this.handlers.resize = () => {
|
|
303
|
-
|
|
304
|
-
|
|
390
|
+
if (resizeTimeout) {
|
|
391
|
+
this.clearManagedTimeout(resizeTimeout);
|
|
392
|
+
}
|
|
393
|
+
resizeTimeout = this.setManagedTimeout(() => {
|
|
394
|
+
// Only auto-position if user hasn't manually moved it
|
|
395
|
+
if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
|
|
396
|
+
this.positionTranscript();
|
|
397
|
+
}
|
|
398
|
+
}, 100);
|
|
305
399
|
};
|
|
306
400
|
window.addEventListener('resize', this.handlers.resize);
|
|
307
401
|
}
|
|
308
402
|
|
|
403
|
+
createResizeHandles() {
|
|
404
|
+
if (!this.transcriptWindow) return;
|
|
405
|
+
|
|
406
|
+
const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
|
|
407
|
+
this.transcriptResizeHandles = directions.map(direction => {
|
|
408
|
+
const handle = DOMUtils.createElement('div', {
|
|
409
|
+
className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
|
|
410
|
+
attributes: {
|
|
411
|
+
'data-direction': direction,
|
|
412
|
+
'data-vidply-managed-resize': 'true',
|
|
413
|
+
'aria-hidden': 'true'
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
handle.style.display = 'none';
|
|
418
|
+
this.transcriptWindow.appendChild(handle);
|
|
419
|
+
return handle;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
309
423
|
/**
|
|
310
424
|
* Position transcript window next to video
|
|
311
425
|
*/
|
|
312
426
|
positionTranscript() {
|
|
313
427
|
if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
|
|
314
428
|
|
|
429
|
+
// Don't auto-position if user has manually positioned it
|
|
430
|
+
if (this.draggableResizable && this.draggableResizable.manuallyPositioned) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
315
434
|
const isMobile = window.innerWidth < 640;
|
|
316
435
|
const videoRect = this.player.videoWrapper.getBoundingClientRect();
|
|
317
436
|
|
|
@@ -352,8 +471,12 @@ export class TranscriptManager {
|
|
|
352
471
|
this.transcriptWindow.style.top = 'auto';
|
|
353
472
|
this.transcriptWindow.style.maxHeight = 'calc(100vh - 180px)'; // Leave space for controls
|
|
354
473
|
this.transcriptWindow.style.height = 'auto';
|
|
355
|
-
|
|
356
|
-
|
|
474
|
+
const fullscreenMinWidth = 260;
|
|
475
|
+
const fullscreenAvailable = Math.max(fullscreenMinWidth, window.innerWidth - 40);
|
|
476
|
+
const fullscreenDesired = parseFloat(this.transcriptWindow.style.width) || 400;
|
|
477
|
+
const fullscreenWidth = Math.max(fullscreenMinWidth, Math.min(fullscreenDesired, fullscreenAvailable));
|
|
478
|
+
this.transcriptWindow.style.width = `${fullscreenWidth}px`;
|
|
479
|
+
this.transcriptWindow.style.maxWidth = 'none';
|
|
357
480
|
this.transcriptWindow.style.borderRadius = '8px';
|
|
358
481
|
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
359
482
|
this.transcriptWindow.style.borderTop = '';
|
|
@@ -363,16 +486,35 @@ export class TranscriptManager {
|
|
|
363
486
|
this.player.container.appendChild(this.transcriptWindow);
|
|
364
487
|
}
|
|
365
488
|
} else {
|
|
366
|
-
// Desktop mode: position
|
|
489
|
+
// Desktop mode: position in right side of viewport
|
|
490
|
+
const transcriptWidth = parseFloat(this.transcriptWindow.style.width) || 400;
|
|
491
|
+
const padding = 20;
|
|
492
|
+
const minWidth = 260;
|
|
493
|
+
const containerRect = this.player.container.getBoundingClientRect();
|
|
494
|
+
|
|
495
|
+
const ensureContainerPositioned = () => {
|
|
496
|
+
const computed = window.getComputedStyle(this.player.container);
|
|
497
|
+
if (computed.position === 'static') {
|
|
498
|
+
this.player.container.style.position = 'relative';
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
ensureContainerPositioned();
|
|
503
|
+
|
|
504
|
+
const left = (videoRect.right - containerRect.left) + padding;
|
|
505
|
+
const availableWidth = window.innerWidth - videoRect.right - padding;
|
|
506
|
+
const appliedWidth = Math.max(minWidth, Math.min(transcriptWidth, availableWidth));
|
|
507
|
+
const appliedHeight = videoRect.height;
|
|
508
|
+
|
|
367
509
|
this.transcriptWindow.style.position = 'absolute';
|
|
368
|
-
this.transcriptWindow.style.left = `${
|
|
510
|
+
this.transcriptWindow.style.left = `${left}px`;
|
|
369
511
|
this.transcriptWindow.style.right = 'auto';
|
|
370
512
|
this.transcriptWindow.style.bottom = 'auto';
|
|
371
513
|
this.transcriptWindow.style.top = '0';
|
|
372
|
-
this.transcriptWindow.style.height = `${
|
|
514
|
+
this.transcriptWindow.style.height = `${appliedHeight}px`;
|
|
373
515
|
this.transcriptWindow.style.maxHeight = 'none';
|
|
374
|
-
this.transcriptWindow.style.width =
|
|
375
|
-
this.transcriptWindow.style.maxWidth = '
|
|
516
|
+
this.transcriptWindow.style.width = `${appliedWidth}px`;
|
|
517
|
+
this.transcriptWindow.style.maxWidth = 'none';
|
|
376
518
|
this.transcriptWindow.style.borderRadius = '8px';
|
|
377
519
|
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
378
520
|
this.transcriptWindow.style.borderTop = '';
|
|
@@ -388,6 +530,84 @@ export class TranscriptManager {
|
|
|
388
530
|
}
|
|
389
531
|
}
|
|
390
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Get available transcript languages from tracks
|
|
535
|
+
*/
|
|
536
|
+
getAvailableTranscriptLanguages() {
|
|
537
|
+
const textTracks = this.player.textTracks;
|
|
538
|
+
const languages = new Map();
|
|
539
|
+
|
|
540
|
+
// Collect all caption/subtitle tracks with their languages
|
|
541
|
+
textTracks.forEach(track => {
|
|
542
|
+
if ((track.kind === 'captions' || track.kind === 'subtitles') && track.language) {
|
|
543
|
+
if (!languages.has(track.language)) {
|
|
544
|
+
languages.set(track.language, {
|
|
545
|
+
language: track.language,
|
|
546
|
+
label: track.label || track.language,
|
|
547
|
+
track: track
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return Array.from(languages.values());
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Update language selector dropdown
|
|
558
|
+
*/
|
|
559
|
+
updateLanguageSelector() {
|
|
560
|
+
if (!this.languageSelector) return;
|
|
561
|
+
|
|
562
|
+
this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
|
|
563
|
+
|
|
564
|
+
// Clear existing options
|
|
565
|
+
this.languageSelector.innerHTML = '';
|
|
566
|
+
|
|
567
|
+
// Only show selector if there are 2+ languages
|
|
568
|
+
if (this.availableTranscriptLanguages.length < 2) {
|
|
569
|
+
this.languageSelector.style.display = 'none';
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Show selector and populate options
|
|
574
|
+
this.languageSelector.style.display = 'block';
|
|
575
|
+
|
|
576
|
+
this.availableTranscriptLanguages.forEach((langInfo, index) => {
|
|
577
|
+
const option = DOMUtils.createElement('option', {
|
|
578
|
+
textContent: langInfo.label,
|
|
579
|
+
attributes: {
|
|
580
|
+
'value': langInfo.language
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
this.languageSelector.appendChild(option);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Set current selection
|
|
587
|
+
if (this.currentTranscriptLanguage) {
|
|
588
|
+
this.languageSelector.value = this.currentTranscriptLanguage;
|
|
589
|
+
} else if (this.availableTranscriptLanguages.length > 0) {
|
|
590
|
+
// Default to first language or active track
|
|
591
|
+
const activeTrack = this.player.textTracks.find(
|
|
592
|
+
track => (track.kind === 'captions' || track.kind === 'subtitles') && track.mode === 'showing'
|
|
593
|
+
);
|
|
594
|
+
this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
|
|
595
|
+
this.languageSelector.value = this.currentTranscriptLanguage;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Remove existing change listener if any
|
|
599
|
+
if (this.languageSelectorHandler) {
|
|
600
|
+
this.languageSelector.removeEventListener('change', this.languageSelectorHandler);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Handle language change
|
|
604
|
+
this.languageSelectorHandler = (e) => {
|
|
605
|
+
this.currentTranscriptLanguage = e.target.value;
|
|
606
|
+
this.loadTranscriptData();
|
|
607
|
+
};
|
|
608
|
+
this.languageSelector.addEventListener('change', this.languageSelectorHandler);
|
|
609
|
+
}
|
|
610
|
+
|
|
391
611
|
/**
|
|
392
612
|
* Load transcript data from caption/subtitle tracks
|
|
393
613
|
*/
|
|
@@ -396,13 +616,39 @@ export class TranscriptManager {
|
|
|
396
616
|
this.transcriptContent.innerHTML = '';
|
|
397
617
|
|
|
398
618
|
// Get all text tracks
|
|
399
|
-
const textTracks =
|
|
619
|
+
const textTracks = this.player.textTracks;
|
|
620
|
+
|
|
621
|
+
// Find track for selected language, or default to first available
|
|
622
|
+
let captionTrack = null;
|
|
623
|
+
if (this.currentTranscriptLanguage) {
|
|
624
|
+
captionTrack = textTracks.find(
|
|
625
|
+
track => (track.kind === 'captions' || track.kind === 'subtitles') &&
|
|
626
|
+
track.language === this.currentTranscriptLanguage
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Fallback to first available caption/subtitle track
|
|
631
|
+
if (!captionTrack) {
|
|
632
|
+
captionTrack = textTracks.find(
|
|
633
|
+
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
634
|
+
);
|
|
635
|
+
if (captionTrack) {
|
|
636
|
+
this.currentTranscriptLanguage = captionTrack.language;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Find description track matching the selected language
|
|
641
|
+
let descriptionTrack = null;
|
|
642
|
+
if (this.currentTranscriptLanguage) {
|
|
643
|
+
descriptionTrack = textTracks.find(
|
|
644
|
+
track => track.kind === 'descriptions' && track.language === this.currentTranscriptLanguage
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
// Fallback to first available description track if no match found
|
|
648
|
+
if (!descriptionTrack) {
|
|
649
|
+
descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
650
|
+
}
|
|
400
651
|
|
|
401
|
-
// Find different track types
|
|
402
|
-
const captionTrack = textTracks.find(
|
|
403
|
-
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
404
|
-
);
|
|
405
|
-
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
406
652
|
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
407
653
|
|
|
408
654
|
// We need at least one track type
|
|
@@ -443,7 +689,7 @@ export class TranscriptManager {
|
|
|
443
689
|
});
|
|
444
690
|
|
|
445
691
|
// Fallback timeout
|
|
446
|
-
|
|
692
|
+
this.setManagedTimeout(() => {
|
|
447
693
|
this.loadTranscriptData();
|
|
448
694
|
}, 500);
|
|
449
695
|
|
|
@@ -489,28 +735,84 @@ export class TranscriptManager {
|
|
|
489
735
|
|
|
490
736
|
// Apply current styles to newly loaded entries
|
|
491
737
|
this.applyTranscriptStyles();
|
|
738
|
+
|
|
739
|
+
// Update language selector after loading
|
|
740
|
+
this.updateLanguageSelector();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Setup metadata handling on player load
|
|
745
|
+
* This runs independently of transcript loading
|
|
746
|
+
*/
|
|
747
|
+
setupMetadataHandlingOnLoad() {
|
|
748
|
+
// Wait for metadata to be loaded
|
|
749
|
+
const setupMetadata = () => {
|
|
750
|
+
const textTracks = this.player.textTracks;
|
|
751
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
752
|
+
|
|
753
|
+
if (metadataTrack) {
|
|
754
|
+
// Enable the metadata track so cuechange events fire
|
|
755
|
+
// Use 'hidden' mode so it doesn't display anything, but events still work
|
|
756
|
+
if (metadataTrack.mode === 'disabled') {
|
|
757
|
+
metadataTrack.mode = 'hidden';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Check if we already added the listener
|
|
761
|
+
if (this.metadataCueChangeHandler) {
|
|
762
|
+
metadataTrack.removeEventListener('cuechange', this.metadataCueChangeHandler);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Add event listener for cue changes
|
|
766
|
+
this.metadataCueChangeHandler = () => {
|
|
767
|
+
const activeCues = Array.from(metadataTrack.activeCues || []);
|
|
768
|
+
if (activeCues.length > 0) {
|
|
769
|
+
// Debug logging (can be removed in production)
|
|
770
|
+
if (this.player.options.debug) {
|
|
771
|
+
console.log('[VidPly Metadata] Active cues:', activeCues.map(c => ({
|
|
772
|
+
start: c.startTime,
|
|
773
|
+
end: c.endTime,
|
|
774
|
+
text: c.text
|
|
775
|
+
})));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
activeCues.forEach(cue => {
|
|
779
|
+
this.handleMetadataCue(cue);
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
metadataTrack.addEventListener('cuechange', this.metadataCueChangeHandler);
|
|
784
|
+
|
|
785
|
+
// Debug: Log metadata track setup
|
|
786
|
+
if (this.player.options.debug) {
|
|
787
|
+
const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
|
|
788
|
+
console.log('[VidPly Metadata] Track enabled,', cueCount, 'cues available');
|
|
789
|
+
}
|
|
790
|
+
} else if (this.player.options.debug) {
|
|
791
|
+
console.warn('[VidPly Metadata] No metadata track found');
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// Try immediately
|
|
796
|
+
setupMetadata();
|
|
797
|
+
|
|
798
|
+
// Also try after loadedmetadata event
|
|
799
|
+
this.player.on('loadedmetadata', setupMetadata);
|
|
492
800
|
}
|
|
493
801
|
|
|
494
802
|
/**
|
|
495
803
|
* Setup metadata handling
|
|
496
804
|
* Metadata cues are not displayed but can be used programmatically
|
|
805
|
+
* This is called when transcript data is loaded (for storing cues)
|
|
497
806
|
*/
|
|
498
807
|
setupMetadataHandling() {
|
|
499
808
|
if (!this.metadataCues || this.metadataCues.length === 0) {
|
|
500
809
|
return;
|
|
501
810
|
}
|
|
502
811
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (metadataTrack) {
|
|
508
|
-
metadataTrack.addEventListener('cuechange', () => {
|
|
509
|
-
const activeCues = Array.from(metadataTrack.activeCues || []);
|
|
510
|
-
activeCues.forEach(cue => {
|
|
511
|
-
this.handleMetadataCue(cue);
|
|
512
|
-
});
|
|
513
|
-
});
|
|
812
|
+
// The actual event handling is set up in setupMetadataHandlingOnLoad()
|
|
813
|
+
// This method just stores the cues for reference
|
|
814
|
+
if (this.player.options.debug) {
|
|
815
|
+
console.log('[VidPly Metadata]', this.metadataCues.length, 'cues stored from transcript load');
|
|
514
816
|
}
|
|
515
817
|
}
|
|
516
818
|
|
|
@@ -521,6 +823,14 @@ export class TranscriptManager {
|
|
|
521
823
|
handleMetadataCue(cue) {
|
|
522
824
|
const text = cue.text.trim();
|
|
523
825
|
|
|
826
|
+
// Debug logging
|
|
827
|
+
if (this.player.options.debug) {
|
|
828
|
+
console.log('[VidPly Metadata] Processing cue:', {
|
|
829
|
+
time: cue.startTime,
|
|
830
|
+
text: text
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
524
834
|
// Emit a generic metadata event that developers can listen to
|
|
525
835
|
this.player.emit('metadata', {
|
|
526
836
|
time: cue.startTime,
|
|
@@ -531,16 +841,39 @@ export class TranscriptManager {
|
|
|
531
841
|
|
|
532
842
|
// Parse for specific commands (examples based on wwa_meta.vtt format)
|
|
533
843
|
if (text.includes('PAUSE')) {
|
|
534
|
-
//
|
|
844
|
+
// Automatically pause the video
|
|
845
|
+
if (!this.player.state.paused) {
|
|
846
|
+
if (this.player.options.debug) {
|
|
847
|
+
console.log('[VidPly Metadata] Pausing video at', cue.startTime);
|
|
848
|
+
}
|
|
849
|
+
this.player.pause();
|
|
850
|
+
}
|
|
851
|
+
// Also emit event for developers who want to listen
|
|
535
852
|
this.player.emit('metadata:pause', { time: cue.startTime, text: text });
|
|
536
853
|
}
|
|
537
854
|
|
|
538
855
|
// Parse for focus directives
|
|
539
856
|
const focusMatch = text.match(/FOCUS:([\w#-]+)/);
|
|
540
857
|
if (focusMatch) {
|
|
858
|
+
const targetSelector = focusMatch[1];
|
|
859
|
+
// Automatically focus the target element
|
|
860
|
+
const targetElement = document.querySelector(targetSelector);
|
|
861
|
+
if (targetElement) {
|
|
862
|
+
if (this.player.options.debug) {
|
|
863
|
+
console.log('[VidPly Metadata] Focusing element:', targetSelector);
|
|
864
|
+
}
|
|
865
|
+
// Use setTimeout to ensure DOM is ready
|
|
866
|
+
this.setManagedTimeout(() => {
|
|
867
|
+
targetElement.focus();
|
|
868
|
+
}, 10);
|
|
869
|
+
} else if (this.player.options.debug) {
|
|
870
|
+
console.warn('[VidPly Metadata] Element not found:', targetSelector);
|
|
871
|
+
}
|
|
872
|
+
// Also emit event for developers who want to listen
|
|
541
873
|
this.player.emit('metadata:focus', {
|
|
542
874
|
time: cue.startTime,
|
|
543
|
-
target:
|
|
875
|
+
target: targetSelector,
|
|
876
|
+
element: targetElement,
|
|
544
877
|
text: text
|
|
545
878
|
});
|
|
546
879
|
}
|
|
@@ -548,6 +881,9 @@ export class TranscriptManager {
|
|
|
548
881
|
// Parse for hashtag references
|
|
549
882
|
const hashtags = text.match(/#[\w-]+/g);
|
|
550
883
|
if (hashtags) {
|
|
884
|
+
if (this.player.options.debug) {
|
|
885
|
+
console.log('[VidPly Metadata] Hashtags found:', hashtags);
|
|
886
|
+
}
|
|
551
887
|
this.player.emit('metadata:hashtags', {
|
|
552
888
|
time: cue.startTime,
|
|
553
889
|
hashtags: hashtags,
|
|
@@ -668,7 +1004,7 @@ export class TranscriptManager {
|
|
|
668
1004
|
* Scroll transcript window to show active entry
|
|
669
1005
|
*/
|
|
670
1006
|
scrollToEntry(entryElement) {
|
|
671
|
-
if (!this.transcriptContent) return;
|
|
1007
|
+
if (!this.transcriptContent || !this.autoscrollEnabled) return;
|
|
672
1008
|
|
|
673
1009
|
const contentRect = this.transcriptContent.getBoundingClientRect();
|
|
674
1010
|
const entryRect = entryElement.getBoundingClientRect();
|
|
@@ -683,6 +1019,15 @@ export class TranscriptManager {
|
|
|
683
1019
|
});
|
|
684
1020
|
}
|
|
685
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Save autoscroll preference to localStorage
|
|
1025
|
+
*/
|
|
1026
|
+
saveAutoscrollPreference() {
|
|
1027
|
+
const savedPreferences = this.storage.getTranscriptPreferences() || {};
|
|
1028
|
+
savedPreferences.autoscroll = this.autoscrollEnabled;
|
|
1029
|
+
this.storage.saveTranscriptPreferences(savedPreferences);
|
|
1030
|
+
}
|
|
686
1031
|
|
|
687
1032
|
/**
|
|
688
1033
|
* Setup drag and drop functionality
|
|
@@ -690,352 +1035,127 @@ export class TranscriptManager {
|
|
|
690
1035
|
setupDragAndDrop() {
|
|
691
1036
|
if (!this.transcriptHeader || !this.transcriptWindow) return;
|
|
692
1037
|
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
// Don't drag if clicking on settings button
|
|
701
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Don't drag if clicking on settings menu
|
|
706
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Don't drag if clicking on style dialog
|
|
711
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
this.startDragging(e.clientX, e.clientY);
|
|
716
|
-
e.preventDefault();
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
this.handlers.mousemove = (e) => {
|
|
720
|
-
if (this.isDragging) {
|
|
721
|
-
this.drag(e.clientX, e.clientY);
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
this.handlers.mouseup = () => {
|
|
726
|
-
if (this.isDragging) {
|
|
727
|
-
this.stopDragging();
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
this.handlers.touchstart = (e) => {
|
|
732
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// Don't drag if touching settings button
|
|
737
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Don't drag if touching settings menu
|
|
742
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Don't drag if touching style dialog
|
|
747
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const isMobile = window.innerWidth < 640;
|
|
752
|
-
const isFullscreen = this.player.state.fullscreen;
|
|
753
|
-
const touch = e.touches[0];
|
|
754
|
-
|
|
755
|
-
if (isMobile && !isFullscreen) {
|
|
756
|
-
// Mobile (not fullscreen): No dragging/swiping, transcript is part of layout
|
|
757
|
-
return;
|
|
758
|
-
} else {
|
|
759
|
-
// Desktop or fullscreen: Normal dragging
|
|
760
|
-
this.startDragging(touch.clientX, touch.clientY);
|
|
761
|
-
}
|
|
762
|
-
};
|
|
1038
|
+
// Check if we're on mobile and not in fullscreen (no dragging in this case)
|
|
1039
|
+
const isMobile = window.innerWidth < 640;
|
|
1040
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
1041
|
+
|
|
1042
|
+
if (isMobile && !isFullscreen) {
|
|
1043
|
+
return; // No drag/resize on mobile (not fullscreen)
|
|
1044
|
+
}
|
|
763
1045
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1046
|
+
// Create DraggableResizable utility
|
|
1047
|
+
this.draggableResizable = new DraggableResizable(this.transcriptWindow, {
|
|
1048
|
+
dragHandle: this.transcriptHeader,
|
|
1049
|
+
resizeHandles: this.transcriptResizeHandles,
|
|
1050
|
+
constrainToViewport: true,
|
|
1051
|
+
classPrefix: `${this.player.options.classPrefix}-transcript`,
|
|
1052
|
+
keyboardDragKey: 'd',
|
|
1053
|
+
keyboardResizeKey: 'r',
|
|
1054
|
+
keyboardStep: 10,
|
|
1055
|
+
keyboardStepLarge: 50,
|
|
1056
|
+
minWidth: 300,
|
|
1057
|
+
minHeight: 200,
|
|
1058
|
+
maxWidth: () => Math.max(320, window.innerWidth - 40),
|
|
1059
|
+
maxHeight: () => Math.max(200, window.innerHeight - 120),
|
|
1060
|
+
pointerResizeIndicatorText: i18n.t('transcript.resizeModeHint'),
|
|
1061
|
+
onPointerResizeToggle: (enabled) => this.onPointerResizeModeChange(enabled),
|
|
1062
|
+
onDragStart: (e) => {
|
|
1063
|
+
// Don't drag if clicking on certain elements
|
|
1064
|
+
const ignoreSelectors = [
|
|
1065
|
+
`.${this.player.options.classPrefix}-transcript-close`,
|
|
1066
|
+
`.${this.player.options.classPrefix}-transcript-settings`,
|
|
1067
|
+
`.${this.player.options.classPrefix}-transcript-language-select`,
|
|
1068
|
+
`.${this.player.options.classPrefix}-transcript-settings-menu`,
|
|
1069
|
+
`.${this.player.options.classPrefix}-transcript-style-dialog`
|
|
1070
|
+
];
|
|
1071
|
+
|
|
1072
|
+
for (const selector of ignoreSelectors) {
|
|
1073
|
+
if (e.target.closest(selector)) {
|
|
1074
|
+
return false; // Prevent drag
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return true; // Allow drag
|
|
776
1079
|
}
|
|
777
|
-
};
|
|
1080
|
+
});
|
|
778
1081
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
};
|
|
1082
|
+
// Add custom keyboard handler for special keys (Escape, Home)
|
|
1083
|
+
this.customKeyHandler = (e) => {
|
|
1084
|
+
const key = e.key.toLowerCase();
|
|
1085
|
+
const alreadyPrevented = e.defaultPrevented;
|
|
785
1086
|
|
|
786
|
-
|
|
787
|
-
// Handle arrow keys only in keyboard drag mode
|
|
788
|
-
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
|
789
|
-
if (!this.keyboardDragMode) {
|
|
790
|
-
// Not in drag mode, let other handlers deal with it
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// In drag mode - move the window
|
|
1087
|
+
if (key === 'home') {
|
|
795
1088
|
e.preventDefault();
|
|
796
1089
|
e.stopPropagation();
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
806
|
-
if (computedStyle.transform !== 'none') {
|
|
807
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
808
|
-
currentLeft = rect.left;
|
|
809
|
-
currentTop = rect.top;
|
|
810
|
-
this.transcriptWindow.style.transform = 'none';
|
|
811
|
-
this.transcriptWindow.style.left = `${currentLeft}px`;
|
|
812
|
-
this.transcriptWindow.style.top = `${currentTop}px`;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Calculate new position based on arrow key
|
|
816
|
-
let newX = currentLeft;
|
|
817
|
-
let newY = currentTop;
|
|
818
|
-
|
|
819
|
-
switch(e.key) {
|
|
820
|
-
case 'ArrowLeft':
|
|
821
|
-
newX -= step;
|
|
822
|
-
break;
|
|
823
|
-
case 'ArrowRight':
|
|
824
|
-
newX += step;
|
|
825
|
-
break;
|
|
826
|
-
case 'ArrowUp':
|
|
827
|
-
newY -= step;
|
|
828
|
-
break;
|
|
829
|
-
case 'ArrowDown':
|
|
830
|
-
newY += step;
|
|
831
|
-
break;
|
|
1090
|
+
if (this.draggableResizable) {
|
|
1091
|
+
if (this.draggableResizable.pointerResizeMode) {
|
|
1092
|
+
this.draggableResizable.disablePointerResizeMode();
|
|
1093
|
+
}
|
|
1094
|
+
this.draggableResizable.manuallyPositioned = false;
|
|
1095
|
+
this.positionTranscript();
|
|
1096
|
+
this.updateResizeOptionState();
|
|
1097
|
+
this.announceLive(i18n.t('transcript.positionReset'));
|
|
832
1098
|
}
|
|
833
|
-
|
|
834
|
-
// Set new position directly
|
|
835
|
-
this.transcriptWindow.style.left = `${newX}px`;
|
|
836
|
-
this.transcriptWindow.style.top = `${newY}px`;
|
|
837
1099
|
return;
|
|
838
1100
|
}
|
|
839
1101
|
|
|
840
|
-
|
|
841
|
-
|
|
1102
|
+
if (key === 'r') {
|
|
1103
|
+
if (alreadyPrevented) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
842
1106
|
e.preventDefault();
|
|
843
1107
|
e.stopPropagation();
|
|
844
|
-
this.
|
|
1108
|
+
const enabled = this.toggleResizeMode();
|
|
1109
|
+
if (enabled) {
|
|
1110
|
+
this.transcriptWindow.focus();
|
|
1111
|
+
}
|
|
845
1112
|
return;
|
|
846
1113
|
}
|
|
847
1114
|
|
|
848
|
-
if (
|
|
1115
|
+
if (key === 'escape') {
|
|
849
1116
|
e.preventDefault();
|
|
850
1117
|
e.stopPropagation();
|
|
1118
|
+
if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
|
|
1119
|
+
this.draggableResizable.disablePointerResizeMode();
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
851
1122
|
if (this.styleDialogVisible) {
|
|
852
|
-
// Close style dialog first
|
|
853
1123
|
this.hideStyleDialog();
|
|
854
|
-
} else if (this.keyboardDragMode) {
|
|
855
|
-
|
|
856
|
-
this.
|
|
1124
|
+
} else if (this.draggableResizable && this.draggableResizable.keyboardDragMode) {
|
|
1125
|
+
this.draggableResizable.disableKeyboardDragMode();
|
|
1126
|
+
this.announceLive(i18n.t('transcript.dragModeDisabled'));
|
|
857
1127
|
} else if (this.settingsMenuVisible) {
|
|
858
|
-
// Close settings menu
|
|
859
1128
|
this.hideSettingsMenu();
|
|
860
1129
|
} else {
|
|
861
|
-
|
|
862
|
-
this.hideTranscript();
|
|
1130
|
+
this.hideTranscript({ focusButton: true });
|
|
863
1131
|
}
|
|
864
1132
|
return;
|
|
865
1133
|
}
|
|
866
1134
|
};
|
|
867
|
-
|
|
868
|
-
// Add event listeners using stored handlers
|
|
869
|
-
this.transcriptHeader.addEventListener('mousedown', this.handlers.mousedown);
|
|
870
|
-
document.addEventListener('mousemove', this.handlers.mousemove);
|
|
871
|
-
document.addEventListener('mouseup', this.handlers.mouseup);
|
|
872
1135
|
|
|
873
|
-
this.
|
|
874
|
-
document.addEventListener('touchmove', this.handlers.touchmove);
|
|
875
|
-
document.addEventListener('touchend', this.handlers.touchend);
|
|
876
|
-
|
|
877
|
-
this.transcriptHeader.addEventListener('keydown', this.handlers.keydown);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Start dragging
|
|
882
|
-
*/
|
|
883
|
-
startDragging(clientX, clientY) {
|
|
884
|
-
// Get current rendered position (this is where it actually appears on screen)
|
|
885
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
886
|
-
|
|
887
|
-
// Get the parent container position (player container)
|
|
888
|
-
const containerRect = this.player.container.getBoundingClientRect();
|
|
889
|
-
|
|
890
|
-
// Calculate position RELATIVE to container (not viewport)
|
|
891
|
-
const relativeLeft = rect.left - containerRect.left;
|
|
892
|
-
const relativeTop = rect.top - containerRect.top;
|
|
893
|
-
|
|
894
|
-
// If window is centered with transform, convert to absolute position
|
|
895
|
-
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
896
|
-
if (computedStyle.transform !== 'none') {
|
|
897
|
-
// Remove transform and set position relative to container
|
|
898
|
-
this.transcriptWindow.style.transform = 'none';
|
|
899
|
-
this.transcriptWindow.style.left = `${relativeLeft}px`;
|
|
900
|
-
this.transcriptWindow.style.top = `${relativeTop}px`;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// Calculate offset based on viewport coordinates (where user clicked)
|
|
904
|
-
this.dragOffsetX = clientX - rect.left;
|
|
905
|
-
this.dragOffsetY = clientY - rect.top;
|
|
906
|
-
|
|
907
|
-
this.isDragging = true;
|
|
908
|
-
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
909
|
-
document.body.style.cursor = 'grabbing';
|
|
910
|
-
document.body.style.userSelect = 'none';
|
|
1136
|
+
this.transcriptWindow.addEventListener('keydown', this.customKeyHandler);
|
|
911
1137
|
}
|
|
912
1138
|
|
|
913
|
-
/**
|
|
914
|
-
* Perform drag
|
|
915
|
-
*/
|
|
916
|
-
drag(clientX, clientY) {
|
|
917
|
-
if (!this.isDragging) return;
|
|
918
|
-
|
|
919
|
-
// Calculate new viewport position based on mouse position minus the offset
|
|
920
|
-
const newViewportX = clientX - this.dragOffsetX;
|
|
921
|
-
const newViewportY = clientY - this.dragOffsetY;
|
|
922
|
-
|
|
923
|
-
// Convert to position relative to container
|
|
924
|
-
const containerRect = this.player.container.getBoundingClientRect();
|
|
925
|
-
const newX = newViewportX - containerRect.left;
|
|
926
|
-
const newY = newViewportY - containerRect.top;
|
|
927
|
-
|
|
928
|
-
// During drag, set position relative to container
|
|
929
|
-
this.transcriptWindow.style.left = `${newX}px`;
|
|
930
|
-
this.transcriptWindow.style.top = `${newY}px`;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Stop dragging
|
|
935
|
-
*/
|
|
936
|
-
stopDragging() {
|
|
937
|
-
this.isDragging = false;
|
|
938
|
-
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
939
|
-
document.body.style.cursor = '';
|
|
940
|
-
document.body.style.userSelect = '';
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* Set window position with boundary constraints
|
|
945
|
-
*/
|
|
946
|
-
setPosition(x, y) {
|
|
947
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
948
|
-
|
|
949
|
-
// Use document dimensions for fixed positioning
|
|
950
|
-
const viewportWidth = document.documentElement.clientWidth;
|
|
951
|
-
const viewportHeight = document.documentElement.clientHeight;
|
|
952
|
-
|
|
953
|
-
// Very relaxed boundaries - allow window to go mostly off-screen
|
|
954
|
-
// Just keep a small part visible so user can always drag it back
|
|
955
|
-
const minVisible = 100; // Keep at least 100px visible
|
|
956
|
-
const minX = -(rect.width - minVisible); // Can go way off-screen to the left
|
|
957
|
-
const minY = -(rect.height - minVisible); // Can go way off-screen to the top
|
|
958
|
-
const maxX = viewportWidth - minVisible; // Can go way off-screen to the right
|
|
959
|
-
const maxY = viewportHeight - minVisible; // Can go way off-screen to the bottom
|
|
960
|
-
|
|
961
|
-
// Clamp position to boundaries (very loose)
|
|
962
|
-
x = Math.max(minX, Math.min(x, maxX));
|
|
963
|
-
y = Math.max(minY, Math.min(y, maxY));
|
|
964
|
-
|
|
965
|
-
this.transcriptWindow.style.left = `${x}px`;
|
|
966
|
-
this.transcriptWindow.style.top = `${y}px`;
|
|
967
|
-
this.transcriptWindow.style.transform = 'none';
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
/**
|
|
971
|
-
* Reset position to center
|
|
972
|
-
*/
|
|
973
|
-
resetPosition() {
|
|
974
|
-
this.transcriptWindow.style.left = '50%';
|
|
975
|
-
this.transcriptWindow.style.top = '50%';
|
|
976
|
-
this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
|
|
977
|
-
}
|
|
978
1139
|
|
|
979
1140
|
/**
|
|
980
1141
|
* Toggle keyboard drag mode
|
|
981
1142
|
*/
|
|
982
1143
|
toggleKeyboardDragMode() {
|
|
983
|
-
if (this.
|
|
984
|
-
this.
|
|
985
|
-
|
|
986
|
-
this.
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (this.settingsButton) {
|
|
999
|
-
this.settingsButton.setAttribute('aria-label', 'Keyboard drag mode active. Use arrow keys to move window. Press D or Escape to exit.');
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Add visual indicator
|
|
1003
|
-
const indicator = DOMUtils.createElement('div', {
|
|
1004
|
-
className: `${this.player.options.classPrefix}-transcript-drag-indicator`,
|
|
1005
|
-
textContent: i18n.t('transcript.keyboardDragActive')
|
|
1006
|
-
});
|
|
1007
|
-
this.transcriptHeader.appendChild(indicator);
|
|
1008
|
-
|
|
1009
|
-
// Hide settings menu if open
|
|
1010
|
-
if (this.settingsMenuVisible) {
|
|
1011
|
-
this.hideSettingsMenu();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Focus the header for keyboard navigation
|
|
1015
|
-
this.transcriptHeader.focus();
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* Disable keyboard drag mode
|
|
1020
|
-
*/
|
|
1021
|
-
disableKeyboardDragMode() {
|
|
1022
|
-
this.keyboardDragMode = false;
|
|
1023
|
-
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
|
|
1024
|
-
|
|
1025
|
-
// Update settings button aria label
|
|
1026
|
-
if (this.settingsButton) {
|
|
1027
|
-
this.settingsButton.setAttribute('aria-label', 'Transcript settings. Press Enter to open menu, or D to enable drag mode');
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Remove visual indicator
|
|
1031
|
-
const indicator = this.transcriptHeader.querySelector(`.${this.player.options.classPrefix}-transcript-drag-indicator`);
|
|
1032
|
-
if (indicator) {
|
|
1033
|
-
indicator.remove();
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Focus back to settings button
|
|
1037
|
-
if (this.settingsButton) {
|
|
1038
|
-
this.settingsButton.focus();
|
|
1144
|
+
if (this.draggableResizable) {
|
|
1145
|
+
const wasEnabled = this.draggableResizable.keyboardDragMode;
|
|
1146
|
+
this.draggableResizable.toggleKeyboardDragMode();
|
|
1147
|
+
const isEnabled = this.draggableResizable.keyboardDragMode;
|
|
1148
|
+
if (!wasEnabled && isEnabled) {
|
|
1149
|
+
this.enableMoveMode();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Hide settings menu if open
|
|
1153
|
+
if (this.settingsMenuVisible) {
|
|
1154
|
+
this.hideSettingsMenu();
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Focus the window for keyboard navigation
|
|
1158
|
+
this.transcriptWindow.focus();
|
|
1039
1159
|
}
|
|
1040
1160
|
}
|
|
1041
1161
|
|
|
@@ -1071,6 +1191,16 @@ export class TranscriptManager {
|
|
|
1071
1191
|
if (this.settingsMenu) {
|
|
1072
1192
|
this.settingsMenu.style.display = 'block';
|
|
1073
1193
|
this.settingsMenuVisible = true;
|
|
1194
|
+
if (this.settingsButton) {
|
|
1195
|
+
this.settingsButton.setAttribute('aria-expanded', 'true');
|
|
1196
|
+
}
|
|
1197
|
+
this.updateResizeOptionState();
|
|
1198
|
+
setTimeout(() => {
|
|
1199
|
+
const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
|
|
1200
|
+
if (firstItem) {
|
|
1201
|
+
firstItem.focus();
|
|
1202
|
+
}
|
|
1203
|
+
}, 0);
|
|
1074
1204
|
return;
|
|
1075
1205
|
}
|
|
1076
1206
|
// Create settings menu
|
|
@@ -1126,19 +1256,38 @@ export class TranscriptManager {
|
|
|
1126
1256
|
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1127
1257
|
attributes: {
|
|
1128
1258
|
'type': 'button',
|
|
1129
|
-
'aria-label': i18n.t('transcript.resizeWindow')
|
|
1259
|
+
'aria-label': i18n.t('transcript.resizeWindow'),
|
|
1260
|
+
'aria-pressed': 'false'
|
|
1130
1261
|
}
|
|
1131
1262
|
});
|
|
1132
1263
|
const resizeIcon = createIconElement('resize');
|
|
1133
1264
|
const resizeText = DOMUtils.createElement('span', {
|
|
1265
|
+
className: `${this.player.options.classPrefix}-transcript-settings-text`,
|
|
1134
1266
|
textContent: i18n.t('transcript.resizeWindow')
|
|
1135
1267
|
});
|
|
1136
1268
|
resizeOption.appendChild(resizeIcon);
|
|
1137
1269
|
resizeOption.appendChild(resizeText);
|
|
1138
|
-
resizeOption.addEventListener('click', () => {
|
|
1139
|
-
|
|
1140
|
-
|
|
1270
|
+
resizeOption.addEventListener('click', (event) => {
|
|
1271
|
+
event.preventDefault();
|
|
1272
|
+
event.stopPropagation();
|
|
1273
|
+
|
|
1274
|
+
const enabled = this.toggleResizeMode({ focus: false });
|
|
1275
|
+
|
|
1276
|
+
if (enabled) {
|
|
1277
|
+
this.hideSettingsMenu({ focusButton: false });
|
|
1278
|
+
// Focus transcript window after handles appear
|
|
1279
|
+
this.setManagedTimeout(() => {
|
|
1280
|
+
if (this.transcriptWindow) {
|
|
1281
|
+
this.transcriptWindow.focus();
|
|
1282
|
+
}
|
|
1283
|
+
}, 20);
|
|
1284
|
+
} else {
|
|
1285
|
+
this.hideSettingsMenu({ focusButton: true });
|
|
1286
|
+
}
|
|
1141
1287
|
});
|
|
1288
|
+
this.resizeOptionButton = resizeOption;
|
|
1289
|
+
this.resizeOptionText = resizeText;
|
|
1290
|
+
this.updateResizeOptionState();
|
|
1142
1291
|
|
|
1143
1292
|
// Close option
|
|
1144
1293
|
const closeOption = DOMUtils.createElement('button', {
|
|
@@ -1178,6 +1327,7 @@ export class TranscriptManager {
|
|
|
1178
1327
|
if (this.settingsButton) {
|
|
1179
1328
|
this.settingsButton.setAttribute('aria-expanded', 'true');
|
|
1180
1329
|
}
|
|
1330
|
+
this.updateResizeOptionState();
|
|
1181
1331
|
|
|
1182
1332
|
// Focus first menu item
|
|
1183
1333
|
setTimeout(() => {
|
|
@@ -1191,7 +1341,7 @@ export class TranscriptManager {
|
|
|
1191
1341
|
/**
|
|
1192
1342
|
* Hide settings menu
|
|
1193
1343
|
*/
|
|
1194
|
-
hideSettingsMenu() {
|
|
1344
|
+
hideSettingsMenu({ focusButton = true } = {}) {
|
|
1195
1345
|
if (this.settingsMenu) {
|
|
1196
1346
|
this.settingsMenu.style.display = 'none';
|
|
1197
1347
|
this.settingsMenuVisible = false;
|
|
@@ -1200,8 +1350,10 @@ export class TranscriptManager {
|
|
|
1200
1350
|
// Update aria-expanded
|
|
1201
1351
|
if (this.settingsButton) {
|
|
1202
1352
|
this.settingsButton.setAttribute('aria-expanded', 'false');
|
|
1203
|
-
|
|
1204
|
-
|
|
1353
|
+
if (focusButton) {
|
|
1354
|
+
// Return focus to settings button
|
|
1355
|
+
this.settingsButton.focus();
|
|
1356
|
+
}
|
|
1205
1357
|
}
|
|
1206
1358
|
}
|
|
1207
1359
|
}
|
|
@@ -1210,6 +1362,8 @@ export class TranscriptManager {
|
|
|
1210
1362
|
* Enable move mode (gives visual feedback)
|
|
1211
1363
|
*/
|
|
1212
1364
|
enableMoveMode() {
|
|
1365
|
+
this.hideResizeModeIndicator();
|
|
1366
|
+
|
|
1213
1367
|
// Add visual feedback for move mode
|
|
1214
1368
|
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
|
|
1215
1369
|
|
|
@@ -1232,193 +1386,82 @@ export class TranscriptManager {
|
|
|
1232
1386
|
/**
|
|
1233
1387
|
* Toggle resize mode
|
|
1234
1388
|
*/
|
|
1235
|
-
toggleResizeMode() {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (this.resizeEnabled) {
|
|
1239
|
-
this.enableResizeHandles();
|
|
1240
|
-
} else {
|
|
1241
|
-
this.disableResizeHandles();
|
|
1389
|
+
toggleResizeMode({ focus = true } = {}) {
|
|
1390
|
+
if (!this.draggableResizable) {
|
|
1391
|
+
return false;
|
|
1242
1392
|
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
/**
|
|
1246
|
-
* Enable resize handles
|
|
1247
|
-
*/
|
|
1248
|
-
enableResizeHandles() {
|
|
1249
|
-
if (!this.transcriptWindow) return;
|
|
1250
|
-
|
|
1251
|
-
// Add resize handles if they don't exist
|
|
1252
|
-
const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
|
|
1253
|
-
|
|
1254
|
-
directions.forEach(direction => {
|
|
1255
|
-
const handle = DOMUtils.createElement('div', {
|
|
1256
|
-
className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
|
|
1257
|
-
attributes: {
|
|
1258
|
-
'data-direction': direction
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
handle.addEventListener('mousedown', (e) => this.startResize(e, direction));
|
|
1263
|
-
handle.addEventListener('touchstart', (e) => this.startResize(e.touches[0], direction));
|
|
1264
1393
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
// Setup resize event handlers
|
|
1271
|
-
this.handlers.resizeMove = (e) => {
|
|
1272
|
-
if (this.isResizing) {
|
|
1273
|
-
this.performResize(e.clientX, e.clientY);
|
|
1274
|
-
}
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
this.handlers.resizeEnd = () => {
|
|
1278
|
-
if (this.isResizing) {
|
|
1279
|
-
this.stopResize();
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
|
|
1283
|
-
this.handlers.resizeTouchMove = (e) => {
|
|
1284
|
-
if (this.isResizing) {
|
|
1285
|
-
this.performResize(e.touches[0].clientX, e.touches[0].clientY);
|
|
1286
|
-
e.preventDefault();
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1394
|
+
if (this.draggableResizable.pointerResizeMode) {
|
|
1395
|
+
this.draggableResizable.disablePointerResizeMode({ focus });
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1289
1398
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
document.addEventListener('touchmove', this.handlers.resizeTouchMove);
|
|
1293
|
-
document.addEventListener('touchend', this.handlers.resizeEnd);
|
|
1399
|
+
this.draggableResizable.enablePointerResizeMode({ focus });
|
|
1400
|
+
return true;
|
|
1294
1401
|
}
|
|
1295
1402
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1403
|
+
updateResizeOptionState() {
|
|
1404
|
+
if (!this.resizeOptionButton) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const isEnabled = !!(this.draggableResizable && this.draggableResizable.pointerResizeMode);
|
|
1409
|
+
const label = isEnabled
|
|
1410
|
+
? (i18n.t('transcript.disableResizeWindow') || 'Disable Resize Mode')
|
|
1411
|
+
: i18n.t('transcript.resizeWindow');
|
|
1305
1412
|
|
|
1306
|
-
this.
|
|
1413
|
+
this.resizeOptionButton.setAttribute('aria-pressed', isEnabled ? 'true' : 'false');
|
|
1414
|
+
this.resizeOptionButton.setAttribute('aria-label', label);
|
|
1415
|
+
this.resizeOptionButton.setAttribute('title', label);
|
|
1307
1416
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
document.removeEventListener('mousemove', this.handlers.resizeMove);
|
|
1417
|
+
if (this.resizeOptionText) {
|
|
1418
|
+
this.resizeOptionText.textContent = label;
|
|
1311
1419
|
}
|
|
1312
|
-
if (this.handlers.resizeEnd) {
|
|
1313
|
-
document.removeEventListener('mouseup', this.handlers.resizeEnd);
|
|
1314
|
-
}
|
|
1315
|
-
if (this.handlers.resizeTouchMove) {
|
|
1316
|
-
document.removeEventListener('touchmove', this.handlers.resizeTouchMove);
|
|
1317
|
-
}
|
|
1318
|
-
document.removeEventListener('touchend', this.handlers.resizeEnd);
|
|
1319
1420
|
}
|
|
1320
1421
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
e.stopPropagation();
|
|
1326
|
-
e.preventDefault();
|
|
1327
|
-
|
|
1328
|
-
this.isResizing = true;
|
|
1329
|
-
this.resizeDirection = direction;
|
|
1330
|
-
this.resizeStartX = e.clientX;
|
|
1331
|
-
this.resizeStartY = e.clientY;
|
|
1332
|
-
|
|
1333
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
1334
|
-
this.resizeStartWidth = rect.width;
|
|
1335
|
-
this.resizeStartHeight = rect.height;
|
|
1336
|
-
|
|
1337
|
-
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizing`);
|
|
1338
|
-
document.body.style.cursor = this.getResizeCursor(direction);
|
|
1339
|
-
document.body.style.userSelect = 'none';
|
|
1340
|
-
}
|
|
1422
|
+
showResizeModeIndicator() {
|
|
1423
|
+
if (!this.transcriptHeader) {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1341
1426
|
|
|
1342
|
-
|
|
1343
|
-
* Perform resize
|
|
1344
|
-
*/
|
|
1345
|
-
performResize(clientX, clientY) {
|
|
1346
|
-
if (!this.isResizing) return;
|
|
1427
|
+
this.hideResizeModeIndicator();
|
|
1347
1428
|
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1429
|
+
const indicator = DOMUtils.createElement('div', {
|
|
1430
|
+
className: `${this.player.options.classPrefix}-transcript-resize-tooltip`,
|
|
1431
|
+
textContent: i18n.t('transcript.resizeModeHint') || 'Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit.'
|
|
1432
|
+
});
|
|
1350
1433
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1434
|
+
this.transcriptHeader.appendChild(indicator);
|
|
1435
|
+
this.resizeModeIndicator = indicator;
|
|
1353
1436
|
|
|
1354
|
-
|
|
1437
|
+
this.resizeModeIndicatorTimeout = this.setManagedTimeout(() => {
|
|
1438
|
+
this.hideResizeModeIndicator();
|
|
1439
|
+
}, 3000);
|
|
1440
|
+
}
|
|
1355
1441
|
|
|
1356
|
-
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (direction.includes('w')) {
|
|
1361
|
-
newWidth = this.resizeStartWidth - deltaX;
|
|
1362
|
-
}
|
|
1363
|
-
if (direction.includes('s')) {
|
|
1364
|
-
newHeight = this.resizeStartHeight + deltaY;
|
|
1365
|
-
}
|
|
1366
|
-
if (direction.includes('n')) {
|
|
1367
|
-
newHeight = this.resizeStartHeight - deltaY;
|
|
1442
|
+
hideResizeModeIndicator() {
|
|
1443
|
+
if (this.resizeModeIndicatorTimeout) {
|
|
1444
|
+
this.clearManagedTimeout(this.resizeModeIndicatorTimeout);
|
|
1445
|
+
this.resizeModeIndicatorTimeout = null;
|
|
1368
1446
|
}
|
|
1369
1447
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const minHeight = 200;
|
|
1373
|
-
const maxWidth = window.innerWidth - 40;
|
|
1374
|
-
const maxHeight = window.innerHeight - 40;
|
|
1375
|
-
|
|
1376
|
-
newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
|
|
1377
|
-
newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
|
|
1378
|
-
|
|
1379
|
-
// Apply new dimensions
|
|
1380
|
-
this.transcriptWindow.style.width = `${newWidth}px`;
|
|
1381
|
-
this.transcriptWindow.style.height = `${newHeight}px`;
|
|
1382
|
-
this.transcriptWindow.style.maxWidth = `${newWidth}px`;
|
|
1383
|
-
this.transcriptWindow.style.maxHeight = `${newHeight}px`;
|
|
1384
|
-
|
|
1385
|
-
// Adjust position if resizing from top or left
|
|
1386
|
-
if (direction.includes('w')) {
|
|
1387
|
-
const currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
|
|
1388
|
-
this.transcriptWindow.style.left = `${currentLeft + (this.resizeStartWidth - newWidth)}px`;
|
|
1389
|
-
}
|
|
1390
|
-
if (direction.includes('n')) {
|
|
1391
|
-
const currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
|
|
1392
|
-
this.transcriptWindow.style.top = `${currentTop + (this.resizeStartHeight - newHeight)}px`;
|
|
1448
|
+
if (this.resizeModeIndicator && this.resizeModeIndicator.parentNode) {
|
|
1449
|
+
this.resizeModeIndicator.remove();
|
|
1393
1450
|
}
|
|
1394
|
-
}
|
|
1395
1451
|
|
|
1396
|
-
|
|
1397
|
-
* Stop resizing
|
|
1398
|
-
*/
|
|
1399
|
-
stopResize() {
|
|
1400
|
-
this.isResizing = false;
|
|
1401
|
-
this.resizeDirection = null;
|
|
1402
|
-
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizing`);
|
|
1403
|
-
document.body.style.cursor = '';
|
|
1404
|
-
document.body.style.userSelect = '';
|
|
1452
|
+
this.resizeModeIndicator = null;
|
|
1405
1453
|
}
|
|
1406
1454
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
'
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
'
|
|
1416
|
-
|
|
1417
|
-
'nw': 'nwse-resize',
|
|
1418
|
-
'se': 'nwse-resize',
|
|
1419
|
-
'sw': 'nesw-resize'
|
|
1420
|
-
};
|
|
1421
|
-
return cursors[direction] || 'default';
|
|
1455
|
+
onPointerResizeModeChange(enabled) {
|
|
1456
|
+
this.updateResizeOptionState();
|
|
1457
|
+
|
|
1458
|
+
if (enabled) {
|
|
1459
|
+
this.showResizeModeIndicator();
|
|
1460
|
+
this.announceLive(i18n.t('transcript.resizeModeEnabled'));
|
|
1461
|
+
} else {
|
|
1462
|
+
this.hideResizeModeIndicator();
|
|
1463
|
+
this.announceLive(i18n.t('transcript.resizeModeDisabled'));
|
|
1464
|
+
}
|
|
1422
1465
|
}
|
|
1423
1466
|
|
|
1424
1467
|
/**
|
|
@@ -1718,35 +1761,58 @@ export class TranscriptManager {
|
|
|
1718
1761
|
});
|
|
1719
1762
|
}
|
|
1720
1763
|
|
|
1764
|
+
/**
|
|
1765
|
+
* Set a managed timeout that will be cleaned up on destroy
|
|
1766
|
+
* @param {Function} callback - Callback function
|
|
1767
|
+
* @param {number} delay - Delay in milliseconds
|
|
1768
|
+
* @returns {number} Timeout ID
|
|
1769
|
+
*/
|
|
1770
|
+
setManagedTimeout(callback, delay) {
|
|
1771
|
+
const timeoutId = setTimeout(() => {
|
|
1772
|
+
this.timeouts.delete(timeoutId);
|
|
1773
|
+
callback();
|
|
1774
|
+
}, delay);
|
|
1775
|
+
this.timeouts.add(timeoutId);
|
|
1776
|
+
return timeoutId;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Clear a managed timeout
|
|
1781
|
+
* @param {number} timeoutId - Timeout ID to clear
|
|
1782
|
+
*/
|
|
1783
|
+
clearManagedTimeout(timeoutId) {
|
|
1784
|
+
if (timeoutId) {
|
|
1785
|
+
clearTimeout(timeoutId);
|
|
1786
|
+
this.timeouts.delete(timeoutId);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1721
1790
|
/**
|
|
1722
1791
|
* Cleanup
|
|
1723
1792
|
*/
|
|
1724
1793
|
destroy() {
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1794
|
+
this.hideResizeModeIndicator();
|
|
1795
|
+
|
|
1796
|
+
// Destroy draggableResizable utility
|
|
1797
|
+
if (this.draggableResizable) {
|
|
1798
|
+
if (this.draggableResizable.pointerResizeMode) {
|
|
1799
|
+
this.draggableResizable.disablePointerResizeMode();
|
|
1800
|
+
this.updateResizeOptionState();
|
|
1801
|
+
}
|
|
1802
|
+
this.draggableResizable.destroy();
|
|
1803
|
+
this.draggableResizable = null;
|
|
1728
1804
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1805
|
+
|
|
1806
|
+
// Remove custom key handler
|
|
1807
|
+
if (this.transcriptWindow && this.customKeyHandler) {
|
|
1808
|
+
this.transcriptWindow.removeEventListener('keydown', this.customKeyHandler);
|
|
1809
|
+
this.customKeyHandler = null;
|
|
1731
1810
|
}
|
|
1732
1811
|
|
|
1733
1812
|
// Remove timeupdate listener from player
|
|
1734
1813
|
if (this.handlers.timeupdate) {
|
|
1735
1814
|
this.player.off('timeupdate', this.handlers.timeupdate);
|
|
1736
1815
|
}
|
|
1737
|
-
|
|
1738
|
-
// Remove drag event listeners
|
|
1739
|
-
if (this.transcriptHeader) {
|
|
1740
|
-
if (this.handlers.mousedown) {
|
|
1741
|
-
this.transcriptHeader.removeEventListener('mousedown', this.handlers.mousedown);
|
|
1742
|
-
}
|
|
1743
|
-
if (this.handlers.touchstart) {
|
|
1744
|
-
this.transcriptHeader.removeEventListener('touchstart', this.handlers.touchstart);
|
|
1745
|
-
}
|
|
1746
|
-
if (this.handlers.keydown) {
|
|
1747
|
-
this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
1816
|
|
|
1751
1817
|
// Remove settings button event listeners
|
|
1752
1818
|
if (this.settingsButton) {
|
|
@@ -1763,19 +1829,7 @@ export class TranscriptManager {
|
|
|
1763
1829
|
this.styleDialog.removeEventListener('keydown', this.handlers.styleDialogKeydown);
|
|
1764
1830
|
}
|
|
1765
1831
|
|
|
1766
|
-
// Remove document
|
|
1767
|
-
if (this.handlers.mousemove) {
|
|
1768
|
-
document.removeEventListener('mousemove', this.handlers.mousemove);
|
|
1769
|
-
}
|
|
1770
|
-
if (this.handlers.mouseup) {
|
|
1771
|
-
document.removeEventListener('mouseup', this.handlers.mouseup);
|
|
1772
|
-
}
|
|
1773
|
-
if (this.handlers.touchmove) {
|
|
1774
|
-
document.removeEventListener('touchmove', this.handlers.touchmove);
|
|
1775
|
-
}
|
|
1776
|
-
if (this.handlers.touchend) {
|
|
1777
|
-
document.removeEventListener('touchend', this.handlers.touchend);
|
|
1778
|
-
}
|
|
1832
|
+
// Remove document click listener
|
|
1779
1833
|
if (this.handlers.documentClick) {
|
|
1780
1834
|
document.removeEventListener('click', this.handlers.documentClick);
|
|
1781
1835
|
}
|
|
@@ -1785,6 +1839,10 @@ export class TranscriptManager {
|
|
|
1785
1839
|
window.removeEventListener('resize', this.handlers.resize);
|
|
1786
1840
|
}
|
|
1787
1841
|
|
|
1842
|
+
// Cleanup all managed timeouts
|
|
1843
|
+
this.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
|
1844
|
+
this.timeouts.clear();
|
|
1845
|
+
|
|
1788
1846
|
// Clear handlers
|
|
1789
1847
|
this.handlers = null;
|
|
1790
1848
|
|
|
@@ -1799,5 +1857,14 @@ export class TranscriptManager {
|
|
|
1799
1857
|
this.transcriptEntries = [];
|
|
1800
1858
|
this.settingsMenu = null;
|
|
1801
1859
|
this.styleDialog = null;
|
|
1860
|
+
this.transcriptResizeHandles = [];
|
|
1861
|
+
this.resizeOptionButton = null;
|
|
1862
|
+
this.resizeOptionText = null;
|
|
1863
|
+
this.liveRegion = null;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
announceLive(message) {
|
|
1867
|
+
if (!this.liveRegion) return;
|
|
1868
|
+
this.liveRegion.textContent = message || '';
|
|
1802
1869
|
}
|
|
1803
1870
|
}
|