Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import videojs from 'video.js';
import { VJS_EVENTS } from 'constants/player';

const PRIMARY_SHORTCUTS = [
{ keys: ['Space', 'K'], label: __('Play/Pause (hold to speed up)') },
{ keys: ['J', 'L'], label: __('Seek -10s / +10s') },
{ keys: ['Left', 'Right'], label: __('Seek -5s / +5s') },
{ keys: ['Up', 'Down'], label: __('Volume up/down') },
{ keys: 'M', label: __('Mute/unmute') },
{ keys: 'F', label: __('Fullscreen') },
];

const SECONDARY_SHORTCUTS = [
{ keys: ['Shift', '?'], separator: ' + ', label: __('Show shortcuts') },
{ keys: ['Shift', '.'], separator: ' + ', label: __('Speed up') },
{ keys: ['Shift', ','], separator: ' + ', label: __('Slow down') },
{ keys: '0-9', label: __('Jump to 0-90%') },
{ keys: 'T', label: __('Theater mode') },
{ keys: ['Shift', 'N'], separator: ' + ', label: __('Play next') },
{ keys: ['Shift', 'P'], separator: ' + ', label: __('Play previous') },
{ keys: ',', label: __('Back one frame (paused)') },
{ keys: '.', label: __('Forward one frame (paused)') },
];

function formatKeys(keys, separator) {
const parts = Array.isArray(keys) ? keys : [keys];
const joiner = separator || ' / ';
return parts.map((key) => `<kbd>${key}</kbd>`).join(joiner);
}

function renderShortcutItems(items) {
return items
.map((item) => {
const label = item.label;
const isCompact = typeof label === 'string' && (label.length > 30 || label.indexOf('(') !== -1);
const actionClass = isCompact ? 'vjs-shortcuts-action vjs-shortcuts-action--compact' : 'vjs-shortcuts-action';
return `
<li class="vjs-shortcuts-item">
<div class="vjs-shortcuts-keys">${formatKeys(item.keys, item.separator)}</div>
<div class="${actionClass}">${label}</div>
</li>
`;
})
.join('');
}

function buildOverlayMarkup() {
return `
<div class="vjs-shortcuts-card">
<div class="vjs-shortcuts-header">
<div class="vjs-shortcuts-title">${__('Keyboard shortcuts')}</div>
<div class="vjs-shortcuts-actions">
<button type="button" class="vjs-shortcuts-close" aria-label="${__('Close')}">${__('Close')}</button>
</div>
</div>
<div class="vjs-shortcuts-body">
<ul class="vjs-shortcuts-list vjs-shortcuts-list--primary">
${renderShortcutItems(PRIMARY_SHORTCUTS)}
</ul>
<ul class="vjs-shortcuts-list vjs-shortcuts-list--secondary">
${renderShortcutItems(SECONDARY_SHORTCUTS)}
</ul>
</div>
</div>
`;
}

export function ensureKeyboardShortcutsOverlay(player) {
if (!player) return null;
if (player.keyboardShortcutsOverlay) return player.keyboardShortcutsOverlay;

const overlayEl = videojs.dom.createEl('div', {
className: 'vjs-shortcuts-overlay',
});
overlayEl.setAttribute('role', 'dialog');
overlayEl.setAttribute('aria-label', __('Keyboard shortcuts'));
overlayEl.setAttribute('aria-hidden', 'true');
overlayEl.innerHTML = buildOverlayMarkup();

const playerEl = player.el();
if (!playerEl) return null;
playerEl.appendChild(overlayEl);

const closeButton = overlayEl.querySelector('.vjs-shortcuts-close');
let isOpen = false;

const open = () => {
if (isOpen) return;
isOpen = true;
overlayEl.classList.add('vjs-shortcuts-overlay--open');
overlayEl.setAttribute('aria-hidden', 'false');
player.userActive(true);
};

const close = () => {
if (!isOpen) return;
isOpen = false;
overlayEl.classList.remove('vjs-shortcuts-overlay--open');
overlayEl.setAttribute('aria-hidden', 'true');
};

const toggle = (forceState) => {
if (typeof forceState === 'boolean') {
forceState ? open() : close();
return;
}
isOpen ? close() : open();
};

if (closeButton) {
closeButton.addEventListener('click', () => close());
}

overlayEl.addEventListener('click', (event) => {
if (event.target === overlayEl) close();
});

player.on(VJS_EVENTS.PLAYER_CLOSED, () => close());

const api = {
open,
close,
toggle,
isOpen: () => isOpen,
};

player.keyboardShortcutsOverlay = api;
player.toggleKeyboardShortcutsOverlay = toggle;

return api;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
.video-js {
.vjs-shortcuts-overlay {
align-items: center;
background: rgba(0, 0, 0, 0.65);
display: none;
bottom: 0;
justify-content: center;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 30;
}

.vjs-shortcuts-overlay--open {
display: flex;
}

.vjs-shortcuts-card {
background: rgba(0, 0, 0, 0.88);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
box-sizing: border-box;
color: #fff;
display: flex;
flex-direction: column;
max-height: calc(100% - 16px);
max-width: 520px;
overflow: hidden;
padding: 14px 16px;
width: min(78vw, 520px);
}

.vjs-shortcuts-header {
align-items: center;
column-gap: 8px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
margin-bottom: 4px;
row-gap: 4px;
}

.vjs-shortcuts-actions {
align-items: center;
display: flex;
gap: 6px;
justify-content: flex-end;
justify-self: end;
}

.vjs-shortcuts-title {
font-size: 16px;
font-weight: 600;
}

.vjs-shortcuts-close {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: #fff;
cursor: pointer;
font-size: 12px;
padding: 6px 10px;
}

.vjs-shortcuts-body {
box-sizing: border-box;
display: grid;
gap: 6px;
column-gap: 32px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
}

.vjs-shortcuts-list {
box-sizing: border-box;
display: grid;
gap: 4px;
list-style: none;
margin: 0;
min-width: 0;
padding: 0;
width: 100%;
}

.vjs-shortcuts-list--secondary {
display: grid;
}

.vjs-shortcuts-item {
align-items: center;
display: grid;
column-gap: 3px;
min-width: 0;
row-gap: 3px;
grid-template-columns: minmax(88px, 116px) 1fr;
}

.vjs-shortcuts-keys {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
}

.vjs-shortcuts-keys kbd {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
display: inline-block;
font-size: 11px;
padding: 1px 5px;
}

.vjs-shortcuts-action {
font-size: 12px;
line-height: 1.25;
min-width: 0;
opacity: 0.85;
overflow-wrap: anywhere;
}

.vjs-shortcuts-action--compact {
font-size: 11px;
line-height: 1.15;
}

@media (max-width: 720px) {
.vjs-shortcuts-card {
max-height: calc(100% - 16px);
padding: 12px 12px;
width: calc(100% - 24px);
}

.vjs-shortcuts-body {
grid-template-columns: 1fr;
}

.vjs-shortcuts-title {
font-size: 14px;
}

.vjs-shortcuts-close {
font-size: 11px;
padding: 5px 8px;
}

.vjs-shortcuts-item {
gap: 4px;
grid-template-columns: minmax(88px, 120px) 1fr;
}

.vjs-shortcuts-action {
font-size: 11px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import videojs from 'video.js';
import { VJS_COMP } from 'constants/player';
import { platform } from 'util/platform';

const SettingMenuItem = videojs.getComponent('SettingMenuItem');

class KeyboardShortcutsMenuItem extends SettingMenuItem {
constructor(player, options) {
super(player, {
...options,
label: __('Keyboard shortcuts'),
name: VJS_COMP.KEYBOARD_SHORTCUTS_MENU_ITEM,
icon: '',
});

this.addClass('vjs-setting-keyboard-shortcuts');
this.updateVisibility();
}

createEl() {
const { icon, label } = this.options_;
return videojs.dom.createEl('li', {
className: 'vjs-menu-item vjs-setting-menu-item',
innerHTML: `
<div class="vjs-icon-placeholder ${icon || ''}"></div>
<div class="vjs-setting-menu-label">${this.localize(label)}</div>
<div class="vjs-spacer"></div>
`,
});
}

handleClick() {
const player = this.player_;
if (player && typeof player.toggleKeyboardShortcutsOverlay === 'function') {
player.toggleKeyboardShortcutsOverlay(true);
}

const menuButton = this.menu && this.menu.options_ && this.menu.options_.menuButton;
if (menuButton && typeof menuButton.hideMenu === 'function') {
menuButton.hideMenu();
}
}

updateVisibility() {
if (platform.isMobile()) {
this.hide();
} else {
this.show();
}
}
}

videojs.registerComponent(VJS_COMP.KEYBOARD_SHORTCUTS_MENU_ITEM, KeyboardShortcutsMenuItem);
export default KeyboardShortcutsMenuItem;
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ import './videojs-plus/Style/VideoJS.scss';
import './videojs-plus/Components/Poster.scss';
import './videojs-plus/Components/SettingMenu/SettingMenuButton';
import type { Player } from '../../videojs';
import { VJS_COMP } from 'constants/player';

// (1) Register settings (must be after all the imports above):
import './menuItems/AutoPlayNextMenuItem';
import './menuItems/KeyboardShortcutsMenuItem';
import './menuItems/SnapshotMenuItem';
import './menuItems/LoopMenuItem';

// (2) Define the display order
const DISPLAY_ORDER = ['PlaybackRateSettingItem', 'AutoPlayNextMenuItem', 'LoopMenuItem', 'SnapshotMenuItem'];
const DISPLAY_ORDER = [
'PlaybackRateSettingItem',
VJS_COMP.KEYBOARD_SHORTCUTS_MENU_ITEM,
'AutoPlayNextMenuItem',
'LoopMenuItem',
'SnapshotMenuItem',
];

// ****************************************************************************
// settingsMenu
Expand Down
Loading
Loading