vidply 1.0.4 → 1.0.6
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/LICENSE +22 -22
- package/README.md +593 -517
- package/dist/vidply.css +1807 -1807
- package/dist/vidply.esm.js +268 -182
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +6 -6
- package/dist/vidply.esm.min.meta.json +40 -34
- package/dist/vidply.js +268 -182
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.js +6 -6
- package/dist/vidply.min.meta.json +40 -34
- package/package.json +57 -57
- package/src/controls/CaptionManager.js +248 -248
- package/src/controls/ControlBar.js +4 -4
- package/src/controls/KeyboardManager.js +233 -233
- package/src/controls/SettingsDialog.js +417 -417
- package/src/controls/TranscriptManager.js +728 -728
- package/src/core/Player.js +1186 -1134
- package/src/i18n/i18n.js +66 -66
- package/src/i18n/translations.js +561 -511
- package/src/icons/Icons.js +183 -183
- package/src/index.js +95 -95
- package/src/renderers/HLSRenderer.js +302 -302
- package/src/renderers/HTML5Renderer.js +298 -298
- package/src/renderers/VimeoRenderer.js +257 -257
- package/src/renderers/YouTubeRenderer.js +274 -274
- package/src/styles/vidply.css +1807 -1807
- package/src/utils/DOMUtils.js +154 -154
- package/src/utils/EventEmitter.js +53 -53
- package/src/utils/TimeUtils.js +87 -82
|
@@ -1,728 +1,728 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transcript Manager Component
|
|
3
|
-
* Manages transcript display and interaction
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
7
|
-
import { TimeUtils } from '../utils/TimeUtils.js';
|
|
8
|
-
import { createIconElement } from '../icons/Icons.js';
|
|
9
|
-
import { i18n } from '../i18n/i18n.js';
|
|
10
|
-
|
|
11
|
-
export class TranscriptManager {
|
|
12
|
-
constructor(player) {
|
|
13
|
-
this.player = player;
|
|
14
|
-
this.transcriptWindow = null;
|
|
15
|
-
this.transcriptEntries = [];
|
|
16
|
-
this.currentActiveEntry = null;
|
|
17
|
-
this.isVisible = false;
|
|
18
|
-
|
|
19
|
-
// Dragging state
|
|
20
|
-
this.isDragging = false;
|
|
21
|
-
this.dragOffsetX = 0;
|
|
22
|
-
this.dragOffsetY = 0;
|
|
23
|
-
|
|
24
|
-
// Store event handlers for cleanup
|
|
25
|
-
this.handlers = {
|
|
26
|
-
timeupdate: () => this.updateActiveEntry(),
|
|
27
|
-
resize: null,
|
|
28
|
-
mousemove: null,
|
|
29
|
-
mouseup: null,
|
|
30
|
-
touchmove: null,
|
|
31
|
-
touchend: null,
|
|
32
|
-
mousedown: null,
|
|
33
|
-
touchstart: null,
|
|
34
|
-
keydown: null
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
this.init();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
init() {
|
|
41
|
-
// Listen for time updates to highlight active transcript entry
|
|
42
|
-
this.player.on('timeupdate', this.handlers.timeupdate);
|
|
43
|
-
|
|
44
|
-
// Reposition transcript when entering/exiting fullscreen
|
|
45
|
-
this.player.on('fullscreenchange', () => {
|
|
46
|
-
if (this.isVisible) {
|
|
47
|
-
// Add a small delay to ensure DOM has updated after fullscreen transition
|
|
48
|
-
setTimeout(() => this.positionTranscript(), 100);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Toggle transcript window visibility
|
|
55
|
-
*/
|
|
56
|
-
toggleTranscript() {
|
|
57
|
-
if (this.isVisible) {
|
|
58
|
-
this.hideTranscript();
|
|
59
|
-
} else {
|
|
60
|
-
this.showTranscript();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Show transcript window
|
|
66
|
-
*/
|
|
67
|
-
showTranscript() {
|
|
68
|
-
if (this.transcriptWindow) {
|
|
69
|
-
this.transcriptWindow.style.display = 'flex';
|
|
70
|
-
this.isVisible = true;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Create transcript window
|
|
75
|
-
this.createTranscriptWindow();
|
|
76
|
-
this.loadTranscriptData();
|
|
77
|
-
|
|
78
|
-
// Show the window
|
|
79
|
-
if (this.transcriptWindow) {
|
|
80
|
-
this.transcriptWindow.style.display = 'flex';
|
|
81
|
-
// Re-position after showing (in case window was resized while hidden)
|
|
82
|
-
setTimeout(() => this.positionTranscript(), 0);
|
|
83
|
-
}
|
|
84
|
-
this.isVisible = true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Hide transcript window
|
|
89
|
-
*/
|
|
90
|
-
hideTranscript() {
|
|
91
|
-
if (this.transcriptWindow) {
|
|
92
|
-
this.transcriptWindow.style.display = 'none';
|
|
93
|
-
this.isVisible = false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Create the transcript window UI
|
|
99
|
-
*/
|
|
100
|
-
createTranscriptWindow() {
|
|
101
|
-
this.transcriptWindow = DOMUtils.createElement('div', {
|
|
102
|
-
className: `${this.player.options.classPrefix}-transcript-window`,
|
|
103
|
-
attributes: {
|
|
104
|
-
'role': 'dialog',
|
|
105
|
-
'aria-label': 'Video Transcript',
|
|
106
|
-
'tabindex': '-1'
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Header (draggable)
|
|
111
|
-
this.transcriptHeader = DOMUtils.createElement('div', {
|
|
112
|
-
className: `${this.player.options.classPrefix}-transcript-header`,
|
|
113
|
-
attributes: {
|
|
114
|
-
'aria-label': 'Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.',
|
|
115
|
-
'tabindex': '0'
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const title = DOMUtils.createElement('h3', {
|
|
120
|
-
textContent: i18n.t('transcript.title')
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const closeButton = DOMUtils.createElement('button', {
|
|
124
|
-
className: `${this.player.options.classPrefix}-transcript-close`,
|
|
125
|
-
attributes: {
|
|
126
|
-
'type': 'button',
|
|
127
|
-
'aria-label': i18n.t('transcript.close')
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
closeButton.appendChild(createIconElement('close'));
|
|
131
|
-
closeButton.addEventListener('click', () => this.hideTranscript());
|
|
132
|
-
|
|
133
|
-
this.transcriptHeader.appendChild(title);
|
|
134
|
-
this.transcriptHeader.appendChild(closeButton);
|
|
135
|
-
|
|
136
|
-
// Content container
|
|
137
|
-
this.transcriptContent = DOMUtils.createElement('div', {
|
|
138
|
-
className: `${this.player.options.classPrefix}-transcript-content`
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
this.transcriptWindow.appendChild(this.transcriptHeader);
|
|
142
|
-
this.transcriptWindow.appendChild(this.transcriptContent);
|
|
143
|
-
|
|
144
|
-
// Append to player container
|
|
145
|
-
this.player.container.appendChild(this.transcriptWindow);
|
|
146
|
-
|
|
147
|
-
// Position it next to the video wrapper
|
|
148
|
-
this.positionTranscript();
|
|
149
|
-
|
|
150
|
-
// Setup drag functionality
|
|
151
|
-
this.setupDragAndDrop();
|
|
152
|
-
|
|
153
|
-
// Re-position on window resize (debounced)
|
|
154
|
-
let resizeTimeout;
|
|
155
|
-
this.handlers.resize = () => {
|
|
156
|
-
clearTimeout(resizeTimeout);
|
|
157
|
-
resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
|
|
158
|
-
};
|
|
159
|
-
window.addEventListener('resize', this.handlers.resize);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Position transcript window next to video
|
|
164
|
-
*/
|
|
165
|
-
positionTranscript() {
|
|
166
|
-
if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
|
|
167
|
-
|
|
168
|
-
const isMobile = window.innerWidth < 640;
|
|
169
|
-
const videoRect = this.player.videoWrapper.getBoundingClientRect();
|
|
170
|
-
|
|
171
|
-
// Check if player is in fullscreen mode
|
|
172
|
-
const isFullscreen = this.player.state.fullscreen;
|
|
173
|
-
|
|
174
|
-
if (isMobile && !isFullscreen) {
|
|
175
|
-
// Mobile: Position underneath the video and controls as part of the layout
|
|
176
|
-
this.transcriptWindow.style.position = 'relative';
|
|
177
|
-
this.transcriptWindow.style.left = '0';
|
|
178
|
-
this.transcriptWindow.style.right = '0';
|
|
179
|
-
this.transcriptWindow.style.bottom = 'auto';
|
|
180
|
-
this.transcriptWindow.style.top = 'auto';
|
|
181
|
-
this.transcriptWindow.style.width = '100%';
|
|
182
|
-
this.transcriptWindow.style.maxWidth = '100%';
|
|
183
|
-
this.transcriptWindow.style.maxHeight = '400px';
|
|
184
|
-
this.transcriptWindow.style.height = 'auto';
|
|
185
|
-
this.transcriptWindow.style.borderRadius = '0';
|
|
186
|
-
this.transcriptWindow.style.transform = 'none';
|
|
187
|
-
this.transcriptWindow.style.border = 'none';
|
|
188
|
-
this.transcriptWindow.style.borderTop = '1px solid var(--vidply-border-light)';
|
|
189
|
-
this.transcriptWindow.style.boxShadow = 'none';
|
|
190
|
-
// Disable dragging on mobile
|
|
191
|
-
if (this.transcriptHeader) {
|
|
192
|
-
this.transcriptHeader.style.cursor = 'default';
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Ensure transcript is at the container level for proper stacking
|
|
196
|
-
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
197
|
-
this.player.container.appendChild(this.transcriptWindow);
|
|
198
|
-
}
|
|
199
|
-
} else if (isFullscreen) {
|
|
200
|
-
// In fullscreen: position in bottom right corner inside the video
|
|
201
|
-
this.transcriptWindow.style.position = 'fixed';
|
|
202
|
-
this.transcriptWindow.style.left = 'auto';
|
|
203
|
-
this.transcriptWindow.style.right = '20px';
|
|
204
|
-
this.transcriptWindow.style.bottom = '80px'; // Above controls
|
|
205
|
-
this.transcriptWindow.style.top = 'auto';
|
|
206
|
-
this.transcriptWindow.style.maxHeight = 'calc(100vh - 180px)'; // Leave space for controls
|
|
207
|
-
this.transcriptWindow.style.height = 'auto';
|
|
208
|
-
this.transcriptWindow.style.width = '400px';
|
|
209
|
-
this.transcriptWindow.style.maxWidth = '400px';
|
|
210
|
-
this.transcriptWindow.style.borderRadius = '8px';
|
|
211
|
-
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
212
|
-
this.transcriptWindow.style.borderTop = '';
|
|
213
|
-
|
|
214
|
-
// Move back to container for fullscreen
|
|
215
|
-
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
216
|
-
this.player.container.appendChild(this.transcriptWindow);
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
// Desktop mode: position next to video
|
|
220
|
-
this.transcriptWindow.style.position = 'absolute';
|
|
221
|
-
this.transcriptWindow.style.left = `${videoRect.width + 8}px`;
|
|
222
|
-
this.transcriptWindow.style.right = 'auto';
|
|
223
|
-
this.transcriptWindow.style.bottom = 'auto';
|
|
224
|
-
this.transcriptWindow.style.top = '0';
|
|
225
|
-
this.transcriptWindow.style.height = `${videoRect.height}px`;
|
|
226
|
-
this.transcriptWindow.style.maxHeight = 'none';
|
|
227
|
-
this.transcriptWindow.style.width = '400px';
|
|
228
|
-
this.transcriptWindow.style.maxWidth = '400px';
|
|
229
|
-
this.transcriptWindow.style.borderRadius = '8px';
|
|
230
|
-
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
231
|
-
this.transcriptWindow.style.borderTop = '';
|
|
232
|
-
// Enable dragging on desktop
|
|
233
|
-
if (this.transcriptHeader) {
|
|
234
|
-
this.transcriptHeader.style.cursor = 'move';
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Move back to container for desktop
|
|
238
|
-
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
239
|
-
this.player.container.appendChild(this.transcriptWindow);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Load transcript data from caption/subtitle tracks
|
|
246
|
-
*/
|
|
247
|
-
loadTranscriptData() {
|
|
248
|
-
this.transcriptEntries = [];
|
|
249
|
-
this.transcriptContent.innerHTML = '';
|
|
250
|
-
|
|
251
|
-
// Get caption/subtitle tracks
|
|
252
|
-
const textTracks = Array.from(this.player.element.textTracks);
|
|
253
|
-
const transcriptTrack = textTracks.find(
|
|
254
|
-
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
if (!transcriptTrack) {
|
|
258
|
-
this.showNoTranscriptMessage();
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Enable track to load cues
|
|
263
|
-
if (transcriptTrack.mode === 'disabled') {
|
|
264
|
-
transcriptTrack.mode = 'hidden';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (!transcriptTrack.cues || transcriptTrack.cues.length === 0) {
|
|
268
|
-
// Wait for cues to load
|
|
269
|
-
const loadingMessage = DOMUtils.createElement('div', {
|
|
270
|
-
className: `${this.player.options.classPrefix}-transcript-loading`,
|
|
271
|
-
textContent: i18n.t('transcript.loading')
|
|
272
|
-
});
|
|
273
|
-
this.transcriptContent.appendChild(loadingMessage);
|
|
274
|
-
|
|
275
|
-
const onLoad = () => {
|
|
276
|
-
this.loadTranscriptData();
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
transcriptTrack.addEventListener('load', onLoad, { once: true });
|
|
280
|
-
|
|
281
|
-
// Fallback timeout
|
|
282
|
-
setTimeout(() => {
|
|
283
|
-
if (transcriptTrack.cues && transcriptTrack.cues.length > 0) {
|
|
284
|
-
this.loadTranscriptData();
|
|
285
|
-
}
|
|
286
|
-
}, 500);
|
|
287
|
-
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Build transcript from cues
|
|
292
|
-
const cues = Array.from(transcriptTrack.cues);
|
|
293
|
-
cues.forEach((cue, index) => {
|
|
294
|
-
const entry = this.createTranscriptEntry(cue, index);
|
|
295
|
-
this.transcriptEntries.push({
|
|
296
|
-
element: entry,
|
|
297
|
-
cue: cue,
|
|
298
|
-
startTime: cue.startTime,
|
|
299
|
-
endTime: cue.endTime
|
|
300
|
-
});
|
|
301
|
-
this.transcriptContent.appendChild(entry);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Create a single transcript entry element
|
|
307
|
-
*/
|
|
308
|
-
createTranscriptEntry(cue, index) {
|
|
309
|
-
const entry = DOMUtils.createElement('div', {
|
|
310
|
-
className: `${this.player.options.classPrefix}-transcript-entry`,
|
|
311
|
-
attributes: {
|
|
312
|
-
'data-start': String(cue.startTime),
|
|
313
|
-
'data-end': String(cue.endTime),
|
|
314
|
-
'role': 'button',
|
|
315
|
-
'tabindex': '0'
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const timestamp = DOMUtils.createElement('span', {
|
|
320
|
-
className: `${this.player.options.classPrefix}-transcript-time`,
|
|
321
|
-
textContent: TimeUtils.formatTime(cue.startTime)
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
const text = DOMUtils.createElement('span', {
|
|
325
|
-
className: `${this.player.options.classPrefix}-transcript-text`,
|
|
326
|
-
textContent: this.stripVTTFormatting(cue.text)
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
entry.appendChild(timestamp);
|
|
330
|
-
entry.appendChild(text);
|
|
331
|
-
|
|
332
|
-
// Click to seek
|
|
333
|
-
const seekToTime = () => {
|
|
334
|
-
this.player.seek(cue.startTime);
|
|
335
|
-
if (this.player.state.paused) {
|
|
336
|
-
this.player.play();
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
entry.addEventListener('click', seekToTime);
|
|
341
|
-
entry.addEventListener('keydown', (e) => {
|
|
342
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
343
|
-
e.preventDefault();
|
|
344
|
-
seekToTime();
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
return entry;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Strip VTT formatting tags from text
|
|
353
|
-
*/
|
|
354
|
-
stripVTTFormatting(text) {
|
|
355
|
-
// Remove VTT tags like <v Speaker>, <c>, etc.
|
|
356
|
-
return text
|
|
357
|
-
.replace(/<[^>]+>/g, '')
|
|
358
|
-
.replace(/\n/g, ' ')
|
|
359
|
-
.trim();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Show message when no transcript is available
|
|
364
|
-
*/
|
|
365
|
-
showNoTranscriptMessage() {
|
|
366
|
-
const message = DOMUtils.createElement('div', {
|
|
367
|
-
className: `${this.player.options.classPrefix}-transcript-empty`,
|
|
368
|
-
textContent: i18n.t('transcript.noTranscript')
|
|
369
|
-
});
|
|
370
|
-
this.transcriptContent.appendChild(message);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Update active transcript entry based on current time
|
|
375
|
-
*/
|
|
376
|
-
updateActiveEntry() {
|
|
377
|
-
if (!this.isVisible || this.transcriptEntries.length === 0) return;
|
|
378
|
-
|
|
379
|
-
const currentTime = this.player.state.currentTime;
|
|
380
|
-
|
|
381
|
-
// Find the entry that matches current time
|
|
382
|
-
const activeEntry = this.transcriptEntries.find(
|
|
383
|
-
entry => currentTime >= entry.startTime && currentTime < entry.endTime
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
if (activeEntry && activeEntry !== this.currentActiveEntry) {
|
|
387
|
-
// Remove previous active class
|
|
388
|
-
if (this.currentActiveEntry) {
|
|
389
|
-
this.currentActiveEntry.element.classList.remove(
|
|
390
|
-
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Add active class to current entry
|
|
395
|
-
activeEntry.element.classList.add(
|
|
396
|
-
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
// Scroll to active entry
|
|
400
|
-
this.scrollToEntry(activeEntry.element);
|
|
401
|
-
|
|
402
|
-
this.currentActiveEntry = activeEntry;
|
|
403
|
-
} else if (!activeEntry && this.currentActiveEntry) {
|
|
404
|
-
// No active entry, remove active class
|
|
405
|
-
this.currentActiveEntry.element.classList.remove(
|
|
406
|
-
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
407
|
-
);
|
|
408
|
-
this.currentActiveEntry = null;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Scroll transcript window to show active entry
|
|
414
|
-
*/
|
|
415
|
-
scrollToEntry(entryElement) {
|
|
416
|
-
if (!this.transcriptContent) return;
|
|
417
|
-
|
|
418
|
-
const contentRect = this.transcriptContent.getBoundingClientRect();
|
|
419
|
-
const entryRect = entryElement.getBoundingClientRect();
|
|
420
|
-
|
|
421
|
-
// Check if entry is out of view
|
|
422
|
-
if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
|
|
423
|
-
// Scroll to center the entry
|
|
424
|
-
const scrollTop = entryElement.offsetTop - (this.transcriptContent.clientHeight / 2) + (entryElement.clientHeight / 2);
|
|
425
|
-
this.transcriptContent.scrollTo({
|
|
426
|
-
top: scrollTop,
|
|
427
|
-
behavior: 'smooth'
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Setup drag and drop functionality
|
|
434
|
-
*/
|
|
435
|
-
setupDragAndDrop() {
|
|
436
|
-
if (!this.transcriptHeader || !this.transcriptWindow) return;
|
|
437
|
-
|
|
438
|
-
// Create and store handler functions
|
|
439
|
-
this.handlers.mousedown = (e) => {
|
|
440
|
-
// Don't drag if clicking on close button
|
|
441
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
this.startDragging(e.clientX, e.clientY);
|
|
446
|
-
e.preventDefault();
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
this.handlers.mousemove = (e) => {
|
|
450
|
-
if (this.isDragging) {
|
|
451
|
-
this.drag(e.clientX, e.clientY);
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
this.handlers.mouseup = () => {
|
|
456
|
-
if (this.isDragging) {
|
|
457
|
-
this.stopDragging();
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
this.handlers.touchstart = (e) => {
|
|
462
|
-
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const isMobile = window.innerWidth < 640;
|
|
467
|
-
const isFullscreen = this.player.state.fullscreen;
|
|
468
|
-
const touch = e.touches[0];
|
|
469
|
-
|
|
470
|
-
if (isMobile && !isFullscreen) {
|
|
471
|
-
// Mobile (not fullscreen): No dragging/swiping, transcript is part of layout
|
|
472
|
-
return;
|
|
473
|
-
} else {
|
|
474
|
-
// Desktop or fullscreen: Normal dragging
|
|
475
|
-
this.startDragging(touch.clientX, touch.clientY);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
this.handlers.touchmove = (e) => {
|
|
480
|
-
const isMobile = window.innerWidth < 640;
|
|
481
|
-
const isFullscreen = this.player.state.fullscreen;
|
|
482
|
-
|
|
483
|
-
if (isMobile && !isFullscreen) {
|
|
484
|
-
// Mobile (not fullscreen): No dragging/swiping
|
|
485
|
-
return;
|
|
486
|
-
} else if (this.isDragging) {
|
|
487
|
-
// Desktop or fullscreen: Normal drag
|
|
488
|
-
const touch = e.touches[0];
|
|
489
|
-
this.drag(touch.clientX, touch.clientY);
|
|
490
|
-
e.preventDefault();
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
this.handlers.touchend = () => {
|
|
495
|
-
if (this.isDragging) {
|
|
496
|
-
// Stop dragging
|
|
497
|
-
this.stopDragging();
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
this.handlers.keydown = (e) => {
|
|
502
|
-
// Check if this is a navigation key
|
|
503
|
-
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'Escape'].includes(e.key)) {
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Prevent default behavior and stop event from bubbling to transcript entries
|
|
508
|
-
e.preventDefault();
|
|
509
|
-
e.stopPropagation();
|
|
510
|
-
|
|
511
|
-
// Handle special keys first
|
|
512
|
-
if (e.key === 'Home') {
|
|
513
|
-
this.resetPosition();
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (e.key === 'Escape') {
|
|
518
|
-
this.hideTranscript();
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
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
|
-
};
|
|
562
|
-
|
|
563
|
-
// Add event listeners using stored handlers
|
|
564
|
-
this.transcriptHeader.addEventListener('mousedown', this.handlers.mousedown);
|
|
565
|
-
document.addEventListener('mousemove', this.handlers.mousemove);
|
|
566
|
-
document.addEventListener('mouseup', this.handlers.mouseup);
|
|
567
|
-
|
|
568
|
-
this.transcriptHeader.addEventListener('touchstart', this.handlers.touchstart);
|
|
569
|
-
document.addEventListener('touchmove', this.handlers.touchmove);
|
|
570
|
-
document.addEventListener('touchend', this.handlers.touchend);
|
|
571
|
-
|
|
572
|
-
this.transcriptHeader.addEventListener('keydown', this.handlers.keydown);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Start dragging
|
|
577
|
-
*/
|
|
578
|
-
startDragging(clientX, clientY) {
|
|
579
|
-
// Get current rendered position (this is where it actually appears on screen)
|
|
580
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
581
|
-
|
|
582
|
-
// Get the parent container position (player container)
|
|
583
|
-
const containerRect = this.player.container.getBoundingClientRect();
|
|
584
|
-
|
|
585
|
-
// Calculate position RELATIVE to container (not viewport)
|
|
586
|
-
const relativeLeft = rect.left - containerRect.left;
|
|
587
|
-
const relativeTop = rect.top - containerRect.top;
|
|
588
|
-
|
|
589
|
-
// If window is centered with transform, convert to absolute position
|
|
590
|
-
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
591
|
-
if (computedStyle.transform !== 'none') {
|
|
592
|
-
// Remove transform and set position relative to container
|
|
593
|
-
this.transcriptWindow.style.transform = 'none';
|
|
594
|
-
this.transcriptWindow.style.left = `${relativeLeft}px`;
|
|
595
|
-
this.transcriptWindow.style.top = `${relativeTop}px`;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Calculate offset based on viewport coordinates (where user clicked)
|
|
599
|
-
this.dragOffsetX = clientX - rect.left;
|
|
600
|
-
this.dragOffsetY = clientY - rect.top;
|
|
601
|
-
|
|
602
|
-
this.isDragging = true;
|
|
603
|
-
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
604
|
-
document.body.style.cursor = 'grabbing';
|
|
605
|
-
document.body.style.userSelect = 'none';
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Perform drag
|
|
610
|
-
*/
|
|
611
|
-
drag(clientX, clientY) {
|
|
612
|
-
if (!this.isDragging) return;
|
|
613
|
-
|
|
614
|
-
// Calculate new viewport position based on mouse position minus the offset
|
|
615
|
-
const newViewportX = clientX - this.dragOffsetX;
|
|
616
|
-
const newViewportY = clientY - this.dragOffsetY;
|
|
617
|
-
|
|
618
|
-
// Convert to position relative to container
|
|
619
|
-
const containerRect = this.player.container.getBoundingClientRect();
|
|
620
|
-
const newX = newViewportX - containerRect.left;
|
|
621
|
-
const newY = newViewportY - containerRect.top;
|
|
622
|
-
|
|
623
|
-
// During drag, set position relative to container
|
|
624
|
-
this.transcriptWindow.style.left = `${newX}px`;
|
|
625
|
-
this.transcriptWindow.style.top = `${newY}px`;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Stop dragging
|
|
630
|
-
*/
|
|
631
|
-
stopDragging() {
|
|
632
|
-
this.isDragging = false;
|
|
633
|
-
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
634
|
-
document.body.style.cursor = '';
|
|
635
|
-
document.body.style.userSelect = '';
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Set window position with boundary constraints
|
|
640
|
-
*/
|
|
641
|
-
setPosition(x, y) {
|
|
642
|
-
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
643
|
-
|
|
644
|
-
// Use document dimensions for fixed positioning
|
|
645
|
-
const viewportWidth = document.documentElement.clientWidth;
|
|
646
|
-
const viewportHeight = document.documentElement.clientHeight;
|
|
647
|
-
|
|
648
|
-
// Very relaxed boundaries - allow window to go mostly off-screen
|
|
649
|
-
// Just keep a small part visible so user can always drag it back
|
|
650
|
-
const minVisible = 100; // Keep at least 100px visible
|
|
651
|
-
const minX = -(rect.width - minVisible); // Can go way off-screen to the left
|
|
652
|
-
const minY = -(rect.height - minVisible); // Can go way off-screen to the top
|
|
653
|
-
const maxX = viewportWidth - minVisible; // Can go way off-screen to the right
|
|
654
|
-
const maxY = viewportHeight - minVisible; // Can go way off-screen to the bottom
|
|
655
|
-
|
|
656
|
-
// Clamp position to boundaries (very loose)
|
|
657
|
-
x = Math.max(minX, Math.min(x, maxX));
|
|
658
|
-
y = Math.max(minY, Math.min(y, maxY));
|
|
659
|
-
|
|
660
|
-
this.transcriptWindow.style.left = `${x}px`;
|
|
661
|
-
this.transcriptWindow.style.top = `${y}px`;
|
|
662
|
-
this.transcriptWindow.style.transform = 'none';
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Reset position to center
|
|
667
|
-
*/
|
|
668
|
-
resetPosition() {
|
|
669
|
-
this.transcriptWindow.style.left = '50%';
|
|
670
|
-
this.transcriptWindow.style.top = '50%';
|
|
671
|
-
this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Cleanup
|
|
676
|
-
*/
|
|
677
|
-
destroy() {
|
|
678
|
-
// Remove timeupdate listener from player
|
|
679
|
-
if (this.handlers.timeupdate) {
|
|
680
|
-
this.player.off('timeupdate', this.handlers.timeupdate);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Remove drag event listeners
|
|
684
|
-
if (this.transcriptHeader) {
|
|
685
|
-
if (this.handlers.mousedown) {
|
|
686
|
-
this.transcriptHeader.removeEventListener('mousedown', this.handlers.mousedown);
|
|
687
|
-
}
|
|
688
|
-
if (this.handlers.touchstart) {
|
|
689
|
-
this.transcriptHeader.removeEventListener('touchstart', this.handlers.touchstart);
|
|
690
|
-
}
|
|
691
|
-
if (this.handlers.keydown) {
|
|
692
|
-
this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Remove document-level listeners
|
|
697
|
-
if (this.handlers.mousemove) {
|
|
698
|
-
document.removeEventListener('mousemove', this.handlers.mousemove);
|
|
699
|
-
}
|
|
700
|
-
if (this.handlers.mouseup) {
|
|
701
|
-
document.removeEventListener('mouseup', this.handlers.mouseup);
|
|
702
|
-
}
|
|
703
|
-
if (this.handlers.touchmove) {
|
|
704
|
-
document.removeEventListener('touchmove', this.handlers.touchmove);
|
|
705
|
-
}
|
|
706
|
-
if (this.handlers.touchend) {
|
|
707
|
-
document.removeEventListener('touchend', this.handlers.touchend);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Remove window-level listeners
|
|
711
|
-
if (this.handlers.resize) {
|
|
712
|
-
window.removeEventListener('resize', this.handlers.resize);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Clear handlers
|
|
716
|
-
this.handlers = null;
|
|
717
|
-
|
|
718
|
-
// Remove DOM element
|
|
719
|
-
if (this.transcriptWindow && this.transcriptWindow.parentNode) {
|
|
720
|
-
this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
this.transcriptWindow = null;
|
|
724
|
-
this.transcriptHeader = null;
|
|
725
|
-
this.transcriptContent = null;
|
|
726
|
-
this.transcriptEntries = [];
|
|
727
|
-
}
|
|
728
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Transcript Manager Component
|
|
3
|
+
* Manages transcript display and interaction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DOMUtils } from '../utils/DOMUtils.js';
|
|
7
|
+
import { TimeUtils } from '../utils/TimeUtils.js';
|
|
8
|
+
import { createIconElement } from '../icons/Icons.js';
|
|
9
|
+
import { i18n } from '../i18n/i18n.js';
|
|
10
|
+
|
|
11
|
+
export class TranscriptManager {
|
|
12
|
+
constructor(player) {
|
|
13
|
+
this.player = player;
|
|
14
|
+
this.transcriptWindow = null;
|
|
15
|
+
this.transcriptEntries = [];
|
|
16
|
+
this.currentActiveEntry = null;
|
|
17
|
+
this.isVisible = false;
|
|
18
|
+
|
|
19
|
+
// Dragging state
|
|
20
|
+
this.isDragging = false;
|
|
21
|
+
this.dragOffsetX = 0;
|
|
22
|
+
this.dragOffsetY = 0;
|
|
23
|
+
|
|
24
|
+
// Store event handlers for cleanup
|
|
25
|
+
this.handlers = {
|
|
26
|
+
timeupdate: () => this.updateActiveEntry(),
|
|
27
|
+
resize: null,
|
|
28
|
+
mousemove: null,
|
|
29
|
+
mouseup: null,
|
|
30
|
+
touchmove: null,
|
|
31
|
+
touchend: null,
|
|
32
|
+
mousedown: null,
|
|
33
|
+
touchstart: null,
|
|
34
|
+
keydown: null
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.init();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init() {
|
|
41
|
+
// Listen for time updates to highlight active transcript entry
|
|
42
|
+
this.player.on('timeupdate', this.handlers.timeupdate);
|
|
43
|
+
|
|
44
|
+
// Reposition transcript when entering/exiting fullscreen
|
|
45
|
+
this.player.on('fullscreenchange', () => {
|
|
46
|
+
if (this.isVisible) {
|
|
47
|
+
// Add a small delay to ensure DOM has updated after fullscreen transition
|
|
48
|
+
setTimeout(() => this.positionTranscript(), 100);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Toggle transcript window visibility
|
|
55
|
+
*/
|
|
56
|
+
toggleTranscript() {
|
|
57
|
+
if (this.isVisible) {
|
|
58
|
+
this.hideTranscript();
|
|
59
|
+
} else {
|
|
60
|
+
this.showTranscript();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Show transcript window
|
|
66
|
+
*/
|
|
67
|
+
showTranscript() {
|
|
68
|
+
if (this.transcriptWindow) {
|
|
69
|
+
this.transcriptWindow.style.display = 'flex';
|
|
70
|
+
this.isVisible = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create transcript window
|
|
75
|
+
this.createTranscriptWindow();
|
|
76
|
+
this.loadTranscriptData();
|
|
77
|
+
|
|
78
|
+
// Show the window
|
|
79
|
+
if (this.transcriptWindow) {
|
|
80
|
+
this.transcriptWindow.style.display = 'flex';
|
|
81
|
+
// Re-position after showing (in case window was resized while hidden)
|
|
82
|
+
setTimeout(() => this.positionTranscript(), 0);
|
|
83
|
+
}
|
|
84
|
+
this.isVisible = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Hide transcript window
|
|
89
|
+
*/
|
|
90
|
+
hideTranscript() {
|
|
91
|
+
if (this.transcriptWindow) {
|
|
92
|
+
this.transcriptWindow.style.display = 'none';
|
|
93
|
+
this.isVisible = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create the transcript window UI
|
|
99
|
+
*/
|
|
100
|
+
createTranscriptWindow() {
|
|
101
|
+
this.transcriptWindow = DOMUtils.createElement('div', {
|
|
102
|
+
className: `${this.player.options.classPrefix}-transcript-window`,
|
|
103
|
+
attributes: {
|
|
104
|
+
'role': 'dialog',
|
|
105
|
+
'aria-label': 'Video Transcript',
|
|
106
|
+
'tabindex': '-1'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Header (draggable)
|
|
111
|
+
this.transcriptHeader = DOMUtils.createElement('div', {
|
|
112
|
+
className: `${this.player.options.classPrefix}-transcript-header`,
|
|
113
|
+
attributes: {
|
|
114
|
+
'aria-label': 'Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.',
|
|
115
|
+
'tabindex': '0'
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const title = DOMUtils.createElement('h3', {
|
|
120
|
+
textContent: i18n.t('transcript.title')
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const closeButton = DOMUtils.createElement('button', {
|
|
124
|
+
className: `${this.player.options.classPrefix}-transcript-close`,
|
|
125
|
+
attributes: {
|
|
126
|
+
'type': 'button',
|
|
127
|
+
'aria-label': i18n.t('transcript.close')
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
closeButton.appendChild(createIconElement('close'));
|
|
131
|
+
closeButton.addEventListener('click', () => this.hideTranscript());
|
|
132
|
+
|
|
133
|
+
this.transcriptHeader.appendChild(title);
|
|
134
|
+
this.transcriptHeader.appendChild(closeButton);
|
|
135
|
+
|
|
136
|
+
// Content container
|
|
137
|
+
this.transcriptContent = DOMUtils.createElement('div', {
|
|
138
|
+
className: `${this.player.options.classPrefix}-transcript-content`
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.transcriptWindow.appendChild(this.transcriptHeader);
|
|
142
|
+
this.transcriptWindow.appendChild(this.transcriptContent);
|
|
143
|
+
|
|
144
|
+
// Append to player container
|
|
145
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
146
|
+
|
|
147
|
+
// Position it next to the video wrapper
|
|
148
|
+
this.positionTranscript();
|
|
149
|
+
|
|
150
|
+
// Setup drag functionality
|
|
151
|
+
this.setupDragAndDrop();
|
|
152
|
+
|
|
153
|
+
// Re-position on window resize (debounced)
|
|
154
|
+
let resizeTimeout;
|
|
155
|
+
this.handlers.resize = () => {
|
|
156
|
+
clearTimeout(resizeTimeout);
|
|
157
|
+
resizeTimeout = setTimeout(() => this.positionTranscript(), 100);
|
|
158
|
+
};
|
|
159
|
+
window.addEventListener('resize', this.handlers.resize);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Position transcript window next to video
|
|
164
|
+
*/
|
|
165
|
+
positionTranscript() {
|
|
166
|
+
if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
|
|
167
|
+
|
|
168
|
+
const isMobile = window.innerWidth < 640;
|
|
169
|
+
const videoRect = this.player.videoWrapper.getBoundingClientRect();
|
|
170
|
+
|
|
171
|
+
// Check if player is in fullscreen mode
|
|
172
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
173
|
+
|
|
174
|
+
if (isMobile && !isFullscreen) {
|
|
175
|
+
// Mobile: Position underneath the video and controls as part of the layout
|
|
176
|
+
this.transcriptWindow.style.position = 'relative';
|
|
177
|
+
this.transcriptWindow.style.left = '0';
|
|
178
|
+
this.transcriptWindow.style.right = '0';
|
|
179
|
+
this.transcriptWindow.style.bottom = 'auto';
|
|
180
|
+
this.transcriptWindow.style.top = 'auto';
|
|
181
|
+
this.transcriptWindow.style.width = '100%';
|
|
182
|
+
this.transcriptWindow.style.maxWidth = '100%';
|
|
183
|
+
this.transcriptWindow.style.maxHeight = '400px';
|
|
184
|
+
this.transcriptWindow.style.height = 'auto';
|
|
185
|
+
this.transcriptWindow.style.borderRadius = '0';
|
|
186
|
+
this.transcriptWindow.style.transform = 'none';
|
|
187
|
+
this.transcriptWindow.style.border = 'none';
|
|
188
|
+
this.transcriptWindow.style.borderTop = '1px solid var(--vidply-border-light)';
|
|
189
|
+
this.transcriptWindow.style.boxShadow = 'none';
|
|
190
|
+
// Disable dragging on mobile
|
|
191
|
+
if (this.transcriptHeader) {
|
|
192
|
+
this.transcriptHeader.style.cursor = 'default';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Ensure transcript is at the container level for proper stacking
|
|
196
|
+
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
197
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
198
|
+
}
|
|
199
|
+
} else if (isFullscreen) {
|
|
200
|
+
// In fullscreen: position in bottom right corner inside the video
|
|
201
|
+
this.transcriptWindow.style.position = 'fixed';
|
|
202
|
+
this.transcriptWindow.style.left = 'auto';
|
|
203
|
+
this.transcriptWindow.style.right = '20px';
|
|
204
|
+
this.transcriptWindow.style.bottom = '80px'; // Above controls
|
|
205
|
+
this.transcriptWindow.style.top = 'auto';
|
|
206
|
+
this.transcriptWindow.style.maxHeight = 'calc(100vh - 180px)'; // Leave space for controls
|
|
207
|
+
this.transcriptWindow.style.height = 'auto';
|
|
208
|
+
this.transcriptWindow.style.width = '400px';
|
|
209
|
+
this.transcriptWindow.style.maxWidth = '400px';
|
|
210
|
+
this.transcriptWindow.style.borderRadius = '8px';
|
|
211
|
+
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
212
|
+
this.transcriptWindow.style.borderTop = '';
|
|
213
|
+
|
|
214
|
+
// Move back to container for fullscreen
|
|
215
|
+
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
216
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// Desktop mode: position next to video
|
|
220
|
+
this.transcriptWindow.style.position = 'absolute';
|
|
221
|
+
this.transcriptWindow.style.left = `${videoRect.width + 8}px`;
|
|
222
|
+
this.transcriptWindow.style.right = 'auto';
|
|
223
|
+
this.transcriptWindow.style.bottom = 'auto';
|
|
224
|
+
this.transcriptWindow.style.top = '0';
|
|
225
|
+
this.transcriptWindow.style.height = `${videoRect.height}px`;
|
|
226
|
+
this.transcriptWindow.style.maxHeight = 'none';
|
|
227
|
+
this.transcriptWindow.style.width = '400px';
|
|
228
|
+
this.transcriptWindow.style.maxWidth = '400px';
|
|
229
|
+
this.transcriptWindow.style.borderRadius = '8px';
|
|
230
|
+
this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
|
|
231
|
+
this.transcriptWindow.style.borderTop = '';
|
|
232
|
+
// Enable dragging on desktop
|
|
233
|
+
if (this.transcriptHeader) {
|
|
234
|
+
this.transcriptHeader.style.cursor = 'move';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Move back to container for desktop
|
|
238
|
+
if (this.transcriptWindow.parentNode !== this.player.container) {
|
|
239
|
+
this.player.container.appendChild(this.transcriptWindow);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Load transcript data from caption/subtitle tracks
|
|
246
|
+
*/
|
|
247
|
+
loadTranscriptData() {
|
|
248
|
+
this.transcriptEntries = [];
|
|
249
|
+
this.transcriptContent.innerHTML = '';
|
|
250
|
+
|
|
251
|
+
// Get caption/subtitle tracks
|
|
252
|
+
const textTracks = Array.from(this.player.element.textTracks);
|
|
253
|
+
const transcriptTrack = textTracks.find(
|
|
254
|
+
track => track.kind === 'captions' || track.kind === 'subtitles'
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (!transcriptTrack) {
|
|
258
|
+
this.showNoTranscriptMessage();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Enable track to load cues
|
|
263
|
+
if (transcriptTrack.mode === 'disabled') {
|
|
264
|
+
transcriptTrack.mode = 'hidden';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!transcriptTrack.cues || transcriptTrack.cues.length === 0) {
|
|
268
|
+
// Wait for cues to load
|
|
269
|
+
const loadingMessage = DOMUtils.createElement('div', {
|
|
270
|
+
className: `${this.player.options.classPrefix}-transcript-loading`,
|
|
271
|
+
textContent: i18n.t('transcript.loading')
|
|
272
|
+
});
|
|
273
|
+
this.transcriptContent.appendChild(loadingMessage);
|
|
274
|
+
|
|
275
|
+
const onLoad = () => {
|
|
276
|
+
this.loadTranscriptData();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
transcriptTrack.addEventListener('load', onLoad, { once: true });
|
|
280
|
+
|
|
281
|
+
// Fallback timeout
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
if (transcriptTrack.cues && transcriptTrack.cues.length > 0) {
|
|
284
|
+
this.loadTranscriptData();
|
|
285
|
+
}
|
|
286
|
+
}, 500);
|
|
287
|
+
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Build transcript from cues
|
|
292
|
+
const cues = Array.from(transcriptTrack.cues);
|
|
293
|
+
cues.forEach((cue, index) => {
|
|
294
|
+
const entry = this.createTranscriptEntry(cue, index);
|
|
295
|
+
this.transcriptEntries.push({
|
|
296
|
+
element: entry,
|
|
297
|
+
cue: cue,
|
|
298
|
+
startTime: cue.startTime,
|
|
299
|
+
endTime: cue.endTime
|
|
300
|
+
});
|
|
301
|
+
this.transcriptContent.appendChild(entry);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create a single transcript entry element
|
|
307
|
+
*/
|
|
308
|
+
createTranscriptEntry(cue, index) {
|
|
309
|
+
const entry = DOMUtils.createElement('div', {
|
|
310
|
+
className: `${this.player.options.classPrefix}-transcript-entry`,
|
|
311
|
+
attributes: {
|
|
312
|
+
'data-start': String(cue.startTime),
|
|
313
|
+
'data-end': String(cue.endTime),
|
|
314
|
+
'role': 'button',
|
|
315
|
+
'tabindex': '0'
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const timestamp = DOMUtils.createElement('span', {
|
|
320
|
+
className: `${this.player.options.classPrefix}-transcript-time`,
|
|
321
|
+
textContent: TimeUtils.formatTime(cue.startTime)
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const text = DOMUtils.createElement('span', {
|
|
325
|
+
className: `${this.player.options.classPrefix}-transcript-text`,
|
|
326
|
+
textContent: this.stripVTTFormatting(cue.text)
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
entry.appendChild(timestamp);
|
|
330
|
+
entry.appendChild(text);
|
|
331
|
+
|
|
332
|
+
// Click to seek
|
|
333
|
+
const seekToTime = () => {
|
|
334
|
+
this.player.seek(cue.startTime);
|
|
335
|
+
if (this.player.state.paused) {
|
|
336
|
+
this.player.play();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
entry.addEventListener('click', seekToTime);
|
|
341
|
+
entry.addEventListener('keydown', (e) => {
|
|
342
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
seekToTime();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return entry;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Strip VTT formatting tags from text
|
|
353
|
+
*/
|
|
354
|
+
stripVTTFormatting(text) {
|
|
355
|
+
// Remove VTT tags like <v Speaker>, <c>, etc.
|
|
356
|
+
return text
|
|
357
|
+
.replace(/<[^>]+>/g, '')
|
|
358
|
+
.replace(/\n/g, ' ')
|
|
359
|
+
.trim();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Show message when no transcript is available
|
|
364
|
+
*/
|
|
365
|
+
showNoTranscriptMessage() {
|
|
366
|
+
const message = DOMUtils.createElement('div', {
|
|
367
|
+
className: `${this.player.options.classPrefix}-transcript-empty`,
|
|
368
|
+
textContent: i18n.t('transcript.noTranscript')
|
|
369
|
+
});
|
|
370
|
+
this.transcriptContent.appendChild(message);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Update active transcript entry based on current time
|
|
375
|
+
*/
|
|
376
|
+
updateActiveEntry() {
|
|
377
|
+
if (!this.isVisible || this.transcriptEntries.length === 0) return;
|
|
378
|
+
|
|
379
|
+
const currentTime = this.player.state.currentTime;
|
|
380
|
+
|
|
381
|
+
// Find the entry that matches current time
|
|
382
|
+
const activeEntry = this.transcriptEntries.find(
|
|
383
|
+
entry => currentTime >= entry.startTime && currentTime < entry.endTime
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (activeEntry && activeEntry !== this.currentActiveEntry) {
|
|
387
|
+
// Remove previous active class
|
|
388
|
+
if (this.currentActiveEntry) {
|
|
389
|
+
this.currentActiveEntry.element.classList.remove(
|
|
390
|
+
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Add active class to current entry
|
|
395
|
+
activeEntry.element.classList.add(
|
|
396
|
+
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Scroll to active entry
|
|
400
|
+
this.scrollToEntry(activeEntry.element);
|
|
401
|
+
|
|
402
|
+
this.currentActiveEntry = activeEntry;
|
|
403
|
+
} else if (!activeEntry && this.currentActiveEntry) {
|
|
404
|
+
// No active entry, remove active class
|
|
405
|
+
this.currentActiveEntry.element.classList.remove(
|
|
406
|
+
`${this.player.options.classPrefix}-transcript-entry-active`
|
|
407
|
+
);
|
|
408
|
+
this.currentActiveEntry = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Scroll transcript window to show active entry
|
|
414
|
+
*/
|
|
415
|
+
scrollToEntry(entryElement) {
|
|
416
|
+
if (!this.transcriptContent) return;
|
|
417
|
+
|
|
418
|
+
const contentRect = this.transcriptContent.getBoundingClientRect();
|
|
419
|
+
const entryRect = entryElement.getBoundingClientRect();
|
|
420
|
+
|
|
421
|
+
// Check if entry is out of view
|
|
422
|
+
if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
|
|
423
|
+
// Scroll to center the entry
|
|
424
|
+
const scrollTop = entryElement.offsetTop - (this.transcriptContent.clientHeight / 2) + (entryElement.clientHeight / 2);
|
|
425
|
+
this.transcriptContent.scrollTo({
|
|
426
|
+
top: scrollTop,
|
|
427
|
+
behavior: 'smooth'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Setup drag and drop functionality
|
|
434
|
+
*/
|
|
435
|
+
setupDragAndDrop() {
|
|
436
|
+
if (!this.transcriptHeader || !this.transcriptWindow) return;
|
|
437
|
+
|
|
438
|
+
// Create and store handler functions
|
|
439
|
+
this.handlers.mousedown = (e) => {
|
|
440
|
+
// Don't drag if clicking on close button
|
|
441
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.startDragging(e.clientX, e.clientY);
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
this.handlers.mousemove = (e) => {
|
|
450
|
+
if (this.isDragging) {
|
|
451
|
+
this.drag(e.clientX, e.clientY);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
this.handlers.mouseup = () => {
|
|
456
|
+
if (this.isDragging) {
|
|
457
|
+
this.stopDragging();
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
this.handlers.touchstart = (e) => {
|
|
462
|
+
if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const isMobile = window.innerWidth < 640;
|
|
467
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
468
|
+
const touch = e.touches[0];
|
|
469
|
+
|
|
470
|
+
if (isMobile && !isFullscreen) {
|
|
471
|
+
// Mobile (not fullscreen): No dragging/swiping, transcript is part of layout
|
|
472
|
+
return;
|
|
473
|
+
} else {
|
|
474
|
+
// Desktop or fullscreen: Normal dragging
|
|
475
|
+
this.startDragging(touch.clientX, touch.clientY);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
this.handlers.touchmove = (e) => {
|
|
480
|
+
const isMobile = window.innerWidth < 640;
|
|
481
|
+
const isFullscreen = this.player.state.fullscreen;
|
|
482
|
+
|
|
483
|
+
if (isMobile && !isFullscreen) {
|
|
484
|
+
// Mobile (not fullscreen): No dragging/swiping
|
|
485
|
+
return;
|
|
486
|
+
} else if (this.isDragging) {
|
|
487
|
+
// Desktop or fullscreen: Normal drag
|
|
488
|
+
const touch = e.touches[0];
|
|
489
|
+
this.drag(touch.clientX, touch.clientY);
|
|
490
|
+
e.preventDefault();
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
this.handlers.touchend = () => {
|
|
495
|
+
if (this.isDragging) {
|
|
496
|
+
// Stop dragging
|
|
497
|
+
this.stopDragging();
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
this.handlers.keydown = (e) => {
|
|
502
|
+
// Check if this is a navigation key
|
|
503
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'Escape'].includes(e.key)) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Prevent default behavior and stop event from bubbling to transcript entries
|
|
508
|
+
e.preventDefault();
|
|
509
|
+
e.stopPropagation();
|
|
510
|
+
|
|
511
|
+
// Handle special keys first
|
|
512
|
+
if (e.key === 'Home') {
|
|
513
|
+
this.resetPosition();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (e.key === 'Escape') {
|
|
518
|
+
this.hideTranscript();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
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
|
+
};
|
|
562
|
+
|
|
563
|
+
// Add event listeners using stored handlers
|
|
564
|
+
this.transcriptHeader.addEventListener('mousedown', this.handlers.mousedown);
|
|
565
|
+
document.addEventListener('mousemove', this.handlers.mousemove);
|
|
566
|
+
document.addEventListener('mouseup', this.handlers.mouseup);
|
|
567
|
+
|
|
568
|
+
this.transcriptHeader.addEventListener('touchstart', this.handlers.touchstart);
|
|
569
|
+
document.addEventListener('touchmove', this.handlers.touchmove);
|
|
570
|
+
document.addEventListener('touchend', this.handlers.touchend);
|
|
571
|
+
|
|
572
|
+
this.transcriptHeader.addEventListener('keydown', this.handlers.keydown);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Start dragging
|
|
577
|
+
*/
|
|
578
|
+
startDragging(clientX, clientY) {
|
|
579
|
+
// Get current rendered position (this is where it actually appears on screen)
|
|
580
|
+
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
581
|
+
|
|
582
|
+
// Get the parent container position (player container)
|
|
583
|
+
const containerRect = this.player.container.getBoundingClientRect();
|
|
584
|
+
|
|
585
|
+
// Calculate position RELATIVE to container (not viewport)
|
|
586
|
+
const relativeLeft = rect.left - containerRect.left;
|
|
587
|
+
const relativeTop = rect.top - containerRect.top;
|
|
588
|
+
|
|
589
|
+
// If window is centered with transform, convert to absolute position
|
|
590
|
+
const computedStyle = window.getComputedStyle(this.transcriptWindow);
|
|
591
|
+
if (computedStyle.transform !== 'none') {
|
|
592
|
+
// Remove transform and set position relative to container
|
|
593
|
+
this.transcriptWindow.style.transform = 'none';
|
|
594
|
+
this.transcriptWindow.style.left = `${relativeLeft}px`;
|
|
595
|
+
this.transcriptWindow.style.top = `${relativeTop}px`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Calculate offset based on viewport coordinates (where user clicked)
|
|
599
|
+
this.dragOffsetX = clientX - rect.left;
|
|
600
|
+
this.dragOffsetY = clientY - rect.top;
|
|
601
|
+
|
|
602
|
+
this.isDragging = true;
|
|
603
|
+
this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
604
|
+
document.body.style.cursor = 'grabbing';
|
|
605
|
+
document.body.style.userSelect = 'none';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Perform drag
|
|
610
|
+
*/
|
|
611
|
+
drag(clientX, clientY) {
|
|
612
|
+
if (!this.isDragging) return;
|
|
613
|
+
|
|
614
|
+
// Calculate new viewport position based on mouse position minus the offset
|
|
615
|
+
const newViewportX = clientX - this.dragOffsetX;
|
|
616
|
+
const newViewportY = clientY - this.dragOffsetY;
|
|
617
|
+
|
|
618
|
+
// Convert to position relative to container
|
|
619
|
+
const containerRect = this.player.container.getBoundingClientRect();
|
|
620
|
+
const newX = newViewportX - containerRect.left;
|
|
621
|
+
const newY = newViewportY - containerRect.top;
|
|
622
|
+
|
|
623
|
+
// During drag, set position relative to container
|
|
624
|
+
this.transcriptWindow.style.left = `${newX}px`;
|
|
625
|
+
this.transcriptWindow.style.top = `${newY}px`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Stop dragging
|
|
630
|
+
*/
|
|
631
|
+
stopDragging() {
|
|
632
|
+
this.isDragging = false;
|
|
633
|
+
this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
|
|
634
|
+
document.body.style.cursor = '';
|
|
635
|
+
document.body.style.userSelect = '';
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Set window position with boundary constraints
|
|
640
|
+
*/
|
|
641
|
+
setPosition(x, y) {
|
|
642
|
+
const rect = this.transcriptWindow.getBoundingClientRect();
|
|
643
|
+
|
|
644
|
+
// Use document dimensions for fixed positioning
|
|
645
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
646
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
647
|
+
|
|
648
|
+
// Very relaxed boundaries - allow window to go mostly off-screen
|
|
649
|
+
// Just keep a small part visible so user can always drag it back
|
|
650
|
+
const minVisible = 100; // Keep at least 100px visible
|
|
651
|
+
const minX = -(rect.width - minVisible); // Can go way off-screen to the left
|
|
652
|
+
const minY = -(rect.height - minVisible); // Can go way off-screen to the top
|
|
653
|
+
const maxX = viewportWidth - minVisible; // Can go way off-screen to the right
|
|
654
|
+
const maxY = viewportHeight - minVisible; // Can go way off-screen to the bottom
|
|
655
|
+
|
|
656
|
+
// Clamp position to boundaries (very loose)
|
|
657
|
+
x = Math.max(minX, Math.min(x, maxX));
|
|
658
|
+
y = Math.max(minY, Math.min(y, maxY));
|
|
659
|
+
|
|
660
|
+
this.transcriptWindow.style.left = `${x}px`;
|
|
661
|
+
this.transcriptWindow.style.top = `${y}px`;
|
|
662
|
+
this.transcriptWindow.style.transform = 'none';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Reset position to center
|
|
667
|
+
*/
|
|
668
|
+
resetPosition() {
|
|
669
|
+
this.transcriptWindow.style.left = '50%';
|
|
670
|
+
this.transcriptWindow.style.top = '50%';
|
|
671
|
+
this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Cleanup
|
|
676
|
+
*/
|
|
677
|
+
destroy() {
|
|
678
|
+
// Remove timeupdate listener from player
|
|
679
|
+
if (this.handlers.timeupdate) {
|
|
680
|
+
this.player.off('timeupdate', this.handlers.timeupdate);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Remove drag event listeners
|
|
684
|
+
if (this.transcriptHeader) {
|
|
685
|
+
if (this.handlers.mousedown) {
|
|
686
|
+
this.transcriptHeader.removeEventListener('mousedown', this.handlers.mousedown);
|
|
687
|
+
}
|
|
688
|
+
if (this.handlers.touchstart) {
|
|
689
|
+
this.transcriptHeader.removeEventListener('touchstart', this.handlers.touchstart);
|
|
690
|
+
}
|
|
691
|
+
if (this.handlers.keydown) {
|
|
692
|
+
this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Remove document-level listeners
|
|
697
|
+
if (this.handlers.mousemove) {
|
|
698
|
+
document.removeEventListener('mousemove', this.handlers.mousemove);
|
|
699
|
+
}
|
|
700
|
+
if (this.handlers.mouseup) {
|
|
701
|
+
document.removeEventListener('mouseup', this.handlers.mouseup);
|
|
702
|
+
}
|
|
703
|
+
if (this.handlers.touchmove) {
|
|
704
|
+
document.removeEventListener('touchmove', this.handlers.touchmove);
|
|
705
|
+
}
|
|
706
|
+
if (this.handlers.touchend) {
|
|
707
|
+
document.removeEventListener('touchend', this.handlers.touchend);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Remove window-level listeners
|
|
711
|
+
if (this.handlers.resize) {
|
|
712
|
+
window.removeEventListener('resize', this.handlers.resize);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Clear handlers
|
|
716
|
+
this.handlers = null;
|
|
717
|
+
|
|
718
|
+
// Remove DOM element
|
|
719
|
+
if (this.transcriptWindow && this.transcriptWindow.parentNode) {
|
|
720
|
+
this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
this.transcriptWindow = null;
|
|
724
|
+
this.transcriptHeader = null;
|
|
725
|
+
this.transcriptContent = null;
|
|
726
|
+
this.transcriptEntries = [];
|
|
727
|
+
}
|
|
728
|
+
}
|