Work on controls

This commit is contained in:
Sam Potts 2018-06-18 21:39:47 +10:00
parent 599883e684
commit ffd864ed39
11 changed files with 240 additions and 269 deletions

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

249
dist/plyr.js vendored
View File

@ -382,12 +382,19 @@ typeof navigator === "object" && (function (global, factory) {
// Inaert an element after another // Inaert an element after another
function insertAfter(element, target) { function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) {
return;
}
target.parentNode.insertBefore(element, target.nextSibling); target.parentNode.insertBefore(element, target.nextSibling);
} }
// Insert a DocumentFragment // Insert a DocumentFragment
function insertElement(type, parent, attributes, text) { function insertElement(type, parent, attributes, text) {
// Inject the new <element> if (!is.element(parent)) {
return;
}
parent.appendChild(createElement(type, attributes, text)); parent.appendChild(createElement(type, attributes, text));
} }
@ -407,6 +414,10 @@ typeof navigator === "object" && (function (global, factory) {
// Remove all child elements // Remove all child elements
function emptyElement(element) { function emptyElement(element) {
if (!is.element(element)) {
return;
}
var length = element.childNodes.length; var length = element.childNodes.length;
@ -1590,31 +1601,16 @@ typeof navigator === "object" && (function (global, factory) {
_ref$checked = _ref.checked, _ref$checked = _ref.checked,
checked = _ref$checked === undefined ? false : _ref$checked; checked = _ref$checked === undefined ? false : _ref$checked;
var item = createElement('li'); var item = createElement('button', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'button',
var label = createElement('label', {
class: this.config.classNames.control
});
var radio = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'radio',
name: 'plyr-' + type,
value: value, value: value,
checked: checked, 'aria-checked': checked
class: 'plyr__sr-only' }), title);
}));
var faux = createElement('span', { hidden: '' });
label.appendChild(radio);
label.appendChild(faux);
label.insertAdjacentHTML('beforeend', title);
if (is.element(badge)) { if (is.element(badge)) {
label.appendChild(badge); item.appendChild(badge);
} }
item.appendChild(label);
list.appendChild(item); list.appendChild(item);
}, },
@ -1879,8 +1875,8 @@ typeof navigator === "object" && (function (global, factory) {
// Hide/show a tab // Hide/show a tab
toggleTab: function toggleTab(setting, toggle) { toggleMenuButton: function toggleMenuButton(setting, toggle) {
toggleHidden(this.elements.settings.tabs[setting], !toggle); toggleHidden(this.elements.settings.buttons[setting], !toggle);
}, },
@ -1889,12 +1885,12 @@ typeof navigator === "object" && (function (global, factory) {
var _this3 = this; var _this3 = this;
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.quality)) { if (!is.element(this.elements.settings.menus.quality)) {
return; return;
} }
var type = 'quality'; var type = 'quality';
var list = this.elements.settings.panes.quality.querySelector('ul'); var list = this.elements.settings.menus.quality.querySelector('[role="menu"]');
// Set options if passed and filter based on uniqueness and config // Set options if passed and filter based on uniqueness and config
if (is.array(options)) { if (is.array(options)) {
@ -1905,7 +1901,7 @@ typeof navigator === "object" && (function (global, factory) {
// Toggle the pane and tab // Toggle the pane and tab
var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleMenuButton.call(this, type, toggle);
// Check if we need to toggle the parent // Check if we need to toggle the parent
controls.checkMenu.call(this); controls.checkMenu.call(this);
@ -1977,7 +1973,7 @@ typeof navigator === "object" && (function (global, factory) {
// Update the selected setting // Update the selected setting
updateSetting: function updateSetting(setting, container, input) { updateSetting: function updateSetting(setting, container, input) {
var pane = this.elements.settings.panes[setting]; var pane = this.elements.settings.menus[setting];
var value = null; var value = null;
var list = container; var list = container;
@ -2006,7 +2002,7 @@ typeof navigator === "object" && (function (global, factory) {
// Get the list if we need to // Get the list if we need to
if (!is.element(list)) { if (!is.element(list)) {
list = pane && pane.querySelector('ul'); list = pane && pane.querySelector('[role="menu"]');
} }
// If there's no list it means it's not been rendered... // If there's no list it means it's not been rendered...
@ -2015,14 +2011,14 @@ typeof navigator === "object" && (function (global, factory) {
} }
// Update the label // Update the label
var label = this.elements.settings.tabs[setting].querySelector('.' + this.config.classNames.menu.value); var label = this.elements.settings.buttons[setting].querySelector('.' + this.config.classNames.menu.value);
label.innerHTML = controls.getLabel.call(this, setting, value); label.innerHTML = controls.getLabel.call(this, setting, value);
// Find the radio option and check it // Find the radio option and check it
var target = list && list.querySelector('input[value="' + value + '"]'); var target = list && list.querySelector('button[value="' + value + '"]');
if (is.element(target)) { if (is.element(target)) {
target.checked = true; target.setAttribute('aria-checked', true);
} }
}, },
@ -2030,17 +2026,17 @@ typeof navigator === "object" && (function (global, factory) {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.loop)) { if (!is.element(this.elements.settings.menus.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panes.loop.querySelector('ul'); const list = this.elements.settings.menus.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.tabs.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panes.loop, false); toggleHidden(this.elements.settings.menus.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleTab.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
@ -2072,11 +2068,11 @@ typeof navigator === "object" && (function (global, factory) {
// TODO: Captions or language? Currently it's mixed // TODO: Captions or language? Currently it's mixed
var type = 'captions'; var type = 'captions';
var list = this.elements.settings.panes.captions.querySelector('ul'); var list = this.elements.settings.menus.captions.querySelector('[role="menu"]');
var tracks = captions.getTracks.call(this); var tracks = captions.getTracks.call(this);
// Toggle the pane and tab // Toggle the pane and tab
controls.toggleTab.call(this, type, tracks.length); controls.toggleMenuButton.call(this, type, tracks.length);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@ -2127,7 +2123,7 @@ typeof navigator === "object" && (function (global, factory) {
} }
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.speed)) { if (!is.element(this.elements.settings.menus.speed)) {
return; return;
} }
@ -2147,7 +2143,7 @@ typeof navigator === "object" && (function (global, factory) {
// Toggle the pane and tab // Toggle the pane and tab
var toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; var toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleMenuButton.call(this, type, toggle);
// Check if we need to toggle the parent // Check if we need to toggle the parent
controls.checkMenu.call(this); controls.checkMenu.call(this);
@ -2158,7 +2154,7 @@ typeof navigator === "object" && (function (global, factory) {
} }
// Get the list to populate // Get the list to populate
var list = this.elements.settings.panes.speed.querySelector('ul'); var list = this.elements.settings.menus.speed.querySelector('[role="menu"]');
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@ -2179,10 +2175,10 @@ typeof navigator === "object" && (function (global, factory) {
// Check if we need to hide/show the settings menu // Check if we need to hide/show the settings menu
checkMenu: function checkMenu() { checkMenu: function checkMenu() {
var tabs = this.elements.settings.tabs; var buttons = this.elements.settings.buttons;
var visible = !is.empty(tabs) && Object.values(tabs).some(function (tab) { var visible = !is.empty(buttons) && Object.values(buttons).some(function (button) {
return !tab.hidden; return !button.hidden;
}); });
toggleHidden(this.elements.settings.menu, !visible); toggleHidden(this.elements.settings.menu, !visible);
@ -2191,19 +2187,19 @@ typeof navigator === "object" && (function (global, factory) {
// Show/hide menu // Show/hide menu
toggleMenu: function toggleMenu(event) { toggleMenu: function toggleMenu(event) {
var form = this.elements.settings.form; var popup = this.elements.settings.popup;
var button = this.elements.buttons.settings; var button = this.elements.buttons.settings;
// Menu and button are required // Menu and button are required
if (!is.element(form) || !is.element(button)) { if (!is.element(popup) || !is.element(button)) {
return; return;
} }
var show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden'); var show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden');
if (is.event(event)) { if (is.event(event)) {
var isMenuItem = is.element(form) && form.contains(event.target); var isMenuItem = is.element(popup) && popup.contains(event.target);
var isButton = event.target === this.elements.buttons.settings; var isButton = event.target === this.elements.buttons.settings;
// If the click was inside the form or if the click // If the click was inside the form or if the click
@ -2224,14 +2220,14 @@ typeof navigator === "object" && (function (global, factory) {
button.setAttribute('aria-expanded', show); button.setAttribute('aria-expanded', show);
} }
if (is.element(form)) { if (is.element(popup)) {
toggleHidden(form, !show); toggleHidden(popup, !show);
toggleClass(this.elements.container, this.config.classNames.menu.open, show); toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) { if (show) {
form.removeAttribute('tabindex'); popup.removeAttribute('tabindex');
} else { } else {
form.setAttribute('tabindex', -1); popup.setAttribute('tabindex', -1);
} }
} }
}, },
@ -2245,10 +2241,10 @@ typeof navigator === "object" && (function (global, factory) {
clone.removeAttribute('hidden'); clone.removeAttribute('hidden');
// Prevent input's being unchecked due to the name being identical // Prevent input's being unchecked due to the name being identical
Array.from(clone.querySelectorAll('input[name]')).forEach(function (input) { /* Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
var name = input.getAttribute('name'); const name = input.getAttribute('name');
input.setAttribute('name', name + '-clone'); input.setAttribute('name', `${name}-clone`);
}); }); */
// Append to parent so we get the "real" size // Append to parent so we get the "real" size
tab.parentNode.appendChild(clone); tab.parentNode.appendChild(clone);
@ -2268,34 +2264,37 @@ typeof navigator === "object" && (function (global, factory) {
// Toggle Menu // Toggle Menu
showTab: function showTab() { showMenu: function showMenu() {
var _this6 = this; var _this6 = this;
var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var menu = this.elements.settings.menu; var menu = this.elements.settings.menu;
var pane = document.getElementById(target); var pane = document.getElementById('plyr-settings-' + this.id + '-' + type);
console.warn('plyr-settings-' + this.id + '-' + type);
// Nothing to show, bail // Nothing to show, bail
if (!is.element(pane)) { if (!is.element(pane)) {
console.warn('No pane found');
return; return;
} }
// Are we targeting a tab? If not, bail // Are we targeting a tab? If not, bail
var isTab = pane.getAttribute('role') === 'tabpanel'; /* const isTab = pane.getAttribute('role') === 'tabpanel';
if (!isTab) { if (!isTab) {
return; return;
} } */
// Hide all other tabs // Hide all other tabs
// Get other tabs // Get other tabs
var current = menu.querySelector('[role="tabpanel"]:not([hidden])'); var current = menu.querySelector('[id^=plyr-settings-' + this.id + ']:not([hidden])');
var container = current.parentNode; var container = current.parentNode;
// Set other toggles to be expanded false // Set other toggles to be expanded false
Array.from(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]')).forEach(function (toggle) { /* Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => {
toggle.setAttribute('aria-expanded', false); toggle.setAttribute('aria-expanded', false);
}); }); */
// If we can do fancy animations, we'll animate the height/width // If we can do fancy animations, we'll animate the height/width
if (support.transitions && !support.reducedMotion) { if (support.transitions && !support.reducedMotion) {
@ -2331,16 +2330,16 @@ typeof navigator === "object" && (function (global, factory) {
// Set attributes on current tab // Set attributes on current tab
toggleHidden(current, true); toggleHidden(current, true);
current.setAttribute('tabindex', -1); // current.setAttribute('tabindex', -1);
// Set attributes on target // Set attributes on target
toggleHidden(pane, false); toggleHidden(pane, false);
var tabs = getElements.call(this, '[aria-controls="' + target + '"]'); /* const tabs = getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(function (tab) { Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true); tab.setAttribute('aria-expanded', true);
}); });
pane.removeAttribute('tabindex'); pane.removeAttribute('tabindex'); */
// Focus the first item // Focus the first item
pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus(); pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus();
@ -2453,55 +2452,46 @@ typeof navigator === "object" && (function (global, factory) {
// Settings button / menu // Settings button / menu
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
var menu = createElement('div', { var control = createElement('div', {
class: 'plyr__menu', class: 'plyr__menu',
hidden: '' hidden: ''
}); });
menu.appendChild(controls.createButton.call(this, 'settings', { control.appendChild(controls.createButton.call(this, 'settings', {
id: 'plyr-settings-toggle-' + data.id, id: 'plyr-settings-toggle-' + data.id,
'aria-haspopup': true, 'aria-haspopup': true,
'aria-controls': 'plyr-settings-' + data.id, 'aria-controls': 'plyr-settings-' + data.id,
'aria-expanded': false 'aria-expanded': false
})); }));
var form = createElement('form', { var popup = createElement('div', {
class: 'plyr__menu__container', class: 'plyr__menu__container',
id: 'plyr-settings-' + data.id, id: 'plyr-settings-' + data.id,
hidden: '', hidden: '',
'aria-labelled-by': 'plyr-settings-toggle-' + data.id, 'aria-labelled-by': 'plyr-settings-toggle-' + data.id
role: 'tablist',
tabindex: -1
}); });
var inner = createElement('div'); var inner = createElement('div');
var home = createElement('div', { var home = createElement('div', {
id: 'plyr-settings-' + data.id + '-home', id: 'plyr-settings-' + data.id + '-home'
'aria-labelled-by': 'plyr-settings-toggle-' + data.id,
role: 'tabpanel'
}); });
// Create the tab list // Create the menu
var tabs = createElement('ul', { var menu = createElement('div', {
role: 'tablist' role: 'menu'
}); });
// Build the tabs // Build the menu items
this.config.settings.forEach(function (type) { this.config.settings.forEach(function (type) {
var tab = createElement('li', { var menuItem = createElement('button', extend(getAttributesFromSelector(_this7.config.selectors.buttons.settings), {
role: 'tab',
hidden: ''
});
var button = createElement('button', extend(getAttributesFromSelector(_this7.config.selectors.buttons.settings), {
type: 'button', type: 'button',
class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--forward', class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--forward',
id: 'plyr-settings-' + data.id + '-' + type + '-tab', 'role': 'menuitem',
'aria-haspopup': true, 'aria-haspopup': true
'aria-controls': 'plyr-settings-' + data.id + '-' + type, }));
'aria-expanded': false
}), i18n.get(type, _this7.config)); var flex = createElement('span', null, i18n.get(type, _this7.config));
var value = createElement('span', { var value = createElement('span', {
class: _this7.config.classNames.menu.value class: _this7.config.classNames.menu.value
@ -2510,50 +2500,46 @@ typeof navigator === "object" && (function (global, factory) {
// Speed contains HTML entities // Speed contains HTML entities
value.innerHTML = data[type]; value.innerHTML = data[type];
button.appendChild(value); flex.appendChild(value);
tab.appendChild(button); menuItem.appendChild(flex);
tabs.appendChild(tab); menu.appendChild(menuItem);
_this7.elements.settings.tabs[type] = tab; // Build the panes
});
home.appendChild(tabs);
inner.appendChild(home);
// Build the panes
this.config.settings.forEach(function (type) {
var pane = createElement('div', { var pane = createElement('div', {
id: 'plyr-settings-' + data.id + '-' + type, id: 'plyr-settings-' + data.id + '-' + type,
hidden: '', hidden: ''
'aria-labelled-by': 'plyr-settings-' + data.id + '-' + type + '-tab',
role: 'tabpanel',
tabindex: -1
}); });
var back = createElement('button', { // Back button
pane.appendChild(createElement('button', {
type: 'button', type: 'button',
class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--back', class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--back'
'aria-haspopup': true, }, i18n.get(type, _this7.config)));
'aria-controls': 'plyr-settings-' + data.id + '-home',
'aria-expanded': false
}, i18n.get(type, _this7.config));
pane.appendChild(back); // Menu
pane.appendChild(createElement('div', {
role: 'menu'
}));
var options = createElement('ul');
pane.appendChild(options);
inner.appendChild(pane); inner.appendChild(pane);
_this7.elements.settings.panes[type] = pane; menuItem.addEventListener('click', function () {
controls.showMenu.call(_this7, type);
});
_this7.elements.settings.buttons[type] = menuItem;
_this7.elements.settings.menus[type] = pane;
}); });
form.appendChild(inner); home.appendChild(menu);
menu.appendChild(form); inner.appendChild(home);
container.appendChild(menu);
this.elements.settings.form = form; popup.appendChild(inner);
this.elements.settings.menu = menu; control.appendChild(popup);
container.appendChild(control);
this.elements.settings.popup = popup;
this.elements.settings.menu = control;
} }
// Picture in picture button // Picture in picture button
@ -4617,13 +4603,12 @@ typeof navigator === "object" && (function (global, factory) {
}); });
// Settings menu // Settings menu
bind(this.player.elements.settings.form, 'click', function (event) { bind(this.player.elements.settings.popup, 'click', function (event) {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click // Go back to home tab on click
var showHomeTab = function showHomeTab() { var showHomeTab = function showHomeTab() {
var id = 'plyr-settings-' + _this4.player.id + '-home'; controls.showMenu.call(_this4.player, 'home');
controls.showTab.call(_this4.player, id);
}; };
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
@ -4642,9 +4627,6 @@ typeof navigator === "object" && (function (global, factory) {
_this4.player.speed = parseFloat(event.target.value); _this4.player.speed = parseFloat(event.target.value);
showHomeTab(); showHomeTab();
}, 'speed'); }, 'speed');
} else {
var tab = event.target;
controls.showTab.call(_this4.player, tab.getAttribute('aria-controls'));
} }
}); });
@ -6952,16 +6934,17 @@ typeof navigator === "object" && (function (global, factory) {
// Elements cache // Elements cache
this.elements = { this.elements = {
container: null, container: null,
captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
progress: {}, progress: {},
inputs: {}, inputs: {},
settings: { settings: {
popup: null,
menu: null, menu: null,
panes: {}, menus: {},
tabs: {} buttons: {}
}, }
captions: null
}; };
// Captions // Captions

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

198
src/js/controls.js vendored
View File

@ -374,34 +374,20 @@ const controls = {
// Create a settings menu item // Create a settings menu item
createMenuItem({ value, list, type, title, badge = null, checked = false }) { createMenuItem({ value, list, type, title, badge = null, checked = false }) {
const item = createElement('li'); const item = createElement(
'button',
const label = createElement('label', {
class: this.config.classNames.control,
});
const radio = createElement(
'input',
extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'radio', type: 'button',
name: `plyr-${type}`,
value, value,
checked, 'aria-checked': checked,
class: 'plyr__sr-only',
}), }),
title,
); );
const faux = createElement('span', { hidden: '' });
label.appendChild(radio);
label.appendChild(faux);
label.insertAdjacentHTML('beforeend', title);
if (is.element(badge)) { if (is.element(badge)) {
label.appendChild(badge); item.appendChild(badge);
} }
item.appendChild(label);
list.appendChild(item); list.appendChild(item);
}, },
@ -656,19 +642,19 @@ const controls = {
}, },
// Hide/show a tab // Hide/show a tab
toggleTab(setting, toggle) { toggleMenuButton(setting, toggle) {
toggleHidden(this.elements.settings.tabs[setting], !toggle); toggleHidden(this.elements.settings.buttons[setting], !toggle);
}, },
// Set the quality menu // Set the quality menu
setQualityMenu(options) { setQualityMenu(options) {
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.quality)) { if (!is.element(this.elements.settings.menus.quality)) {
return; return;
} }
const type = 'quality'; const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul'); const list = this.elements.settings.menus.quality.querySelector('[role="menu"]');
// Set options if passed and filter based on uniqueness and config // Set options if passed and filter based on uniqueness and config
if (is.array(options)) { if (is.array(options)) {
@ -677,7 +663,7 @@ const controls = {
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleMenuButton.call(this, type, toggle);
// Check if we need to toggle the parent // Check if we need to toggle the parent
controls.checkMenu.call(this); controls.checkMenu.call(this);
@ -749,7 +735,7 @@ const controls = {
// Update the selected setting // Update the selected setting
updateSetting(setting, container, input) { updateSetting(setting, container, input) {
const pane = this.elements.settings.panes[setting]; const pane = this.elements.settings.menus[setting];
let value = null; let value = null;
let list = container; let list = container;
@ -778,7 +764,7 @@ const controls = {
// Get the list if we need to // Get the list if we need to
if (!is.element(list)) { if (!is.element(list)) {
list = pane && pane.querySelector('ul'); list = pane && pane.querySelector('[role="menu"]');
} }
// If there's no list it means it's not been rendered... // If there's no list it means it's not been rendered...
@ -787,34 +773,34 @@ const controls = {
} }
// Update the label // Update the label
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`); const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);
label.innerHTML = controls.getLabel.call(this, setting, value); label.innerHTML = controls.getLabel.call(this, setting, value);
// Find the radio option and check it // Find the radio option and check it
const target = list && list.querySelector(`input[value="${value}"]`); const target = list && list.querySelector(`button[value="${value}"]`);
if (is.element(target)) { if (is.element(target)) {
target.checked = true; target.setAttribute('aria-checked', true);
} }
}, },
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.loop)) { if (!is.element(this.elements.settings.menus.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panes.loop.querySelector('ul'); const list = this.elements.settings.menus.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.tabs.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panes.loop, false); toggleHidden(this.elements.settings.menus.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleTab.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@ -849,11 +835,11 @@ const controls = {
setCaptionsMenu() { setCaptionsMenu() {
// TODO: Captions or language? Currently it's mixed // TODO: Captions or language? Currently it's mixed
const type = 'captions'; const type = 'captions';
const list = this.elements.settings.panes.captions.querySelector('ul'); const list = this.elements.settings.menus.captions.querySelector('[role="menu"]');
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
// Toggle the pane and tab // Toggle the pane and tab
controls.toggleTab.call(this, type, tracks.length); controls.toggleMenuButton.call(this, type, tracks.length);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@ -899,7 +885,7 @@ const controls = {
} }
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.speed)) { if (!is.element(this.elements.settings.menus.speed)) {
return; return;
} }
@ -917,7 +903,7 @@ const controls = {
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleMenuButton.call(this, type, toggle);
// Check if we need to toggle the parent // Check if we need to toggle the parent
controls.checkMenu.call(this); controls.checkMenu.call(this);
@ -928,7 +914,7 @@ const controls = {
} }
// Get the list to populate // Get the list to populate
const list = this.elements.settings.panes.speed.querySelector('ul'); const list = this.elements.settings.menus.speed.querySelector('[role="menu"]');
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@ -948,26 +934,26 @@ const controls = {
// Check if we need to hide/show the settings menu // Check if we need to hide/show the settings menu
checkMenu() { checkMenu() {
const { tabs } = this.elements.settings; const { buttons } = this.elements.settings;
const visible = !is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden); const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);
toggleHidden(this.elements.settings.menu, !visible); toggleHidden(this.elements.settings.menu, !visible);
}, },
// Show/hide menu // Show/hide menu
toggleMenu(event) { toggleMenu(event) {
const { form } = this.elements.settings; const { popup } = this.elements.settings;
const button = this.elements.buttons.settings; const button = this.elements.buttons.settings;
// Menu and button are required // Menu and button are required
if (!is.element(form) || !is.element(button)) { if (!is.element(popup) || !is.element(button)) {
return; return;
} }
const show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden'); const show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden');
if (is.event(event)) { if (is.event(event)) {
const isMenuItem = is.element(form) && form.contains(event.target); const isMenuItem = is.element(popup) && popup.contains(event.target);
const isButton = event.target === this.elements.buttons.settings; const isButton = event.target === this.elements.buttons.settings;
// If the click was inside the form or if the click // If the click was inside the form or if the click
@ -988,14 +974,14 @@ const controls = {
button.setAttribute('aria-expanded', show); button.setAttribute('aria-expanded', show);
} }
if (is.element(form)) { if (is.element(popup)) {
toggleHidden(form, !show); toggleHidden(popup, !show);
toggleClass(this.elements.container, this.config.classNames.menu.open, show); toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) { if (show) {
form.removeAttribute('tabindex'); popup.removeAttribute('tabindex');
} else { } else {
form.setAttribute('tabindex', -1); popup.setAttribute('tabindex', -1);
} }
} }
}, },
@ -1008,10 +994,10 @@ const controls = {
clone.removeAttribute('hidden'); clone.removeAttribute('hidden');
// Prevent input's being unchecked due to the name being identical // Prevent input's being unchecked due to the name being identical
Array.from(clone.querySelectorAll('input[name]')).forEach(input => { /* Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
const name = input.getAttribute('name'); const name = input.getAttribute('name');
input.setAttribute('name', `${name}-clone`); input.setAttribute('name', `${name}-clone`);
}); }); */
// Append to parent so we get the "real" size // Append to parent so we get the "real" size
tab.parentNode.appendChild(clone); tab.parentNode.appendChild(clone);
@ -1030,30 +1016,33 @@ const controls = {
}, },
// Toggle Menu // Toggle Menu
showTab(target = '') { showMenu(type = '') {
const { menu } = this.elements.settings; const { menu } = this.elements.settings;
const pane = document.getElementById(target); const pane = document.getElementById(`plyr-settings-${this.id}-${type}`);
console.warn(`plyr-settings-${this.id}-${type}`);
// Nothing to show, bail // Nothing to show, bail
if (!is.element(pane)) { if (!is.element(pane)) {
console.warn('No pane found');
return; return;
} }
// Are we targeting a tab? If not, bail // Are we targeting a tab? If not, bail
const isTab = pane.getAttribute('role') === 'tabpanel'; /* const isTab = pane.getAttribute('role') === 'tabpanel';
if (!isTab) { if (!isTab) {
return; return;
} } */
// Hide all other tabs // Hide all other tabs
// Get other tabs // Get other tabs
const current = menu.querySelector('[role="tabpanel"]:not([hidden])'); const current = menu.querySelector(`[id^=plyr-settings-${this.id}]:not([hidden])`);
const container = current.parentNode; const container = current.parentNode;
// Set other toggles to be expanded false // Set other toggles to be expanded false
Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => { /* Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => {
toggle.setAttribute('aria-expanded', false); toggle.setAttribute('aria-expanded', false);
}); }); */
// If we can do fancy animations, we'll animate the height/width // If we can do fancy animations, we'll animate the height/width
if (support.transitions && !support.reducedMotion) { if (support.transitions && !support.reducedMotion) {
@ -1089,16 +1078,16 @@ const controls = {
// Set attributes on current tab // Set attributes on current tab
toggleHidden(current, true); toggleHidden(current, true);
current.setAttribute('tabindex', -1); // current.setAttribute('tabindex', -1);
// Set attributes on target // Set attributes on target
toggleHidden(pane, false); toggleHidden(pane, false);
const tabs = getElements.call(this, `[aria-controls="${target}"]`); /* const tabs = getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => { Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true); tab.setAttribute('aria-expanded', true);
}); });
pane.removeAttribute('tabindex'); pane.removeAttribute('tabindex'); */
// Focus the first item // Focus the first item
pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus(); pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus();
@ -1220,12 +1209,12 @@ const controls = {
// Settings button / menu // Settings button / menu
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
const menu = createElement('div', { const control = createElement('div', {
class: 'plyr__menu', class: 'plyr__menu',
hidden: '', hidden: '',
}); });
menu.appendChild( control.appendChild(
controls.createButton.call(this, 'settings', { controls.createButton.call(this, 'settings', {
id: `plyr-settings-toggle-${data.id}`, id: `plyr-settings-toggle-${data.id}`,
'aria-haspopup': true, 'aria-haspopup': true,
@ -1234,48 +1223,38 @@ const controls = {
}), }),
); );
const form = createElement('form', { const popup = createElement('div', {
class: 'plyr__menu__container', class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`, id: `plyr-settings-${data.id}`,
hidden: '', hidden: '',
'aria-labelled-by': `plyr-settings-toggle-${data.id}`, 'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tablist',
tabindex: -1,
}); });
const inner = createElement('div'); const inner = createElement('div');
const home = createElement('div', { const home = createElement('div', {
id: `plyr-settings-${data.id}-home`, id: `plyr-settings-${data.id}-home`,
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tabpanel',
}); });
// Create the tab list // Create the menu
const tabs = createElement('ul', { const menu = createElement('div', {
role: 'tablist', role: 'menu',
}); });
// Build the tabs // Build the menu items
this.config.settings.forEach(type => { this.config.settings.forEach(type => {
const tab = createElement('li', { const menuItem = createElement(
role: 'tab',
hidden: '',
});
const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
type: 'button', type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
id: `plyr-settings-${data.id}-${type}-tab`, 'role': 'menuitem',
'aria-haspopup': true, 'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}-${type}`,
'aria-expanded': false,
}), }),
i18n.get(type, this.config),
); );
const flex = createElement('span', null, i18n.get(type, this.config))
const value = createElement('span', { const value = createElement('span', {
class: this.config.classNames.menu.value, class: this.config.classNames.menu.value,
}); });
@ -1283,54 +1262,51 @@ const controls = {
// Speed contains HTML entities // Speed contains HTML entities
value.innerHTML = data[type]; value.innerHTML = data[type];
button.appendChild(value); flex.appendChild(value);
tab.appendChild(button); menuItem.appendChild(flex);
tabs.appendChild(tab); menu.appendChild(menuItem);
this.elements.settings.tabs[type] = tab;
});
home.appendChild(tabs); // Build the panes
inner.appendChild(home);
// Build the panes
this.config.settings.forEach(type => {
const pane = createElement('div', { const pane = createElement('div', {
id: `plyr-settings-${data.id}-${type}`, id: `plyr-settings-${data.id}-${type}`,
hidden: '', hidden: '',
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
role: 'tabpanel',
tabindex: -1,
}); });
const back = createElement( // Back button
pane.appendChild(createElement(
'button', 'button',
{ {
type: 'button', type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}-home`,
'aria-expanded': false,
}, },
i18n.get(type, this.config), i18n.get(type, this.config),
); ));
pane.appendChild(back); // Menu
pane.appendChild(createElement('div', {
role: 'menu',
}));
const options = createElement('ul');
pane.appendChild(options);
inner.appendChild(pane); inner.appendChild(pane);
this.elements.settings.panes[type] = pane; menuItem.addEventListener('click', () => {
controls.showMenu.call(this, type);
});
this.elements.settings.buttons[type] = menuItem;
this.elements.settings.menus[type] = pane;
}); });
form.appendChild(inner); home.appendChild(menu);
menu.appendChild(form); inner.appendChild(home);
container.appendChild(menu);
this.elements.settings.form = form; popup.appendChild(inner);
this.elements.settings.menu = menu; control.appendChild(popup);
container.appendChild(control);
this.elements.settings.popup = popup;
this.elements.settings.menu = control;
} }
// Picture in picture button // Picture in picture button

View File

@ -486,13 +486,12 @@ class Listeners {
}); });
// Settings menu // Settings menu
bind(this.player.elements.settings.form, 'click', event => { bind(this.player.elements.settings.popup, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click // Go back to home tab on click
const showHomeTab = () => { const showHomeTab = () => {
const id = `plyr-settings-${this.player.id}-home`; controls.showMenu.call(this.player, 'home');
controls.showTab.call(this.player, id);
}; };
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
@ -523,9 +522,6 @@ class Listeners {
}, },
'speed', 'speed',
); );
} else {
const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
} }
}); });

View File

@ -75,16 +75,17 @@ class Plyr {
// Elements cache // Elements cache
this.elements = { this.elements = {
container: null, container: null,
captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
progress: {}, progress: {},
inputs: {}, inputs: {},
settings: { settings: {
popup: null,
menu: null, menu: null,
panes: {}, menus: {},
tabs: {}, buttons: {},
}, },
captions: null,
}; };
// Captions // Captions

View File

@ -70,12 +70,19 @@ export function createElement(type, attributes, text) {
// Inaert an element after another // Inaert an element after another
export function insertAfter(element, target) { export function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) {
return;
}
target.parentNode.insertBefore(element, target.nextSibling); target.parentNode.insertBefore(element, target.nextSibling);
} }
// Insert a DocumentFragment // Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) { export function insertElement(type, parent, attributes, text) {
// Inject the new <element> if (!is.element(parent)) {
return;
}
parent.appendChild(createElement(type, attributes, text)); parent.appendChild(createElement(type, attributes, text));
} }
@ -95,6 +102,10 @@ export function removeElement(element) {
// Remove all child elements // Remove all child elements
export function emptyElement(element) { export function emptyElement(element) {
if (!is.element(element)) {
return;
}
let { length } = element.childNodes; let { length } = element.childNodes;
while (length > 0) { while (length > 0) {

View File

@ -54,18 +54,16 @@
width: 0; width: 0;
} }
ul { [role='menu'] {
list-style: none;
margin: 0;
overflow: hidden;
padding: $plyr-control-padding; padding: $plyr-control-padding;
}
li { [role='menuitem'],
margin-top: 2px; [role='menuitemradio'] {
margin-top: 2px;
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
}
} }
} }
@ -75,10 +73,16 @@
color: $plyr-menu-color; color: $plyr-menu-color;
display: flex; display: flex;
font-size: $plyr-font-size-menu; font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2); padding: ceil($plyr-control-padding / 2) ceil($plyr-control-padding * 1.5);
user-select: none; user-select: none;
width: 100%; width: 100%;
> span {
align-items: inherit;
display: flex;
width: 100%;
}
&::after { &::after {
border: 4px solid transparent; border: 4px solid transparent;
content: ''; content: '';