vidply 1.0.6 → 1.0.8
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/README.md +19 -4
- package/dist/vidply.css +640 -25
- package/dist/vidply.esm.js +2208 -177
- package/dist/vidply.esm.js.map +4 -4
- package/dist/vidply.esm.min.js +6 -6
- package/dist/vidply.esm.min.meta.json +38 -15
- package/dist/vidply.js +2208 -177
- package/dist/vidply.js.map +4 -4
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +6 -6
- package/dist/vidply.min.meta.json +38 -15
- package/package.json +2 -2
- package/src/controls/CaptionManager.js +30 -0
- package/src/controls/ControlBar.js +11 -4
- package/src/controls/SettingsDialog.js +3 -3
- package/src/controls/TranscriptManager.js +1147 -72
- package/src/core/Player.js +1435 -26
- package/src/i18n/translations.js +70 -15
- package/src/icons/Icons.js +8 -4
- package/src/styles/vidply.css +640 -25
- package/src/utils/StorageManager.js +156 -0
|
@@ -7,20 +7,60 @@ import { DOMUtils } from '../utils/DOMUtils.js';
|
|
|
7
7
|
import { TimeUtils } from '../utils/TimeUtils.js';
|
|
8
8
|
import { createIconElement } from '../icons/Icons.js';
|
|
9
9
|
import { i18n } from '../i18n/i18n.js';
|
|
10
|
+
import { StorageManager } from '../utils/StorageManager.js';
|
|
10
11
|
|
|
11
12
|
export class TranscriptManager {
|
|
12
13
|
constructor(player) {
|
|
13
14
|
this.player = player;
|
|
14
15
|
this.transcriptWindow = null;
|
|
15
16
|
this.transcriptEntries = [];
|
|
17
|
+
this.metadataCues = [];
|
|
16
18
|
this.currentActiveEntry = null;
|
|
17
19
|
this.isVisible = false;
|
|
18
20
|
|
|
21
|
+
// Storage manager
|
|
22
|
+
this.storage = new StorageManager('vidply');
|
|
23
|
+
|
|
19
24
|
// Dragging state
|
|
20
25
|
this.isDragging = false;
|
|
21
26
|
this.dragOffsetX = 0;
|
|
22
27
|
this.dragOffsetY = 0;
|
|
23
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;
|
|
37
|
+
|
|
38
|
+
// Settings menu state
|
|
39
|
+
this.settingsMenuVisible = false;
|
|
40
|
+
this.settingsMenu = null;
|
|
41
|
+
this.settingsButton = null;
|
|
42
|
+
this.settingsMenuJustOpened = false;
|
|
43
|
+
|
|
44
|
+
// Keyboard drag mode
|
|
45
|
+
this.keyboardDragMode = false;
|
|
46
|
+
|
|
47
|
+
// Style dialog state
|
|
48
|
+
this.styleDialog = null;
|
|
49
|
+
this.styleDialogVisible = false;
|
|
50
|
+
this.styleDialogJustOpened = false;
|
|
51
|
+
|
|
52
|
+
// Load saved preferences from localStorage
|
|
53
|
+
const savedPreferences = this.storage.getTranscriptPreferences();
|
|
54
|
+
|
|
55
|
+
// Transcript styling options (with defaults, then player options, then saved preferences)
|
|
56
|
+
this.transcriptStyle = {
|
|
57
|
+
fontSize: savedPreferences?.fontSize || this.player.options.transcriptFontSize || '100%',
|
|
58
|
+
fontFamily: savedPreferences?.fontFamily || this.player.options.transcriptFontFamily || 'sans-serif',
|
|
59
|
+
color: savedPreferences?.color || this.player.options.transcriptColor || '#ffffff',
|
|
60
|
+
backgroundColor: savedPreferences?.backgroundColor || this.player.options.transcriptBackgroundColor || '#1e1e1e',
|
|
61
|
+
opacity: savedPreferences?.opacity ?? this.player.options.transcriptOpacity ?? 0.98
|
|
62
|
+
};
|
|
63
|
+
|
|
24
64
|
// Store event handlers for cleanup
|
|
25
65
|
this.handlers = {
|
|
26
66
|
timeupdate: () => this.updateActiveEntry(),
|
|
@@ -31,7 +71,11 @@ export class TranscriptManager {
|
|
|
31
71
|
touchend: null,
|
|
32
72
|
mousedown: null,
|
|
33
73
|
touchstart: null,
|
|
34
|
-
keydown: null
|
|
74
|
+
keydown: null,
|
|
75
|
+
settingsClick: null,
|
|
76
|
+
settingsKeydown: null,
|
|
77
|
+
documentClick: null,
|
|
78
|
+
styleDialogKeydown: null
|
|
35
79
|
};
|
|
36
80
|
|
|
37
81
|
this.init();
|
|
@@ -68,6 +112,13 @@ export class TranscriptManager {
|
|
|
68
112
|
if (this.transcriptWindow) {
|
|
69
113
|
this.transcriptWindow.style.display = 'flex';
|
|
70
114
|
this.isVisible = true;
|
|
115
|
+
|
|
116
|
+
// Focus the settings button for keyboard accessibility
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (this.settingsButton) {
|
|
119
|
+
this.settingsButton.focus();
|
|
120
|
+
}
|
|
121
|
+
}, 150);
|
|
71
122
|
return;
|
|
72
123
|
}
|
|
73
124
|
|
|
@@ -80,6 +131,13 @@ export class TranscriptManager {
|
|
|
80
131
|
this.transcriptWindow.style.display = 'flex';
|
|
81
132
|
// Re-position after showing (in case window was resized while hidden)
|
|
82
133
|
setTimeout(() => this.positionTranscript(), 0);
|
|
134
|
+
|
|
135
|
+
// Focus the settings button for keyboard accessibility
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
if (this.settingsButton) {
|
|
138
|
+
this.settingsButton.focus();
|
|
139
|
+
}
|
|
140
|
+
}, 150);
|
|
83
141
|
}
|
|
84
142
|
this.isVisible = true;
|
|
85
143
|
}
|
|
@@ -116,10 +174,62 @@ export class TranscriptManager {
|
|
|
116
174
|
}
|
|
117
175
|
});
|
|
118
176
|
|
|
177
|
+
// Header left side (settings button + title)
|
|
178
|
+
this.headerLeft = DOMUtils.createElement('div', {
|
|
179
|
+
className: `${this.player.options.classPrefix}-transcript-header-left`
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Settings button
|
|
183
|
+
this.settingsButton = DOMUtils.createElement('button', {
|
|
184
|
+
className: `${this.player.options.classPrefix}-transcript-settings`,
|
|
185
|
+
attributes: {
|
|
186
|
+
'type': 'button',
|
|
187
|
+
'aria-label': i18n.t('transcript.settings'),
|
|
188
|
+
'aria-expanded': 'false'
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
this.settingsButton.appendChild(createIconElement('settings'));
|
|
192
|
+
this.handlers.settingsClick = (e) => {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
if (this.settingsMenuVisible) {
|
|
196
|
+
this.hideSettingsMenu();
|
|
197
|
+
} else {
|
|
198
|
+
this.showSettingsMenu();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
this.settingsButton.addEventListener('click', this.handlers.settingsClick);
|
|
202
|
+
|
|
203
|
+
// Keyboard handler for settings button
|
|
204
|
+
this.handlers.settingsKeydown = (e) => {
|
|
205
|
+
// D key to toggle keyboard drag mode
|
|
206
|
+
if (e.key === 'd' || e.key === 'D') {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
e.stopPropagation();
|
|
209
|
+
this.toggleKeyboardDragMode();
|
|
210
|
+
}
|
|
211
|
+
// R key to toggle resize mode
|
|
212
|
+
else if (e.key === 'r' || e.key === 'R') {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
this.toggleResizeMode();
|
|
216
|
+
}
|
|
217
|
+
// Escape to close menu if open
|
|
218
|
+
else if (e.key === 'Escape' && this.settingsMenuVisible) {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
this.hideSettingsMenu();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
this.settingsButton.addEventListener('keydown', this.handlers.settingsKeydown);
|
|
225
|
+
|
|
119
226
|
const title = DOMUtils.createElement('h3', {
|
|
120
227
|
textContent: i18n.t('transcript.title')
|
|
121
228
|
});
|
|
122
229
|
|
|
230
|
+
this.headerLeft.appendChild(this.settingsButton);
|
|
231
|
+
this.headerLeft.appendChild(title);
|
|
232
|
+
|
|
123
233
|
const closeButton = DOMUtils.createElement('button', {
|
|
124
234
|
className: `${this.player.options.classPrefix}-transcript-close`,
|
|
125
235
|
attributes: {
|
|
@@ -130,7 +240,7 @@ export class TranscriptManager {
|
|
|
130
240
|
closeButton.appendChild(createIconElement('close'));
|
|
131
241
|
closeButton.addEventListener('click', () => this.hideTranscript());
|
|
132
242
|
|
|
133
|
-
this.transcriptHeader.appendChild(
|
|
243
|
+
this.transcriptHeader.appendChild(this.headerLeft);
|
|
134
244
|
this.transcriptHeader.appendChild(closeButton);
|
|
135
245
|
|
|
136
246
|
// Content container
|
|
@@ -150,6 +260,43 @@ export class TranscriptManager {
|
|
|
150
260
|
// Setup drag functionality
|
|
151
261
|
this.setupDragAndDrop();
|
|
152
262
|
|
|
263
|
+
// Setup document click handler to close settings menu and style dialog
|
|
264
|
+
// DON'T add it yet - it will be added when the menu is first opened
|
|
265
|
+
this.handlers.documentClick = (e) => {
|
|
266
|
+
// Ignore if menu was just opened (prevents immediate closing)
|
|
267
|
+
if (this.settingsMenuJustOpened) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Ignore if style dialog was just opened (prevents immediate closing)
|
|
272
|
+
if (this.styleDialogJustOpened) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Ignore clicks on the settings button itself
|
|
277
|
+
if (this.settingsButton && this.settingsButton.contains(e.target)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Ignore clicks on the settings menu items
|
|
282
|
+
if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Close settings menu if clicking outside
|
|
287
|
+
if (this.settingsMenuVisible) {
|
|
288
|
+
this.hideSettingsMenu();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Close style dialog if clicking outside (but not on settings button)
|
|
292
|
+
if (this.styleDialogVisible && this.styleDialog &&
|
|
293
|
+
!this.styleDialog.contains(e.target)) {
|
|
294
|
+
this.hideStyleDialog();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
// Store flag to track if handler has been added
|
|
298
|
+
this.documentClickHandlerAdded = false;
|
|
299
|
+
|
|
153
300
|
// Re-position on window resize (debounced)
|
|
154
301
|
let resizeTimeout;
|
|
155
302
|
this.handlers.resize = () => {
|
|
@@ -248,23 +395,34 @@ export class TranscriptManager {
|
|
|
248
395
|
this.transcriptEntries = [];
|
|
249
396
|
this.transcriptContent.innerHTML = '';
|
|
250
397
|
|
|
251
|
-
// Get
|
|
398
|
+
// Get all text tracks
|
|
252
399
|
const textTracks = Array.from(this.player.element.textTracks);
|
|
253
|
-
|
|
400
|
+
|
|
401
|
+
// Find different track types
|
|
402
|
+
const captionTrack = textTracks.find(
|
|
254
403
|
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
255
404
|
);
|
|
405
|
+
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
406
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
256
407
|
|
|
257
|
-
|
|
408
|
+
// We need at least one track type
|
|
409
|
+
if (!captionTrack && !descriptionTrack && !metadataTrack) {
|
|
258
410
|
this.showNoTranscriptMessage();
|
|
259
411
|
return;
|
|
260
412
|
}
|
|
261
413
|
|
|
262
|
-
// Enable
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
414
|
+
// Enable all tracks to load cues
|
|
415
|
+
const tracksToLoad = [captionTrack, descriptionTrack, metadataTrack].filter(Boolean);
|
|
416
|
+
tracksToLoad.forEach(track => {
|
|
417
|
+
if (track.mode === 'disabled') {
|
|
418
|
+
track.mode = 'hidden';
|
|
419
|
+
}
|
|
420
|
+
});
|
|
266
421
|
|
|
267
|
-
if
|
|
422
|
+
// Check if any tracks are still loading
|
|
423
|
+
const needsLoading = tracksToLoad.some(track => !track.cues || track.cues.length === 0);
|
|
424
|
+
|
|
425
|
+
if (needsLoading) {
|
|
268
426
|
// Wait for cues to load
|
|
269
427
|
const loadingMessage = DOMUtils.createElement('div', {
|
|
270
428
|
className: `${this.player.options.classPrefix}-transcript-loading`,
|
|
@@ -272,45 +430,142 @@ export class TranscriptManager {
|
|
|
272
430
|
});
|
|
273
431
|
this.transcriptContent.appendChild(loadingMessage);
|
|
274
432
|
|
|
433
|
+
let loaded = 0;
|
|
275
434
|
const onLoad = () => {
|
|
276
|
-
|
|
435
|
+
loaded++;
|
|
436
|
+
if (loaded >= tracksToLoad.length) {
|
|
437
|
+
this.loadTranscriptData();
|
|
438
|
+
}
|
|
277
439
|
};
|
|
278
440
|
|
|
279
|
-
|
|
441
|
+
tracksToLoad.forEach(track => {
|
|
442
|
+
track.addEventListener('load', onLoad, { once: true });
|
|
443
|
+
});
|
|
280
444
|
|
|
281
445
|
// Fallback timeout
|
|
282
446
|
setTimeout(() => {
|
|
283
|
-
|
|
284
|
-
this.loadTranscriptData();
|
|
285
|
-
}
|
|
447
|
+
this.loadTranscriptData();
|
|
286
448
|
}, 500);
|
|
287
449
|
|
|
288
450
|
return;
|
|
289
451
|
}
|
|
290
452
|
|
|
291
|
-
//
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
453
|
+
// Collect all cues from all tracks with their type
|
|
454
|
+
const allCues = [];
|
|
455
|
+
|
|
456
|
+
if (captionTrack && captionTrack.cues) {
|
|
457
|
+
Array.from(captionTrack.cues).forEach(cue => {
|
|
458
|
+
allCues.push({ cue, type: 'caption' });
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (descriptionTrack && descriptionTrack.cues) {
|
|
463
|
+
Array.from(descriptionTrack.cues).forEach(cue => {
|
|
464
|
+
allCues.push({ cue, type: 'description' });
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Store metadata separately for programmatic use (don't display in transcript)
|
|
469
|
+
if (metadataTrack && metadataTrack.cues) {
|
|
470
|
+
this.metadataCues = Array.from(metadataTrack.cues);
|
|
471
|
+
this.setupMetadataHandling();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Sort all cues by start time
|
|
475
|
+
allCues.sort((a, b) => a.cue.startTime - b.cue.startTime);
|
|
476
|
+
|
|
477
|
+
// Build transcript from captions and descriptions only
|
|
478
|
+
allCues.forEach((item, index) => {
|
|
479
|
+
const entry = this.createTranscriptEntry(item.cue, index, item.type);
|
|
295
480
|
this.transcriptEntries.push({
|
|
296
481
|
element: entry,
|
|
297
|
-
cue: cue,
|
|
298
|
-
|
|
299
|
-
|
|
482
|
+
cue: item.cue,
|
|
483
|
+
type: item.type,
|
|
484
|
+
startTime: item.cue.startTime,
|
|
485
|
+
endTime: item.cue.endTime
|
|
300
486
|
});
|
|
301
487
|
this.transcriptContent.appendChild(entry);
|
|
302
488
|
});
|
|
489
|
+
|
|
490
|
+
// Apply current styles to newly loaded entries
|
|
491
|
+
this.applyTranscriptStyles();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Setup metadata handling
|
|
496
|
+
* Metadata cues are not displayed but can be used programmatically
|
|
497
|
+
*/
|
|
498
|
+
setupMetadataHandling() {
|
|
499
|
+
if (!this.metadataCues || this.metadataCues.length === 0) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Listen for cuechange events on the metadata track to trigger custom actions
|
|
504
|
+
const textTracks = Array.from(this.player.element.textTracks);
|
|
505
|
+
const metadataTrack = textTracks.find(track => track.kind === 'metadata');
|
|
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
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Handle individual metadata cues
|
|
519
|
+
* Parses metadata text and emits events or triggers actions
|
|
520
|
+
*/
|
|
521
|
+
handleMetadataCue(cue) {
|
|
522
|
+
const text = cue.text.trim();
|
|
523
|
+
|
|
524
|
+
// Emit a generic metadata event that developers can listen to
|
|
525
|
+
this.player.emit('metadata', {
|
|
526
|
+
time: cue.startTime,
|
|
527
|
+
endTime: cue.endTime,
|
|
528
|
+
text: text,
|
|
529
|
+
cue: cue
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Parse for specific commands (examples based on wwa_meta.vtt format)
|
|
533
|
+
if (text.includes('PAUSE')) {
|
|
534
|
+
// Emit pause suggestion event (don't auto-pause, let developer decide)
|
|
535
|
+
this.player.emit('metadata:pause', { time: cue.startTime, text: text });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Parse for focus directives
|
|
539
|
+
const focusMatch = text.match(/FOCUS:([\w#-]+)/);
|
|
540
|
+
if (focusMatch) {
|
|
541
|
+
this.player.emit('metadata:focus', {
|
|
542
|
+
time: cue.startTime,
|
|
543
|
+
target: focusMatch[1],
|
|
544
|
+
text: text
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Parse for hashtag references
|
|
549
|
+
const hashtags = text.match(/#[\w-]+/g);
|
|
550
|
+
if (hashtags) {
|
|
551
|
+
this.player.emit('metadata:hashtags', {
|
|
552
|
+
time: cue.startTime,
|
|
553
|
+
hashtags: hashtags,
|
|
554
|
+
text: text
|
|
555
|
+
});
|
|
556
|
+
}
|
|
303
557
|
}
|
|
304
558
|
|
|
305
559
|
/**
|
|
306
560
|
* Create a single transcript entry element
|
|
307
561
|
*/
|
|
308
|
-
createTranscriptEntry(cue, index) {
|
|
562
|
+
createTranscriptEntry(cue, index, type = 'caption') {
|
|
309
563
|
const entry = DOMUtils.createElement('div', {
|
|
310
|
-
className: `${this.player.options.classPrefix}-transcript-entry`,
|
|
564
|
+
className: `${this.player.options.classPrefix}-transcript-entry ${this.player.options.classPrefix}-transcript-${type}`,
|
|
311
565
|
attributes: {
|
|
312
566
|
'data-start': String(cue.startTime),
|
|
313
567
|
'data-end': String(cue.endTime),
|
|
568
|
+
'data-type': type,
|
|
314
569
|
'role': 'button',
|
|
315
570
|
'tabindex': '0'
|
|
316
571
|
}
|
|
@@ -442,6 +697,21 @@ export class TranscriptManager {
|
|
|
442
697
|
return;
|
|
443
698
|
}
|
|
444
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
|
+
|
|
445
715
|
this.startDragging(e.clientX, e.clientY);
|
|
446
716
|
e.preventDefault();
|
|
447
717
|
};
|
|
@@ -463,6 +733,21 @@ export class TranscriptManager {
|
|
|
463
733
|
return;
|
|
464
734
|
}
|
|
465
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
|
+
|
|
466
751
|
const isMobile = window.innerWidth < 640;
|
|
467
752
|
const isFullscreen = this.player.state.fullscreen;
|
|
468
753
|
const touch = e.touches[0];
|
|
@@ -499,65 +784,85 @@ export class TranscriptManager {
|
|
|
499
784
|
};
|
|
500
785
|
|
|
501
786
|
this.handlers.keydown = (e) => {
|
|
502
|
-
//
|
|
503
|
-
if (
|
|
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
|
|
795
|
+
e.preventDefault();
|
|
796
|
+
e.stopPropagation();
|
|
797
|
+
|
|
798
|
+
const step = e.shiftKey ? 50 : 10; // Larger steps with Shift key
|
|
799
|
+
|
|
800
|
+
// Get current position
|
|
801
|
+
let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
|
|
802
|
+
let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
|
|
803
|
+
|
|
804
|
+
// If window is still centered with transform, convert to absolute position first
|
|
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;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Set new position directly
|
|
835
|
+
this.transcriptWindow.style.left = `${newX}px`;
|
|
836
|
+
this.transcriptWindow.style.top = `${newY}px`;
|
|
504
837
|
return;
|
|
505
838
|
}
|
|
506
839
|
|
|
507
|
-
//
|
|
508
|
-
e.preventDefault();
|
|
509
|
-
e.stopPropagation();
|
|
510
|
-
|
|
511
|
-
// Handle special keys first
|
|
840
|
+
// Handle other special keys
|
|
512
841
|
if (e.key === 'Home') {
|
|
842
|
+
e.preventDefault();
|
|
843
|
+
e.stopPropagation();
|
|
513
844
|
this.resetPosition();
|
|
514
845
|
return;
|
|
515
846
|
}
|
|
516
847
|
|
|
517
848
|
if (e.key === 'Escape') {
|
|
518
|
-
|
|
849
|
+
e.preventDefault();
|
|
850
|
+
e.stopPropagation();
|
|
851
|
+
if (this.styleDialogVisible) {
|
|
852
|
+
// Close style dialog first
|
|
853
|
+
this.hideStyleDialog();
|
|
854
|
+
} else if (this.keyboardDragMode) {
|
|
855
|
+
// Exit drag mode
|
|
856
|
+
this.disableKeyboardDragMode();
|
|
857
|
+
} else if (this.settingsMenuVisible) {
|
|
858
|
+
// Close settings menu
|
|
859
|
+
this.hideSettingsMenu();
|
|
860
|
+
} else {
|
|
861
|
+
// Close transcript
|
|
862
|
+
this.hideTranscript();
|
|
863
|
+
}
|
|
519
864
|
return;
|
|
520
865
|
}
|
|
521
|
-
|
|
522
|
-
const step = e.shiftKey ? 50 : 10; // Larger steps with Shift key
|
|
523
|
-
|
|
524
|
-
// Get current position
|
|
525
|
-
let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
|
|
526
|
-
let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
|
|
527
|
-
|
|
528
|
-
// If window is still centered with transform, convert to absolute position first
|
|
529
|
-
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
530
|
-
if (computedStyle.transform !== 'none') {
|
|
531
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
532
|
-
currentLeft = rect.left;
|
|
533
|
-
currentTop = rect.top;
|
|
534
|
-
this.transcriptWindow.style.transform = 'none';
|
|
535
|
-
this.transcriptWindow.style.left = `${currentLeft}px`;
|
|
536
|
-
this.transcriptWindow.style.top = `${currentTop}px`;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Calculate new position based on arrow key
|
|
540
|
-
let newX = currentLeft;
|
|
541
|
-
let newY = currentTop;
|
|
542
|
-
|
|
543
|
-
switch(e.key) {
|
|
544
|
-
case 'ArrowLeft':
|
|
545
|
-
newX -= step;
|
|
546
|
-
break;
|
|
547
|
-
case 'ArrowRight':
|
|
548
|
-
newX += step;
|
|
549
|
-
break;
|
|
550
|
-
case 'ArrowUp':
|
|
551
|
-
newY -= step;
|
|
552
|
-
break;
|
|
553
|
-
case 'ArrowDown':
|
|
554
|
-
newY += step;
|
|
555
|
-
break;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Set new position directly
|
|
559
|
-
this.transcriptWindow.style.left = `${newX}px`;
|
|
560
|
-
this.transcriptWindow.style.top = `${newY}px`;
|
|
561
866
|
};
|
|
562
867
|
|
|
563
868
|
// Add event listeners using stored handlers
|
|
@@ -671,10 +976,760 @@ export class TranscriptManager {
|
|
|
671
976
|
this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
|
|
672
977
|
}
|
|
673
978
|
|
|
979
|
+
/**
|
|
980
|
+
* Toggle keyboard drag mode
|
|
981
|
+
*/
|
|
982
|
+
toggleKeyboardDragMode() {
|
|
983
|
+
if (this.keyboardDragMode) {
|
|
984
|
+
this.disableKeyboardDragMode();
|
|
985
|
+
} else {
|
|
986
|
+
this.enableKeyboardDragMode();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Enable keyboard drag mode
|
|
992
|
+
*/
|
|
993
|
+
enableKeyboardDragMode() {
|
|
994
|
+
this.keyboardDragMode = true;
|
|
995
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
|
|
996
|
+
|
|
997
|
+
// Update settings button aria label
|
|
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();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Toggle settings menu visibility
|
|
1044
|
+
*/
|
|
1045
|
+
toggleSettingsMenu() {
|
|
1046
|
+
if (this.settingsMenuVisible) {
|
|
1047
|
+
this.hideSettingsMenu();
|
|
1048
|
+
} else {
|
|
1049
|
+
this.showSettingsMenu();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Show settings menu
|
|
1055
|
+
*/
|
|
1056
|
+
showSettingsMenu() {
|
|
1057
|
+
// Set flag to prevent immediate closing
|
|
1058
|
+
this.settingsMenuJustOpened = true;
|
|
1059
|
+
setTimeout(() => {
|
|
1060
|
+
this.settingsMenuJustOpened = false;
|
|
1061
|
+
}, 350);
|
|
1062
|
+
|
|
1063
|
+
// Add document click handler on FIRST menu open (not at window creation)
|
|
1064
|
+
if (!this.documentClickHandlerAdded) {
|
|
1065
|
+
setTimeout(() => {
|
|
1066
|
+
document.addEventListener('click', this.handlers.documentClick);
|
|
1067
|
+
this.documentClickHandlerAdded = true;
|
|
1068
|
+
}, 300);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (this.settingsMenu) {
|
|
1072
|
+
this.settingsMenu.style.display = 'block';
|
|
1073
|
+
this.settingsMenuVisible = true;
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
// Create settings menu
|
|
1077
|
+
this.settingsMenu = DOMUtils.createElement('div', {
|
|
1078
|
+
className: `${this.player.options.classPrefix}-transcript-settings-menu`
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// Keyboard drag option
|
|
1082
|
+
const keyboardDragOption = DOMUtils.createElement('button', {
|
|
1083
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1084
|
+
attributes: {
|
|
1085
|
+
'type': 'button',
|
|
1086
|
+
'aria-label': i18n.t('transcript.keyboardDragMode')
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
const keyboardIcon = createIconElement('move');
|
|
1090
|
+
const keyboardText = DOMUtils.createElement('span', {
|
|
1091
|
+
textContent: i18n.t('transcript.keyboardDragMode')
|
|
1092
|
+
});
|
|
1093
|
+
keyboardDragOption.appendChild(keyboardIcon);
|
|
1094
|
+
keyboardDragOption.appendChild(keyboardText);
|
|
1095
|
+
keyboardDragOption.addEventListener('click', () => {
|
|
1096
|
+
this.toggleKeyboardDragMode();
|
|
1097
|
+
this.hideSettingsMenu();
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// Style option
|
|
1101
|
+
const styleOption = DOMUtils.createElement('button', {
|
|
1102
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1103
|
+
attributes: {
|
|
1104
|
+
'type': 'button',
|
|
1105
|
+
'aria-label': i18n.t('transcript.styleTranscript')
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
const styleIcon = createIconElement('settings');
|
|
1109
|
+
const styleText = DOMUtils.createElement('span', {
|
|
1110
|
+
textContent: i18n.t('transcript.styleTranscript')
|
|
1111
|
+
});
|
|
1112
|
+
styleOption.appendChild(styleIcon);
|
|
1113
|
+
styleOption.appendChild(styleText);
|
|
1114
|
+
styleOption.addEventListener('click', (e) => {
|
|
1115
|
+
e.preventDefault();
|
|
1116
|
+
e.stopPropagation();
|
|
1117
|
+
this.hideSettingsMenu();
|
|
1118
|
+
// Delay to ensure menu is fully closed before opening dialog
|
|
1119
|
+
setTimeout(() => {
|
|
1120
|
+
this.showStyleDialog();
|
|
1121
|
+
}, 50);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Resize option
|
|
1125
|
+
const resizeOption = DOMUtils.createElement('button', {
|
|
1126
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1127
|
+
attributes: {
|
|
1128
|
+
'type': 'button',
|
|
1129
|
+
'aria-label': i18n.t('transcript.resizeWindow')
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
const resizeIcon = createIconElement('resize');
|
|
1133
|
+
const resizeText = DOMUtils.createElement('span', {
|
|
1134
|
+
textContent: i18n.t('transcript.resizeWindow')
|
|
1135
|
+
});
|
|
1136
|
+
resizeOption.appendChild(resizeIcon);
|
|
1137
|
+
resizeOption.appendChild(resizeText);
|
|
1138
|
+
resizeOption.addEventListener('click', () => {
|
|
1139
|
+
this.toggleResizeMode();
|
|
1140
|
+
this.hideSettingsMenu();
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// Close option
|
|
1144
|
+
const closeOption = DOMUtils.createElement('button', {
|
|
1145
|
+
className: `${this.player.options.classPrefix}-transcript-settings-item`,
|
|
1146
|
+
attributes: {
|
|
1147
|
+
'type': 'button',
|
|
1148
|
+
'aria-label': i18n.t('transcript.closeMenu')
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
const closeIcon = createIconElement('close');
|
|
1152
|
+
const closeText = DOMUtils.createElement('span', {
|
|
1153
|
+
textContent: i18n.t('transcript.closeMenu')
|
|
1154
|
+
});
|
|
1155
|
+
closeOption.appendChild(closeIcon);
|
|
1156
|
+
closeOption.appendChild(closeText);
|
|
1157
|
+
closeOption.addEventListener('click', () => {
|
|
1158
|
+
this.hideSettingsMenu();
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
this.settingsMenu.appendChild(keyboardDragOption);
|
|
1162
|
+
this.settingsMenu.appendChild(resizeOption);
|
|
1163
|
+
this.settingsMenu.appendChild(styleOption);
|
|
1164
|
+
this.settingsMenu.appendChild(closeOption);
|
|
1165
|
+
|
|
1166
|
+
// Append menu to header left container for proper positioning
|
|
1167
|
+
if (this.headerLeft) {
|
|
1168
|
+
this.headerLeft.appendChild(this.settingsMenu);
|
|
1169
|
+
} else {
|
|
1170
|
+
this.transcriptHeader.appendChild(this.settingsMenu);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Set the menu as visible and display it
|
|
1174
|
+
this.settingsMenuVisible = true;
|
|
1175
|
+
this.settingsMenu.style.display = 'block';
|
|
1176
|
+
|
|
1177
|
+
// Update aria-expanded
|
|
1178
|
+
if (this.settingsButton) {
|
|
1179
|
+
this.settingsButton.setAttribute('aria-expanded', 'true');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Focus first menu item
|
|
1183
|
+
setTimeout(() => {
|
|
1184
|
+
const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
|
|
1185
|
+
if (firstItem) {
|
|
1186
|
+
firstItem.focus();
|
|
1187
|
+
}
|
|
1188
|
+
}, 0);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Hide settings menu
|
|
1193
|
+
*/
|
|
1194
|
+
hideSettingsMenu() {
|
|
1195
|
+
if (this.settingsMenu) {
|
|
1196
|
+
this.settingsMenu.style.display = 'none';
|
|
1197
|
+
this.settingsMenuVisible = false;
|
|
1198
|
+
this.settingsMenuJustOpened = false;
|
|
1199
|
+
|
|
1200
|
+
// Update aria-expanded
|
|
1201
|
+
if (this.settingsButton) {
|
|
1202
|
+
this.settingsButton.setAttribute('aria-expanded', 'false');
|
|
1203
|
+
// Return focus to settings button
|
|
1204
|
+
this.settingsButton.focus();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Enable move mode (gives visual feedback)
|
|
1211
|
+
*/
|
|
1212
|
+
enableMoveMode() {
|
|
1213
|
+
// Add visual feedback for move mode
|
|
1214
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
|
|
1215
|
+
|
|
1216
|
+
// Show tooltip about keyboard drag option
|
|
1217
|
+
const tooltip = DOMUtils.createElement('div', {
|
|
1218
|
+
className: `${this.player.options.classPrefix}-transcript-move-tooltip`,
|
|
1219
|
+
textContent: 'Drag with mouse or press D for keyboard drag mode'
|
|
1220
|
+
});
|
|
1221
|
+
this.transcriptHeader.appendChild(tooltip);
|
|
1222
|
+
|
|
1223
|
+
// Remove after 2 seconds
|
|
1224
|
+
setTimeout(() => {
|
|
1225
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-move-mode`);
|
|
1226
|
+
if (tooltip.parentNode) {
|
|
1227
|
+
tooltip.remove();
|
|
1228
|
+
}
|
|
1229
|
+
}, 2000);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Toggle resize mode
|
|
1234
|
+
*/
|
|
1235
|
+
toggleResizeMode() {
|
|
1236
|
+
this.resizeEnabled = !this.resizeEnabled;
|
|
1237
|
+
|
|
1238
|
+
if (this.resizeEnabled) {
|
|
1239
|
+
this.enableResizeHandles();
|
|
1240
|
+
} else {
|
|
1241
|
+
this.disableResizeHandles();
|
|
1242
|
+
}
|
|
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
|
+
|
|
1265
|
+
this.transcriptWindow.appendChild(handle);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizable`);
|
|
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
|
+
};
|
|
1289
|
+
|
|
1290
|
+
document.addEventListener('mousemove', this.handlers.resizeMove);
|
|
1291
|
+
document.addEventListener('mouseup', this.handlers.resizeEnd);
|
|
1292
|
+
document.addEventListener('touchmove', this.handlers.resizeTouchMove);
|
|
1293
|
+
document.addEventListener('touchend', this.handlers.resizeEnd);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Disable resize handles
|
|
1298
|
+
*/
|
|
1299
|
+
disableResizeHandles() {
|
|
1300
|
+
if (!this.transcriptWindow) return;
|
|
1301
|
+
|
|
1302
|
+
// Remove all resize handles
|
|
1303
|
+
const handles = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-resize-handle`);
|
|
1304
|
+
handles.forEach(handle => handle.remove());
|
|
1305
|
+
|
|
1306
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizable`);
|
|
1307
|
+
|
|
1308
|
+
// Remove resize event handlers
|
|
1309
|
+
if (this.handlers.resizeMove) {
|
|
1310
|
+
document.removeEventListener('mousemove', this.handlers.resizeMove);
|
|
1311
|
+
}
|
|
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
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Start resizing
|
|
1323
|
+
*/
|
|
1324
|
+
startResize(e, direction) {
|
|
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
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Perform resize
|
|
1344
|
+
*/
|
|
1345
|
+
performResize(clientX, clientY) {
|
|
1346
|
+
if (!this.isResizing) return;
|
|
1347
|
+
|
|
1348
|
+
const deltaX = clientX - this.resizeStartX;
|
|
1349
|
+
const deltaY = clientY - this.resizeStartY;
|
|
1350
|
+
|
|
1351
|
+
let newWidth = this.resizeStartWidth;
|
|
1352
|
+
let newHeight = this.resizeStartHeight;
|
|
1353
|
+
|
|
1354
|
+
const direction = this.resizeDirection;
|
|
1355
|
+
|
|
1356
|
+
// Calculate new dimensions based on direction
|
|
1357
|
+
if (direction.includes('e')) {
|
|
1358
|
+
newWidth = this.resizeStartWidth + deltaX;
|
|
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;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Apply minimum and maximum constraints
|
|
1371
|
+
const minWidth = 300;
|
|
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`;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
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 = '';
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Get cursor style for resize direction
|
|
1409
|
+
*/
|
|
1410
|
+
getResizeCursor(direction) {
|
|
1411
|
+
const cursors = {
|
|
1412
|
+
'n': 'ns-resize',
|
|
1413
|
+
's': 'ns-resize',
|
|
1414
|
+
'e': 'ew-resize',
|
|
1415
|
+
'w': 'ew-resize',
|
|
1416
|
+
'ne': 'nesw-resize',
|
|
1417
|
+
'nw': 'nwse-resize',
|
|
1418
|
+
'se': 'nwse-resize',
|
|
1419
|
+
'sw': 'nesw-resize'
|
|
1420
|
+
};
|
|
1421
|
+
return cursors[direction] || 'default';
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Show style dialog
|
|
1426
|
+
*/
|
|
1427
|
+
showStyleDialog() {
|
|
1428
|
+
// If dialog already exists, just show it
|
|
1429
|
+
if (this.styleDialog) {
|
|
1430
|
+
this.styleDialog.style.display = 'block';
|
|
1431
|
+
this.styleDialogVisible = true;
|
|
1432
|
+
|
|
1433
|
+
// Set flag to prevent immediate closing from document click
|
|
1434
|
+
this.styleDialogJustOpened = true;
|
|
1435
|
+
setTimeout(() => {
|
|
1436
|
+
this.styleDialogJustOpened = false;
|
|
1437
|
+
}, 350);
|
|
1438
|
+
|
|
1439
|
+
// Focus first control
|
|
1440
|
+
setTimeout(() => {
|
|
1441
|
+
const firstSelect = this.styleDialog.querySelector('select, input');
|
|
1442
|
+
if (firstSelect) {
|
|
1443
|
+
firstSelect.focus();
|
|
1444
|
+
}
|
|
1445
|
+
}, 0);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Create style dialog
|
|
1450
|
+
this.styleDialog = DOMUtils.createElement('div', {
|
|
1451
|
+
className: `${this.player.options.classPrefix}-transcript-style-dialog`
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// Dialog title
|
|
1455
|
+
const title = DOMUtils.createElement('h4', {
|
|
1456
|
+
textContent: i18n.t('transcript.styleTitle'),
|
|
1457
|
+
className: `${this.player.options.classPrefix}-transcript-style-title`
|
|
1458
|
+
});
|
|
1459
|
+
this.styleDialog.appendChild(title);
|
|
1460
|
+
|
|
1461
|
+
// Font Size
|
|
1462
|
+
const fontSizeControl = this.createStyleSelectControl(
|
|
1463
|
+
i18n.t('captions.fontSize'),
|
|
1464
|
+
'fontSize',
|
|
1465
|
+
[
|
|
1466
|
+
{ label: i18n.t('fontSizes.small'), value: '87.5%' },
|
|
1467
|
+
{ label: i18n.t('fontSizes.normal'), value: '100%' },
|
|
1468
|
+
{ label: i18n.t('fontSizes.large'), value: '125%' },
|
|
1469
|
+
{ label: i18n.t('fontSizes.xlarge'), value: '150%' }
|
|
1470
|
+
]
|
|
1471
|
+
);
|
|
1472
|
+
this.styleDialog.appendChild(fontSizeControl);
|
|
1473
|
+
|
|
1474
|
+
// Font Family
|
|
1475
|
+
const fontFamilyControl = this.createStyleSelectControl(
|
|
1476
|
+
i18n.t('captions.fontFamily'),
|
|
1477
|
+
'fontFamily',
|
|
1478
|
+
[
|
|
1479
|
+
{ label: i18n.t('fontFamilies.sansSerif'), value: 'sans-serif' },
|
|
1480
|
+
{ label: i18n.t('fontFamilies.serif'), value: 'serif' },
|
|
1481
|
+
{ label: i18n.t('fontFamilies.monospace'), value: 'monospace' }
|
|
1482
|
+
]
|
|
1483
|
+
);
|
|
1484
|
+
this.styleDialog.appendChild(fontFamilyControl);
|
|
1485
|
+
|
|
1486
|
+
// Text Color
|
|
1487
|
+
const colorControl = this.createStyleColorControl(i18n.t('captions.color'), 'color');
|
|
1488
|
+
this.styleDialog.appendChild(colorControl);
|
|
1489
|
+
|
|
1490
|
+
// Background Color
|
|
1491
|
+
const bgColorControl = this.createStyleColorControl(i18n.t('captions.backgroundColor'), 'backgroundColor');
|
|
1492
|
+
this.styleDialog.appendChild(bgColorControl);
|
|
1493
|
+
|
|
1494
|
+
// Opacity
|
|
1495
|
+
const opacityControl = this.createStyleOpacityControl(i18n.t('captions.opacity'), 'opacity');
|
|
1496
|
+
this.styleDialog.appendChild(opacityControl);
|
|
1497
|
+
|
|
1498
|
+
// Close button
|
|
1499
|
+
const closeBtn = DOMUtils.createElement('button', {
|
|
1500
|
+
className: `${this.player.options.classPrefix}-transcript-style-close`,
|
|
1501
|
+
textContent: i18n.t('settings.close'),
|
|
1502
|
+
attributes: {
|
|
1503
|
+
'type': 'button'
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
closeBtn.addEventListener('click', () => this.hideStyleDialog());
|
|
1507
|
+
this.styleDialog.appendChild(closeBtn);
|
|
1508
|
+
|
|
1509
|
+
// ESC key handler for style dialog
|
|
1510
|
+
this.handlers.styleDialogKeydown = (e) => {
|
|
1511
|
+
if (e.key === 'Escape') {
|
|
1512
|
+
e.preventDefault();
|
|
1513
|
+
e.stopPropagation();
|
|
1514
|
+
this.hideStyleDialog();
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
this.styleDialog.addEventListener('keydown', this.handlers.styleDialogKeydown);
|
|
1518
|
+
|
|
1519
|
+
// Append to header left container (same as settings menu) for correct positioning
|
|
1520
|
+
if (this.headerLeft) {
|
|
1521
|
+
this.headerLeft.appendChild(this.styleDialog);
|
|
1522
|
+
} else {
|
|
1523
|
+
this.transcriptHeader.appendChild(this.styleDialog);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Apply current styles
|
|
1527
|
+
this.applyTranscriptStyles();
|
|
1528
|
+
|
|
1529
|
+
// Important: Set visible state and display before focusing
|
|
1530
|
+
this.styleDialogVisible = true;
|
|
1531
|
+
this.styleDialog.style.display = 'block';
|
|
1532
|
+
|
|
1533
|
+
// Set flag to prevent immediate closing from document click
|
|
1534
|
+
this.styleDialogJustOpened = true;
|
|
1535
|
+
setTimeout(() => {
|
|
1536
|
+
this.styleDialogJustOpened = false;
|
|
1537
|
+
}, 350);
|
|
1538
|
+
|
|
1539
|
+
// Focus first control for keyboard accessibility
|
|
1540
|
+
setTimeout(() => {
|
|
1541
|
+
const firstSelect = this.styleDialog.querySelector('select, input');
|
|
1542
|
+
if (firstSelect) {
|
|
1543
|
+
firstSelect.focus();
|
|
1544
|
+
}
|
|
1545
|
+
}, 0);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Hide style dialog
|
|
1550
|
+
*/
|
|
1551
|
+
hideStyleDialog() {
|
|
1552
|
+
if (this.styleDialog) {
|
|
1553
|
+
this.styleDialog.style.display = 'none';
|
|
1554
|
+
this.styleDialogVisible = false;
|
|
1555
|
+
|
|
1556
|
+
// Return focus to settings button
|
|
1557
|
+
if (this.settingsButton) {
|
|
1558
|
+
this.settingsButton.focus();
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Create style select control
|
|
1565
|
+
*/
|
|
1566
|
+
createStyleSelectControl(label, property, options) {
|
|
1567
|
+
const group = DOMUtils.createElement('div', {
|
|
1568
|
+
className: `${this.player.options.classPrefix}-transcript-style-group`
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const labelEl = DOMUtils.createElement('label', {
|
|
1572
|
+
textContent: label
|
|
1573
|
+
});
|
|
1574
|
+
group.appendChild(labelEl);
|
|
1575
|
+
|
|
1576
|
+
const select = DOMUtils.createElement('select', {
|
|
1577
|
+
className: `${this.player.options.classPrefix}-transcript-style-select`
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
options.forEach(opt => {
|
|
1581
|
+
const option = DOMUtils.createElement('option', {
|
|
1582
|
+
textContent: opt.label,
|
|
1583
|
+
attributes: {
|
|
1584
|
+
'value': opt.value
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
if (this.transcriptStyle[property] === opt.value) {
|
|
1588
|
+
option.selected = true;
|
|
1589
|
+
}
|
|
1590
|
+
select.appendChild(option);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
select.addEventListener('change', (e) => {
|
|
1594
|
+
this.transcriptStyle[property] = e.target.value;
|
|
1595
|
+
this.applyTranscriptStyles();
|
|
1596
|
+
this.savePreferences();
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
group.appendChild(select);
|
|
1600
|
+
return group;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Create style color control
|
|
1605
|
+
*/
|
|
1606
|
+
createStyleColorControl(label, property) {
|
|
1607
|
+
const group = DOMUtils.createElement('div', {
|
|
1608
|
+
className: `${this.player.options.classPrefix}-transcript-style-group`
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
const labelEl = DOMUtils.createElement('label', {
|
|
1612
|
+
textContent: label
|
|
1613
|
+
});
|
|
1614
|
+
group.appendChild(labelEl);
|
|
1615
|
+
|
|
1616
|
+
const input = DOMUtils.createElement('input', {
|
|
1617
|
+
attributes: {
|
|
1618
|
+
'type': 'color',
|
|
1619
|
+
'value': this.transcriptStyle[property]
|
|
1620
|
+
},
|
|
1621
|
+
className: `${this.player.options.classPrefix}-transcript-style-color`
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
input.addEventListener('input', (e) => {
|
|
1625
|
+
this.transcriptStyle[property] = e.target.value;
|
|
1626
|
+
this.applyTranscriptStyles();
|
|
1627
|
+
this.savePreferences();
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
group.appendChild(input);
|
|
1631
|
+
return group;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Create style opacity control
|
|
1636
|
+
*/
|
|
1637
|
+
createStyleOpacityControl(label, property) {
|
|
1638
|
+
const group = DOMUtils.createElement('div', {
|
|
1639
|
+
className: `${this.player.options.classPrefix}-transcript-style-group`
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
const labelEl = DOMUtils.createElement('label', {
|
|
1643
|
+
textContent: label
|
|
1644
|
+
});
|
|
1645
|
+
group.appendChild(labelEl);
|
|
1646
|
+
|
|
1647
|
+
const valueDisplay = DOMUtils.createElement('span', {
|
|
1648
|
+
textContent: Math.round(this.transcriptStyle[property] * 100) + '%',
|
|
1649
|
+
className: `${this.player.options.classPrefix}-transcript-style-value`
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
const input = DOMUtils.createElement('input', {
|
|
1653
|
+
attributes: {
|
|
1654
|
+
'type': 'range',
|
|
1655
|
+
'min': '0',
|
|
1656
|
+
'max': '1',
|
|
1657
|
+
'step': '0.1',
|
|
1658
|
+
'value': String(this.transcriptStyle[property])
|
|
1659
|
+
},
|
|
1660
|
+
className: `${this.player.options.classPrefix}-transcript-style-range`
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
input.addEventListener('input', (e) => {
|
|
1664
|
+
const value = parseFloat(e.target.value);
|
|
1665
|
+
this.transcriptStyle[property] = value;
|
|
1666
|
+
valueDisplay.textContent = Math.round(value * 100) + '%';
|
|
1667
|
+
this.applyTranscriptStyles();
|
|
1668
|
+
this.savePreferences();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
const inputContainer = DOMUtils.createElement('div', {
|
|
1672
|
+
className: `${this.player.options.classPrefix}-transcript-style-range-container`
|
|
1673
|
+
});
|
|
1674
|
+
inputContainer.appendChild(input);
|
|
1675
|
+
inputContainer.appendChild(valueDisplay);
|
|
1676
|
+
|
|
1677
|
+
group.appendChild(labelEl);
|
|
1678
|
+
group.appendChild(inputContainer);
|
|
1679
|
+
return group;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Save transcript preferences to localStorage
|
|
1684
|
+
*/
|
|
1685
|
+
savePreferences() {
|
|
1686
|
+
this.storage.saveTranscriptPreferences(this.transcriptStyle);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Apply transcript styles
|
|
1691
|
+
*/
|
|
1692
|
+
applyTranscriptStyles() {
|
|
1693
|
+
if (!this.transcriptWindow) return;
|
|
1694
|
+
|
|
1695
|
+
// Apply to transcript window background
|
|
1696
|
+
this.transcriptWindow.style.backgroundColor = this.transcriptStyle.backgroundColor;
|
|
1697
|
+
this.transcriptWindow.style.opacity = String(this.transcriptStyle.opacity);
|
|
1698
|
+
|
|
1699
|
+
// Apply to content area
|
|
1700
|
+
if (this.transcriptContent) {
|
|
1701
|
+
this.transcriptContent.style.fontSize = this.transcriptStyle.fontSize;
|
|
1702
|
+
this.transcriptContent.style.fontFamily = this.transcriptStyle.fontFamily;
|
|
1703
|
+
this.transcriptContent.style.color = this.transcriptStyle.color;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Apply to all text entries (important: override CSS defaults)
|
|
1707
|
+
const textEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-text`);
|
|
1708
|
+
textEntries.forEach(entry => {
|
|
1709
|
+
entry.style.fontSize = this.transcriptStyle.fontSize;
|
|
1710
|
+
entry.style.fontFamily = this.transcriptStyle.fontFamily;
|
|
1711
|
+
entry.style.color = this.transcriptStyle.color;
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
// Apply to timestamp entries as well
|
|
1715
|
+
const timeEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-time`);
|
|
1716
|
+
timeEntries.forEach(entry => {
|
|
1717
|
+
entry.style.fontFamily = this.transcriptStyle.fontFamily;
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
674
1721
|
/**
|
|
675
1722
|
* Cleanup
|
|
676
1723
|
*/
|
|
677
1724
|
destroy() {
|
|
1725
|
+
// Disable modes if active
|
|
1726
|
+
if (this.resizeEnabled) {
|
|
1727
|
+
this.disableResizeHandles();
|
|
1728
|
+
}
|
|
1729
|
+
if (this.keyboardDragMode) {
|
|
1730
|
+
this.disableKeyboardDragMode();
|
|
1731
|
+
}
|
|
1732
|
+
|
|
678
1733
|
// Remove timeupdate listener from player
|
|
679
1734
|
if (this.handlers.timeupdate) {
|
|
680
1735
|
this.player.off('timeupdate', this.handlers.timeupdate);
|
|
@@ -692,6 +1747,21 @@ export class TranscriptManager {
|
|
|
692
1747
|
this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
|
|
693
1748
|
}
|
|
694
1749
|
}
|
|
1750
|
+
|
|
1751
|
+
// Remove settings button event listeners
|
|
1752
|
+
if (this.settingsButton) {
|
|
1753
|
+
if (this.handlers.settingsClick) {
|
|
1754
|
+
this.settingsButton.removeEventListener('click', this.handlers.settingsClick);
|
|
1755
|
+
}
|
|
1756
|
+
if (this.handlers.settingsKeydown) {
|
|
1757
|
+
this.settingsButton.removeEventListener('keydown', this.handlers.settingsKeydown);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Remove style dialog event listeners
|
|
1762
|
+
if (this.styleDialog && this.handlers.styleDialogKeydown) {
|
|
1763
|
+
this.styleDialog.removeEventListener('keydown', this.handlers.styleDialogKeydown);
|
|
1764
|
+
}
|
|
695
1765
|
|
|
696
1766
|
// Remove document-level listeners
|
|
697
1767
|
if (this.handlers.mousemove) {
|
|
@@ -706,6 +1776,9 @@ export class TranscriptManager {
|
|
|
706
1776
|
if (this.handlers.touchend) {
|
|
707
1777
|
document.removeEventListener('touchend', this.handlers.touchend);
|
|
708
1778
|
}
|
|
1779
|
+
if (this.handlers.documentClick) {
|
|
1780
|
+
document.removeEventListener('click', this.handlers.documentClick);
|
|
1781
|
+
}
|
|
709
1782
|
|
|
710
1783
|
// Remove window-level listeners
|
|
711
1784
|
if (this.handlers.resize) {
|
|
@@ -724,5 +1797,7 @@ export class TranscriptManager {
|
|
|
724
1797
|
this.transcriptHeader = null;
|
|
725
1798
|
this.transcriptContent = null;
|
|
726
1799
|
this.transcriptEntries = [];
|
|
1800
|
+
this.settingsMenu = null;
|
|
1801
|
+
this.styleDialog = null;
|
|
727
1802
|
}
|
|
728
1803
|
}
|