vidply 1.0.0

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.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Keyboard Accessibility Manager
3
+ */
4
+
5
+ export class KeyboardManager {
6
+ constructor(player) {
7
+ this.player = player;
8
+ this.shortcuts = player.options.keyboardShortcuts;
9
+
10
+ this.init();
11
+ }
12
+
13
+ init() {
14
+ this.attachEvents();
15
+ }
16
+
17
+ attachEvents() {
18
+ // Listen for keyboard events on the player container
19
+ this.player.container.addEventListener('keydown', (e) => {
20
+ this.handleKeydown(e);
21
+ });
22
+
23
+ // Make player container focusable
24
+ if (!this.player.container.hasAttribute('tabindex')) {
25
+ this.player.container.setAttribute('tabindex', '0');
26
+ }
27
+ }
28
+
29
+ handleKeydown(e) {
30
+ // Don't handle if target is an input element
31
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
32
+ return;
33
+ }
34
+
35
+ const key = e.key;
36
+ let handled = false;
37
+
38
+ // Check each shortcut category
39
+ for (const [action, keys] of Object.entries(this.shortcuts)) {
40
+ if (keys.includes(key)) {
41
+ handled = this.executeAction(action, e);
42
+ if (handled) {
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+ this.announceAction(action);
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ executeAction(action, event) {
53
+ switch (action) {
54
+ case 'play-pause':
55
+ this.player.toggle();
56
+ return true;
57
+
58
+ case 'volume-up':
59
+ this.player.setVolume(Math.min(1, this.player.state.volume + 0.1));
60
+ return true;
61
+
62
+ case 'volume-down':
63
+ this.player.setVolume(Math.max(0, this.player.state.volume - 0.1));
64
+ return true;
65
+
66
+ case 'seek-forward':
67
+ this.player.seekForward();
68
+ return true;
69
+
70
+ case 'seek-backward':
71
+ this.player.seekBackward();
72
+ return true;
73
+
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
+ case 'mute':
83
+ this.player.toggleMute();
84
+ return true;
85
+
86
+ case 'fullscreen':
87
+ this.player.toggleFullscreen();
88
+ return true;
89
+
90
+ case 'captions':
91
+ // If only one caption track, toggle on/off
92
+ // If multiple tracks, open caption menu
93
+ if (this.player.captionManager && this.player.captionManager.tracks.length > 1) {
94
+ const captionsButton = document.querySelector('.vidply-captions');
95
+ if (captionsButton && this.player.controlBar) {
96
+ this.player.controlBar.showCaptionsMenu(captionsButton);
97
+ }
98
+ } else {
99
+ this.player.toggleCaptions();
100
+ }
101
+ return true;
102
+
103
+ case 'speed-up':
104
+ this.player.setPlaybackSpeed(
105
+ Math.min(2, this.player.state.playbackSpeed + 0.25)
106
+ );
107
+ return true;
108
+
109
+ case 'speed-down':
110
+ this.player.setPlaybackSpeed(
111
+ Math.max(0.25, this.player.state.playbackSpeed - 0.25)
112
+ );
113
+ return true;
114
+
115
+ case 'settings':
116
+ this.player.showSettings();
117
+ return true;
118
+
119
+ default:
120
+ return false;
121
+ }
122
+ }
123
+
124
+ announceAction(action) {
125
+ if (!this.player.options.screenReaderAnnouncements) return;
126
+
127
+ let message = '';
128
+
129
+ switch (action) {
130
+ case 'play-pause':
131
+ message = this.player.state.playing ? 'Playing' : 'Paused';
132
+ break;
133
+ case 'volume-up':
134
+ message = `Volume ${Math.round(this.player.state.volume * 100)}%`;
135
+ break;
136
+ case 'volume-down':
137
+ message = `Volume ${Math.round(this.player.state.volume * 100)}%`;
138
+ break;
139
+ case 'mute':
140
+ message = this.player.state.muted ? 'Muted' : 'Unmuted';
141
+ break;
142
+ case 'fullscreen':
143
+ message = this.player.state.fullscreen ? 'Fullscreen' : 'Exit fullscreen';
144
+ break;
145
+ case 'captions':
146
+ message = this.player.state.captionsEnabled ? 'Captions on' : 'Captions off';
147
+ break;
148
+ case 'speed-up':
149
+ case 'speed-down':
150
+ message = `Speed ${this.player.state.playbackSpeed}x`;
151
+ break;
152
+ }
153
+
154
+ if (message) {
155
+ this.announce(message);
156
+ }
157
+ }
158
+
159
+ announce(message, priority = 'polite') {
160
+ // Create or get announcement element
161
+ let announcer = document.getElementById('vidply-announcer');
162
+
163
+ if (!announcer) {
164
+ announcer = document.createElement('div');
165
+ announcer.id = 'vidply-announcer';
166
+ announcer.className = 'vidply-sr-only';
167
+ announcer.setAttribute('aria-live', priority);
168
+ announcer.setAttribute('aria-atomic', 'true');
169
+ announcer.style.cssText = `
170
+ position: absolute;
171
+ left: -10000px;
172
+ width: 1px;
173
+ height: 1px;
174
+ overflow: hidden;
175
+ `;
176
+ document.body.appendChild(announcer);
177
+ }
178
+
179
+ // Clear and set new message
180
+ announcer.textContent = '';
181
+ setTimeout(() => {
182
+ announcer.textContent = message;
183
+ }, 100);
184
+ }
185
+
186
+ updateShortcut(action, keys) {
187
+ if (Array.isArray(keys)) {
188
+ this.shortcuts[action] = keys;
189
+ }
190
+ }
191
+
192
+ destroy() {
193
+ // Event listeners are automatically removed when the container is destroyed
194
+ }
195
+ }
196
+
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Settings Dialog Component
3
+ */
4
+
5
+ import { DOMUtils } from '../utils/DOMUtils.js';
6
+ import { createIconElement } from '../icons/Icons.js';
7
+ import { i18n } from '../i18n/i18n.js';
8
+
9
+ export class SettingsDialog {
10
+ constructor(player) {
11
+ this.player = player;
12
+ this.element = null;
13
+ this.isOpen = false;
14
+
15
+ this.init();
16
+ }
17
+
18
+ init() {
19
+ this.createElement();
20
+ }
21
+
22
+ createElement() {
23
+ // Create overlay
24
+ this.overlay = DOMUtils.createElement('div', {
25
+ className: `${this.player.options.classPrefix}-settings-overlay`,
26
+ attributes: {
27
+ 'role': 'dialog',
28
+ 'aria-modal': 'true',
29
+ 'aria-label': i18n.t('settings.title')
30
+ }
31
+ });
32
+
33
+ this.overlay.style.display = 'none';
34
+
35
+ // Create dialog
36
+ this.element = DOMUtils.createElement('div', {
37
+ className: `${this.player.options.classPrefix}-settings-dialog`
38
+ });
39
+
40
+ // Header
41
+ const header = DOMUtils.createElement('div', {
42
+ className: `${this.player.options.classPrefix}-settings-header`
43
+ });
44
+
45
+ const title = DOMUtils.createElement('h2', {
46
+ textContent: i18n.t('settings.title'),
47
+ attributes: {
48
+ 'id': `${this.player.options.classPrefix}-settings-title`
49
+ }
50
+ });
51
+
52
+ const closeButton = DOMUtils.createElement('button', {
53
+ className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-settings-close`,
54
+ attributes: {
55
+ 'type': 'button',
56
+ 'aria-label': i18n.t('settings.close')
57
+ }
58
+ });
59
+ closeButton.appendChild(createIconElement('close'));
60
+ closeButton.addEventListener('click', () => this.hide());
61
+
62
+ header.appendChild(title);
63
+ header.appendChild(closeButton);
64
+
65
+ // Content
66
+ const content = DOMUtils.createElement('div', {
67
+ className: `${this.player.options.classPrefix}-settings-content`
68
+ });
69
+
70
+ content.appendChild(this.createSpeedSettings());
71
+
72
+ if (this.player.captionManager && this.player.captionManager.tracks.length > 0) {
73
+ content.appendChild(this.createCaptionSettings());
74
+ }
75
+
76
+ // Footer
77
+ const footer = DOMUtils.createElement('div', {
78
+ className: `${this.player.options.classPrefix}-settings-footer`
79
+ });
80
+
81
+ const resetButton = DOMUtils.createElement('button', {
82
+ className: `${this.player.options.classPrefix}-button`,
83
+ textContent: i18n.t('settings.reset'),
84
+ attributes: {
85
+ 'type': 'button'
86
+ }
87
+ });
88
+ resetButton.addEventListener('click', () => this.resetSettings());
89
+
90
+ footer.appendChild(resetButton);
91
+
92
+ // Assemble dialog
93
+ this.element.appendChild(header);
94
+ this.element.appendChild(content);
95
+ this.element.appendChild(footer);
96
+
97
+ this.overlay.appendChild(this.element);
98
+ this.player.container.appendChild(this.overlay);
99
+
100
+ // Attach events
101
+ this.overlay.addEventListener('click', (e) => {
102
+ if (e.target === this.overlay) {
103
+ this.hide();
104
+ }
105
+ });
106
+
107
+ // Escape key to close
108
+ document.addEventListener('keydown', (e) => {
109
+ if (e.key === 'Escape' && this.isOpen) {
110
+ this.hide();
111
+ }
112
+ });
113
+ }
114
+
115
+ formatSpeedLabel(speed) {
116
+ // Special case: 1x is "Normal" (translated)
117
+ if (speed === 1) {
118
+ return i18n.t('speeds.normal');
119
+ }
120
+
121
+ // For other speeds, format with locale-specific decimal separator
122
+ const speedStr = speed.toLocaleString(i18n.getLanguage(), {
123
+ minimumFractionDigits: 0,
124
+ maximumFractionDigits: 2
125
+ });
126
+
127
+ return `${speedStr}×`;
128
+ }
129
+
130
+ createSpeedSettings() {
131
+ const section = DOMUtils.createElement('div', {
132
+ className: `${this.player.options.classPrefix}-settings-section`
133
+ });
134
+
135
+ const label = DOMUtils.createElement('label', {
136
+ textContent: i18n.t('settings.speed'),
137
+ attributes: {
138
+ 'for': `${this.player.options.classPrefix}-speed-select`
139
+ }
140
+ });
141
+
142
+ const select = DOMUtils.createElement('select', {
143
+ className: `${this.player.options.classPrefix}-settings-select`,
144
+ attributes: {
145
+ 'id': `${this.player.options.classPrefix}-speed-select`
146
+ }
147
+ });
148
+
149
+ const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
150
+
151
+ speeds.forEach(speed => {
152
+ const option = DOMUtils.createElement('option', {
153
+ textContent: this.formatSpeedLabel(speed),
154
+ attributes: {
155
+ 'value': String(speed)
156
+ }
157
+ });
158
+
159
+ if (speed === this.player.state.playbackSpeed) {
160
+ option.selected = true;
161
+ }
162
+
163
+ select.appendChild(option);
164
+ });
165
+
166
+ select.addEventListener('change', (e) => {
167
+ this.player.setPlaybackSpeed(parseFloat(e.target.value));
168
+ });
169
+
170
+ section.appendChild(label);
171
+ section.appendChild(select);
172
+
173
+ return section;
174
+ }
175
+
176
+ createCaptionSettings() {
177
+ const section = DOMUtils.createElement('div', {
178
+ className: `${this.player.options.classPrefix}-settings-section`
179
+ });
180
+
181
+ const heading = DOMUtils.createElement('h3', {
182
+ textContent: i18n.t('settings.captions')
183
+ });
184
+
185
+ section.appendChild(heading);
186
+
187
+ // Caption track selection
188
+ const trackLabel = DOMUtils.createElement('label', {
189
+ textContent: i18n.t('captions.select'),
190
+ attributes: {
191
+ 'for': `${this.player.options.classPrefix}-caption-track-select`
192
+ }
193
+ });
194
+
195
+ const trackSelect = DOMUtils.createElement('select', {
196
+ className: `${this.player.options.classPrefix}-settings-select`,
197
+ attributes: {
198
+ 'id': `${this.player.options.classPrefix}-caption-track-select`
199
+ }
200
+ });
201
+
202
+ // Off option
203
+ const offOption = DOMUtils.createElement('option', {
204
+ textContent: i18n.t('captions.off'),
205
+ attributes: { 'value': '-1' }
206
+ });
207
+ trackSelect.appendChild(offOption);
208
+
209
+ // Available tracks
210
+ const tracks = this.player.captionManager.getAvailableTracks();
211
+ tracks.forEach(track => {
212
+ const option = DOMUtils.createElement('option', {
213
+ textContent: track.label,
214
+ attributes: { 'value': String(track.index) }
215
+ });
216
+ trackSelect.appendChild(option);
217
+ });
218
+
219
+ trackSelect.addEventListener('change', (e) => {
220
+ const index = parseInt(e.target.value);
221
+ if (index === -1) {
222
+ this.player.disableCaptions();
223
+ } else {
224
+ this.player.captionManager.switchTrack(index);
225
+ }
226
+ });
227
+
228
+ section.appendChild(trackLabel);
229
+ section.appendChild(trackSelect);
230
+
231
+ // Font size
232
+ section.appendChild(this.createCaptionStyleControl('fontSize', i18n.t('captions.fontSize'), [
233
+ { label: i18n.t('fontSizes.small'), value: '80%' },
234
+ { label: i18n.t('fontSizes.medium'), value: '100%' },
235
+ { label: i18n.t('fontSizes.large'), value: '120%' },
236
+ { label: i18n.t('fontSizes.xlarge'), value: '150%' }
237
+ ]));
238
+
239
+ // Font family
240
+ section.appendChild(this.createCaptionStyleControl('fontFamily', i18n.t('captions.fontFamily'), [
241
+ { label: i18n.t('fontFamilies.sansSerif'), value: 'sans-serif' },
242
+ { label: i18n.t('fontFamilies.serif'), value: 'serif' },
243
+ { label: i18n.t('fontFamilies.monospace'), value: 'monospace' }
244
+ ]));
245
+
246
+ // Color controls
247
+ section.appendChild(this.createColorControl('color', i18n.t('captions.color')));
248
+ section.appendChild(this.createColorControl('backgroundColor', i18n.t('captions.backgroundColor')));
249
+
250
+ // Opacity
251
+ section.appendChild(this.createRangeControl('opacity', i18n.t('captions.opacity'), 0, 1, 0.1));
252
+
253
+ return section;
254
+ }
255
+
256
+ createCaptionStyleControl(property, label, options) {
257
+ const wrapper = DOMUtils.createElement('div', {
258
+ className: `${this.player.options.classPrefix}-settings-control`
259
+ });
260
+
261
+ const labelEl = DOMUtils.createElement('label', {
262
+ textContent: label,
263
+ attributes: {
264
+ 'for': `${this.player.options.classPrefix}-caption-${property}`
265
+ }
266
+ });
267
+
268
+ const select = DOMUtils.createElement('select', {
269
+ className: `${this.player.options.classPrefix}-settings-select`,
270
+ attributes: {
271
+ 'id': `${this.player.options.classPrefix}-caption-${property}`
272
+ }
273
+ });
274
+
275
+ options.forEach(opt => {
276
+ const option = DOMUtils.createElement('option', {
277
+ textContent: opt.label,
278
+ attributes: { 'value': opt.value }
279
+ });
280
+
281
+ if (opt.value === this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]) {
282
+ option.selected = true;
283
+ }
284
+
285
+ select.appendChild(option);
286
+ });
287
+
288
+ select.addEventListener('change', (e) => {
289
+ this.player.captionManager.setCaptionStyle(property, e.target.value);
290
+ });
291
+
292
+ wrapper.appendChild(labelEl);
293
+ wrapper.appendChild(select);
294
+
295
+ return wrapper;
296
+ }
297
+
298
+ createColorControl(property, label) {
299
+ const wrapper = DOMUtils.createElement('div', {
300
+ className: `${this.player.options.classPrefix}-settings-control`
301
+ });
302
+
303
+ const labelEl = DOMUtils.createElement('label', {
304
+ textContent: label,
305
+ attributes: {
306
+ 'for': `${this.player.options.classPrefix}-caption-${property}`
307
+ }
308
+ });
309
+
310
+ const input = DOMUtils.createElement('input', {
311
+ className: `${this.player.options.classPrefix}-settings-color`,
312
+ attributes: {
313
+ 'type': 'color',
314
+ 'id': `${this.player.options.classPrefix}-caption-${property}`,
315
+ 'value': this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]
316
+ }
317
+ });
318
+
319
+ input.addEventListener('change', (e) => {
320
+ this.player.captionManager.setCaptionStyle(property, e.target.value);
321
+ });
322
+
323
+ wrapper.appendChild(labelEl);
324
+ wrapper.appendChild(input);
325
+
326
+ return wrapper;
327
+ }
328
+
329
+ createRangeControl(property, label, min, max, step) {
330
+ const wrapper = DOMUtils.createElement('div', {
331
+ className: `${this.player.options.classPrefix}-settings-control`
332
+ });
333
+
334
+ const labelEl = DOMUtils.createElement('label', {
335
+ textContent: label,
336
+ attributes: {
337
+ 'for': `${this.player.options.classPrefix}-caption-${property}`
338
+ }
339
+ });
340
+
341
+ const input = DOMUtils.createElement('input', {
342
+ className: `${this.player.options.classPrefix}-settings-range`,
343
+ attributes: {
344
+ 'type': 'range',
345
+ 'id': `${this.player.options.classPrefix}-caption-${property}`,
346
+ 'min': String(min),
347
+ 'max': String(max),
348
+ 'step': String(step),
349
+ 'value': String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
350
+ }
351
+ });
352
+
353
+ const valueDisplay = DOMUtils.createElement('span', {
354
+ className: `${this.player.options.classPrefix}-settings-value`,
355
+ textContent: String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
356
+ });
357
+
358
+ input.addEventListener('input', (e) => {
359
+ const value = parseFloat(e.target.value);
360
+ valueDisplay.textContent = value.toFixed(1);
361
+ this.player.captionManager.setCaptionStyle(property, value);
362
+ });
363
+
364
+ wrapper.appendChild(labelEl);
365
+ wrapper.appendChild(input);
366
+ wrapper.appendChild(valueDisplay);
367
+
368
+ return wrapper;
369
+ }
370
+
371
+ resetSettings() {
372
+ // Reset to default values
373
+ this.player.setPlaybackSpeed(1);
374
+
375
+ if (this.player.captionManager) {
376
+ this.player.captionManager.setCaptionStyle('fontSize', '100%');
377
+ this.player.captionManager.setCaptionStyle('fontFamily', 'sans-serif');
378
+ this.player.captionManager.setCaptionStyle('color', '#FFFFFF');
379
+ this.player.captionManager.setCaptionStyle('backgroundColor', '#000000');
380
+ this.player.captionManager.setCaptionStyle('opacity', 0.8);
381
+ }
382
+
383
+ // Refresh dialog
384
+ this.hide();
385
+ setTimeout(() => this.show(), 100);
386
+ }
387
+
388
+ show() {
389
+ this.overlay.style.display = 'flex';
390
+ this.isOpen = true;
391
+
392
+ // Focus the close button
393
+ const closeButton = this.element.querySelector(`.${this.player.options.classPrefix}-settings-close`);
394
+ if (closeButton) {
395
+ closeButton.focus();
396
+ }
397
+
398
+ this.player.emit('settingsopen');
399
+ }
400
+
401
+ hide() {
402
+ this.overlay.style.display = 'none';
403
+ this.isOpen = false;
404
+
405
+ // Return focus to settings button
406
+ this.player.container.focus();
407
+
408
+ this.player.emit('settingsclose');
409
+ }
410
+
411
+ destroy() {
412
+ if (this.overlay && this.overlay.parentNode) {
413
+ this.overlay.parentNode.removeChild(this.overlay);
414
+ }
415
+ }
416
+ }
417
+