vidply 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -7
- package/dist/vidply.css +36 -17
- package/dist/vidply.esm.js +159 -364
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +11 -45
- package/dist/vidply.js +159 -364
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +11 -45
- package/package.json +1 -1
- package/src/controls/CaptionManager.js +216 -218
- package/src/controls/ControlBar.js +144 -26
- package/src/controls/KeyboardManager.js +50 -13
- package/src/core/Player.js +994 -1004
- package/src/icons/Icons.js +10 -10
- package/src/styles/vidply.css +36 -17
|
@@ -109,6 +109,51 @@ export class ControlBar {
|
|
|
109
109
|
}, 100);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Helper method to add keyboard navigation to menus (arrow keys)
|
|
113
|
+
attachMenuKeyboardNavigation(menu) {
|
|
114
|
+
const menuItems = Array.from(menu.querySelectorAll(`.${this.player.options.classPrefix}-menu-item`));
|
|
115
|
+
|
|
116
|
+
if (menuItems.length === 0) return;
|
|
117
|
+
|
|
118
|
+
const handleKeyDown = (e) => {
|
|
119
|
+
const currentIndex = menuItems.indexOf(document.activeElement);
|
|
120
|
+
|
|
121
|
+
switch (e.key) {
|
|
122
|
+
case 'ArrowDown':
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
const nextIndex = (currentIndex + 1) % menuItems.length;
|
|
125
|
+
menuItems[nextIndex].focus();
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'ArrowUp':
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
|
|
131
|
+
menuItems[prevIndex].focus();
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'Home':
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
menuItems[0].focus();
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case 'End':
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
menuItems[menuItems.length - 1].focus();
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'Enter':
|
|
145
|
+
case ' ':
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
if (document.activeElement && menuItems.includes(document.activeElement)) {
|
|
148
|
+
document.activeElement.click();
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
menu.addEventListener('keydown', handleKeyDown);
|
|
155
|
+
}
|
|
156
|
+
|
|
112
157
|
createElement() {
|
|
113
158
|
this.element = DOMUtils.createElement('div', {
|
|
114
159
|
className: `${this.player.options.classPrefix}-controls`,
|
|
@@ -771,7 +816,8 @@ export class ControlBar {
|
|
|
771
816
|
className: `${this.player.options.classPrefix}-menu-item`,
|
|
772
817
|
attributes: {
|
|
773
818
|
'type': 'button',
|
|
774
|
-
'role': 'menuitem'
|
|
819
|
+
'role': 'menuitem',
|
|
820
|
+
'tabindex': '-1'
|
|
775
821
|
}
|
|
776
822
|
});
|
|
777
823
|
|
|
@@ -796,6 +842,17 @@ export class ControlBar {
|
|
|
796
842
|
|
|
797
843
|
menu.appendChild(item);
|
|
798
844
|
}
|
|
845
|
+
|
|
846
|
+
// Add keyboard navigation
|
|
847
|
+
this.attachMenuKeyboardNavigation(menu);
|
|
848
|
+
|
|
849
|
+
// Focus first item
|
|
850
|
+
setTimeout(() => {
|
|
851
|
+
const firstItem = menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
|
|
852
|
+
if (firstItem) {
|
|
853
|
+
firstItem.focus();
|
|
854
|
+
}
|
|
855
|
+
}, 0);
|
|
799
856
|
}
|
|
800
857
|
}
|
|
801
858
|
|
|
@@ -869,6 +926,8 @@ export class ControlBar {
|
|
|
869
926
|
});
|
|
870
927
|
menu.appendChild(noQualityItem);
|
|
871
928
|
} else {
|
|
929
|
+
let activeItem = null;
|
|
930
|
+
|
|
872
931
|
// Auto quality option (only for HLS)
|
|
873
932
|
if (isHLS) {
|
|
874
933
|
const autoItem = DOMUtils.createElement('button', {
|
|
@@ -876,7 +935,8 @@ export class ControlBar {
|
|
|
876
935
|
textContent: i18n.t('player.auto'),
|
|
877
936
|
attributes: {
|
|
878
937
|
'type': 'button',
|
|
879
|
-
'role': 'menuitem'
|
|
938
|
+
'role': 'menuitem',
|
|
939
|
+
'tabindex': '-1'
|
|
880
940
|
}
|
|
881
941
|
});
|
|
882
942
|
|
|
@@ -885,6 +945,7 @@ export class ControlBar {
|
|
|
885
945
|
if (isAuto) {
|
|
886
946
|
autoItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
|
|
887
947
|
autoItem.appendChild(createIconElement('check'));
|
|
948
|
+
activeItem = autoItem;
|
|
888
949
|
}
|
|
889
950
|
|
|
890
951
|
autoItem.addEventListener('click', () => {
|
|
@@ -904,7 +965,8 @@ export class ControlBar {
|
|
|
904
965
|
textContent: quality.name || `${quality.height}p`,
|
|
905
966
|
attributes: {
|
|
906
967
|
'type': 'button',
|
|
907
|
-
'role': 'menuitem'
|
|
968
|
+
'role': 'menuitem',
|
|
969
|
+
'tabindex': '-1'
|
|
908
970
|
}
|
|
909
971
|
});
|
|
910
972
|
|
|
@@ -912,6 +974,7 @@ export class ControlBar {
|
|
|
912
974
|
if (quality.index === currentQuality) {
|
|
913
975
|
item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
|
|
914
976
|
item.appendChild(createIconElement('check'));
|
|
977
|
+
activeItem = item;
|
|
915
978
|
}
|
|
916
979
|
|
|
917
980
|
item.addEventListener('click', () => {
|
|
@@ -923,6 +986,17 @@ export class ControlBar {
|
|
|
923
986
|
|
|
924
987
|
menu.appendChild(item);
|
|
925
988
|
});
|
|
989
|
+
|
|
990
|
+
// Add keyboard navigation
|
|
991
|
+
this.attachMenuKeyboardNavigation(menu);
|
|
992
|
+
|
|
993
|
+
// Focus active item or first item
|
|
994
|
+
setTimeout(() => {
|
|
995
|
+
const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
|
|
996
|
+
if (focusTarget) {
|
|
997
|
+
focusTarget.focus();
|
|
998
|
+
}
|
|
999
|
+
}, 0);
|
|
926
1000
|
}
|
|
927
1001
|
} else {
|
|
928
1002
|
// No quality support
|
|
@@ -1054,6 +1128,14 @@ export class ControlBar {
|
|
|
1054
1128
|
|
|
1055
1129
|
// Close menu on outside click (but not when interacting with controls)
|
|
1056
1130
|
this.attachMenuCloseHandler(menu, button, true);
|
|
1131
|
+
|
|
1132
|
+
// Auto-focus the first select element
|
|
1133
|
+
setTimeout(() => {
|
|
1134
|
+
const firstSelect = menu.querySelector('select');
|
|
1135
|
+
if (firstSelect) {
|
|
1136
|
+
firstSelect.focus();
|
|
1137
|
+
}
|
|
1138
|
+
}, 0);
|
|
1057
1139
|
}
|
|
1058
1140
|
|
|
1059
1141
|
createStyleControl(label, property, options) {
|
|
@@ -1308,6 +1390,7 @@ export class ControlBar {
|
|
|
1308
1390
|
});
|
|
1309
1391
|
|
|
1310
1392
|
const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
1393
|
+
let activeItem = null;
|
|
1311
1394
|
|
|
1312
1395
|
speeds.forEach(speed => {
|
|
1313
1396
|
const item = DOMUtils.createElement('button', {
|
|
@@ -1315,13 +1398,15 @@ export class ControlBar {
|
|
|
1315
1398
|
textContent: this.formatSpeedLabel(speed),
|
|
1316
1399
|
attributes: {
|
|
1317
1400
|
'type': 'button',
|
|
1318
|
-
'role': 'menuitem'
|
|
1401
|
+
'role': 'menuitem',
|
|
1402
|
+
'tabindex': '-1'
|
|
1319
1403
|
}
|
|
1320
1404
|
});
|
|
1321
1405
|
|
|
1322
1406
|
if (speed === this.player.state.playbackSpeed) {
|
|
1323
1407
|
item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
|
|
1324
1408
|
item.appendChild(createIconElement('check'));
|
|
1409
|
+
activeItem = item;
|
|
1325
1410
|
}
|
|
1326
1411
|
|
|
1327
1412
|
item.addEventListener('click', () => {
|
|
@@ -1335,8 +1420,19 @@ export class ControlBar {
|
|
|
1335
1420
|
// Append menu directly to button for proper positioning
|
|
1336
1421
|
button.appendChild(menu);
|
|
1337
1422
|
|
|
1423
|
+
// Add keyboard navigation
|
|
1424
|
+
this.attachMenuKeyboardNavigation(menu);
|
|
1425
|
+
|
|
1338
1426
|
// Close menu on outside click
|
|
1339
1427
|
this.attachMenuCloseHandler(menu, button);
|
|
1428
|
+
|
|
1429
|
+
// Focus the active item or first item
|
|
1430
|
+
setTimeout(() => {
|
|
1431
|
+
const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
|
|
1432
|
+
if (focusTarget) {
|
|
1433
|
+
focusTarget.focus();
|
|
1434
|
+
}
|
|
1435
|
+
}, 0);
|
|
1340
1436
|
}
|
|
1341
1437
|
|
|
1342
1438
|
createCaptionsButton() {
|
|
@@ -1394,19 +1490,23 @@ export class ControlBar {
|
|
|
1394
1490
|
return;
|
|
1395
1491
|
}
|
|
1396
1492
|
|
|
1493
|
+
let activeItem = null;
|
|
1494
|
+
|
|
1397
1495
|
// Off option
|
|
1398
1496
|
const offItem = DOMUtils.createElement('button', {
|
|
1399
1497
|
className: `${this.player.options.classPrefix}-menu-item`,
|
|
1400
1498
|
textContent: i18n.t('captions.off'),
|
|
1401
1499
|
attributes: {
|
|
1402
1500
|
'type': 'button',
|
|
1403
|
-
'role': 'menuitem'
|
|
1501
|
+
'role': 'menuitem',
|
|
1502
|
+
'tabindex': '-1'
|
|
1404
1503
|
}
|
|
1405
1504
|
});
|
|
1406
1505
|
|
|
1407
1506
|
if (!this.player.state.captionsEnabled) {
|
|
1408
1507
|
offItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
|
|
1409
1508
|
offItem.appendChild(createIconElement('check'));
|
|
1509
|
+
activeItem = offItem;
|
|
1410
1510
|
}
|
|
1411
1511
|
|
|
1412
1512
|
offItem.addEventListener('click', () => {
|
|
@@ -1426,7 +1526,8 @@ export class ControlBar {
|
|
|
1426
1526
|
attributes: {
|
|
1427
1527
|
'type': 'button',
|
|
1428
1528
|
'role': 'menuitem',
|
|
1429
|
-
'lang': track.language
|
|
1529
|
+
'lang': track.language,
|
|
1530
|
+
'tabindex': '-1'
|
|
1430
1531
|
}
|
|
1431
1532
|
});
|
|
1432
1533
|
|
|
@@ -1435,6 +1536,7 @@ export class ControlBar {
|
|
|
1435
1536
|
this.player.captionManager.currentTrack === this.player.captionManager.tracks[track.index]) {
|
|
1436
1537
|
item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
|
|
1437
1538
|
item.appendChild(createIconElement('check'));
|
|
1539
|
+
activeItem = item;
|
|
1438
1540
|
}
|
|
1439
1541
|
|
|
1440
1542
|
item.addEventListener('click', () => {
|
|
@@ -1449,8 +1551,19 @@ export class ControlBar {
|
|
|
1449
1551
|
// Append menu directly to button for proper positioning
|
|
1450
1552
|
button.appendChild(menu);
|
|
1451
1553
|
|
|
1452
|
-
//
|
|
1554
|
+
// Add keyboard navigation for the menu
|
|
1555
|
+
this.attachMenuKeyboardNavigation(menu);
|
|
1556
|
+
|
|
1557
|
+
// Close menu on outside click and Escape key
|
|
1453
1558
|
this.attachMenuCloseHandler(menu, button);
|
|
1559
|
+
|
|
1560
|
+
// Focus the active item or the first item
|
|
1561
|
+
setTimeout(() => {
|
|
1562
|
+
const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
|
|
1563
|
+
if (focusTarget) {
|
|
1564
|
+
focusTarget.focus();
|
|
1565
|
+
}
|
|
1566
|
+
}, 0);
|
|
1454
1567
|
}
|
|
1455
1568
|
|
|
1456
1569
|
updateCaptionsButton() {
|
|
@@ -1689,33 +1802,38 @@ export class ControlBar {
|
|
|
1689
1802
|
}
|
|
1690
1803
|
|
|
1691
1804
|
updateVolumeDisplay() {
|
|
1692
|
-
if (!this.controls.volumeFill) return;
|
|
1693
|
-
|
|
1694
1805
|
const percent = this.player.state.volume * 100;
|
|
1695
|
-
this.controls.volumeFill.style.height = `${percent}%`;
|
|
1696
1806
|
|
|
1697
|
-
// Update
|
|
1807
|
+
// Update volume fill bar if it exists
|
|
1808
|
+
if (this.controls.volumeFill) {
|
|
1809
|
+
this.controls.volumeFill.style.height = `${percent}%`;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Update mute button icon (should always work even if slider not shown)
|
|
1698
1813
|
if (this.controls.mute) {
|
|
1699
1814
|
const icon = this.controls.mute.querySelector('.vidply-icon');
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1815
|
+
if (icon) {
|
|
1816
|
+
let iconName;
|
|
1817
|
+
|
|
1818
|
+
if (this.player.state.muted || this.player.state.volume === 0) {
|
|
1819
|
+
iconName = 'volumeMuted';
|
|
1820
|
+
} else if (this.player.state.volume < 0.3) {
|
|
1821
|
+
iconName = 'volumeLow';
|
|
1822
|
+
} else if (this.player.state.volume < 0.7) {
|
|
1823
|
+
iconName = 'volumeMedium';
|
|
1824
|
+
} else {
|
|
1825
|
+
iconName = 'volumeHigh';
|
|
1826
|
+
}
|
|
1711
1827
|
|
|
1712
|
-
|
|
1828
|
+
icon.innerHTML = createIconElement(iconName).innerHTML;
|
|
1713
1829
|
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1830
|
+
this.controls.mute.setAttribute('aria-label',
|
|
1831
|
+
this.player.state.muted ? i18n.t('player.unmute') : i18n.t('player.mute')
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1717
1834
|
}
|
|
1718
1835
|
|
|
1836
|
+
// Update volume slider attribute if it exists
|
|
1719
1837
|
if (this.controls.volumeSlider) {
|
|
1720
1838
|
this.controls.volumeSlider.setAttribute('aria-valuenow', String(Math.round(percent)));
|
|
1721
1839
|
}
|
|
@@ -47,6 +47,11 @@ export class KeyboardManager {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
// Log unhandled keys for debugging (in development)
|
|
52
|
+
if (!handled && this.player.options.debug) {
|
|
53
|
+
console.log('[VidPly] Unhandled key:', e.key, 'code:', e.code, 'shiftKey:', e.shiftKey);
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
executeAction(action, event) {
|
|
@@ -71,14 +76,6 @@ export class KeyboardManager {
|
|
|
71
76
|
this.player.seekBackward();
|
|
72
77
|
return true;
|
|
73
78
|
|
|
74
|
-
case 'seek-forward-large':
|
|
75
|
-
this.player.seekForward(this.player.options.seekIntervalLarge);
|
|
76
|
-
return true;
|
|
77
|
-
|
|
78
|
-
case 'seek-backward-large':
|
|
79
|
-
this.player.seekBackward(this.player.options.seekIntervalLarge);
|
|
80
|
-
return true;
|
|
81
|
-
|
|
82
79
|
case 'mute':
|
|
83
80
|
this.player.toggleMute();
|
|
84
81
|
return true;
|
|
@@ -91,15 +88,27 @@ export class KeyboardManager {
|
|
|
91
88
|
// If only one caption track, toggle on/off
|
|
92
89
|
// If multiple tracks, open caption menu
|
|
93
90
|
if (this.player.captionManager && this.player.captionManager.tracks.length > 1) {
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
// Get captions button from control bar
|
|
92
|
+
const captionsButton = this.player.controlBar && this.player.controlBar.controls.captions;
|
|
93
|
+
if (captionsButton) {
|
|
96
94
|
this.player.controlBar.showCaptionsMenu(captionsButton);
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback to toggle if button doesn't exist
|
|
97
|
+
this.player.toggleCaptions();
|
|
97
98
|
}
|
|
98
99
|
} else {
|
|
99
100
|
this.player.toggleCaptions();
|
|
100
101
|
}
|
|
101
102
|
return true;
|
|
102
103
|
|
|
104
|
+
case 'caption-style-menu':
|
|
105
|
+
// Open caption style menu
|
|
106
|
+
if (this.player.controlBar && this.player.controlBar.controls.captionStyle) {
|
|
107
|
+
this.player.controlBar.showCaptionStyleMenu(this.player.controlBar.controls.captionStyle);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
|
|
103
112
|
case 'speed-up':
|
|
104
113
|
this.player.setPlaybackSpeed(
|
|
105
114
|
Math.min(2, this.player.state.playbackSpeed + 0.25)
|
|
@@ -112,9 +121,37 @@ export class KeyboardManager {
|
|
|
112
121
|
);
|
|
113
122
|
return true;
|
|
114
123
|
|
|
115
|
-
case '
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
case 'speed-menu':
|
|
125
|
+
// Open speed menu
|
|
126
|
+
if (this.player.controlBar && this.player.controlBar.controls.speed) {
|
|
127
|
+
this.player.controlBar.showSpeedMenu(this.player.controlBar.controls.speed);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
|
|
132
|
+
case 'quality-menu':
|
|
133
|
+
// Open quality menu
|
|
134
|
+
if (this.player.controlBar && this.player.controlBar.controls.quality) {
|
|
135
|
+
this.player.controlBar.showQualityMenu(this.player.controlBar.controls.quality);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
|
|
140
|
+
case 'chapters-menu':
|
|
141
|
+
// Open chapters menu
|
|
142
|
+
if (this.player.controlBar && this.player.controlBar.controls.chapters) {
|
|
143
|
+
this.player.controlBar.showChaptersMenu(this.player.controlBar.controls.chapters);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
|
|
148
|
+
case 'transcript-toggle':
|
|
149
|
+
// Toggle transcript
|
|
150
|
+
if (this.player.transcriptManager) {
|
|
151
|
+
this.player.transcriptManager.toggleTranscript();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
118
155
|
|
|
119
156
|
default:
|
|
120
157
|
return false;
|