Work on menus
This commit is contained in:
243
src/js/controls.js
vendored
243
src/js/controls.js
vendored
@ -370,18 +370,22 @@ const controls = {
|
||||
type: 'button',
|
||||
role: 'menuitemradio',
|
||||
class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),
|
||||
value,
|
||||
'aria-checked': checked,
|
||||
})
|
||||
value,
|
||||
}),
|
||||
);
|
||||
|
||||
const flex = createElement('span');
|
||||
|
||||
// We have to set as HTML incase of special characters
|
||||
item.innerHTML = title;
|
||||
flex.innerHTML = title;
|
||||
|
||||
if (is.element(badge)) {
|
||||
item.appendChild(badge);
|
||||
flex.appendChild(badge);
|
||||
}
|
||||
|
||||
item.appendChild(flex);
|
||||
|
||||
Object.defineProperty(item, 'checked', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
@ -399,6 +403,34 @@ const controls = {
|
||||
},
|
||||
});
|
||||
|
||||
this.listeners.bind(
|
||||
item,
|
||||
'click',
|
||||
() => {
|
||||
item.checked = true;
|
||||
|
||||
switch (type) {
|
||||
case 'language':
|
||||
this.currentTrack = Number(value);
|
||||
break;
|
||||
|
||||
case 'quality':
|
||||
this.quality = value;
|
||||
break;
|
||||
|
||||
case 'speed':
|
||||
this.speed = parseFloat(value);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
controls.showMenuPanel.call(this, 'home');
|
||||
},
|
||||
type,
|
||||
);
|
||||
|
||||
list.appendChild(item);
|
||||
},
|
||||
|
||||
@ -657,95 +689,6 @@ const controls = {
|
||||
toggleHidden(this.elements.settings.buttons[setting], !toggle);
|
||||
},
|
||||
|
||||
// Set the quality menu
|
||||
setQualityMenu(options) {
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panels.quality)) {
|
||||
console.warn('Not an element');
|
||||
return;
|
||||
}
|
||||
|
||||
const type = 'quality';
|
||||
const list = this.elements.settings.panels.quality.querySelector('[role="menu"]');
|
||||
|
||||
// Set options if passed and filter based on uniqueness and config
|
||||
if (is.array(options)) {
|
||||
this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));
|
||||
}
|
||||
|
||||
// Toggle the pane and tab
|
||||
console.warn(this.options.quality);
|
||||
const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
|
||||
controls.toggleMenuButton.call(this, type, toggle);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If we're hiding, nothing more to do
|
||||
if (!toggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
|
||||
// Get the badge HTML for HD, 4K etc
|
||||
const getBadge = quality => {
|
||||
const label = i18n.get(`qualityBadge.${quality}`, this.config);
|
||||
|
||||
if (!label.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return controls.createBadge.call(this, label);
|
||||
};
|
||||
|
||||
// Sort options by the config and then render options
|
||||
this.options.quality
|
||||
.sort((a, b) => {
|
||||
const sorting = this.config.quality.options;
|
||||
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||
})
|
||||
.forEach(quality => {
|
||||
controls.createMenuItem.call(this, {
|
||||
value: quality,
|
||||
list,
|
||||
type,
|
||||
title: controls.getLabel.call(this, 'quality', quality),
|
||||
badge: getBadge(quality),
|
||||
});
|
||||
});
|
||||
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
|
||||
// Translate a value into a nice label
|
||||
getLabel(setting, value) {
|
||||
switch (setting) {
|
||||
case 'speed':
|
||||
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
||||
|
||||
case 'quality':
|
||||
if (is.number(value)) {
|
||||
const label = i18n.get(`qualityLabel.${value}`, this.config);
|
||||
|
||||
if (!label.length) {
|
||||
return `${value}p`;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
return toTitleCase(value);
|
||||
|
||||
case 'captions':
|
||||
return captions.getLabel.call(this);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Update the selected setting
|
||||
updateSetting(setting, container, input) {
|
||||
const pane = this.elements.settings.panels[setting];
|
||||
@ -797,6 +740,93 @@ const controls = {
|
||||
}
|
||||
},
|
||||
|
||||
// Translate a value into a nice label
|
||||
getLabel(setting, value) {
|
||||
switch (setting) {
|
||||
case 'speed':
|
||||
return value === 1 ? i18n.get('normal', this.config) : `${value}×`;
|
||||
|
||||
case 'quality':
|
||||
if (is.number(value)) {
|
||||
const label = i18n.get(`qualityLabel.${value}`, this.config);
|
||||
|
||||
if (!label.length) {
|
||||
return `${value}p`;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
return toTitleCase(value);
|
||||
|
||||
case 'captions':
|
||||
return captions.getLabel.call(this);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Set the quality menu
|
||||
setQualityMenu(options) {
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panels.quality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = 'quality';
|
||||
const list = this.elements.settings.panels.quality.querySelector('[role="menu"]');
|
||||
|
||||
// Set options if passed and filter based on uniqueness and config
|
||||
if (is.array(options)) {
|
||||
this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));
|
||||
}
|
||||
|
||||
// Toggle the pane and tab
|
||||
const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
|
||||
controls.toggleMenuButton.call(this, type, toggle);
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If we're hiding, nothing more to do
|
||||
if (!toggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the badge HTML for HD, 4K etc
|
||||
const getBadge = quality => {
|
||||
const label = i18n.get(`qualityBadge.${quality}`, this.config);
|
||||
|
||||
if (!label.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return controls.createBadge.call(this, label);
|
||||
};
|
||||
|
||||
// Sort options by the config and then render options
|
||||
this.options.quality
|
||||
.sort((a, b) => {
|
||||
const sorting = this.config.quality.options;
|
||||
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||
})
|
||||
.forEach(quality => {
|
||||
controls.createMenuItem.call(this, {
|
||||
value: quality,
|
||||
list,
|
||||
type,
|
||||
title: controls.getLabel.call(this, 'quality', quality),
|
||||
badge: getBadge(quality),
|
||||
});
|
||||
});
|
||||
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
|
||||
// Set the looping options
|
||||
/* setLoopMenu() {
|
||||
// Menu required
|
||||
@ -846,13 +876,19 @@ const controls = {
|
||||
|
||||
// Set a list of available captions languages
|
||||
setCaptionsMenu() {
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panels.captions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Captions or language? Currently it's mixed
|
||||
const type = 'captions';
|
||||
const list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
|
||||
const tracks = captions.getTracks.call(this);
|
||||
const toggle = Boolean(tracks.length);
|
||||
|
||||
// Toggle the pane and tab
|
||||
controls.toggleMenuButton.call(this, type, tracks.length);
|
||||
controls.toggleMenuButton.call(this, type, toggle);
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
@ -861,7 +897,7 @@ const controls = {
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If there's no captions, bail
|
||||
if (!tracks.length) {
|
||||
if (!toggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -892,17 +928,13 @@ const controls = {
|
||||
|
||||
// Set a list of available captions languages
|
||||
setSpeedMenu(options) {
|
||||
// Do nothing if not selected
|
||||
if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panels.speed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = 'speed';
|
||||
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
|
||||
|
||||
// Set the speed options
|
||||
if (is.array(options)) {
|
||||
@ -918,6 +950,9 @@ const controls = {
|
||||
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
|
||||
controls.toggleMenuButton.call(this, type, toggle);
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
|
||||
// Check if we need to toggle the parent
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
@ -926,12 +961,6 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the list to populate
|
||||
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
|
||||
// Create items
|
||||
this.options.speed.forEach(speed => {
|
||||
controls.createMenuItem.call(this, {
|
||||
@ -1069,7 +1098,6 @@ const controls = {
|
||||
|
||||
// Set attributes on current tab
|
||||
toggleHidden(current, true);
|
||||
// current.setAttribute('tabindex', -1);
|
||||
|
||||
// Set attributes on target
|
||||
toggleHidden(target, false);
|
||||
@ -1238,6 +1266,7 @@ const controls = {
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||
role: 'menuitem',
|
||||
'aria-haspopup': true,
|
||||
hidden: '',
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -243,7 +243,8 @@ class Listeners {
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.player.timers.controls);
|
||||
// Timer to prevent flicker when seeking
|
||||
|
||||
// Set new timer to prevent flicker when seeking
|
||||
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
},
|
||||
);
|
||||
@ -394,58 +395,58 @@ class Listeners {
|
||||
});
|
||||
}
|
||||
|
||||
// Run default and custom handlers
|
||||
proxy(event, defaultHandler, customHandlerKey) {
|
||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
let returned = true;
|
||||
|
||||
// Execute custom handler
|
||||
if (hasCustomHandler) {
|
||||
returned = customHandler.call(this.player, event);
|
||||
}
|
||||
|
||||
// Only call default handler if not prevented in custom handler
|
||||
if (returned && is.function(defaultHandler)) {
|
||||
defaultHandler.call(this.player, event);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger custom and default handlers
|
||||
bind(element, type, defaultHandler, customHandlerKey, passive = true) {
|
||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
|
||||
on.call(
|
||||
this.player,
|
||||
element,
|
||||
type,
|
||||
event => this.proxy(event, defaultHandler, customHandlerKey),
|
||||
passive && !hasCustomHandler,
|
||||
);
|
||||
}
|
||||
|
||||
// Listen for control events
|
||||
controls() {
|
||||
// IE doesn't support input event, so we fallback to change
|
||||
const inputEvent = browser.isIE ? 'change' : 'input';
|
||||
|
||||
// Run default and custom handlers
|
||||
const proxy = (event, defaultHandler, customHandlerKey) => {
|
||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
let returned = true;
|
||||
|
||||
// Execute custom handler
|
||||
if (hasCustomHandler) {
|
||||
returned = customHandler.call(this.player, event);
|
||||
}
|
||||
|
||||
// Only call default handler if not prevented in custom handler
|
||||
if (returned && is.function(defaultHandler)) {
|
||||
defaultHandler.call(this.player, event);
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger custom and default handlers
|
||||
const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {
|
||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
|
||||
on.call(
|
||||
this.player,
|
||||
element,
|
||||
type,
|
||||
event => proxy(event, defaultHandler, customHandlerKey),
|
||||
passive && !hasCustomHandler,
|
||||
);
|
||||
};
|
||||
|
||||
// Play/pause toggle
|
||||
Array.from(this.player.elements.buttons.play).forEach(button => {
|
||||
bind(button, 'click', this.player.togglePlay, 'play');
|
||||
this.bind(button, 'click', this.player.togglePlay, 'play');
|
||||
});
|
||||
|
||||
// Pause
|
||||
bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
|
||||
this.bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
|
||||
|
||||
// Rewind
|
||||
bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
|
||||
this.bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
|
||||
|
||||
// Rewind
|
||||
bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
|
||||
this.bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
|
||||
|
||||
// Mute toggle
|
||||
bind(
|
||||
this.bind(
|
||||
this.player.elements.buttons.mute,
|
||||
'click',
|
||||
() => {
|
||||
@ -455,10 +456,10 @@ class Listeners {
|
||||
);
|
||||
|
||||
// Captions toggle
|
||||
bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
|
||||
this.bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
|
||||
|
||||
// Fullscreen toggle
|
||||
bind(
|
||||
this.bind(
|
||||
this.player.elements.buttons.fullscreen,
|
||||
'click',
|
||||
() => {
|
||||
@ -468,7 +469,7 @@ class Listeners {
|
||||
);
|
||||
|
||||
// Picture-in-Picture
|
||||
bind(
|
||||
this.bind(
|
||||
this.player.elements.buttons.pip,
|
||||
'click',
|
||||
() => {
|
||||
@ -478,62 +479,22 @@ class Listeners {
|
||||
);
|
||||
|
||||
// Airplay
|
||||
bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
|
||||
this.bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
|
||||
|
||||
// Settings menu
|
||||
bind(this.player.elements.buttons.settings, 'click', event => {
|
||||
this.bind(this.player.elements.buttons.settings, 'click', event => {
|
||||
controls.toggleMenu.call(this.player, event);
|
||||
});
|
||||
|
||||
// Settings menu
|
||||
bind(this.player.elements.settings.popup, 'click', event => {
|
||||
event.stopPropagation();
|
||||
|
||||
// Go back to home tab on click
|
||||
const showHomeTab = () => {
|
||||
controls.showMenuPanel.call(this.player, 'home');
|
||||
};
|
||||
|
||||
// Settings menu items - use event delegation as items are added/removed
|
||||
if (matches(event.target, this.player.config.selectors.inputs.language)) {
|
||||
proxy(
|
||||
event,
|
||||
() => {
|
||||
this.player.currentTrack = Number(event.target.value);
|
||||
showHomeTab();
|
||||
},
|
||||
'language',
|
||||
);
|
||||
} else if (matches(event.target, this.player.config.selectors.inputs.quality)) {
|
||||
proxy(
|
||||
event,
|
||||
() => {
|
||||
this.player.quality = event.target.value;
|
||||
showHomeTab();
|
||||
},
|
||||
'quality',
|
||||
);
|
||||
} else if (matches(event.target, this.player.config.selectors.inputs.speed)) {
|
||||
proxy(
|
||||
event,
|
||||
() => {
|
||||
this.player.speed = parseFloat(event.target.value);
|
||||
showHomeTab();
|
||||
},
|
||||
'speed',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Set range input alternative "value", which matches the tooltip time (#954)
|
||||
bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||
this.bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||
event.currentTarget.setAttribute('seek-value', percent);
|
||||
});
|
||||
|
||||
// Pause while seeking
|
||||
bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
||||
this.bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
|
||||
const seek = event.currentTarget;
|
||||
|
||||
const code = event.keyCode ? event.keyCode : event.which;
|
||||
@ -559,7 +520,7 @@ class Listeners {
|
||||
});
|
||||
|
||||
// Seek
|
||||
bind(
|
||||
this.bind(
|
||||
this.player.elements.inputs.seek,
|
||||
inputEvent,
|
||||
event => {
|
||||
@ -582,7 +543,7 @@ class Listeners {
|
||||
// Current time invert
|
||||
// Only if one time element is used for both currentTime and duration
|
||||
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
|
||||
bind(this.player.elements.display.currentTime, 'click', () => {
|
||||
this.bind(this.player.elements.display.currentTime, 'click', () => {
|
||||
// Do nothing if we're at the start
|
||||
if (this.player.currentTime === 0) {
|
||||
return;
|
||||
@ -595,7 +556,7 @@ class Listeners {
|
||||
}
|
||||
|
||||
// Volume
|
||||
bind(
|
||||
this.bind(
|
||||
this.player.elements.inputs.volume,
|
||||
inputEvent,
|
||||
event => {
|
||||
@ -607,27 +568,27 @@ class Listeners {
|
||||
// Polyfill for lower fill in <input type="range"> for webkit
|
||||
if (browser.isWebkit) {
|
||||
Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => {
|
||||
bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
|
||||
this.bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
|
||||
});
|
||||
}
|
||||
|
||||
// Seek tooltip
|
||||
bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event =>
|
||||
this.bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event =>
|
||||
controls.updateSeekTooltip.call(this.player, event),
|
||||
);
|
||||
|
||||
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||
bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||
});
|
||||
|
||||
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||
bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
this.bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
|
||||
});
|
||||
|
||||
// Focus in/out on controls
|
||||
bind(this.player.elements.controls, 'focusin focusout', event => {
|
||||
this.bind(this.player.elements.controls, 'focusin focusout', event => {
|
||||
const { config, elements, timers } = this.player;
|
||||
|
||||
// Skip transition to prevent focus from scrolling the parent element
|
||||
@ -654,7 +615,7 @@ class Listeners {
|
||||
});
|
||||
|
||||
// Mouse wheel for volume
|
||||
bind(
|
||||
this.bind(
|
||||
this.player.elements.inputs.volume,
|
||||
'wheel',
|
||||
event => {
|
||||
|
@ -191,7 +191,7 @@
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
margin-right: -$plyr-control-padding;
|
||||
margin-right: -($plyr-control-padding - 2);
|
||||
overflow: hidden;
|
||||
padding-left: ceil($plyr-control-padding * 3.5);
|
||||
pointer-events: none;
|
||||
|
@ -22,3 +22,7 @@
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.plyr [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
Reference in New Issue
Block a user