vidply 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { TimeUtils } from '../utils/TimeUtils.js';
8
8
  import { createIconElement } from '../icons/Icons.js';
9
9
  import { i18n } from '../i18n/i18n.js';
10
10
  import { StorageManager } from '../utils/StorageManager.js';
11
+ import { DraggableResizable } from '../utils/DraggableResizable.js';
11
12
 
12
13
  export class TranscriptManager {
13
14
  constructor(player) {
@@ -21,19 +22,8 @@ export class TranscriptManager {
21
22
  // Storage manager
22
23
  this.storage = new StorageManager('vidply');
23
24
 
24
- // Dragging state
25
- this.isDragging = false;
26
- this.dragOffsetX = 0;
27
- this.dragOffsetY = 0;
28
-
29
- // Resizing state
30
- this.isResizing = false;
31
- this.resizeDirection = null;
32
- this.resizeStartX = 0;
33
- this.resizeStartY = 0;
34
- this.resizeStartWidth = 0;
35
- this.resizeStartHeight = 0;
36
- this.resizeEnabled = false;
25
+ // Draggable/Resizable utility
26
+ this.draggableResizable = null;
37
27
 
38
28
  // Settings menu state
39
29
  this.settingsMenuVisible = false;
@@ -41,8 +31,13 @@ export class TranscriptManager {
41
31
  this.settingsButton = null;
42
32
  this.settingsMenuJustOpened = false;
43
33
 
44
- // Keyboard drag mode
45
- this.keyboardDragMode = false;
34
+ // Resize mode state
35
+ this.resizeOptionButton = null;
36
+ this.resizeOptionText = null;
37
+ this.resizeModeIndicator = null;
38
+ this.resizeModeIndicatorTimeout = null;
39
+ this.transcriptResizeHandles = [];
40
+ this.liveRegion = null;
46
41
 
47
42
  // Style dialog state
48
43
  this.styleDialog = null;
@@ -74,13 +69,6 @@ export class TranscriptManager {
74
69
  this.handlers = {
75
70
  timeupdate: () => this.updateActiveEntry(),
76
71
  resize: null,
77
- mousemove: null,
78
- mouseup: null,
79
- touchmove: null,
80
- touchend: null,
81
- mousedown: null,
82
- touchstart: null,
83
- keydown: null,
84
72
  settingsClick: null,
85
73
  settingsKeydown: null,
86
74
  documentClick: null,
@@ -103,8 +91,11 @@ export class TranscriptManager {
103
91
  // Reposition transcript when entering/exiting fullscreen
104
92
  this.player.on('fullscreenchange', () => {
105
93
  if (this.isVisible) {
106
- // Add a small delay to ensure DOM has updated after fullscreen transition
107
- this.setManagedTimeout(() => this.positionTranscript(), 100);
94
+ // Only auto-position if user hasn't manually positioned it
95
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
96
+ // Add a small delay to ensure DOM has updated after fullscreen transition
97
+ this.setManagedTimeout(() => this.positionTranscript(), 100);
98
+ }
108
99
  }
109
100
  });
110
101
  }
@@ -127,11 +118,15 @@ export class TranscriptManager {
127
118
  if (this.transcriptWindow) {
128
119
  this.transcriptWindow.style.display = 'flex';
129
120
  this.isVisible = true;
121
+
122
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === 'function') {
123
+ this.player.controlBar.updateTranscriptButton();
124
+ }
130
125
 
131
- // Focus the settings button for keyboard accessibility
126
+ // Focus the header for keyboard accessibility
132
127
  this.setManagedTimeout(() => {
133
- if (this.settingsButton) {
134
- this.settingsButton.focus();
128
+ if (this.transcriptHeader) {
129
+ this.transcriptHeader.focus();
135
130
  }
136
131
  }, 150);
137
132
  return;
@@ -144,13 +139,17 @@ export class TranscriptManager {
144
139
  // Show the window
145
140
  if (this.transcriptWindow) {
146
141
  this.transcriptWindow.style.display = 'flex';
147
- // Re-position after showing (in case window was resized while hidden)
148
- this.setManagedTimeout(() => this.positionTranscript(), 0);
149
142
 
150
- // Focus the settings button for keyboard accessibility
143
+ // Only auto-position if user hasn't manually positioned it
144
+ // This prevents overwriting saved positions from localStorage
145
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
146
+ this.setManagedTimeout(() => this.positionTranscript(), 0);
147
+ }
148
+
149
+ // Focus the header for keyboard accessibility
151
150
  this.setManagedTimeout(() => {
152
- if (this.settingsButton) {
153
- this.settingsButton.focus();
151
+ if (this.transcriptHeader) {
152
+ this.transcriptHeader.focus();
154
153
  }
155
154
  }, 150);
156
155
  }
@@ -160,11 +159,29 @@ export class TranscriptManager {
160
159
  /**
161
160
  * Hide transcript window
162
161
  */
163
- hideTranscript() {
162
+ hideTranscript({ focusButton = false } = {}) {
164
163
  if (this.transcriptWindow) {
165
164
  this.transcriptWindow.style.display = 'none';
166
165
  this.isVisible = false;
167
166
  }
167
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
168
+ this.draggableResizable.disablePointerResizeMode();
169
+ this.updateResizeOptionState();
170
+ }
171
+ this.hideResizeModeIndicator();
172
+ this.announceLive('');
173
+
174
+ // Update transcript button state in control bar
175
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === 'function') {
176
+ this.player.controlBar.updateTranscriptButton();
177
+ }
178
+
179
+ if (focusButton) {
180
+ const transcriptButton = this.player.controlBar?.controls?.transcript;
181
+ if (transcriptButton && typeof transcriptButton.focus === 'function') {
182
+ transcriptButton.focus();
183
+ }
184
+ }
168
185
  }
169
186
 
170
187
  /**
@@ -184,7 +201,6 @@ export class TranscriptManager {
184
201
  this.transcriptHeader = DOMUtils.createElement('div', {
185
202
  className: `${this.player.options.classPrefix}-transcript-header`,
186
203
  attributes: {
187
- 'aria-label': 'Drag to reposition transcript. Use arrow keys to move, Home to reset position, Escape to close.',
188
204
  'tabindex': '0'
189
205
  }
190
206
  });
@@ -199,7 +215,7 @@ export class TranscriptManager {
199
215
  className: `${this.player.options.classPrefix}-transcript-settings`,
200
216
  attributes: {
201
217
  'type': 'button',
202
- 'aria-label': i18n.t('transcript.settings'),
218
+ 'aria-label': i18n.t('transcript.settingsMenu'),
203
219
  'aria-expanded': 'false'
204
220
  }
205
221
  });
@@ -239,7 +255,7 @@ export class TranscriptManager {
239
255
  this.settingsButton.addEventListener('keydown', this.handlers.settingsKeydown);
240
256
 
241
257
  const title = DOMUtils.createElement('h3', {
242
- textContent: i18n.t('transcript.title')
258
+ textContent: `${i18n.t('transcript.title')}. ${i18n.t('transcript.dragResizePrompt')}`
243
259
  });
244
260
 
245
261
  // Autoscroll checkbox
@@ -272,8 +288,8 @@ export class TranscriptManager {
272
288
  this.saveAutoscrollPreference();
273
289
  });
274
290
 
291
+ this.transcriptHeader.appendChild(title);
275
292
  this.headerLeft.appendChild(this.settingsButton);
276
- this.headerLeft.appendChild(title);
277
293
  this.headerLeft.appendChild(autoscrollLabel);
278
294
 
279
295
  // Language selector (will be populated after tracks are loaded)
@@ -294,7 +310,7 @@ export class TranscriptManager {
294
310
  }
295
311
  });
296
312
  closeButton.appendChild(createIconElement('close'));
297
- closeButton.addEventListener('click', () => this.hideTranscript());
313
+ closeButton.addEventListener('click', () => this.hideTranscript({ focusButton: true }));
298
314
 
299
315
  this.transcriptHeader.appendChild(this.headerLeft);
300
316
  this.transcriptHeader.appendChild(closeButton);
@@ -307,15 +323,30 @@ export class TranscriptManager {
307
323
  this.transcriptWindow.appendChild(this.transcriptHeader);
308
324
  this.transcriptWindow.appendChild(this.transcriptContent);
309
325
 
326
+ this.createResizeHandles();
327
+
328
+ // Live region for announcements (screen reader feedback)
329
+ this.liveRegion = DOMUtils.createElement('div', {
330
+ className: 'vidply-sr-only',
331
+ attributes: {
332
+ 'aria-live': 'polite',
333
+ 'aria-atomic': 'true'
334
+ }
335
+ });
336
+ this.transcriptWindow.appendChild(this.liveRegion);
337
+
310
338
  // Append to player container
311
339
  this.player.container.appendChild(this.transcriptWindow);
312
340
 
313
- // Position it next to the video wrapper
314
- this.positionTranscript();
315
-
316
- // Setup drag functionality
341
+ // Setup drag functionality FIRST (this will restore saved position if it exists)
317
342
  this.setupDragAndDrop();
318
343
 
344
+ // Then position it next to the video wrapper ONLY if user hasn't manually positioned it
345
+ // This ensures we don't overwrite saved positions from localStorage
346
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
347
+ this.positionTranscript();
348
+ }
349
+
319
350
  // Setup document click handler to close settings menu and style dialog
320
351
  // DON'T add it yet - it will be added when the menu is first opened
321
352
  this.handlers.documentClick = (e) => {
@@ -353,23 +384,53 @@ export class TranscriptManager {
353
384
  // Store flag to track if handler has been added
354
385
  this.documentClickHandlerAdded = false;
355
386
 
356
- // Re-position on window resize (debounced)
387
+ // Re-position on window resize (debounced) - but only if not manually positioned
357
388
  let resizeTimeout;
358
389
  this.handlers.resize = () => {
359
390
  if (resizeTimeout) {
360
391
  this.clearManagedTimeout(resizeTimeout);
361
392
  }
362
- resizeTimeout = this.setManagedTimeout(() => this.positionTranscript(), 100);
393
+ resizeTimeout = this.setManagedTimeout(() => {
394
+ // Only auto-position if user hasn't manually moved it
395
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
396
+ this.positionTranscript();
397
+ }
398
+ }, 100);
363
399
  };
364
400
  window.addEventListener('resize', this.handlers.resize);
365
401
  }
366
402
 
403
+ createResizeHandles() {
404
+ if (!this.transcriptWindow) return;
405
+
406
+ const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
407
+ this.transcriptResizeHandles = directions.map(direction => {
408
+ const handle = DOMUtils.createElement('div', {
409
+ className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
410
+ attributes: {
411
+ 'data-direction': direction,
412
+ 'data-vidply-managed-resize': 'true',
413
+ 'aria-hidden': 'true'
414
+ }
415
+ });
416
+
417
+ handle.style.display = 'none';
418
+ this.transcriptWindow.appendChild(handle);
419
+ return handle;
420
+ });
421
+ }
422
+
367
423
  /**
368
424
  * Position transcript window next to video
369
425
  */
370
426
  positionTranscript() {
371
427
  if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
372
428
 
429
+ // Don't auto-position if user has manually positioned it
430
+ if (this.draggableResizable && this.draggableResizable.manuallyPositioned) {
431
+ return;
432
+ }
433
+
373
434
  const isMobile = window.innerWidth < 640;
374
435
  const videoRect = this.player.videoWrapper.getBoundingClientRect();
375
436
 
@@ -410,8 +471,12 @@ export class TranscriptManager {
410
471
  this.transcriptWindow.style.top = 'auto';
411
472
  this.transcriptWindow.style.maxHeight = 'calc(100vh - 180px)'; // Leave space for controls
412
473
  this.transcriptWindow.style.height = 'auto';
413
- this.transcriptWindow.style.width = '400px';
414
- this.transcriptWindow.style.maxWidth = '400px';
474
+ const fullscreenMinWidth = 260;
475
+ const fullscreenAvailable = Math.max(fullscreenMinWidth, window.innerWidth - 40);
476
+ const fullscreenDesired = parseFloat(this.transcriptWindow.style.width) || 400;
477
+ const fullscreenWidth = Math.max(fullscreenMinWidth, Math.min(fullscreenDesired, fullscreenAvailable));
478
+ this.transcriptWindow.style.width = `${fullscreenWidth}px`;
479
+ this.transcriptWindow.style.maxWidth = 'none';
415
480
  this.transcriptWindow.style.borderRadius = '8px';
416
481
  this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
417
482
  this.transcriptWindow.style.borderTop = '';
@@ -421,16 +486,35 @@ export class TranscriptManager {
421
486
  this.player.container.appendChild(this.transcriptWindow);
422
487
  }
423
488
  } else {
424
- // Desktop mode: position next to video
489
+ // Desktop mode: position in right side of viewport
490
+ const transcriptWidth = parseFloat(this.transcriptWindow.style.width) || 400;
491
+ const padding = 20;
492
+ const minWidth = 260;
493
+ const containerRect = this.player.container.getBoundingClientRect();
494
+
495
+ const ensureContainerPositioned = () => {
496
+ const computed = window.getComputedStyle(this.player.container);
497
+ if (computed.position === 'static') {
498
+ this.player.container.style.position = 'relative';
499
+ }
500
+ };
501
+
502
+ ensureContainerPositioned();
503
+
504
+ const left = (videoRect.right - containerRect.left) + padding;
505
+ const availableWidth = window.innerWidth - videoRect.right - padding;
506
+ const appliedWidth = Math.max(minWidth, Math.min(transcriptWidth, availableWidth));
507
+ const appliedHeight = videoRect.height;
508
+
425
509
  this.transcriptWindow.style.position = 'absolute';
426
- this.transcriptWindow.style.left = `${videoRect.width + 8}px`;
510
+ this.transcriptWindow.style.left = `${left}px`;
427
511
  this.transcriptWindow.style.right = 'auto';
428
512
  this.transcriptWindow.style.bottom = 'auto';
429
513
  this.transcriptWindow.style.top = '0';
430
- this.transcriptWindow.style.height = `${videoRect.height}px`;
514
+ this.transcriptWindow.style.height = `${appliedHeight}px`;
431
515
  this.transcriptWindow.style.maxHeight = 'none';
432
- this.transcriptWindow.style.width = '400px';
433
- this.transcriptWindow.style.maxWidth = '400px';
516
+ this.transcriptWindow.style.width = `${appliedWidth}px`;
517
+ this.transcriptWindow.style.maxWidth = 'none';
434
518
  this.transcriptWindow.style.borderRadius = '8px';
435
519
  this.transcriptWindow.style.border = '1px solid var(--vidply-border)';
436
520
  this.transcriptWindow.style.borderTop = '';
@@ -951,362 +1035,127 @@ export class TranscriptManager {
951
1035
  setupDragAndDrop() {
952
1036
  if (!this.transcriptHeader || !this.transcriptWindow) return;
953
1037
 
954
- // Create and store handler functions
955
- this.handlers.mousedown = (e) => {
956
- // Don't drag if clicking on close button
957
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
958
- return;
959
- }
960
-
961
- // Don't drag if clicking on settings button
962
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
963
- return;
964
- }
965
-
966
- // Don't drag if clicking on language selector
967
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
968
- return;
969
- }
970
-
971
- // Don't drag if clicking on settings menu
972
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
973
- return;
974
- }
975
-
976
- // Don't drag if clicking on style dialog
977
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
978
- return;
979
- }
980
-
981
- this.startDragging(e.clientX, e.clientY);
982
- e.preventDefault();
983
- };
984
-
985
- this.handlers.mousemove = (e) => {
986
- if (this.isDragging) {
987
- this.drag(e.clientX, e.clientY);
988
- }
989
- };
990
-
991
- this.handlers.mouseup = () => {
992
- if (this.isDragging) {
993
- this.stopDragging();
994
- }
995
- };
996
-
997
- this.handlers.touchstart = (e) => {
998
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-close`)) {
999
- return;
1000
- }
1001
-
1002
- // Don't drag if touching settings button
1003
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
1004
- return;
1005
- }
1006
-
1007
- // Don't drag if touching language selector
1008
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-language-select`)) {
1009
- return;
1010
- }
1011
-
1012
- // Don't drag if touching settings menu
1013
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
1014
- return;
1015
- }
1016
-
1017
- // Don't drag if touching style dialog
1018
- if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
1019
- return;
1020
- }
1021
-
1022
- const isMobile = window.innerWidth < 640;
1023
- const isFullscreen = this.player.state.fullscreen;
1024
- const touch = e.touches[0];
1025
-
1026
- if (isMobile && !isFullscreen) {
1027
- // Mobile (not fullscreen): No dragging/swiping, transcript is part of layout
1028
- return;
1029
- } else {
1030
- // Desktop or fullscreen: Normal dragging
1031
- this.startDragging(touch.clientX, touch.clientY);
1032
- }
1033
- };
1034
-
1035
- this.handlers.touchmove = (e) => {
1036
- const isMobile = window.innerWidth < 640;
1037
- const isFullscreen = this.player.state.fullscreen;
1038
-
1039
- if (isMobile && !isFullscreen) {
1040
- // Mobile (not fullscreen): No dragging/swiping
1041
- return;
1042
- } else if (this.isDragging) {
1043
- // Desktop or fullscreen: Normal drag
1044
- const touch = e.touches[0];
1045
- this.drag(touch.clientX, touch.clientY);
1046
- e.preventDefault();
1038
+ // Check if we're on mobile and not in fullscreen (no dragging in this case)
1039
+ const isMobile = window.innerWidth < 640;
1040
+ const isFullscreen = this.player.state.fullscreen;
1041
+
1042
+ if (isMobile && !isFullscreen) {
1043
+ return; // No drag/resize on mobile (not fullscreen)
1044
+ }
1045
+
1046
+ // Create DraggableResizable utility
1047
+ this.draggableResizable = new DraggableResizable(this.transcriptWindow, {
1048
+ dragHandle: this.transcriptHeader,
1049
+ resizeHandles: this.transcriptResizeHandles,
1050
+ constrainToViewport: true,
1051
+ classPrefix: `${this.player.options.classPrefix}-transcript`,
1052
+ keyboardDragKey: 'd',
1053
+ keyboardResizeKey: 'r',
1054
+ keyboardStep: 10,
1055
+ keyboardStepLarge: 50,
1056
+ minWidth: 300,
1057
+ minHeight: 200,
1058
+ maxWidth: () => Math.max(320, window.innerWidth - 40),
1059
+ maxHeight: () => Math.max(200, window.innerHeight - 120),
1060
+ pointerResizeIndicatorText: i18n.t('transcript.resizeModeHint'),
1061
+ onPointerResizeToggle: (enabled) => this.onPointerResizeModeChange(enabled),
1062
+ onDragStart: (e) => {
1063
+ // Don't drag if clicking on certain elements
1064
+ const ignoreSelectors = [
1065
+ `.${this.player.options.classPrefix}-transcript-close`,
1066
+ `.${this.player.options.classPrefix}-transcript-settings`,
1067
+ `.${this.player.options.classPrefix}-transcript-language-select`,
1068
+ `.${this.player.options.classPrefix}-transcript-settings-menu`,
1069
+ `.${this.player.options.classPrefix}-transcript-style-dialog`
1070
+ ];
1071
+
1072
+ for (const selector of ignoreSelectors) {
1073
+ if (e.target.closest(selector)) {
1074
+ return false; // Prevent drag
1075
+ }
1076
+ }
1077
+
1078
+ return true; // Allow drag
1047
1079
  }
1048
- };
1080
+ });
1049
1081
 
1050
- this.handlers.touchend = () => {
1051
- if (this.isDragging) {
1052
- // Stop dragging
1053
- this.stopDragging();
1054
- }
1055
- };
1082
+ // Add custom keyboard handler for special keys (Escape, Home)
1083
+ this.customKeyHandler = (e) => {
1084
+ const key = e.key.toLowerCase();
1085
+ const alreadyPrevented = e.defaultPrevented;
1056
1086
 
1057
- this.handlers.keydown = (e) => {
1058
- // Handle arrow keys only in keyboard drag mode
1059
- if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
1060
- if (!this.keyboardDragMode) {
1061
- // Not in drag mode, let other handlers deal with it
1062
- return;
1063
- }
1064
-
1065
- // In drag mode - move the window
1087
+ if (key === 'home') {
1066
1088
  e.preventDefault();
1067
1089
  e.stopPropagation();
1068
-
1069
- const step = e.shiftKey ? 50 : 10; // Larger steps with Shift key
1070
-
1071
- // Get current position
1072
- let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
1073
- let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
1074
-
1075
- // If window is still centered with transform, convert to absolute position first
1076
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
1077
- if (computedStyle.transform !== 'none') {
1078
- const rect = this.transcriptWindow.getBoundingClientRect();
1079
- currentLeft = rect.left;
1080
- currentTop = rect.top;
1081
- this.transcriptWindow.style.transform = 'none';
1082
- this.transcriptWindow.style.left = `${currentLeft}px`;
1083
- this.transcriptWindow.style.top = `${currentTop}px`;
1084
- }
1085
-
1086
- // Calculate new position based on arrow key
1087
- let newX = currentLeft;
1088
- let newY = currentTop;
1089
-
1090
- switch(e.key) {
1091
- case 'ArrowLeft':
1092
- newX -= step;
1093
- break;
1094
- case 'ArrowRight':
1095
- newX += step;
1096
- break;
1097
- case 'ArrowUp':
1098
- newY -= step;
1099
- break;
1100
- case 'ArrowDown':
1101
- newY += step;
1102
- break;
1090
+ if (this.draggableResizable) {
1091
+ if (this.draggableResizable.pointerResizeMode) {
1092
+ this.draggableResizable.disablePointerResizeMode();
1093
+ }
1094
+ this.draggableResizable.manuallyPositioned = false;
1095
+ this.positionTranscript();
1096
+ this.updateResizeOptionState();
1097
+ this.announceLive(i18n.t('transcript.positionReset'));
1103
1098
  }
1104
-
1105
- // Set new position directly
1106
- this.transcriptWindow.style.left = `${newX}px`;
1107
- this.transcriptWindow.style.top = `${newY}px`;
1108
1099
  return;
1109
1100
  }
1110
1101
 
1111
- // Handle other special keys
1112
- if (e.key === 'Home') {
1102
+ if (key === 'r') {
1103
+ if (alreadyPrevented) {
1104
+ return;
1105
+ }
1113
1106
  e.preventDefault();
1114
1107
  e.stopPropagation();
1115
- this.resetPosition();
1108
+ const enabled = this.toggleResizeMode();
1109
+ if (enabled) {
1110
+ this.transcriptWindow.focus();
1111
+ }
1116
1112
  return;
1117
1113
  }
1118
1114
 
1119
- if (e.key === 'Escape') {
1115
+ if (key === 'escape') {
1120
1116
  e.preventDefault();
1121
1117
  e.stopPropagation();
1118
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
1119
+ this.draggableResizable.disablePointerResizeMode();
1120
+ return;
1121
+ }
1122
1122
  if (this.styleDialogVisible) {
1123
- // Close style dialog first
1124
1123
  this.hideStyleDialog();
1125
- } else if (this.keyboardDragMode) {
1126
- // Exit drag mode
1127
- this.disableKeyboardDragMode();
1124
+ } else if (this.draggableResizable && this.draggableResizable.keyboardDragMode) {
1125
+ this.draggableResizable.disableKeyboardDragMode();
1126
+ this.announceLive(i18n.t('transcript.dragModeDisabled'));
1128
1127
  } else if (this.settingsMenuVisible) {
1129
- // Close settings menu
1130
1128
  this.hideSettingsMenu();
1131
1129
  } else {
1132
- // Close transcript
1133
- this.hideTranscript();
1130
+ this.hideTranscript({ focusButton: true });
1134
1131
  }
1135
1132
  return;
1136
1133
  }
1137
1134
  };
1138
-
1139
- // Add event listeners using stored handlers
1140
- this.transcriptHeader.addEventListener('mousedown', this.handlers.mousedown);
1141
- document.addEventListener('mousemove', this.handlers.mousemove);
1142
- document.addEventListener('mouseup', this.handlers.mouseup);
1143
1135
 
1144
- this.transcriptHeader.addEventListener('touchstart', this.handlers.touchstart);
1145
- document.addEventListener('touchmove', this.handlers.touchmove);
1146
- document.addEventListener('touchend', this.handlers.touchend);
1147
-
1148
- this.transcriptHeader.addEventListener('keydown', this.handlers.keydown);
1136
+ this.transcriptWindow.addEventListener('keydown', this.customKeyHandler);
1149
1137
  }
1150
1138
 
1151
- /**
1152
- * Start dragging
1153
- */
1154
- startDragging(clientX, clientY) {
1155
- // Get current rendered position (this is where it actually appears on screen)
1156
- const rect = this.transcriptWindow.getBoundingClientRect();
1157
-
1158
- // Get the parent container position (player container)
1159
- const containerRect = this.player.container.getBoundingClientRect();
1160
-
1161
- // Calculate position RELATIVE to container (not viewport)
1162
- const relativeLeft = rect.left - containerRect.left;
1163
- const relativeTop = rect.top - containerRect.top;
1164
-
1165
- // If window is centered with transform, convert to absolute position
1166
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
1167
- if (computedStyle.transform !== 'none') {
1168
- // Remove transform and set position relative to container
1169
- this.transcriptWindow.style.transform = 'none';
1170
- this.transcriptWindow.style.left = `${relativeLeft}px`;
1171
- this.transcriptWindow.style.top = `${relativeTop}px`;
1172
- }
1173
-
1174
- // Calculate offset based on viewport coordinates (where user clicked)
1175
- this.dragOffsetX = clientX - rect.left;
1176
- this.dragOffsetY = clientY - rect.top;
1177
-
1178
- this.isDragging = true;
1179
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-dragging`);
1180
- document.body.style.cursor = 'grabbing';
1181
- document.body.style.userSelect = 'none';
1182
- }
1183
-
1184
- /**
1185
- * Perform drag
1186
- */
1187
- drag(clientX, clientY) {
1188
- if (!this.isDragging) return;
1189
-
1190
- // Calculate new viewport position based on mouse position minus the offset
1191
- const newViewportX = clientX - this.dragOffsetX;
1192
- const newViewportY = clientY - this.dragOffsetY;
1193
-
1194
- // Convert to position relative to container
1195
- const containerRect = this.player.container.getBoundingClientRect();
1196
- const newX = newViewportX - containerRect.left;
1197
- const newY = newViewportY - containerRect.top;
1198
-
1199
- // During drag, set position relative to container
1200
- this.transcriptWindow.style.left = `${newX}px`;
1201
- this.transcriptWindow.style.top = `${newY}px`;
1202
- }
1203
-
1204
- /**
1205
- * Stop dragging
1206
- */
1207
- stopDragging() {
1208
- this.isDragging = false;
1209
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-dragging`);
1210
- document.body.style.cursor = '';
1211
- document.body.style.userSelect = '';
1212
- }
1213
-
1214
- /**
1215
- * Set window position with boundary constraints
1216
- */
1217
- setPosition(x, y) {
1218
- const rect = this.transcriptWindow.getBoundingClientRect();
1219
-
1220
- // Use document dimensions for fixed positioning
1221
- const viewportWidth = document.documentElement.clientWidth;
1222
- const viewportHeight = document.documentElement.clientHeight;
1223
-
1224
- // Very relaxed boundaries - allow window to go mostly off-screen
1225
- // Just keep a small part visible so user can always drag it back
1226
- const minVisible = 100; // Keep at least 100px visible
1227
- const minX = -(rect.width - minVisible); // Can go way off-screen to the left
1228
- const minY = -(rect.height - minVisible); // Can go way off-screen to the top
1229
- const maxX = viewportWidth - minVisible; // Can go way off-screen to the right
1230
- const maxY = viewportHeight - minVisible; // Can go way off-screen to the bottom
1231
-
1232
- // Clamp position to boundaries (very loose)
1233
- x = Math.max(minX, Math.min(x, maxX));
1234
- y = Math.max(minY, Math.min(y, maxY));
1235
-
1236
- this.transcriptWindow.style.left = `${x}px`;
1237
- this.transcriptWindow.style.top = `${y}px`;
1238
- this.transcriptWindow.style.transform = 'none';
1239
- }
1240
-
1241
- /**
1242
- * Reset position to center
1243
- */
1244
- resetPosition() {
1245
- this.transcriptWindow.style.left = '50%';
1246
- this.transcriptWindow.style.top = '50%';
1247
- this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
1248
- }
1249
1139
 
1250
1140
  /**
1251
1141
  * Toggle keyboard drag mode
1252
1142
  */
1253
1143
  toggleKeyboardDragMode() {
1254
- if (this.keyboardDragMode) {
1255
- this.disableKeyboardDragMode();
1256
- } else {
1257
- this.enableKeyboardDragMode();
1258
- }
1259
- }
1260
-
1261
- /**
1262
- * Enable keyboard drag mode
1263
- */
1264
- enableKeyboardDragMode() {
1265
- this.keyboardDragMode = true;
1266
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
1267
-
1268
- // Update settings button aria label
1269
- if (this.settingsButton) {
1270
- this.settingsButton.setAttribute('aria-label', 'Keyboard drag mode active. Use arrow keys to move window. Press D or Escape to exit.');
1271
- }
1272
-
1273
- // Add visual indicator
1274
- const indicator = DOMUtils.createElement('div', {
1275
- className: `${this.player.options.classPrefix}-transcript-drag-indicator`,
1276
- textContent: i18n.t('transcript.keyboardDragActive')
1277
- });
1278
- this.transcriptHeader.appendChild(indicator);
1279
-
1280
- // Hide settings menu if open
1281
- if (this.settingsMenuVisible) {
1282
- this.hideSettingsMenu();
1283
- }
1284
-
1285
- // Focus the header for keyboard navigation
1286
- this.transcriptHeader.focus();
1287
- }
1288
-
1289
- /**
1290
- * Disable keyboard drag mode
1291
- */
1292
- disableKeyboardDragMode() {
1293
- this.keyboardDragMode = false;
1294
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
1295
-
1296
- // Update settings button aria label
1297
- if (this.settingsButton) {
1298
- this.settingsButton.setAttribute('aria-label', 'Transcript settings. Press Enter to open menu, or D to enable drag mode');
1299
- }
1300
-
1301
- // Remove visual indicator
1302
- const indicator = this.transcriptHeader.querySelector(`.${this.player.options.classPrefix}-transcript-drag-indicator`);
1303
- if (indicator) {
1304
- indicator.remove();
1305
- }
1306
-
1307
- // Focus back to settings button
1308
- if (this.settingsButton) {
1309
- this.settingsButton.focus();
1144
+ if (this.draggableResizable) {
1145
+ const wasEnabled = this.draggableResizable.keyboardDragMode;
1146
+ this.draggableResizable.toggleKeyboardDragMode();
1147
+ const isEnabled = this.draggableResizable.keyboardDragMode;
1148
+ if (!wasEnabled && isEnabled) {
1149
+ this.enableMoveMode();
1150
+ }
1151
+
1152
+ // Hide settings menu if open
1153
+ if (this.settingsMenuVisible) {
1154
+ this.hideSettingsMenu();
1155
+ }
1156
+
1157
+ // Focus the window for keyboard navigation
1158
+ this.transcriptWindow.focus();
1310
1159
  }
1311
1160
  }
1312
1161
 
@@ -1342,6 +1191,16 @@ export class TranscriptManager {
1342
1191
  if (this.settingsMenu) {
1343
1192
  this.settingsMenu.style.display = 'block';
1344
1193
  this.settingsMenuVisible = true;
1194
+ if (this.settingsButton) {
1195
+ this.settingsButton.setAttribute('aria-expanded', 'true');
1196
+ }
1197
+ this.updateResizeOptionState();
1198
+ setTimeout(() => {
1199
+ const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
1200
+ if (firstItem) {
1201
+ firstItem.focus();
1202
+ }
1203
+ }, 0);
1345
1204
  return;
1346
1205
  }
1347
1206
  // Create settings menu
@@ -1397,19 +1256,38 @@ export class TranscriptManager {
1397
1256
  className: `${this.player.options.classPrefix}-transcript-settings-item`,
1398
1257
  attributes: {
1399
1258
  'type': 'button',
1400
- 'aria-label': i18n.t('transcript.resizeWindow')
1259
+ 'aria-label': i18n.t('transcript.resizeWindow'),
1260
+ 'aria-pressed': 'false'
1401
1261
  }
1402
1262
  });
1403
1263
  const resizeIcon = createIconElement('resize');
1404
1264
  const resizeText = DOMUtils.createElement('span', {
1265
+ className: `${this.player.options.classPrefix}-transcript-settings-text`,
1405
1266
  textContent: i18n.t('transcript.resizeWindow')
1406
1267
  });
1407
1268
  resizeOption.appendChild(resizeIcon);
1408
1269
  resizeOption.appendChild(resizeText);
1409
- resizeOption.addEventListener('click', () => {
1410
- this.toggleResizeMode();
1411
- this.hideSettingsMenu();
1270
+ resizeOption.addEventListener('click', (event) => {
1271
+ event.preventDefault();
1272
+ event.stopPropagation();
1273
+
1274
+ const enabled = this.toggleResizeMode({ focus: false });
1275
+
1276
+ if (enabled) {
1277
+ this.hideSettingsMenu({ focusButton: false });
1278
+ // Focus transcript window after handles appear
1279
+ this.setManagedTimeout(() => {
1280
+ if (this.transcriptWindow) {
1281
+ this.transcriptWindow.focus();
1282
+ }
1283
+ }, 20);
1284
+ } else {
1285
+ this.hideSettingsMenu({ focusButton: true });
1286
+ }
1412
1287
  });
1288
+ this.resizeOptionButton = resizeOption;
1289
+ this.resizeOptionText = resizeText;
1290
+ this.updateResizeOptionState();
1413
1291
 
1414
1292
  // Close option
1415
1293
  const closeOption = DOMUtils.createElement('button', {
@@ -1449,6 +1327,7 @@ export class TranscriptManager {
1449
1327
  if (this.settingsButton) {
1450
1328
  this.settingsButton.setAttribute('aria-expanded', 'true');
1451
1329
  }
1330
+ this.updateResizeOptionState();
1452
1331
 
1453
1332
  // Focus first menu item
1454
1333
  setTimeout(() => {
@@ -1462,7 +1341,7 @@ export class TranscriptManager {
1462
1341
  /**
1463
1342
  * Hide settings menu
1464
1343
  */
1465
- hideSettingsMenu() {
1344
+ hideSettingsMenu({ focusButton = true } = {}) {
1466
1345
  if (this.settingsMenu) {
1467
1346
  this.settingsMenu.style.display = 'none';
1468
1347
  this.settingsMenuVisible = false;
@@ -1471,8 +1350,10 @@ export class TranscriptManager {
1471
1350
  // Update aria-expanded
1472
1351
  if (this.settingsButton) {
1473
1352
  this.settingsButton.setAttribute('aria-expanded', 'false');
1474
- // Return focus to settings button
1475
- this.settingsButton.focus();
1353
+ if (focusButton) {
1354
+ // Return focus to settings button
1355
+ this.settingsButton.focus();
1356
+ }
1476
1357
  }
1477
1358
  }
1478
1359
  }
@@ -1481,6 +1362,8 @@ export class TranscriptManager {
1481
1362
  * Enable move mode (gives visual feedback)
1482
1363
  */
1483
1364
  enableMoveMode() {
1365
+ this.hideResizeModeIndicator();
1366
+
1484
1367
  // Add visual feedback for move mode
1485
1368
  this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
1486
1369
 
@@ -1503,193 +1386,82 @@ export class TranscriptManager {
1503
1386
  /**
1504
1387
  * Toggle resize mode
1505
1388
  */
1506
- toggleResizeMode() {
1507
- this.resizeEnabled = !this.resizeEnabled;
1508
-
1509
- if (this.resizeEnabled) {
1510
- this.enableResizeHandles();
1511
- } else {
1512
- this.disableResizeHandles();
1389
+ toggleResizeMode({ focus = true } = {}) {
1390
+ if (!this.draggableResizable) {
1391
+ return false;
1513
1392
  }
1514
- }
1515
-
1516
- /**
1517
- * Enable resize handles
1518
- */
1519
- enableResizeHandles() {
1520
- if (!this.transcriptWindow) return;
1521
1393
 
1522
- // Add resize handles if they don't exist
1523
- const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
1524
-
1525
- directions.forEach(direction => {
1526
- const handle = DOMUtils.createElement('div', {
1527
- className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
1528
- attributes: {
1529
- 'data-direction': direction
1530
- }
1531
- });
1532
-
1533
- handle.addEventListener('mousedown', (e) => this.startResize(e, direction));
1534
- handle.addEventListener('touchstart', (e) => this.startResize(e.touches[0], direction));
1535
-
1536
- this.transcriptWindow.appendChild(handle);
1537
- });
1538
-
1539
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizable`);
1540
-
1541
- // Setup resize event handlers
1542
- this.handlers.resizeMove = (e) => {
1543
- if (this.isResizing) {
1544
- this.performResize(e.clientX, e.clientY);
1545
- }
1546
- };
1547
-
1548
- this.handlers.resizeEnd = () => {
1549
- if (this.isResizing) {
1550
- this.stopResize();
1551
- }
1552
- };
1553
-
1554
- this.handlers.resizeTouchMove = (e) => {
1555
- if (this.isResizing) {
1556
- this.performResize(e.touches[0].clientX, e.touches[0].clientY);
1557
- e.preventDefault();
1558
- }
1559
- };
1394
+ if (this.draggableResizable.pointerResizeMode) {
1395
+ this.draggableResizable.disablePointerResizeMode({ focus });
1396
+ return false;
1397
+ }
1560
1398
 
1561
- document.addEventListener('mousemove', this.handlers.resizeMove);
1562
- document.addEventListener('mouseup', this.handlers.resizeEnd);
1563
- document.addEventListener('touchmove', this.handlers.resizeTouchMove);
1564
- document.addEventListener('touchend', this.handlers.resizeEnd);
1399
+ this.draggableResizable.enablePointerResizeMode({ focus });
1400
+ return true;
1565
1401
  }
1566
1402
 
1567
- /**
1568
- * Disable resize handles
1569
- */
1570
- disableResizeHandles() {
1571
- if (!this.transcriptWindow) return;
1572
-
1573
- // Remove all resize handles
1574
- const handles = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-resize-handle`);
1575
- handles.forEach(handle => handle.remove());
1403
+ updateResizeOptionState() {
1404
+ if (!this.resizeOptionButton) {
1405
+ return;
1406
+ }
1407
+
1408
+ const isEnabled = !!(this.draggableResizable && this.draggableResizable.pointerResizeMode);
1409
+ const label = isEnabled
1410
+ ? (i18n.t('transcript.disableResizeWindow') || 'Disable Resize Mode')
1411
+ : i18n.t('transcript.resizeWindow');
1576
1412
 
1577
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizable`);
1413
+ this.resizeOptionButton.setAttribute('aria-pressed', isEnabled ? 'true' : 'false');
1414
+ this.resizeOptionButton.setAttribute('aria-label', label);
1415
+ this.resizeOptionButton.setAttribute('title', label);
1578
1416
 
1579
- // Remove resize event handlers
1580
- if (this.handlers.resizeMove) {
1581
- document.removeEventListener('mousemove', this.handlers.resizeMove);
1582
- }
1583
- if (this.handlers.resizeEnd) {
1584
- document.removeEventListener('mouseup', this.handlers.resizeEnd);
1585
- }
1586
- if (this.handlers.resizeTouchMove) {
1587
- document.removeEventListener('touchmove', this.handlers.resizeTouchMove);
1417
+ if (this.resizeOptionText) {
1418
+ this.resizeOptionText.textContent = label;
1588
1419
  }
1589
- document.removeEventListener('touchend', this.handlers.resizeEnd);
1590
1420
  }
1591
1421
 
1592
- /**
1593
- * Start resizing
1594
- */
1595
- startResize(e, direction) {
1596
- e.stopPropagation();
1597
- e.preventDefault();
1598
-
1599
- this.isResizing = true;
1600
- this.resizeDirection = direction;
1601
- this.resizeStartX = e.clientX;
1602
- this.resizeStartY = e.clientY;
1603
-
1604
- const rect = this.transcriptWindow.getBoundingClientRect();
1605
- this.resizeStartWidth = rect.width;
1606
- this.resizeStartHeight = rect.height;
1607
-
1608
- this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizing`);
1609
- document.body.style.cursor = this.getResizeCursor(direction);
1610
- document.body.style.userSelect = 'none';
1611
- }
1422
+ showResizeModeIndicator() {
1423
+ if (!this.transcriptHeader) {
1424
+ return;
1425
+ }
1612
1426
 
1613
- /**
1614
- * Perform resize
1615
- */
1616
- performResize(clientX, clientY) {
1617
- if (!this.isResizing) return;
1427
+ this.hideResizeModeIndicator();
1618
1428
 
1619
- const deltaX = clientX - this.resizeStartX;
1620
- const deltaY = clientY - this.resizeStartY;
1429
+ const indicator = DOMUtils.createElement('div', {
1430
+ className: `${this.player.options.classPrefix}-transcript-resize-tooltip`,
1431
+ textContent: i18n.t('transcript.resizeModeHint') || 'Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit.'
1432
+ });
1621
1433
 
1622
- let newWidth = this.resizeStartWidth;
1623
- let newHeight = this.resizeStartHeight;
1434
+ this.transcriptHeader.appendChild(indicator);
1435
+ this.resizeModeIndicator = indicator;
1624
1436
 
1625
- const direction = this.resizeDirection;
1437
+ this.resizeModeIndicatorTimeout = this.setManagedTimeout(() => {
1438
+ this.hideResizeModeIndicator();
1439
+ }, 3000);
1440
+ }
1626
1441
 
1627
- // Calculate new dimensions based on direction
1628
- if (direction.includes('e')) {
1629
- newWidth = this.resizeStartWidth + deltaX;
1630
- }
1631
- if (direction.includes('w')) {
1632
- newWidth = this.resizeStartWidth - deltaX;
1633
- }
1634
- if (direction.includes('s')) {
1635
- newHeight = this.resizeStartHeight + deltaY;
1636
- }
1637
- if (direction.includes('n')) {
1638
- newHeight = this.resizeStartHeight - deltaY;
1442
+ hideResizeModeIndicator() {
1443
+ if (this.resizeModeIndicatorTimeout) {
1444
+ this.clearManagedTimeout(this.resizeModeIndicatorTimeout);
1445
+ this.resizeModeIndicatorTimeout = null;
1639
1446
  }
1640
1447
 
1641
- // Apply minimum and maximum constraints
1642
- const minWidth = 300;
1643
- const minHeight = 200;
1644
- const maxWidth = window.innerWidth - 40;
1645
- const maxHeight = window.innerHeight - 40;
1646
-
1647
- newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
1648
- newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
1649
-
1650
- // Apply new dimensions
1651
- this.transcriptWindow.style.width = `${newWidth}px`;
1652
- this.transcriptWindow.style.height = `${newHeight}px`;
1653
- this.transcriptWindow.style.maxWidth = `${newWidth}px`;
1654
- this.transcriptWindow.style.maxHeight = `${newHeight}px`;
1655
-
1656
- // Adjust position if resizing from top or left
1657
- if (direction.includes('w')) {
1658
- const currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
1659
- this.transcriptWindow.style.left = `${currentLeft + (this.resizeStartWidth - newWidth)}px`;
1660
- }
1661
- if (direction.includes('n')) {
1662
- const currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
1663
- this.transcriptWindow.style.top = `${currentTop + (this.resizeStartHeight - newHeight)}px`;
1448
+ if (this.resizeModeIndicator && this.resizeModeIndicator.parentNode) {
1449
+ this.resizeModeIndicator.remove();
1664
1450
  }
1665
- }
1666
1451
 
1667
- /**
1668
- * Stop resizing
1669
- */
1670
- stopResize() {
1671
- this.isResizing = false;
1672
- this.resizeDirection = null;
1673
- this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizing`);
1674
- document.body.style.cursor = '';
1675
- document.body.style.userSelect = '';
1452
+ this.resizeModeIndicator = null;
1676
1453
  }
1677
1454
 
1678
- /**
1679
- * Get cursor style for resize direction
1680
- */
1681
- getResizeCursor(direction) {
1682
- const cursors = {
1683
- 'n': 'ns-resize',
1684
- 's': 'ns-resize',
1685
- 'e': 'ew-resize',
1686
- 'w': 'ew-resize',
1687
- 'ne': 'nesw-resize',
1688
- 'nw': 'nwse-resize',
1689
- 'se': 'nwse-resize',
1690
- 'sw': 'nesw-resize'
1691
- };
1692
- return cursors[direction] || 'default';
1455
+ onPointerResizeModeChange(enabled) {
1456
+ this.updateResizeOptionState();
1457
+
1458
+ if (enabled) {
1459
+ this.showResizeModeIndicator();
1460
+ this.announceLive(i18n.t('transcript.resizeModeEnabled'));
1461
+ } else {
1462
+ this.hideResizeModeIndicator();
1463
+ this.announceLive(i18n.t('transcript.resizeModeDisabled'));
1464
+ }
1693
1465
  }
1694
1466
 
1695
1467
  /**
@@ -2019,31 +1791,28 @@ export class TranscriptManager {
2019
1791
  * Cleanup
2020
1792
  */
2021
1793
  destroy() {
2022
- // Disable modes if active
2023
- if (this.resizeEnabled) {
2024
- this.disableResizeHandles();
1794
+ this.hideResizeModeIndicator();
1795
+
1796
+ // Destroy draggableResizable utility
1797
+ if (this.draggableResizable) {
1798
+ if (this.draggableResizable.pointerResizeMode) {
1799
+ this.draggableResizable.disablePointerResizeMode();
1800
+ this.updateResizeOptionState();
1801
+ }
1802
+ this.draggableResizable.destroy();
1803
+ this.draggableResizable = null;
2025
1804
  }
2026
- if (this.keyboardDragMode) {
2027
- this.disableKeyboardDragMode();
1805
+
1806
+ // Remove custom key handler
1807
+ if (this.transcriptWindow && this.customKeyHandler) {
1808
+ this.transcriptWindow.removeEventListener('keydown', this.customKeyHandler);
1809
+ this.customKeyHandler = null;
2028
1810
  }
2029
1811
 
2030
1812
  // Remove timeupdate listener from player
2031
1813
  if (this.handlers.timeupdate) {
2032
1814
  this.player.off('timeupdate', this.handlers.timeupdate);
2033
1815
  }
2034
-
2035
- // Remove drag event listeners
2036
- if (this.transcriptHeader) {
2037
- if (this.handlers.mousedown) {
2038
- this.transcriptHeader.removeEventListener('mousedown', this.handlers.mousedown);
2039
- }
2040
- if (this.handlers.touchstart) {
2041
- this.transcriptHeader.removeEventListener('touchstart', this.handlers.touchstart);
2042
- }
2043
- if (this.handlers.keydown) {
2044
- this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
2045
- }
2046
- }
2047
1816
 
2048
1817
  // Remove settings button event listeners
2049
1818
  if (this.settingsButton) {
@@ -2060,19 +1829,7 @@ export class TranscriptManager {
2060
1829
  this.styleDialog.removeEventListener('keydown', this.handlers.styleDialogKeydown);
2061
1830
  }
2062
1831
 
2063
- // Remove document-level listeners
2064
- if (this.handlers.mousemove) {
2065
- document.removeEventListener('mousemove', this.handlers.mousemove);
2066
- }
2067
- if (this.handlers.mouseup) {
2068
- document.removeEventListener('mouseup', this.handlers.mouseup);
2069
- }
2070
- if (this.handlers.touchmove) {
2071
- document.removeEventListener('touchmove', this.handlers.touchmove);
2072
- }
2073
- if (this.handlers.touchend) {
2074
- document.removeEventListener('touchend', this.handlers.touchend);
2075
- }
1832
+ // Remove document click listener
2076
1833
  if (this.handlers.documentClick) {
2077
1834
  document.removeEventListener('click', this.handlers.documentClick);
2078
1835
  }
@@ -2100,5 +1857,14 @@ export class TranscriptManager {
2100
1857
  this.transcriptEntries = [];
2101
1858
  this.settingsMenu = null;
2102
1859
  this.styleDialog = null;
1860
+ this.transcriptResizeHandles = [];
1861
+ this.resizeOptionButton = null;
1862
+ this.resizeOptionText = null;
1863
+ this.liveRegion = null;
1864
+ }
1865
+
1866
+ announceLive(message) {
1867
+ if (!this.liveRegion) return;
1868
+ this.liveRegion.textContent = message || '';
2103
1869
  }
2104
1870
  }