From ffd864ed39340c081adb9e4a45b3c9cfe4c139e3 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 18 Jun 2018 21:39:47 +1000 Subject: Work on controls --- src/js/controls.js | 198 ++++++++++++++++++----------------------- src/js/listeners.js | 8 +- src/js/plyr.js | 7 +- src/js/utils/elements.js | 13 ++- src/sass/components/menus.scss | 24 ++--- 5 files changed, 119 insertions(+), 131 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index d79aaee7..57e5bd3f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -374,34 +374,20 @@ const controls = { // Create a settings menu item createMenuItem({ value, list, type, title, badge = null, checked = false }) { - const item = createElement('li'); - - const label = createElement('label', { - class: this.config.classNames.control, - }); - - const radio = createElement( - 'input', + const item = createElement( + 'button', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { - type: 'radio', - name: `plyr-${type}`, + type: 'button', value, - checked, - class: 'plyr__sr-only', + 'aria-checked': checked, }), + title, ); - const faux = createElement('span', { hidden: '' }); - - label.appendChild(radio); - label.appendChild(faux); - label.insertAdjacentHTML('beforeend', title); - if (is.element(badge)) { - label.appendChild(badge); + item.appendChild(badge); } - item.appendChild(label); list.appendChild(item); }, @@ -656,19 +642,19 @@ const controls = { }, // Hide/show a tab - toggleTab(setting, toggle) { - toggleHidden(this.elements.settings.tabs[setting], !toggle); + toggleMenuButton(setting, toggle) { + toggleHidden(this.elements.settings.buttons[setting], !toggle); }, // Set the quality menu setQualityMenu(options) { // Menu required - if (!is.element(this.elements.settings.panes.quality)) { + if (!is.element(this.elements.settings.menus.quality)) { return; } 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 if (is.array(options)) { @@ -677,7 +663,7 @@ const controls = { // Toggle the pane and tab 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 controls.checkMenu.call(this); @@ -749,7 +735,7 @@ const controls = { // Update the selected setting updateSetting(setting, container, input) { - const pane = this.elements.settings.panes[setting]; + const pane = this.elements.settings.menus[setting]; let value = null; let list = container; @@ -778,7 +764,7 @@ const controls = { // Get the list if we need to 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... @@ -787,34 +773,34 @@ const controls = { } // 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); // 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)) { - target.checked = true; + target.setAttribute('aria-checked', true); } }, // Set the looping options /* setLoopMenu() { // Menu required - if (!is.element(this.elements.settings.panes.loop)) { + if (!is.element(this.elements.settings.menus.loop)) { return; } 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 - toggleHidden(this.elements.settings.tabs.loop, false); - toggleHidden(this.elements.settings.panes.loop, false); + toggleHidden(this.elements.settings.buttons.loop, false); + toggleHidden(this.elements.settings.menus.loop, false); // Toggle the pane and tab const toggle = !is.empty(this.loop.options); - controls.toggleTab.call(this, 'loop', toggle); + controls.toggleMenuButton.call(this, 'loop', toggle); // Empty the menu emptyElement(list); @@ -849,11 +835,11 @@ const controls = { setCaptionsMenu() { // TODO: Captions or language? Currently it's mixed 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); // Toggle the pane and tab - controls.toggleTab.call(this, type, tracks.length); + controls.toggleMenuButton.call(this, type, tracks.length); // Empty the menu emptyElement(list); @@ -899,7 +885,7 @@ const controls = { } // Menu required - if (!is.element(this.elements.settings.panes.speed)) { + if (!is.element(this.elements.settings.menus.speed)) { return; } @@ -917,7 +903,7 @@ const controls = { // Toggle the pane and tab 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 controls.checkMenu.call(this); @@ -928,7 +914,7 @@ const controls = { } // 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 emptyElement(list); @@ -948,26 +934,26 @@ const controls = { // Check if we need to hide/show the settings menu checkMenu() { - const { tabs } = this.elements.settings; - const visible = !is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden); + const { buttons } = this.elements.settings; + const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); toggleHidden(this.elements.settings.menu, !visible); }, // Show/hide menu toggleMenu(event) { - const { form } = this.elements.settings; + const { popup } = this.elements.settings; const button = this.elements.buttons.settings; // Menu and button are required - if (!is.element(form) || !is.element(button)) { + if (!is.element(popup) || !is.element(button)) { 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)) { - 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; // If the click was inside the form or if the click @@ -988,14 +974,14 @@ const controls = { button.setAttribute('aria-expanded', show); } - if (is.element(form)) { - toggleHidden(form, !show); + if (is.element(popup)) { + toggleHidden(popup, !show); toggleClass(this.elements.container, this.config.classNames.menu.open, show); if (show) { - form.removeAttribute('tabindex'); + popup.removeAttribute('tabindex'); } else { - form.setAttribute('tabindex', -1); + popup.setAttribute('tabindex', -1); } } }, @@ -1008,10 +994,10 @@ const controls = { clone.removeAttribute('hidden'); // 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'); input.setAttribute('name', `${name}-clone`); - }); + }); */ // Append to parent so we get the "real" size tab.parentNode.appendChild(clone); @@ -1030,30 +1016,33 @@ const controls = { }, // Toggle Menu - showTab(target = '') { + showMenu(type = '') { 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 if (!is.element(pane)) { + console.warn('No pane found'); return; } // Are we targeting a tab? If not, bail - const isTab = pane.getAttribute('role') === 'tabpanel'; + /* const isTab = pane.getAttribute('role') === 'tabpanel'; if (!isTab) { return; - } + } */ // Hide all 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; // 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); - }); + }); */ // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -1089,16 +1078,16 @@ const controls = { // Set attributes on current tab toggleHidden(current, true); - current.setAttribute('tabindex', -1); + // current.setAttribute('tabindex', -1); // Set attributes on target toggleHidden(pane, false); - const tabs = getElements.call(this, `[aria-controls="${target}"]`); + /* const tabs = getElements.call(this, `[aria-controls="${target}"]`); Array.from(tabs).forEach(tab => { tab.setAttribute('aria-expanded', true); }); - pane.removeAttribute('tabindex'); + pane.removeAttribute('tabindex'); */ // Focus the first item pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus(); @@ -1220,12 +1209,12 @@ const controls = { // Settings button / menu if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { - const menu = createElement('div', { + const control = createElement('div', { class: 'plyr__menu', hidden: '', }); - menu.appendChild( + control.appendChild( controls.createButton.call(this, 'settings', { id: `plyr-settings-toggle-${data.id}`, 'aria-haspopup': true, @@ -1234,48 +1223,38 @@ const controls = { }), ); - const form = createElement('form', { + const popup = createElement('div', { class: 'plyr__menu__container', id: `plyr-settings-${data.id}`, hidden: '', 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, - role: 'tablist', - tabindex: -1, }); const inner = createElement('div'); const home = createElement('div', { id: `plyr-settings-${data.id}-home`, - 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, - role: 'tabpanel', }); - // Create the tab list - const tabs = createElement('ul', { - role: 'tablist', + // Create the menu + const menu = createElement('div', { + role: 'menu', }); - // Build the tabs + // Build the menu items this.config.settings.forEach(type => { - const tab = createElement('li', { - role: 'tab', - hidden: '', - }); - - const button = createElement( + const menuItem = createElement( 'button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { type: 'button', class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, - id: `plyr-settings-${data.id}-${type}-tab`, + 'role': 'menuitem', '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', { class: this.config.classNames.menu.value, }); @@ -1283,54 +1262,51 @@ const controls = { // Speed contains HTML entities value.innerHTML = data[type]; - button.appendChild(value); - tab.appendChild(button); - tabs.appendChild(tab); + flex.appendChild(value); + menuItem.appendChild(flex); + menu.appendChild(menuItem); - this.elements.settings.tabs[type] = tab; - }); - home.appendChild(tabs); - inner.appendChild(home); - - // Build the panes - this.config.settings.forEach(type => { + // Build the panes const pane = createElement('div', { id: `plyr-settings-${data.id}-${type}`, hidden: '', - 'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`, - role: 'tabpanel', - tabindex: -1, }); - const back = createElement( + // Back button + pane.appendChild(createElement( 'button', { type: 'button', 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), - ); + )); - pane.appendChild(back); + // Menu + pane.appendChild(createElement('div', { + role: 'menu', + })); - const options = createElement('ul'); - - pane.appendChild(options); 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); - menu.appendChild(form); - container.appendChild(menu); + home.appendChild(menu); + inner.appendChild(home); + + popup.appendChild(inner); + control.appendChild(popup); + container.appendChild(control); - this.elements.settings.form = form; - this.elements.settings.menu = menu; + this.elements.settings.popup = popup; + this.elements.settings.menu = control; } // Picture in picture button diff --git a/src/js/listeners.js b/src/js/listeners.js index 9d987508..68a38af2 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -486,13 +486,12 @@ class Listeners { }); // Settings menu - bind(this.player.elements.settings.form, 'click', event => { + bind(this.player.elements.settings.popup, 'click', event => { event.stopPropagation(); // Go back to home tab on click const showHomeTab = () => { - const id = `plyr-settings-${this.player.id}-home`; - controls.showTab.call(this.player, id); + controls.showMenu.call(this.player, 'home'); }; // Settings menu items - use event delegation as items are added/removed @@ -523,9 +522,6 @@ class Listeners { }, 'speed', ); - } else { - const tab = event.target; - controls.showTab.call(this.player, tab.getAttribute('aria-controls')); } }); diff --git a/src/js/plyr.js b/src/js/plyr.js index d4b63874..336a1c38 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -75,16 +75,17 @@ class Plyr { // Elements cache this.elements = { container: null, + captions: null, buttons: {}, display: {}, progress: {}, inputs: {}, settings: { + popup: null, menu: null, - panes: {}, - tabs: {}, + menus: {}, + buttons: {}, }, - captions: null, }; // Captions diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 69e4d46c..7b58c9ff 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -70,12 +70,19 @@ export function createElement(type, attributes, text) { // Inaert an element after another export function insertAfter(element, target) { + if (!is.element(element) || !is.element(target)) { + return; + } + target.parentNode.insertBefore(element, target.nextSibling); } // Insert a DocumentFragment export function insertElement(type, parent, attributes, text) { - // Inject the new + if (!is.element(parent)) { + return; + } + parent.appendChild(createElement(type, attributes, text)); } @@ -95,6 +102,10 @@ export function removeElement(element) { // Remove all child elements export function emptyElement(element) { + if (!is.element(element)) { + return; + } + let { length } = element.childNodes; while (length > 0) { diff --git a/src/sass/components/menus.scss b/src/sass/components/menus.scss index 3ad4039a..b8dbf5e7 100644 --- a/src/sass/components/menus.scss +++ b/src/sass/components/menus.scss @@ -54,18 +54,16 @@ width: 0; } - ul { - list-style: none; - margin: 0; - overflow: hidden; + [role='menu'] { padding: $plyr-control-padding; + } - li { - margin-top: 2px; + [role='menuitem'], + [role='menuitemradio'] { + margin-top: 2px; - &:first-child { - margin-top: 0; - } + &:first-child { + margin-top: 0; } } @@ -75,10 +73,16 @@ color: $plyr-menu-color; display: flex; 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; width: 100%; + > span { + align-items: inherit; + display: flex; + width: 100%; + } + &::after { border: 4px solid transparent; content: ''; -- cgit v1.2.3 From 7b9ef7d757811cb7c855856ef091061d315b13c4 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 18 Jun 2018 23:13:40 +1000 Subject: More work on menus --- src/js/config/defaults.js | 3 + src/js/controls.js | 148 +++++++++++++++++++---------------------- src/js/listeners.js | 2 +- src/js/plyr.js | 2 +- src/sass/components/menus.scss | 53 ++++++++------- 5 files changed, 99 insertions(+), 109 deletions(-) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 1e90a4f0..a898755d 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -354,6 +354,9 @@ const defaults = { isTouch: 'plyr--is-touch', uiSupported: 'plyr--full-ui', noTransition: 'plyr--no-transition', + display: { + time: 'plyr__time', + }, menu: { value: 'plyr__menu__value', badge: 'plyr__badge', diff --git a/src/js/controls.js b/src/js/controls.js index 57e5bd3f..607511cb 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -9,19 +9,7 @@ import support from './support'; import { repaint, transitionEndEvent } from './utils/animation'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; -import { - createElement, - emptyElement, - getAttributesFromSelector, - getElement, - getElements, - hasClass, - removeElement, - setAttributes, - toggleClass, - toggleHidden, - matches, -} from './utils/elements'; +import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, toggleClass, toggleHidden } from './utils/elements'; import { off, on } from './utils/events'; import is from './utils/is'; import loadSprite from './utils/loadSprite'; @@ -360,7 +348,7 @@ const controls = { const container = createElement( 'div', extend(attributes, { - class: `plyr__time ${attributes.class}`, + class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(), 'aria-label': i18n.get(type, this.config), }), '00:00', @@ -374,20 +362,43 @@ const controls = { // Create a settings menu item createMenuItem({ value, list, type, title, badge = null, checked = false }) { + const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); + const item = createElement( 'button', - extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { + extend(attributes, { type: 'button', + role: 'menuitemradio', + class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(), value, 'aria-checked': checked, - }), - title, + }) ); + // We have to set as HTML incase of special characters + item.innerHTML = title; + if (is.element(badge)) { item.appendChild(badge); } + Object.defineProperty(item, 'checked', { + enumerable: true, + get() { + return item.getAttribute('aria-checked') === 'true'; + }, + set(checked) { + // Ensure exclusivity + if (checked) { + Array.from(item.parentNode.children) + .filter(node => matches(node, '[role="menuitemradio"]')) + .forEach(node => node.setAttribute('aria-checked', 'false')); + } + + item.setAttribute('aria-checked', checked ? 'true' : 'false'); + }, + }); + list.appendChild(item); }, @@ -649,12 +660,12 @@ const controls = { // Set the quality menu setQualityMenu(options) { // Menu required - if (!is.element(this.elements.settings.menus.quality)) { + if (!is.element(this.elements.settings.panels.quality)) { return; } const type = 'quality'; - const list = this.elements.settings.menus.quality.querySelector('[role="menu"]'); + 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)) { @@ -735,7 +746,7 @@ const controls = { // Update the selected setting updateSetting(setting, container, input) { - const pane = this.elements.settings.menus[setting]; + const pane = this.elements.settings.panels[setting]; let value = null; let list = container; @@ -777,26 +788,26 @@ const controls = { label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it - const target = list && list.querySelector(`button[value="${value}"]`); + const target = list && list.querySelector(`[value="${value}"]`); if (is.element(target)) { - target.setAttribute('aria-checked', true); + target.checked = true; } }, // Set the looping options /* setLoopMenu() { // Menu required - if (!is.element(this.elements.settings.menus.loop)) { + if (!is.element(this.elements.settings.panels.loop)) { return; } const options = ['start', 'end', 'all', 'reset']; - const list = this.elements.settings.menus.loop.querySelector('[role="menu"]'); + const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); // Show the pane and tab toggleHidden(this.elements.settings.buttons.loop, false); - toggleHidden(this.elements.settings.menus.loop, false); + toggleHidden(this.elements.settings.panels.loop, false); // Toggle the pane and tab const toggle = !is.empty(this.loop.options); @@ -835,7 +846,7 @@ const controls = { setCaptionsMenu() { // TODO: Captions or language? Currently it's mixed const type = 'captions'; - const list = this.elements.settings.menus.captions.querySelector('[role="menu"]'); + const list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); const tracks = captions.getTracks.call(this); // Toggle the pane and tab @@ -885,7 +896,7 @@ const controls = { } // Menu required - if (!is.element(this.elements.settings.menus.speed)) { + if (!is.element(this.elements.settings.panels.speed)) { return; } @@ -914,7 +925,7 @@ const controls = { } // Get the list to populate - const list = this.elements.settings.menus.speed.querySelector('[role="menu"]'); + const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Empty the menu emptyElement(list); @@ -986,19 +997,13 @@ const controls = { } }, - // Get the natural size of a tab - getTabSize(tab) { + // Get the natural size of a menu panel + getMenuSize(tab) { const clone = tab.cloneNode(true); clone.style.position = 'absolute'; clone.style.opacity = 0; clone.removeAttribute('hidden'); - // Prevent input's being unchecked due to the name being identical - /* Array.from(clone.querySelectorAll('input[name]')).forEach(input => { - const name = input.getAttribute('name'); - input.setAttribute('name', `${name}-clone`); - }); */ - // Append to parent so we get the "real" size tab.parentNode.appendChild(clone); @@ -1015,34 +1020,18 @@ const controls = { }; }, - // Toggle Menu - showMenu(type = '') { - const { menu } = this.elements.settings; - const pane = document.getElementById(`plyr-settings-${this.id}-${type}`); - - console.warn(`plyr-settings-${this.id}-${type}`); + // Show a panel in the menu + showMenuPanel(type = '') { + const target = document.getElementById(`plyr-settings-${this.id}-${type}`); // Nothing to show, bail - if (!is.element(pane)) { - console.warn('No pane found'); + if (!is.element(target)) { return; } - // Are we targeting a tab? If not, bail - /* const isTab = pane.getAttribute('role') === 'tabpanel'; - if (!isTab) { - return; - } */ - - // Hide all other tabs - // Get other tabs - const current = menu.querySelector(`[id^=plyr-settings-${this.id}]:not([hidden])`); - const container = current.parentNode; - - // Set other toggles to be expanded false - /* Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => { - toggle.setAttribute('aria-expanded', false); - }); */ + // Hide all other panels + const container = target.parentNode; + const current = Array.from(container.children).find(node => !node.hidden); // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -1051,12 +1040,12 @@ const controls = { container.style.height = `${current.scrollHeight}px`; // Get potential sizes - const size = controls.getTabSize.call(this, pane); + const size = controls.getMenuSize.call(this, target); // Restore auto height/width - const restore = e => { + const restore = event => { // We're only bothered about height and width on the container - if (e.target !== container || !['width', 'height'].includes(e.propertyName)) { + if (event.target !== container || !['width', 'height'].includes(event.propertyName)) { return; } @@ -1081,16 +1070,10 @@ const controls = { // current.setAttribute('tabindex', -1); // Set attributes on target - toggleHidden(pane, false); - - /* const tabs = getElements.call(this, `[aria-controls="${target}"]`); - Array.from(tabs).forEach(tab => { - tab.setAttribute('aria-expanded', true); - }); - pane.removeAttribute('tabindex'); */ + toggleHidden(target, false); // Focus the first item - pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus(); + target.querySelectorAll('[role^="menuitem"]')[0].focus(); }, // Build the default HTML @@ -1248,12 +1231,12 @@ const controls = { extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { type: 'button', class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, - 'role': 'menuitem', + role: 'menuitem', 'aria-haspopup': true, }), ); - const flex = createElement('span', null, i18n.get(type, this.config)) + const flex = createElement('span', null, i18n.get(type, this.config)); const value = createElement('span', { class: this.config.classNames.menu.value, @@ -1266,7 +1249,6 @@ const controls = { menuItem.appendChild(flex); menu.appendChild(menuItem); - // Build the panes const pane = createElement('div', { id: `plyr-settings-${data.id}-${type}`, @@ -1274,28 +1256,34 @@ const controls = { }); // Back button - pane.appendChild(createElement( + const back = createElement( 'button', { type: 'button', class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, }, i18n.get(type, this.config), - )); + ); + back.addEventListener('click', () => { + controls.showMenuPanel.call(this, 'home'); + }); + pane.appendChild(back); // Menu - pane.appendChild(createElement('div', { - role: 'menu', - })); + pane.appendChild( + createElement('div', { + role: 'menu', + }), + ); inner.appendChild(pane); menuItem.addEventListener('click', () => { - controls.showMenu.call(this, type); + controls.showMenuPanel.call(this, type); }); this.elements.settings.buttons[type] = menuItem; - this.elements.settings.menus[type] = pane; + this.elements.settings.panels[type] = pane; }); home.appendChild(menu); diff --git a/src/js/listeners.js b/src/js/listeners.js index 68a38af2..cc9d3889 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -491,7 +491,7 @@ class Listeners { // Go back to home tab on click const showHomeTab = () => { - controls.showMenu.call(this.player, 'home'); + controls.showMenuPanel.call(this.player, 'home'); }; // Settings menu items - use event delegation as items are added/removed diff --git a/src/js/plyr.js b/src/js/plyr.js index 249c296f..2b65cc1d 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -83,7 +83,7 @@ class Plyr { settings: { popup: null, menu: null, - menus: {}, + panels: {}, buttons: {}, }, }; diff --git a/src/sass/components/menus.scss b/src/sass/components/menus.scss index b8dbf5e7..35bfdeaa 100644 --- a/src/sass/components/menus.scss +++ b/src/sass/components/menus.scss @@ -139,50 +139,49 @@ } } - label.plyr__control { + .plyr__control[role='menuitemradio'] { padding-left: $plyr-control-padding; - input[type='radio'] + span { - background: rgba(#000, 0.1); + &::before, + &::after { border-radius: 100%; + } + + &::before { + background: rgba(#000, 0.1); + content: ''; display: block; flex-shrink: 0; height: 16px; margin-right: $plyr-control-spacing; - position: relative; transition: all 0.3s ease; width: 16px; - - &::after { - background: #fff; - border-radius: 100%; - content: ''; - height: 6px; - left: 5px; - opacity: 0; - position: absolute; - top: 5px; - transform: scale(0); - transition: transform 0.3s ease, opacity 0.3s ease; - width: 6px; - } } - input[type='radio']:checked + span { - background: $plyr-color-main; + &::after { + background: #fff; + border: 0; + height: 6px; + left: 12px; + opacity: 0; + top: 50%; + transform: translateY(-50%) scale(0); + transition: transform 0.3s ease, opacity 0.3s ease; + width: 6px; + } + &[aria-checked='true'] { + &::before { + background: $plyr-color-main; + } &::after { opacity: 1; - transform: scale(1); + transform: translateY(-50%) scale(1); } } - input[type='radio']:focus + span { - @include plyr-tab-focus(); - } - - &.plyr__tab-focus input[type='radio'] + span, - &:hover input[type='radio'] + span { + &.plyr__tab-focus::before, + &:hover::before { background: rgba(#000, 0.1); } } -- cgit v1.2.3 From 9e1218547b18e87921d0bd95d259cac4fe538c61 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Tue, 19 Jun 2018 09:11:35 +1000 Subject: WIP --- src/js/controls.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 607511cb..b3435236 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -661,6 +661,7 @@ const controls = { setQualityMenu(options) { // Menu required if (!is.element(this.elements.settings.panels.quality)) { + console.warn('Not an element'); return; } @@ -673,6 +674,7 @@ const controls = { } // 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); @@ -1073,7 +1075,10 @@ const controls = { toggleHidden(target, false); // Focus the first item - target.querySelectorAll('[role^="menuitem"]')[0].focus(); + const firstItem = target.querySelector('[role^="menuitem"]'); + if (firstItem) { + firstItem.focus(); + } }, // Build the default HTML -- cgit v1.2.3 From 1f1d74ba50f2ac4113948a3b92a0b55f05b735c2 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 21 Jun 2018 09:01:16 +1000 Subject: Work on menus --- src/js/controls.js | 231 +++++++++++++++++++++++------------------ src/js/listeners.js | 145 ++++++++++---------------- src/sass/components/menus.scss | 2 +- src/sass/utils/hidden.scss | 4 + 4 files changed, 188 insertions(+), 194 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index b3435236..710ee93b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -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,11 +689,88 @@ const controls = { toggleHidden(this.elements.settings.buttons[setting], !toggle); }, + // Update the selected setting + updateSetting(setting, container, input) { + const pane = this.elements.settings.panels[setting]; + let value = null; + let list = container; + + if (setting === 'captions') { + value = this.currentTrack; + } else { + value = !is.empty(input) ? input : this[setting]; + + // Get default + if (is.empty(value)) { + value = this.config[setting].default; + } + + // Unsupported value + if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { + this.debug.warn(`Unsupported value of '${value}' for ${setting}`); + return; + } + + // Disabled value + if (!this.config[setting].options.includes(value)) { + this.debug.warn(`Disabled value of '${value}' for ${setting}`); + return; + } + } + + // Get the list if we need to + if (!is.element(list)) { + list = pane && pane.querySelector('[role="menu"]'); + } + + // If there's no list it means it's not been rendered... + if (!is.element(list)) { + return; + } + + // Update the label + const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`); + label.innerHTML = controls.getLabel.call(this, setting, value); + + // Find the radio option and check it + const target = list && list.querySelector(`[value="${value}"]`); + + if (is.element(target)) { + target.checked = true; + } + }, + + // 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)) { - console.warn('Not an element'); return; } @@ -674,10 +783,12 @@ const controls = { } // 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); + // Empty the menu + emptyElement(list); + // Check if we need to toggle the parent controls.checkMenu.call(this); @@ -686,9 +797,6 @@ const controls = { 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); @@ -719,84 +827,6 @@ const controls = { 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]; - let value = null; - let list = container; - - if (setting === 'captions') { - value = this.currentTrack; - } else { - value = !is.empty(input) ? input : this[setting]; - - // Get default - if (is.empty(value)) { - value = this.config[setting].default; - } - - // Unsupported value - if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { - this.debug.warn(`Unsupported value of '${value}' for ${setting}`); - return; - } - - // Disabled value - if (!this.config[setting].options.includes(value)) { - this.debug.warn(`Disabled value of '${value}' for ${setting}`); - return; - } - } - - // Get the list if we need to - if (!is.element(list)) { - list = pane && pane.querySelector('[role="menu"]'); - } - - // If there's no list it means it's not been rendered... - if (!is.element(list)) { - return; - } - - // Update the label - const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`); - label.innerHTML = controls.getLabel.call(this, setting, value); - - // Find the radio option and check it - const target = list && list.querySelector(`[value="${value}"]`); - - if (is.element(target)) { - target.checked = true; - } - }, - // 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: '', }), ); diff --git a/src/js/listeners.js b/src/js/listeners.js index cc9d3889..d6786111 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -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 { }); } - // 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 + proxy(event, defaultHandler, customHandlerKey) { + const customHandler = this.player.config.listeners[customHandlerKey]; + const hasCustomHandler = is.function(customHandler); + let returned = true; - // 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); + } - // 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); + } + } - // 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); - // 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 => this.proxy(event, defaultHandler, customHandlerKey), + passive && !hasCustomHandler, + ); + } - on.call( - this.player, - element, - type, - event => 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'; // 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 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 => { diff --git a/src/sass/components/menus.scss b/src/sass/components/menus.scss index 35bfdeaa..be354e46 100644 --- a/src/sass/components/menus.scss +++ b/src/sass/components/menus.scss @@ -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; diff --git a/src/sass/utils/hidden.scss b/src/sass/utils/hidden.scss index e4fa0aec..a42c3be8 100644 --- a/src/sass/utils/hidden.scss +++ b/src/sass/utils/hidden.scss @@ -22,3 +22,7 @@ width: 1px; } } + +.plyr [hidden] { + display: none !important; +} -- cgit v1.2.3 From 3bf1c59bd6a5beb32ee76ba46e37692c2b1c077f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 28 Jun 2018 23:44:07 +1000 Subject: Work on key bindings for menu --- src/js/controls.js | 101 +++++++++++++++++++++++++++++++++++++---------- src/js/listeners.js | 28 +++++++++++-- src/js/utils/elements.js | 15 +++++++ src/sass/lib/mixins.scss | 2 +- 4 files changed, 121 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 710ee93b..0f2db4f3 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -9,7 +9,7 @@ import support from './support'; import { repaint, transitionEndEvent } from './utils/animation'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; -import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, toggleClass, toggleHidden } from './utils/elements'; +import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements'; import { off, on } from './utils/events'; import is from './utils/is'; import loadSprite from './utils/loadSprite'; @@ -175,7 +175,7 @@ const controls = { } if ('class' in attributes) { - if (attributes.class.includes(this.config.classNames.control)) { + if (!attributes.class.includes(this.config.classNames.control)) { attributes.class += ` ${this.config.classNames.control}`; } } else { @@ -1016,12 +1016,20 @@ const controls = { button.setAttribute('aria-expanded', show); } + // Show the actual popup if (is.element(popup)) { toggleHidden(popup, !show); toggleClass(this.elements.container, this.config.classNames.menu.open, show); if (show) { popup.removeAttribute('tabindex'); + + // Focus the first item if key interaction + if (event.type === 'keydown') { + const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); + const firstItem = pane.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, true); + } } else { popup.setAttribute('tabindex', -1); } @@ -1104,9 +1112,7 @@ const controls = { // Focus the first item const firstItem = target.querySelector('[role^="menuitem"]'); - if (firstItem) { - firstItem.focus(); - } + setFocus.call(this, firstItem, true); }, // Build the default HTML @@ -1257,6 +1263,9 @@ const controls = { role: 'menu', }); + home.appendChild(menu); + inner.appendChild(home); + // Build the menu items this.config.settings.forEach(type => { const menuItem = createElement( @@ -1270,6 +1279,26 @@ const controls = { }), ); + // Handle space or -> to open menu + on(menuItem, 'keydown', event => { + // We only care about space and -> + if (![32,39].includes(event.which)) { + return; + } + + // Prevent play / seek + event.preventDefault(); + event.stopPropagation(); + + // Show the respective menu + controls.showMenuPanel.call(this, type); + }, false); + + // Show menu on click + on(menuItem, 'click', () => { + controls.showMenuPanel.call(this, type); + }); + const flex = createElement('span', null, i18n.get(type, this.config)); const value = createElement('span', { @@ -1290,18 +1319,55 @@ const controls = { }); // Back button - const back = createElement( - 'button', - { - type: 'button', - class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, - }, - i18n.get(type, this.config), + const backButton = createElement('button', { + type: 'button', + class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, + }); + + // Visible label + backButton.appendChild( + createElement( + 'span', + { + 'aria-hidden': true, + }, + i18n.get(type, this.config), + ), + ); + + // Screen reader label + backButton.appendChild( + createElement( + 'span', + { + class: this.config.classNames.hidden, + }, + i18n.get('menuBack', this.config), + ), ); - back.addEventListener('click', () => { + + // Handle space or -> to open menu + on(backButton, 'keydown', event => { + // We only care about <- + if (event.which !== 37) { + return; + } + + // Prevent seek + event.preventDefault(); + event.stopPropagation(); + + // Show the respective menu + controls.showMenuPanel.call(this, 'home'); + }, false); + + // Go back + on(backButton, 'click', () => { controls.showMenuPanel.call(this, 'home'); }); - pane.appendChild(back); + + // Add to pane + pane.appendChild(backButton); // Menu pane.appendChild( @@ -1312,17 +1378,10 @@ const controls = { inner.appendChild(pane); - menuItem.addEventListener('click', () => { - controls.showMenuPanel.call(this, type); - }); - this.elements.settings.buttons[type] = menuItem; this.elements.settings.panels[type] = pane; }); - home.appendChild(menu); - inner.appendChild(home); - popup.appendChild(inner); control.appendChild(popup); container.appendChild(control); diff --git a/src/js/listeners.js b/src/js/listeners.js index 05a12147..ffcd81fa 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -483,11 +483,34 @@ class Listeners { // Airplay this.bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); - // Settings menu + // Settings menu - click toggle this.bind(this.player.elements.buttons.settings, 'click', event => { controls.toggleMenu.call(this.player, event); }); + // Settings menu - keyboard toggle + this.bind( + this.player.elements.buttons.settings, + 'keydown', + event => { + // We only care about space + if (event.which !== 32) { + return; + } + + // Prevent scroll + event.preventDefault(); + + // Prevent playing video + event.stopPropagation(); + + // Toggle menu + controls.toggleMenu.call(this.player, event); + }, + null, + false, + ); + // Set range input alternative "value", which matches the tooltip time (#954) this.bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => { const clientRect = this.player.elements.progress.getBoundingClientRect(); @@ -626,8 +649,7 @@ class Listeners { const inverted = event.webkitDirectionInvertedFromDevice; // Get delta from event. Invert if `inverted` is true - const [x, y] = [event.deltaX, -event.deltaY] - .map(value => inverted ? -value : value); + const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value)); // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 7b58c9ff..e7e17041 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -294,3 +294,18 @@ export function trapFocus(element = null, toggle = false) { toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } + +// Set focus and tab focus class +export function setFocus(element = null, tabFocus = false) { + if (!is.element(element)) { + return; + } + + // Set regular focus + element.focus(); + + // If we want to mimic keyboard focus via tab + if (tabFocus) { + toggleClass(element, this.config.classNames.tabFocus); + } +} diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index 8b333f65..7d7d66b0 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -5,7 +5,7 @@ // Nicer focus styles // --------------------------------------- @mixin plyr-tab-focus($color: $plyr-tab-focus-default-color) { - box-shadow: 0 0 0 3px rgba($color, 0.35); + box-shadow: 0 0 0 5px rgba($color, 0.5); outline: 0; } -- cgit v1.2.3 From e63ad7c74bd763043344fd2bd568a64e7ea18622 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 15 Jul 2018 19:23:28 +1000 Subject: Keyboard and focus improvements --- src/js/controls.js | 128 ++++++++----- src/js/listeners.js | 456 ++++++++++++++++++++++++++++++++++++----------- src/js/utils/elements.js | 41 +++-- src/js/utils/events.js | 81 +++++++-- 4 files changed, 527 insertions(+), 179 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 0f162de6..da404441 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1,5 +1,6 @@ // ========================================================================== // Plyr controls +// TODO: This needs to be split into smaller files and cleaned up // ========================================================================== import captions from './captions'; @@ -360,11 +361,53 @@ const controls = { return container; }, + // Bind keyboard shortcuts for a menu item + bindMenuItemShortcuts(menuItem, type) { + // Handle space or -> to open menu + on(menuItem, 'keydown', event => { + // We only care about space and ⬆️ ⬇️️ ➡️ + if (![32,38,39,40].includes(event.which)) { + return; + } + + // Prevent play / seek + event.preventDefault(); + event.stopPropagation(); + + const isRadioButton = matches(menuItem, '[role="menuitemradio"]'); + + // Show the respective menu + if (!isRadioButton && [32,39].includes(event.which)) { + controls.showMenuPanel.call(this, type); + } else { + let target; + + if (event.which !== 32) { + if (event.which === 40 || isRadioButton && event.which === 39) { + target = menuItem.nextElementSibling; + + if (!is.element(target)) { + target = menuItem.parentNode.firstElementChild; + } + } else { + target = menuItem.previousElementSibling; + + if (!is.element(target)) { + target = menuItem.parentNode.lastElementChild; + } + } + + setFocus.call(this, target, true); + } + } + }, false); + }, + // Create a settings menu item createMenuItem({ value, list, type, title, badge = null, checked = false }) { const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); - const item = createElement( + const menuItem = createElement( 'button', extend(attributes, { type: 'button', @@ -384,30 +427,38 @@ const controls = { flex.appendChild(badge); } - item.appendChild(flex); + menuItem.appendChild(flex); - Object.defineProperty(item, 'checked', { + // Replicate radio button behaviour + Object.defineProperty(menuItem, 'checked', { enumerable: true, get() { - return item.getAttribute('aria-checked') === 'true'; + return menuItem.getAttribute('aria-checked') === 'true'; }, set(checked) { // Ensure exclusivity if (checked) { - Array.from(item.parentNode.children) + Array.from(menuItem.parentNode.children) .filter(node => matches(node, '[role="menuitemradio"]')) .forEach(node => node.setAttribute('aria-checked', 'false')); } - item.setAttribute('aria-checked', checked ? 'true' : 'false'); + menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); }, }); this.listeners.bind( - item, - 'click', - () => { - item.checked = true; + menuItem, + 'click keydown', + event => { + if (event.type === 'keydown' && event.which !== 32) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + menuItem.checked = true; switch (type) { case 'language': @@ -429,9 +480,12 @@ const controls = { controls.showMenuPanel.call(this, 'home'); }, type, + false, ); - list.appendChild(item); + controls.bindMenuItemShortcuts.call(this, menuItem, type); + + list.appendChild(menuItem); }, // Format a time for display @@ -993,7 +1047,7 @@ const controls = { }, // Show/hide menu - toggleMenu(event) { + toggleMenu(input) { const { popup } = this.elements.settings; const button = this.elements.buttons.settings; @@ -1002,11 +1056,11 @@ const controls = { return; } - const show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden'); + const show = is.boolean(input) ? input : is.element(popup) && popup.hasAttribute('hidden'); - if (is.event(event)) { - const isMenuItem = is.element(popup) && popup.contains(event.target); - const isButton = event.target === this.elements.buttons.settings; + if (is.event(input)) { + const isMenuItem = is.element(popup) && popup.contains(input.target); + const isButton = input.target === this.elements.buttons.settings; // If the click was inside the form or if the click // wasn't the button or menu item and we're trying to @@ -1017,7 +1071,7 @@ const controls = { // Prevent the toggle being caught by the doc listener if (isButton) { - event.stopPropagation(); + input.stopPropagation(); } } @@ -1031,17 +1085,11 @@ const controls = { toggleHidden(popup, !show); toggleClass(this.elements.container, this.config.classNames.menu.open, show); - if (show) { - popup.removeAttribute('tabindex'); - - // Focus the first item if key interaction - if (event.type === 'keydown') { - const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); - const firstItem = pane.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, true); - } - } else { - popup.setAttribute('tabindex', -1); + // Focus the first item if key interaction + if (show && is.event(input) && input.type === 'keydown') { + const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); + const firstItem = pane.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, true); } } }, @@ -1275,9 +1323,11 @@ const controls = { home.appendChild(menu); inner.appendChild(home); + this.elements.settings.panels.home = home; // Build the menu items this.config.settings.forEach(type => { + // TODO: bundle this with the createMenuItem helper and bindings const menuItem = createElement( 'button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { @@ -1289,20 +1339,8 @@ const controls = { }), ); - // Handle space or -> to open menu - on(menuItem, 'keydown', event => { - // We only care about space and -> - if (![32,39].includes(event.which)) { - return; - } - - // Prevent play / seek - event.preventDefault(); - event.stopPropagation(); - - // Show the respective menu - controls.showMenuPanel.call(this, type); - }, false); + // Bind menu shortcuts for keyboard users + controls.bindMenuItemShortcuts.call(this, menuItem, type); // Show menu on click on(menuItem, 'click', () => { @@ -1356,8 +1394,8 @@ const controls = { ), ); - // Handle space or -> to open menu - on(backButton, 'keydown', event => { + // Go back via keyboard + on(pane, 'keydown', event => { // We only care about <- if (event.which !== 37) { return; @@ -1371,7 +1409,7 @@ const controls = { controls.showMenuPanel.call(this, 'home'); }, false); - // Go back + // Go back via button click on(backButton, 'click', () => { controls.showMenuPanel.call(this, 'home'); }); diff --git a/src/js/listeners.js b/src/js/listeners.js index ffcd81fa..0b803454 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -5,7 +5,13 @@ import controls from './controls'; import ui from './ui'; import browser from './utils/browser'; -import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements'; +import { + getElement, + getElements, + matches, + toggleClass, + toggleHidden, +} from './utils/elements'; import { on, once, toggleListener, triggerEvent } from './utils/events'; import is from './utils/is'; @@ -13,9 +19,12 @@ class Listeners { constructor(player) { this.player = player; this.lastKey = null; + this.focusTimer = null; + this.lastKeyDown = null; this.handleKey = this.handleKey.bind(this); this.toggleMenu = this.toggleMenu.bind(this); + this.setTabFocus = this.setTabFocus.bind(this); this.firstTouch = this.firstTouch.bind(this); } @@ -45,21 +54,51 @@ class Listeners { // Handle the key on keydown // Reset on keyup if (pressed) { - // Which keycodes should we prevent default - const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79]; - // Check focused element // and if the focused element is not editable (e.g. text input) // and any that accept key input http://webaim.org/techniques/keyboard/ - const focused = getFocusElement(); - if ( - is.element(focused) && - (focused !== this.player.elements.inputs.seek && - matches(focused, this.player.config.selectors.editable)) - ) { - return; + const focused = document.activeElement; + if (is.element(focused)) { + const { editable } = this.player.config.selectors; + const { seek } = this.player.elements.inputs; + + if (focused !== seek && matches(focused, editable)) { + return; + } + + if ( + event.which === 32 && + matches(focused, 'button, [role^="menuitem"]') + ) { + return; + } } + // Which keycodes should we prevent default + const preventDefault = [ + 32, + 37, + 38, + 39, + 40, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 56, + 57, + 67, + 70, + 73, + 75, + 76, + 77, + 79, + ]; + // If the code is found prevent default (e.g. prevent scrolling for arrows) if (preventDefault.includes(code)) { event.preventDefault(); @@ -153,7 +192,11 @@ class Listeners { // Escape is handle natively when in full screen // So we only need to worry about non native - if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) { + if ( + !this.player.fullscreen.enabled && + this.player.fullscreen.active && + code === 27 + ) { this.player.fullscreen.toggle(); } @@ -174,48 +217,117 @@ class Listeners { this.player.touch = true; // Add touch class - toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); + toggleClass( + this.player.elements.container, + this.player.config.classNames.isTouch, + true, + ); + } + + setTabFocus(event) { + clearTimeout(this.focusTimer); + + // Ignore any key other than tab + if (event.type === 'keydown' && event.code !== 'Tab') { + return; + } + + // Store reference to event timeStamp + if (event.type === 'keydown') { + this.lastKeyDown = event.timeStamp; + } + + // Remove current classes + const removeCurrent = () => { + const className = this.player.config.classNames.tabFocus; + const current = getElements.call(this.player, `.${className}`); + toggleClass(current, className, false); + }; + + // Determine if a key was pressed to trigger this event + const wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; + + // Ignore focus events if a key was pressed prior + if (event.type === 'focus' && !wasKeyDown) { + return; + } + + // Remove all current + removeCurrent(); + + // Delay the adding of classname until the focus has changed + // This event fires before the focusin event + + this.focusTimer = setTimeout(() => { + const focused = document.activeElement; + + // Ignore if current focus element isn't inside the player + if (!this.player.elements.container.contains(focused)) { + return; + } + + toggleClass( + document.activeElement, + this.player.config.classNames.tabFocus, + true, + ); + }, 10); } // Global window & document listeners global(toggle = true) { // Keyboard shortcuts if (this.player.config.keyboard.global) { - toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); + toggleListener.call( + this.player, + window, + 'keydown keyup', + this.handleKey, + toggle, + false, + ); } // Click anywhere closes menu - toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle); + toggleListener.call( + this.player, + document.body, + 'click', + this.toggleMenu, + toggle, + ); // Detect touch by events once.call(this.player, document.body, 'touchstart', this.firstTouch); + + // Tab focus detection + toggleListener.call( + this.player, + document.body, + 'keydown focus blur', + this.setTabFocus, + toggle, + false, + true, + ); } // Container listeners container() { // Keyboard shortcuts - if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { - on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false); + if ( + !this.player.config.keyboard.global && + this.player.config.keyboard.focused + ) { + on.call( + this.player, + this.player.elements.container, + 'keydown keyup', + this.handleKey, + false, + ); } - // Detect tab focus - // Remove class on blur/focusout - on.call(this.player, this.player.elements.container, 'focusout', event => { - toggleClass(event.target, this.player.config.classNames.tabFocus, false); - }); - // Add classname to tabbed elements - on.call(this.player, this.player.elements.container, 'keydown', event => { - if (event.keyCode !== 9) { - return; - } - - // Delay the adding of classname until the focus has changed - // This event fires before the focusin event - setTimeout(() => { - toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true); - }, 0); - }); - // Toggle controls on mouse events and entering fullscreen on.call( this.player, @@ -231,7 +343,9 @@ class Listeners { } // Show, then hide after a timeout unless another control event occurs - const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); + const show = ['touchstart', 'touchmove', 'mousemove'].includes( + event.type, + ); let delay = 0; @@ -245,7 +359,10 @@ class Listeners { clearTimeout(this.player.timers.controls); // Set new timer to prevent flicker when seeking - this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); + this.player.timers.controls = setTimeout( + () => ui.toggleControls.call(this.player, false), + delay, + ); }, ); } @@ -253,34 +370,50 @@ class Listeners { // Listen for media events media() { // Time change on media - on.call(this.player, this.player.media, 'timeupdate seeking seeked', event => - controls.timeUpdate.call(this.player, event), + on.call( + this.player, + this.player.media, + 'timeupdate seeking seeked', + event => controls.timeUpdate.call(this.player, event), ); // Display duration - on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event => - controls.durationUpdate.call(this.player, event), + on.call( + this.player, + this.player.media, + 'durationchange loadeddata loadedmetadata', + event => controls.durationUpdate.call(this.player, event), ); // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point on.call(this.player, this.player.media, 'canplay', () => { toggleHidden(this.player.elements.volume, !this.player.hasAudio); - toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); + toggleHidden( + this.player.elements.buttons.mute, + !this.player.hasAudio, + ); }); // Handle the media finishing on.call(this.player, this.player.media, 'ended', () => { // Show poster on end - if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { + if ( + this.player.isHTML5 && + this.player.isVideo && + this.player.config.resetOnEnd + ) { // Restart this.player.restart(); } }); // Check for buffer progress - on.call(this.player, this.player.media, 'progress playing seeking seeked', event => - controls.updateProgress.call(this.player, event), + on.call( + this.player, + this.player.media, + 'progress playing seeking seeked', + event => controls.updateProgress.call(this.player, event), ); // Handle volume changes @@ -289,13 +422,19 @@ class Listeners { ); // Handle play/pause - on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event => - ui.checkPlaying.call(this.player, event), + on.call( + this.player, + this.player.media, + 'playing play pause ended emptied timeupdate', + event => ui.checkPlaying.call(this.player, event), ); // Loading state - on.call(this.player, this.player.media, 'waiting canplay seeked playing', event => - ui.checkLoading.call(this.player, event), + on.call( + this.player, + this.player.media, + 'waiting canplay seeked playing', + event => ui.checkLoading.call(this.player, event), ); // If autoplay, then load advertisement if required @@ -308,14 +447,23 @@ class Listeners { // If ads are enabled, wait for them first if (this.player.ads.enabled && !this.player.ads.initialized) { // Wait for manager response - this.player.ads.managerPromise.then(() => this.player.ads.play()).catch(() => this.player.play()); + this.player.ads.managerPromise + .then(() => this.player.ads.play()) + .catch(() => this.player.play()); } }); // Click video - if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { + if ( + this.player.supported.ui && + this.player.config.clickToPlay && + !this.player.isAudio + ) { // Re-fetch the wrapper - const wrapper = getElement.call(this.player, `.${this.player.config.classNames.video}`); + const wrapper = getElement.call( + this.player, + `.${this.player.config.classNames.video}`, + ); // Bail if there's no wrapper (this should never happen) if (!is.element(wrapper)) { @@ -325,7 +473,11 @@ class Listeners { // On click play, pause ore restart on.call(this.player, wrapper, 'click', () => { // Touch devices will just show controls (if we're hiding controls) - if (this.player.config.hideControls && this.player.touch && !this.player.paused) { + if ( + this.player.config.hideControls && + this.player.touch && + !this.player.paused + ) { return; } @@ -356,7 +508,10 @@ class Listeners { // Volume change on.call(this.player, this.player.media, 'volumechange', () => { // Save to storage - this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); + this.player.storage.set({ + volume: this.player.volume, + muted: this.player.muted, + }); }); // Speed change @@ -377,12 +532,20 @@ class Listeners { // Quality change on.call(this.player, this.player.media, 'qualitychange', event => { // Update UI - controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); + controls.updateSetting.call( + this.player, + 'quality', + null, + event.detail.quality, + ); }); // Proxy events to container // Bubble up key events for Edge - const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' '); + const proxyEvents = this.player.config.events + .concat(['keyup', 'keydown']) + .join(' '); + on.call(this.player, this.player.media, proxyEvents, event => { let { detail = {} } = event; @@ -391,7 +554,13 @@ class Listeners { detail = this.player.media.error; } - triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail); + triggerEvent.call( + this.player, + this.player.elements.container, + event.type, + true, + detail, + ); }); } @@ -439,13 +608,28 @@ class Listeners { } // Pause - this.bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); + this.bind( + this.player.elements.buttons.restart, + 'click', + this.player.restart, + 'restart', + ); // Rewind - this.bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); + this.bind( + this.player.elements.buttons.rewind, + 'click', + this.player.rewind, + 'rewind', + ); // Rewind - this.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 this.bind( @@ -458,7 +642,9 @@ class Listeners { ); // Captions toggle - this.bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions()); + this.bind(this.player.elements.buttons.captions, 'click', () => + this.player.toggleCaptions(), + ); // Fullscreen toggle this.bind( @@ -481,7 +667,12 @@ class Listeners { ); // Airplay - this.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 - click toggle this.bind(this.player.elements.buttons.settings, 'click', event => { @@ -512,37 +703,51 @@ class Listeners { ); // Set range input alternative "value", which matches the tooltip time (#954) - 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); - }); + 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 - this.bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { - const seek = event.currentTarget; + 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; - const eventType = event.type; + const code = event.keyCode ? event.keyCode : event.which; + const eventType = event.type; - if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { - return; - } - // Was playing before? - const play = seek.hasAttribute('play-on-seeked'); - - // Done seeking - const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); - - // If we're done seeking and it was playing, resume playback - if (play && done) { - seek.removeAttribute('play-on-seeked'); - this.player.play(); - } else if (!done && this.player.playing) { - seek.setAttribute('play-on-seeked', ''); - this.player.pause(); - } - }); + if ( + (eventType === 'keydown' || eventType === 'keyup') && + (code !== 39 && code !== 37) + ) { + return; + } + // Was playing before? + const play = seek.hasAttribute('play-on-seeked'); + + // Done seeking + const done = ['mouseup', 'touchend', 'keyup'].includes( + event.type, + ); + + // If we're done seeking and it was playing, resume playback + if (play && done) { + seek.removeAttribute('play-on-seeked'); + this.player.play(); + } else if (!done && this.player.playing) { + seek.setAttribute('play-on-seeked', ''); + this.player.pause(); + } + }, + ); // Seek this.bind( @@ -560,14 +765,18 @@ class Listeners { seek.removeAttribute('seek-value'); - this.player.currentTime = seekTo / seek.max * this.player.duration; + this.player.currentTime = + seekTo / seek.max * this.player.duration; }, 'seek', ); // 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)) { + if ( + this.player.config.toggleInvert && + !is.element(this.player.elements.display.duration) + ) { this.bind(this.player.elements.display.currentTime, 'click', () => { // Do nothing if we're at the start if (this.player.currentTime === 0) { @@ -592,32 +801,54 @@ class Listeners { // Polyfill for lower fill in for webkit if (browser.isWebkit) { - Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => { - this.bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target)); + Array.from( + getElements.call(this.player, 'input[type="range"]'), + ).forEach(element => { + this.bind(element, 'input', event => + controls.updateRangeFill.call(this.player, event.target), + ); }); } // Seek tooltip - this.bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => - controls.updateSeekTooltip.call(this.player, 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) - this.bind(this.player.elements.controls, 'mouseenter mouseleave', event => { - this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; - }); + 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) - this.bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); - }); + 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 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 - toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); + toggleClass( + elements.controls, + config.classNames.noTransition, + event.type === 'focusin', + ); // Toggle ui.toggleControls.call(this.player, event.type === 'focusin'); @@ -626,7 +857,11 @@ class Listeners { if (event.type === 'focusin') { // Restore transition setTimeout(() => { - toggleClass(elements.controls, config.classNames.noTransition, false); + toggleClass( + elements.controls, + config.classNames.noTransition, + false, + ); }, 0); // Delay a little more for keyboard users @@ -634,8 +869,12 @@ class Listeners { // Clear timer clearTimeout(timers.controls); + // Hide - timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); + timers.controls = setTimeout( + () => ui.toggleControls.call(this.player, false), + delay, + ); } }); @@ -649,7 +888,9 @@ class Listeners { const inverted = event.webkitDirectionInvertedFromDevice; // Get delta from event. Invert if `inverted` is true - const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value)); + const [x, y] = [event.deltaX, -event.deltaY].map( + value => (inverted ? -value : value), + ); // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); @@ -659,7 +900,10 @@ class Listeners { // Don't break page scrolling at max and min const { volume } = this.player.media; - if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) { + if ( + (direction === 1 && volume < 1) || + (direction === -1 && volume > 0) + ) { event.preventDefault(); } }, diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index e7e17041..3a3dfcfd 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -116,7 +116,11 @@ export function emptyElement(element) { // Replace element export function replaceElement(newChild, oldChild) { - if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { + if ( + !is.element(oldChild) || + !is.element(oldChild.parentNode) || + !is.element(newChild) + ) { return null; } @@ -203,6 +207,10 @@ export function toggleHidden(element, hidden) { // Mirror Element.classList.toggle, with IE compatibility for "force" argument export function toggleClass(element, className, force) { + if (is.nodeList(element)) { + return Array.from(element).map(e => toggleClass(e, className, force)); + } + if (is.element(element)) { let method = 'toggle'; if (typeof force !== 'undefined') { @@ -213,7 +221,7 @@ export function toggleClass(element, className, force) { return element.classList.contains(className); } - return null; + return false; } // Has class name @@ -249,26 +257,16 @@ export function getElement(selector) { return this.elements.container.querySelector(selector); } -// Get the focused element -export function getFocusElement() { - let focused = document.activeElement; - - if (!focused || focused === document.body) { - focused = null; - } else { - focused = document.querySelector(':focus'); - } - - return focused; -} - // Trap focus inside container export function trapFocus(element = null, toggle = false) { if (!is.element(element)) { return; } - const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); + const focusable = getElements.call( + this, + 'button:not(:disabled), input:not(:disabled), [tabindex]', + ); const first = focusable[0]; const last = focusable[focusable.length - 1]; @@ -279,7 +277,7 @@ export function trapFocus(element = null, toggle = false) { } // Get the current focused element - const focused = getFocusElement(); + const focused = document.activeElement; if (focused === last && !event.shiftKey) { // Move focus to first element that can be tabbed if Shift isn't used @@ -292,7 +290,14 @@ export function trapFocus(element = null, toggle = false) { } }; - toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); + toggleListener.call( + this, + this.elements.container, + 'keydown', + trap, + toggle, + false, + ); } // Set focus and tab focus class diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9f734f04..9820fcae 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -27,9 +27,21 @@ const supportsPassiveListeners = (() => { })(); // Toggle event listener -export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { +export function toggleListener( + element, + event, + callback, + toggle = false, + passive = true, + capture = false, +) { // Bail if no element, event, or callback - if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { + if ( + !element || + !('addEventListener' in element) || + is.empty(event) || + !is.function(callback) + ) { return; } @@ -57,28 +69,74 @@ export function toggleListener(element, event, callback, toggle = false, passive this.eventListeners.push({ element, type, callback, options }); } - element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); + element[toggle ? 'addEventListener' : 'removeEventListener']( + type, + callback, + options, + ); }); } // Bind event handler -export function on(element, events = '', callback, passive = true, capture = false) { - toggleListener.call(this, element, events, callback, true, passive, capture); +export function on( + element, + events = '', + callback, + passive = true, + capture = false, +) { + toggleListener.call( + this, + element, + events, + callback, + true, + passive, + capture, + ); } // Unbind event handler -export function off(element, events = '', callback, passive = true, capture = false) { - toggleListener.call(this, element, events, callback, false, passive, capture); +export function off( + element, + events = '', + callback, + passive = true, + capture = false, +) { + toggleListener.call( + this, + element, + events, + callback, + false, + passive, + capture, + ); } // Bind once-only event handler -export function once(element, events = '', callback, passive = true, capture = false) { +export function once( + element, + events = '', + callback, + passive = true, + capture = false, +) { function onceCallback(...args) { off(element, events, onceCallback, passive, capture); callback.apply(this, args); } - toggleListener.call(this, element, events, onceCallback, true, passive, capture); + toggleListener.call( + this, + element, + events, + onceCallback, + true, + passive, + capture, + ); } // Trigger event @@ -115,6 +173,9 @@ export function unbindListeners() { // Run method when / if player is ready export function ready() { return new Promise( - resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)), + resolve => + this.ready + ? setTimeout(resolve, 0) + : on.call(this, this.elements.container, 'ready', resolve), ).then(() => {}); } -- cgit v1.2.3 From 3a8332bdb30e1f64047a0f8a3e60647f0473301b Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 29 Jul 2018 12:32:26 +1000 Subject: Fix for webkit redrawing issue --- src/sass/components/sliders.scss | 18 ++++++++++++++---- src/sass/lib/mixins.scss | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/sass/components/sliders.scss b/src/sass/components/sliders.scss index b9264b05..ee64271b 100644 --- a/src/sass/components/sliders.scss +++ b/src/sass/components/sliders.scss @@ -19,7 +19,11 @@ &::-webkit-slider-runnable-track { @include plyr-range-track(); - background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%)); + background-image: linear-gradient( + to right, + currentColor var(--value, 0%), + transparent var(--value, 0%) + ); } &::-webkit-slider-thumb { @@ -140,15 +144,21 @@ // Pressed styles &:active { &::-webkit-slider-thumb { - @include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color); + @include plyr-range-thumb-active( + $plyr-audio-range-thumb-shadow-color + ); } &::-moz-range-thumb { - @include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color); + @include plyr-range-thumb-active( + $plyr-audio-range-thumb-shadow-color + ); } &::-ms-thumb { - @include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color); + @include plyr-range-thumb-active( + $plyr-audio-range-thumb-shadow-color + ); } } } diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index 7d7d66b0..2aec702c 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -28,7 +28,6 @@ border: 0; border-radius: ($plyr-range-track-height / 2); height: $plyr-range-track-height; - transition: all 0.3s ease; user-select: none; } @@ -45,7 +44,8 @@ } @mixin plyr-range-thumb-active($color: rgba($plyr-range-thumb-bg, 0.5)) { - box-shadow: $plyr-range-thumb-shadow, 0 0 0 $plyr-range-thumb-active-shadow-width $color; + box-shadow: $plyr-range-thumb-shadow, + 0 0 0 $plyr-range-thumb-active-shadow-width $color; } // Fullscreen styles -- cgit v1.2.3 From 599b33e55fb4c0aec78cd0895bcd13f3fed12ad2 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 30 Jul 2018 01:13:12 +1000 Subject: Click to play fix, poster fix, iOS controls fixes --- src/js/controls.js | 14 +- src/js/listeners.js | 432 ++++++++++++++++++++------------------ src/js/plyr.js | 109 ++++++++-- src/js/support.js | 20 +- src/js/ui.js | 82 ++++++-- src/js/utils/animation.js | 14 +- src/sass/components/poster.scss | 7 +- src/sass/components/progress.scss | 1 - src/sass/lib/mixins.scss | 1 - 9 files changed, 412 insertions(+), 268 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index da404441..27611b2f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -378,7 +378,7 @@ const controls = { // Show the respective menu if (!isRadioButton && [32,39].includes(event.which)) { - controls.showMenuPanel.call(this, type); + controls.showMenuPanel.call(this, type, true); } else { let target; @@ -477,7 +477,7 @@ const controls = { break; } - controls.showMenuPanel.call(this, 'home'); + controls.showMenuPanel.call(this, 'home', event.type === 'keydown'); }, type, false, @@ -1118,7 +1118,7 @@ const controls = { }, // Show a panel in the menu - showMenuPanel(type = '') { + showMenuPanel(type = '', tabFocus = false) { const target = document.getElementById(`plyr-settings-${this.id}-${type}`); // Nothing to show, bail @@ -1170,7 +1170,7 @@ const controls = { // Focus the first item const firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, true); + setFocus.call(this, firstItem, tabFocus); }, // Build the default HTML @@ -1344,7 +1344,7 @@ const controls = { // Show menu on click on(menuItem, 'click', () => { - controls.showMenuPanel.call(this, type); + controls.showMenuPanel.call(this, type, false); }); const flex = createElement('span', null, i18n.get(type, this.config)); @@ -1406,12 +1406,12 @@ const controls = { event.stopPropagation(); // Show the respective menu - controls.showMenuPanel.call(this, 'home'); + controls.showMenuPanel.call(this, 'home', true); }, false); // Go back via button click on(backButton, 'click', () => { - controls.showMenuPanel.call(this, 'home'); + controls.showMenuPanel.call(this, 'home', false); }); // Add to pane diff --git a/src/js/listeners.js b/src/js/listeners.js index 0b803454..e19894ba 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -4,10 +4,12 @@ import controls from './controls'; import ui from './ui'; +import { repaint } from './utils/animation'; import browser from './utils/browser'; import { getElement, getElements, + hasClass, matches, toggleClass, toggleHidden, @@ -30,6 +32,7 @@ class Listeners { // Handle key presses handleKey(event) { + const { player } = this; const code = event.keyCode ? event.keyCode : event.which; const pressed = event.type === 'keydown'; const repeat = pressed && code === this.lastKey; @@ -48,7 +51,7 @@ class Listeners { // Seek by the number keys const seekByKey = () => { // Divide the max duration into 10th's and times by the number value - this.player.currentTime = this.player.duration / 10 * (code - 48); + player.currentTime = player.duration / 10 * (code - 48); }; // Handle the key on keydown @@ -59,8 +62,8 @@ class Listeners { // and any that accept key input http://webaim.org/techniques/keyboard/ const focused = document.activeElement; if (is.element(focused)) { - const { editable } = this.player.config.selectors; - const { seek } = this.player.elements.inputs; + const { editable } = player.config.selectors; + const { seek } = player.elements.inputs; if (focused !== seek && matches(focused, editable)) { return; @@ -126,52 +129,52 @@ class Listeners { case 75: // Space and K key if (!repeat) { - this.player.togglePlay(); + player.togglePlay(); } break; case 38: // Arrow up - this.player.increaseVolume(0.1); + player.increaseVolume(0.1); break; case 40: // Arrow down - this.player.decreaseVolume(0.1); + player.decreaseVolume(0.1); break; case 77: // M key if (!repeat) { - this.player.muted = !this.player.muted; + player.muted = !player.muted; } break; case 39: // Arrow forward - this.player.forward(); + player.forward(); break; case 37: // Arrow back - this.player.rewind(); + player.rewind(); break; case 70: // F key - this.player.fullscreen.toggle(); + player.fullscreen.toggle(); break; case 67: // C key if (!repeat) { - this.player.toggleCaptions(); + player.toggleCaptions(); } break; case 76: // L key - this.player.loop = !this.player.loop; + player.loop = !player.loop; break; /* case 73: @@ -193,11 +196,11 @@ class Listeners { // Escape is handle natively when in full screen // So we only need to worry about non native if ( - !this.player.fullscreen.enabled && - this.player.fullscreen.active && + !player.fullscreen.enabled && + player.fullscreen.active && code === 27 ) { - this.player.fullscreen.toggle(); + player.fullscreen.toggle(); } // Store last code for next cycle @@ -214,17 +217,21 @@ class Listeners { // Device is touch enabled firstTouch() { - this.player.touch = true; + const { player } = this; + + player.touch = true; // Add touch class toggleClass( - this.player.elements.container, - this.player.config.classNames.isTouch, + player.elements.container, + player.config.classNames.isTouch, true, ); } setTabFocus(event) { + const { player } = this; + clearTimeout(this.focusTimer); // Ignore any key other than tab @@ -239,8 +246,8 @@ class Listeners { // Remove current classes const removeCurrent = () => { - const className = this.player.config.classNames.tabFocus; - const current = getElements.call(this.player, `.${className}`); + const className = player.config.classNames.tabFocus; + const current = getElements.call(player, `.${className}`); toggleClass(current, className, false); }; @@ -257,18 +264,17 @@ class Listeners { // Delay the adding of classname until the focus has changed // This event fires before the focusin event - this.focusTimer = setTimeout(() => { const focused = document.activeElement; // Ignore if current focus element isn't inside the player - if (!this.player.elements.container.contains(focused)) { + if (!player.elements.container.contains(focused)) { return; } toggleClass( document.activeElement, - this.player.config.classNames.tabFocus, + player.config.classNames.tabFocus, true, ); }, 10); @@ -276,10 +282,12 @@ class Listeners { // Global window & document listeners global(toggle = true) { + const { player } = this; + // Keyboard shortcuts - if (this.player.config.keyboard.global) { + if (player.config.keyboard.global) { toggleListener.call( - this.player, + player, window, 'keydown keyup', this.handleKey, @@ -290,7 +298,7 @@ class Listeners { // Click anywhere closes menu toggleListener.call( - this.player, + player, document.body, 'click', this.toggleMenu, @@ -298,11 +306,11 @@ class Listeners { ); // Detect touch by events - once.call(this.player, document.body, 'touchstart', this.firstTouch); + once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection toggleListener.call( - this.player, + player, document.body, 'keydown focus blur', this.setTabFocus, @@ -314,14 +322,13 @@ class Listeners { // Container listeners container() { + const { player } = this; + // Keyboard shortcuts - if ( - !this.player.config.keyboard.global && - this.player.config.keyboard.focused - ) { + if (!player.config.keyboard.global && player.config.keyboard.focused) { on.call( - this.player, - this.player.elements.container, + player, + player.elements.container, 'keydown keyup', this.handleKey, false, @@ -330,11 +337,11 @@ class Listeners { // Toggle controls on mouse events and entering fullscreen on.call( - this.player, - this.player.elements.container, + player, + player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { - const { controls } = this.player.elements; + const { controls } = player.elements; // Remove button states for fullscreen if (event.type === 'enterfullscreen') { @@ -350,17 +357,17 @@ class Listeners { let delay = 0; if (show) { - ui.toggleControls.call(this.player, true); + ui.toggleControls.call(player, true); // Use longer timeout for touch devices - delay = this.player.touch ? 3000 : 2000; + delay = player.touch ? 3000 : 2000; } // Clear timer - clearTimeout(this.player.timers.controls); + clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking - this.player.timers.controls = setTimeout( - () => ui.toggleControls.call(this.player, false), + player.timers.controls = setTimeout( + () => ui.toggleControls.call(player, false), delay, ); }, @@ -369,100 +376,89 @@ class Listeners { // Listen for media events media() { + const { player } = this; + // Time change on media - on.call( - this.player, - this.player.media, - 'timeupdate seeking seeked', - event => controls.timeUpdate.call(this.player, event), + on.call(player, player.media, 'timeupdate seeking seeked', event => + controls.timeUpdate.call(player, event), ); // Display duration on.call( - this.player, - this.player.media, + player, + player.media, 'durationchange loadeddata loadedmetadata', - event => controls.durationUpdate.call(this.player, event), + event => controls.durationUpdate.call(player, event), ); // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point - on.call(this.player, this.player.media, 'canplay', () => { - toggleHidden(this.player.elements.volume, !this.player.hasAudio); - toggleHidden( - this.player.elements.buttons.mute, - !this.player.hasAudio, - ); + on.call(player, player.media, 'canplay', () => { + toggleHidden(player.elements.volume, !player.hasAudio); + toggleHidden(player.elements.buttons.mute, !player.hasAudio); }); // Handle the media finishing - on.call(this.player, this.player.media, 'ended', () => { + on.call(player, player.media, 'ended', () => { // Show poster on end - if ( - this.player.isHTML5 && - this.player.isVideo && - this.player.config.resetOnEnd - ) { + if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { // Restart - this.player.restart(); + player.restart(); } }); // Check for buffer progress on.call( - this.player, - this.player.media, + player, + player.media, 'progress playing seeking seeked', - event => controls.updateProgress.call(this.player, event), + event => controls.updateProgress.call(player, event), ); // Handle volume changes - on.call(this.player, this.player.media, 'volumechange', event => - controls.updateVolume.call(this.player, event), + on.call(player, player.media, 'volumechange', event => + controls.updateVolume.call(player, event), ); // Handle play/pause on.call( - this.player, - this.player.media, + player, + player.media, 'playing play pause ended emptied timeupdate', - event => ui.checkPlaying.call(this.player, event), + event => ui.checkPlaying.call(player, event), ); // Loading state - on.call( - this.player, - this.player.media, - 'waiting canplay seeked playing', - event => ui.checkLoading.call(this.player, event), + on.call(player, player.media, 'waiting canplay seeked playing', event => + ui.checkLoading.call(player, event), ); // If autoplay, then load advertisement if required // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows - on.call(this.player, this.player.media, 'playing', () => { - if (!this.player.ads) { + on.call(player, player.media, 'playing', () => { + if (!player.ads) { return; } // If ads are enabled, wait for them first - if (this.player.ads.enabled && !this.player.ads.initialized) { + if (player.ads.enabled && !player.ads.initialized) { // Wait for manager response - this.player.ads.managerPromise - .then(() => this.player.ads.play()) - .catch(() => this.player.play()); + player.ads.managerPromise + .then(() => player.ads.play()) + .catch(() => player.play()); } }); // Click video if ( - this.player.supported.ui && - this.player.config.clickToPlay && - !this.player.isAudio + player.supported.ui && + player.config.clickToPlay && + !player.isAudio ) { // Re-fetch the wrapper const wrapper = getElement.call( - this.player, - `.${this.player.config.classNames.video}`, + player, + `.${player.config.classNames.video}`, ); // Bail if there's no wrapper (this should never happen) @@ -471,32 +467,49 @@ class Listeners { } // On click play, pause ore restart - on.call(this.player, wrapper, 'click', () => { - // Touch devices will just show controls (if we're hiding controls) - if ( - this.player.config.hideControls && - this.player.touch && - !this.player.paused - ) { - return; - } + on.call( + player, + player.elements.container, + 'click touchstart', + event => { + const targets = [player.elements.container, wrapper]; + + // Ignore if click if not container or in video wrapper + if ( + !targets.includes(event.target) && + !wrapper.contains(event.target) + ) { + return; + } - if (this.player.paused) { - this.player.play(); - } else if (this.player.ended) { - this.player.restart(); - this.player.play(); - } else { - this.player.pause(); - } - }); + // First touch on touch devices will just show controls (if we're hiding controls) + // If controls are shown then it'll toggle like a pointer device + if ( + player.config.hideControls && + player.touch && + hasClass( + player.elements.container, + player.config.classNames.hideControls, + ) + ) { + return; + } + + if (player.ended) { + player.restart(); + player.play(); + } else { + player.togglePlay(); + } + }, + ); } // Disable right click - if (this.player.supported.ui && this.player.config.disableContextMenu) { + if (player.supported.ui && player.config.disableContextMenu) { on.call( - this.player, - this.player.elements.wrapper, + player, + player.elements.wrapper, 'contextmenu', event => { event.preventDefault(); @@ -506,34 +519,34 @@ class Listeners { } // Volume change - on.call(this.player, this.player.media, 'volumechange', () => { + on.call(player, player.media, 'volumechange', () => { // Save to storage - this.player.storage.set({ - volume: this.player.volume, - muted: this.player.muted, + player.storage.set({ + volume: player.volume, + muted: player.muted, }); }); // Speed change - on.call(this.player, this.player.media, 'ratechange', () => { + on.call(player, player.media, 'ratechange', () => { // Update UI - controls.updateSetting.call(this.player, 'speed'); + controls.updateSetting.call(player, 'speed'); // Save to storage - this.player.storage.set({ speed: this.player.speed }); + player.storage.set({ speed: player.speed }); }); // Quality request - on.call(this.player, this.player.media, 'qualityrequested', event => { + on.call(player, player.media, 'qualityrequested', event => { // Save to storage - this.player.storage.set({ quality: event.detail.quality }); + player.storage.set({ quality: event.detail.quality }); }); // Quality change - on.call(this.player, this.player.media, 'qualitychange', event => { + on.call(player, player.media, 'qualitychange', event => { // Update UI controls.updateSetting.call( - this.player, + player, 'quality', null, event.detail.quality, @@ -542,21 +555,21 @@ class Listeners { // Proxy events to container // Bubble up key events for Edge - const proxyEvents = this.player.config.events + const proxyEvents = player.config.events .concat(['keyup', 'keydown']) .join(' '); - on.call(this.player, this.player.media, proxyEvents, event => { + on.call(player, player.media, proxyEvents, event => { let { detail = {} } = event; // Get error details from media if (event.type === 'error') { - detail = this.player.media.error; + detail = player.media.error; } triggerEvent.call( - this.player, - this.player.elements.container, + player, + player.elements.container, event.type, true, detail, @@ -566,28 +579,30 @@ class Listeners { // Run default and custom handlers proxy(event, defaultHandler, customHandlerKey) { - const customHandler = this.player.config.listeners[customHandlerKey]; + const { player } = this; + const customHandler = player.config.listeners[customHandlerKey]; const hasCustomHandler = is.function(customHandler); let returned = true; // Execute custom handler if (hasCustomHandler) { - returned = customHandler.call(this.player, event); + returned = customHandler.call(player, event); } // Only call default handler if not prevented in custom handler if (returned && is.function(defaultHandler)) { - defaultHandler.call(this.player, event); + defaultHandler.call(player, event); } } // Trigger custom and default handlers bind(element, type, defaultHandler, customHandlerKey, passive = true) { - const customHandler = this.player.config.listeners[customHandlerKey]; + const { player } = this; + const customHandler = player.config.listeners[customHandlerKey]; const hasCustomHandler = is.function(customHandler); on.call( - this.player, + player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), @@ -597,91 +612,93 @@ class Listeners { // Listen for control events controls() { + const { player } = this; + // IE doesn't support input event, so we fallback to change const inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle - if (this.player.elements.buttons.play) { - Array.from(this.player.elements.buttons.play).forEach(button => { - this.bind(button, 'click', this.player.togglePlay, 'play'); + if (player.elements.buttons.play) { + Array.from(player.elements.buttons.play).forEach(button => { + this.bind(button, 'click', player.togglePlay, 'play'); }); } // Pause this.bind( - this.player.elements.buttons.restart, + player.elements.buttons.restart, 'click', - this.player.restart, + player.restart, 'restart', ); // Rewind this.bind( - this.player.elements.buttons.rewind, + player.elements.buttons.rewind, 'click', - this.player.rewind, + player.rewind, 'rewind', ); // Rewind this.bind( - this.player.elements.buttons.fastForward, + player.elements.buttons.fastForward, 'click', - this.player.forward, + player.forward, 'fastForward', ); // Mute toggle this.bind( - this.player.elements.buttons.mute, + player.elements.buttons.mute, 'click', () => { - this.player.muted = !this.player.muted; + player.muted = !player.muted; }, 'mute', ); // Captions toggle - this.bind(this.player.elements.buttons.captions, 'click', () => - this.player.toggleCaptions(), + this.bind(player.elements.buttons.captions, 'click', () => + player.toggleCaptions(), ); // Fullscreen toggle this.bind( - this.player.elements.buttons.fullscreen, + player.elements.buttons.fullscreen, 'click', () => { - this.player.fullscreen.toggle(); + player.fullscreen.toggle(); }, 'fullscreen', ); // Picture-in-Picture this.bind( - this.player.elements.buttons.pip, + player.elements.buttons.pip, 'click', () => { - this.player.pip = 'toggle'; + player.pip = 'toggle'; }, 'pip', ); // Airplay this.bind( - this.player.elements.buttons.airplay, + player.elements.buttons.airplay, 'click', - this.player.airplay, + player.airplay, 'airplay', ); // Settings menu - click toggle - this.bind(this.player.elements.buttons.settings, 'click', event => { - controls.toggleMenu.call(this.player, event); + this.bind(player.elements.buttons.settings, 'click', event => { + controls.toggleMenu.call(player, event); }); // Settings menu - keyboard toggle this.bind( - this.player.elements.buttons.settings, + player.elements.buttons.settings, 'keydown', event => { // We only care about space @@ -696,33 +713,28 @@ class Listeners { event.stopPropagation(); // Toggle menu - controls.toggleMenu.call(this.player, event); + controls.toggleMenu.call(player, event); }, null, false, ); // Set range input alternative "value", which matches the tooltip time (#954) - 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); - }, - ); + this.bind(player.elements.inputs.seek, 'mousedown mousemove', event => { + const rect = player.elements.progress.getBoundingClientRect(); + const percent = 100 / rect.width * (event.pageX - rect.left); + event.currentTarget.setAttribute('seek-value', percent); + }); // Pause while seeking this.bind( - this.player.elements.inputs.seek, + player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { const seek = event.currentTarget; - const code = event.keyCode ? event.keyCode : event.which; const eventType = event.type; + const attribute = 'play-on-seeked'; if ( (eventType === 'keydown' || eventType === 'keyup') && @@ -731,7 +743,7 @@ class Listeners { return; } // Was playing before? - const play = seek.hasAttribute('play-on-seeked'); + const play = seek.hasAttribute(attribute); // Done seeking const done = ['mouseup', 'touchend', 'keyup'].includes( @@ -740,18 +752,18 @@ class Listeners { // If we're done seeking and it was playing, resume playback if (play && done) { - seek.removeAttribute('play-on-seeked'); - this.player.play(); - } else if (!done && this.player.playing) { - seek.setAttribute('play-on-seeked', ''); - this.player.pause(); + seek.removeAttribute(attribute); + player.play(); + } else if (!done && player.playing) { + seek.setAttribute(attribute, ''); + player.pause(); } }, ); // Seek this.bind( - this.player.elements.inputs.seek, + player.elements.inputs.seek, inputEvent, event => { const seek = event.currentTarget; @@ -765,8 +777,13 @@ class Listeners { seek.removeAttribute('seek-value'); - this.player.currentTime = - seekTo / seek.max * this.player.duration; + // Super weird iOS bug where after you interact with an , + // it takes over further interactions on the page. This is a hack + if (browser.isIos) { + repaint(seek); + } + + player.currentTime = seekTo / seek.max * player.duration; }, 'seek', ); @@ -774,65 +791,61 @@ 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) + player.config.toggleInvert && + !is.element(player.elements.display.duration) ) { - this.bind(this.player.elements.display.currentTime, 'click', () => { + this.bind(player.elements.display.currentTime, 'click', () => { // Do nothing if we're at the start - if (this.player.currentTime === 0) { + if (player.currentTime === 0) { return; } - this.player.config.invertTime = !this.player.config.invertTime; + player.config.invertTime = !player.config.invertTime; - controls.timeUpdate.call(this.player); + controls.timeUpdate.call(player); }); } // Volume this.bind( - this.player.elements.inputs.volume, + player.elements.inputs.volume, inputEvent, event => { - this.player.volume = event.target.value; + player.volume = event.target.value; }, 'volume', ); // Polyfill for lower fill in for webkit if (browser.isWebkit) { - Array.from( - getElements.call(this.player, 'input[type="range"]'), - ).forEach(element => { - this.bind(element, 'input', event => - controls.updateRangeFill.call(this.player, event.target), - ); - }); + Array.from(getElements.call(player, 'input[type="range"]')).forEach( + element => { + this.bind(element, 'input', event => + controls.updateRangeFill.call(player, event.target), + ); + }, + ); } // Seek tooltip this.bind( - this.player.elements.progress, + player.elements.progress, 'mouseenter mouseleave mousemove', - event => controls.updateSeekTooltip.call(this.player, event), + event => controls.updateSeekTooltip.call(player, event), ); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) - this.bind( - this.player.elements.controls, - 'mouseenter mouseleave', - event => { - this.player.elements.controls.hover = - !this.player.touch && event.type === 'mouseenter'; - }, - ); + this.bind(player.elements.controls, 'mouseenter mouseleave', event => { + player.elements.controls.hover = + !player.touch && event.type === 'mouseenter'; + }); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) this.bind( - this.player.elements.controls, + player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - this.player.elements.controls.pressed = [ + player.elements.controls.pressed = [ 'mousedown', 'touchstart', ].includes(event.type); @@ -840,21 +853,22 @@ class Listeners { ); // Focus in/out on controls - this.bind(this.player.elements.controls, 'focusin focusout', event => { - const { config, elements, timers } = this.player; + this.bind(player.elements.controls, 'focusin focusout', event => { + const { config, elements, timers } = player; + const isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element toggleClass( elements.controls, config.classNames.noTransition, - event.type === 'focusin', + isFocusIn, ); // Toggle - ui.toggleControls.call(this.player, event.type === 'focusin'); + ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay - if (event.type === 'focusin') { + if (isFocusIn) { // Restore transition setTimeout(() => { toggleClass( @@ -872,7 +886,7 @@ class Listeners { // Hide timers.controls = setTimeout( - () => ui.toggleControls.call(this.player, false), + () => ui.toggleControls.call(player, false), delay, ); } @@ -880,7 +894,7 @@ class Listeners { // Mouse wheel for volume this.bind( - this.player.elements.inputs.volume, + player.elements.inputs.volume, 'wheel', event => { // Detect "natural" scroll - suppored on OS X Safari only @@ -896,10 +910,10 @@ class Listeners { const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2% - this.player.increaseVolume(direction / 50); + player.increaseVolume(direction / 50); // Don't break page scrolling at max and min - const { volume } = this.player.media; + const { volume } = player.media; if ( (direction === 1 && volume < 1) || (direction === -1 && volume > 0) diff --git a/src/js/plyr.js b/src/js/plyr.js index ab1f7866..b4c54ca8 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -52,7 +52,11 @@ class Plyr { } // jQuery, NodeList or Array passed, use first element - if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) { + if ( + (window.jQuery && this.media instanceof jQuery) || + is.nodeList(this.media) || + is.array(this.media) + ) { // eslint-disable-next-line this.media = this.media[0]; } @@ -65,7 +69,9 @@ class Plyr { options || {}, (() => { try { - return JSON.parse(this.media.getAttribute('data-plyr-config')); + return JSON.parse( + this.media.getAttribute('data-plyr-config'), + ); } catch (e) { return {}; } @@ -185,21 +191,30 @@ class Plyr { // TODO: replace fullscreen.iosNative with this playsinline config option // YouTube requires the playsinline in the URL if (this.isYouTube) { - this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); + this.config.playsinline = truthy.includes( + url.searchParams.get('playsinline'), + ); } else { this.config.playsinline = true; } } } else { //
with attributes - this.provider = this.media.getAttribute(this.config.attributes.embed.provider); + this.provider = this.media.getAttribute( + this.config.attributes.embed.provider, + ); // Remove attribute - this.media.removeAttribute(this.config.attributes.embed.provider); + this.media.removeAttribute( + this.config.attributes.embed.provider, + ); } // Unsupported or missing provider - if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { + if ( + is.empty(this.provider) || + !Object.keys(providers).includes(this.provider) + ) { this.debug.error('Setup failed: Invalid provider'); return; } @@ -221,7 +236,10 @@ class Plyr { if (this.media.hasAttribute('autoplay')) { this.config.autoplay = true; } - if (this.media.hasAttribute('playsinline')) { + if ( + this.media.hasAttribute('playsinline') || + this.media.hasAttribute('webkit-playsinline') + ) { this.config.playsinline = true; } if (this.media.hasAttribute('muted')) { @@ -239,7 +257,11 @@ class Plyr { } // Check for support again but with type - this.supported = support.check(this.type, this.provider, this.config.playsinline); + this.supported = support.check( + this.type, + this.provider, + this.config.playsinline, + ); // If no support for even API, bail if (!this.supported.api) { @@ -272,9 +294,14 @@ class Plyr { // Listen for events if debugging if (this.config.debug) { - on.call(this, this.elements.container, this.config.events.join(' '), event => { - this.debug.log(`event: ${event.type}`); - }); + on.call( + this, + this.elements.container, + this.config.events.join(' '), + event => { + this.debug.log(`event: ${event.type}`); + }, + ); } // Setup interface @@ -293,7 +320,9 @@ class Plyr { this.fullscreen = new Fullscreen(this); // Setup ads if provided - this.ads = new Ads(this); + if (this.config.ads.enabled) { + this.ads = new Ads(this); + } // Autoplay if required if (this.config.autoplay) { @@ -422,7 +451,9 @@ class Plyr { * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime */ rewind(seekTime) { - this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime); + this.currentTime = + this.currentTime - + (is.number(seekTime) ? seekTime : this.config.seekTime); } /** @@ -430,7 +461,9 @@ class Plyr { * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime */ forward(seekTime) { - this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime); + this.currentTime = + this.currentTime + + (is.number(seekTime) ? seekTime : this.config.seekTime); } /** @@ -447,7 +480,9 @@ class Plyr { const inputIsValid = is.number(input) && input > 0; // Set - this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; + this.media.currentTime = inputIsValid + ? Math.min(input, this.duration) + : 0; // Logging this.debug.log(`Seeking to ${this.currentTime} seconds`); @@ -497,7 +532,10 @@ class Plyr { // Media duration can be NaN or Infinity before the media has loaded const realDuration = (this.media || {}).duration; - const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; + const duration = + !is.number(realDuration) || realDuration === Infinity + ? 0 + : realDuration; // If config duration is funky, use regular duration return fauxDuration || duration; @@ -691,12 +729,16 @@ class Plyr { if (!options.includes(quality)) { const value = closest(options, quality); - this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`); + this.debug.warn( + `Unsupported quality option: ${quality}, using ${value} instead`, + ); quality = value; } // Trigger request event - triggerEvent.call(this, this.media, 'qualityrequested', false, { quality }); + triggerEvent.call(this, this.media, 'qualityrequested', false, { + quality, + }); // Update config config.selected = quality; @@ -888,7 +930,9 @@ class Plyr { const toggle = is.boolean(input) ? input : this.pip === states.inline; // Toggle based on current state - this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); + this.media.webkitSetPresentationMode( + toggle ? states.pip : states.inline, + ); } /** @@ -921,25 +965,39 @@ class Plyr { // Don't toggle if missing UI support or if it's audio if (this.supported.ui && !this.isAudio) { // Get state before change - const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); + const isHidden = hasClass( + this.elements.container, + this.config.classNames.hideControls, + ); // Negate the argument if not undefined since adding the class to hides the controls const force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state - const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); + const hiding = toggleClass( + this.elements.container, + this.config.classNames.hideControls, + force, + ); // Close menu - if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { + if ( + hiding && + this.config.controls.includes('settings') && + !is.empty(this.config.settings) + ) { controls.toggleMenu.call(this, false); } + // Trigger event on change if (hiding !== isHidden) { const eventName = hiding ? 'controlshidden' : 'controlsshown'; triggerEvent.call(this, this.media, eventName); } + return !hiding; } + return false; } @@ -1017,7 +1075,12 @@ class Plyr { replaceElement(this.elements.original, this.elements.container); // Event - triggerEvent.call(this, this.elements.original, 'destroyed', true); + triggerEvent.call( + this, + this.elements.original, + 'destroyed', + true, + ); // Callback if (is.function(callback)) { diff --git a/src/js/support.js b/src/js/support.js index 6395293f..4681f5c7 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -25,9 +25,13 @@ const support = { // Check for support // Basic functionality vs full UI check(type, provider, playsinline) { - const canPlayInline = browser.isIPhone && playsinline && support.playsinline; + const canPlayInline = + browser.isIPhone && playsinline && support.playsinline; const api = support[type] || provider !== 'html5'; - const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); + const ui = + api && + support.rangeInput && + (type !== 'video' || !browser.isIPhone || canPlayInline); return { api, @@ -37,7 +41,9 @@ const support = { // Picture-in-picture support // Safari only currently - pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(), + pip: (() => + !browser.isIPhone && + is.function(createElement('video').webkitSetPresentationMode))(), // Airplay support // Safari only currently @@ -69,7 +75,9 @@ const support = { } try { - return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); + return Boolean( + type && this.media.canPlayType(type).replace(/no/, ''), + ); } catch (err) { return false; } @@ -94,7 +102,9 @@ const support = { // Reduced motion iOS & MacOS setting // https://webkit.org/blog/7551/responsive-design-for-motion/ - reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches, + reducedMotion: + 'matchMedia' in window && + window.matchMedia('(prefers-reduced-motion)').matches, }; export default support; diff --git a/src/js/ui.js b/src/js/ui.js index 34fe7e82..8c61d805 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -14,8 +14,16 @@ import loadImage from './utils/loadImage'; const ui = { addStyleHook() { - toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); - toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); + toggleClass( + this.elements.container, + this.config.selectors.container.replace('.', ''), + true, + ); + toggleClass( + this.elements.container, + this.config.classNames.uiSupported, + this.supported.ui, + ); }, // Toggle native HTML5 media controls @@ -35,7 +43,9 @@ const ui = { // Don't setup interface if no support if (!this.supported.ui) { - this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); + this.debug.warn( + `Basic support only for ${this.provider} ${this.type}`, + ); // Restore native controls ui.toggleNativeControls.call(this, true); @@ -93,13 +103,25 @@ const ui = { ); // Check for airplay support - toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); + toggleClass( + this.elements.container, + this.config.classNames.airplay.supported, + support.airplay && this.isHTML5, + ); // Add iOS class - toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); + toggleClass( + this.elements.container, + this.config.classNames.isIos, + browser.isIos, + ); // Add touch class - toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); + toggleClass( + this.elements.container, + this.config.classNames.isTouch, + this.touch, + ); // Ready for API calls this.ready = true; @@ -149,7 +171,9 @@ const ui = { } // Default to media type - const title = !is.empty(this.config.title) ? this.config.title : 'video'; + const title = !is.empty(this.config.title) + ? this.config.title + : 'video'; const format = i18n.get('frameTitle', this.config); iframe.setAttribute('title', format.replace('{title}', title)); @@ -158,7 +182,11 @@ const ui = { // Toggle poster togglePoster(enable) { - toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); + toggleClass( + this.elements.container, + this.config.classNames.posterEnabled, + enable, + ); }, // Set the poster image (async) @@ -189,7 +217,9 @@ const ui = { .then(() => { // Prevent race conditions if (poster !== this.poster) { - throw new Error('setPoster cancelled by later call to setPoster'); + throw new Error( + 'setPoster cancelled by later call to setPoster', + ); } }) .then(() => { @@ -207,9 +237,21 @@ const ui = { // Check playing state checkPlaying(event) { // Class hooks - toggleClass(this.elements.container, this.config.classNames.playing, this.playing); - toggleClass(this.elements.container, this.config.classNames.paused, this.paused); - toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); + toggleClass( + this.elements.container, + this.config.classNames.playing, + this.playing, + ); + toggleClass( + this.elements.container, + this.config.classNames.paused, + this.paused, + ); + toggleClass( + this.elements.container, + this.config.classNames.stopped, + this.stopped, + ); // Set state Array.from(this.elements.buttons.play || []).forEach(target => { @@ -235,7 +277,11 @@ const ui = { // Timer to prevent flicker when seeking this.timers.loading = setTimeout(() => { // Update progress bar loading class state - toggleClass(this.elements.container, this.config.classNames.loading, this.loading); + toggleClass( + this.elements.container, + this.config.classNames.loading, + this.loading, + ); // Update controls visibility ui.toggleControls.call(this); @@ -248,7 +294,15 @@ const ui = { if (controls && this.config.hideControls) { // Show controls if force, loading, paused, or button interaction, otherwise hide - this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover)); + this.toggleControls( + Boolean( + force || + this.loading || + this.paused || + controls.pressed || + controls.hover, + ), + ); } }, }; diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index 95e39f03..49bc0b8c 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -15,7 +15,9 @@ export const transitionEndEvent = (() => { transition: 'transitionend', }; - const type = Object.keys(events).find(event => element.style[event] !== undefined); + const type = Object.keys(events).find( + event => element.style[event] !== undefined, + ); return is.string(type) ? events[type] : false; })(); @@ -23,8 +25,12 @@ export const transitionEndEvent = (() => { // Force repaint of element export function repaint(element) { setTimeout(() => { - toggleHidden(element, true); - element.offsetHeight; // eslint-disable-line - toggleHidden(element, false); + try { + toggleHidden(element, true); + element.offsetHeight; // eslint-disable-line + toggleHidden(element, false); + } catch (e) { + // Do nothing + } }, 0); } diff --git a/src/sass/components/poster.scss b/src/sass/components/poster.scss index 9bf7398d..15e87257 100644 --- a/src/sass/components/poster.scss +++ b/src/sass/components/poster.scss @@ -7,17 +7,16 @@ background-position: 50% 50%; background-repeat: no-repeat; background-size: contain; + display: none; height: 100%; left: 0; - opacity: 0; + pointer-events: none; position: absolute; top: 0; - transition: opacity 0.3s ease; width: 100%; z-index: 1; } .plyr--stopped.plyr__poster-enabled .plyr__poster { - opacity: 1; - pointer-events: none; + display: block; } diff --git a/src/sass/components/progress.scss b/src/sass/components/progress.scss index eddd32ab..16992808 100644 --- a/src/sass/components/progress.scss +++ b/src/sass/components/progress.scss @@ -3,7 +3,6 @@ // -------------------------------------------------------------- .plyr__progress { - display: flex; flex: 1; left: $plyr-range-thumb-height / 2; margin-right: $plyr-range-thumb-height; diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index 2aec702c..e6afe046 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -36,7 +36,6 @@ border: 0; border-radius: 100%; box-shadow: $plyr-range-thumb-shadow; - box-sizing: border-box; height: $plyr-range-thumb-height; position: relative; transition: all 0.2s ease; -- cgit v1.2.3 From 248005e8e05529ee5aa558a2126c3f80019be759 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 30 Jul 2018 23:29:02 +1000 Subject: Fix merge --- src/js/plyr.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/js/plyr.js b/src/js/plyr.js index f33b39c7..9437cdbe 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -192,8 +192,7 @@ class Plyr { // YouTube requires the playsinline in the URL if (this.isYouTube) { this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); - this.config.hl = url.searchParams.get('hl'); - ); + this.config.hl = url.searchParams.get('hl'); // TODO: Should this be setting language? } else { this.config.playsinline = true; } -- cgit v1.2.3 From 3a3358e2b473d8a10b1721f4753548a911ba3ace Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 30 Jul 2018 23:29:14 +1000 Subject: Make iOS range fix more universal --- src/js/listeners.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index e19894ba..ca88f3a4 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -761,6 +761,17 @@ class Listeners { }, ); + // Fix range inputs on iOS + // Super weird iOS bug where after you interact with an , + // it takes over further interactions on the page. This is a hack + if (browser.isIos) { + const inputs = getElements.call(player, 'input[type="range"]'); + + Array.from(inputs).forEach(input => + this.bind(input, inputEvent, event => repaint(event.target)), + ); + } + // Seek this.bind( player.elements.inputs.seek, @@ -777,12 +788,6 @@ class Listeners { seek.removeAttribute('seek-value'); - // Super weird iOS bug where after you interact with an , - // it takes over further interactions on the page. This is a hack - if (browser.isIos) { - repaint(seek); - } - player.currentTime = seekTo / seek.max * player.duration; }, 'seek', -- cgit v1.2.3 From db22a8e9c47b373d47652b2cc653f53e0b452de0 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Mon, 30 Jul 2018 23:20:27 +0200 Subject: Improve handling of the 'controls' argument --- src/js/controls.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index e95cfc86..c01e3584 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1117,11 +1117,6 @@ const controls = { // Build the default HTML // TODO: Set order based on order in the config.controls array? create(data) { - // Do nothing if we want no controls - if (is.empty(this.config.controls)) { - return null; - } - // Create the container const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); @@ -1401,13 +1396,19 @@ const controls = { }; let update = true; - if (is.string(this.config.controls) || is.element(this.config.controls)) { - // String or HTMLElement passed as the option + // If function, run it and use output + if (is.function(this.config.controls)) { + this.config.controls = this.config.controls.call(this.props); + } + + // Convert falsy controls to empty array (primarily for empty strings) + if (!this.config.controls) { + this.config.controls = []; + } + + if (is.element(this.config.controls) || is.string(this.config.controls)) { + // HTMLElement or Non-empty string passed as the option container = this.config.controls; - } else if (is.function(this.config.controls)) { - // A custom function to build controls - // The function can return a HTMLElement or String - container = this.config.controls.call(this, props); } else { // Create controls container = controls.create.call(this, { -- cgit v1.2.3 From 9488de30e5ec5363a9fa3298c846a5ba100e4dbb Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 31 Jul 2018 16:17:55 +0200 Subject: Fix #1137: Improve captions positioning consistency --- src/sass/components/captions.scss | 7 ++++--- src/sass/components/controls.scss | 13 +++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/sass/components/captions.scss b/src/sass/components/captions.scss index 8fce581a..1cfca92e 100644 --- a/src/sass/components/captions.scss +++ b/src/sass/components/captions.scss @@ -17,7 +17,6 @@ padding: $plyr-control-spacing; position: absolute; text-align: center; - transform: translateY(-($plyr-control-spacing * 4)); transition: transform 0.4s ease-in-out; width: 100%; @@ -53,6 +52,8 @@ display: block; } -.plyr--hide-controls .plyr__captions { - transform: translateY(-($plyr-control-spacing * 1.5)); +// If the lower controls are shown and not empty +.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions { + transform: translateY(-($plyr-control-spacing * 4)); } + diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss index d06cb232..65b7554c 100644 --- a/src/sass/components/controls.scss +++ b/src/sass/components/controls.scss @@ -2,6 +2,11 @@ // Controls // -------------------------------------------------------------- +// Hide empty controls +.plyr__controls:empty { + display: none; +} + // Hide native controls .plyr--full-ui ::-webkit-media-controls { display: none; @@ -109,11 +114,3 @@ .plyr--fullscreen-enabled [data-plyr='fullscreen'] { display: inline-block; } - -.plyr__controls:empty { - display: none; - - ~ .plyr__captions { - transform: translateY(0); - } -} -- cgit v1.2.3 From 56a485bac63ebc211c3502be81caf3b7efa718ac Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 00:37:55 +1000 Subject: Fix Firefox spacebar issue --- src/js/controls.js | 525 ++++++++++++++++++++++++++++---------- src/js/listeners.js | 23 +- src/sass/components/control.scss | 17 +- src/sass/components/controls.scss | 50 ++-- src/sass/components/menus.scss | 6 +- 5 files changed, 438 insertions(+), 183 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 5c1446d3..4ee4c572 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -23,7 +23,9 @@ const controls = { // Get icon URL getIconUrl() { const url = new URL(this.config.iconUrl, window.location); - const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody); + const cors = + url.host !== window.location.host || + (browser.isIE && !window.svg4everybody); return { url: this.config.iconUrl, @@ -34,37 +36,82 @@ const controls = { // Find the UI controls findElements() { try { - this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); + this.elements.controls = getElement.call( + this, + this.config.selectors.controls.wrapper, + ); // Buttons this.elements.buttons = { - play: getElements.call(this, this.config.selectors.buttons.play), - pause: getElement.call(this, this.config.selectors.buttons.pause), - restart: getElement.call(this, this.config.selectors.buttons.restart), - rewind: getElement.call(this, this.config.selectors.buttons.rewind), - fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), + play: getElements.call( + this, + this.config.selectors.buttons.play, + ), + pause: getElement.call( + this, + this.config.selectors.buttons.pause, + ), + restart: getElement.call( + this, + this.config.selectors.buttons.restart, + ), + rewind: getElement.call( + this, + this.config.selectors.buttons.rewind, + ), + fastForward: getElement.call( + this, + this.config.selectors.buttons.fastForward, + ), mute: getElement.call(this, this.config.selectors.buttons.mute), pip: getElement.call(this, this.config.selectors.buttons.pip), - airplay: getElement.call(this, this.config.selectors.buttons.airplay), - settings: getElement.call(this, this.config.selectors.buttons.settings), - captions: getElement.call(this, this.config.selectors.buttons.captions), - fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), + airplay: getElement.call( + this, + this.config.selectors.buttons.airplay, + ), + settings: getElement.call( + this, + this.config.selectors.buttons.settings, + ), + captions: getElement.call( + this, + this.config.selectors.buttons.captions, + ), + fullscreen: getElement.call( + this, + this.config.selectors.buttons.fullscreen, + ), }; // Progress - this.elements.progress = getElement.call(this, this.config.selectors.progress); + this.elements.progress = getElement.call( + this, + this.config.selectors.progress, + ); // Inputs this.elements.inputs = { seek: getElement.call(this, this.config.selectors.inputs.seek), - volume: getElement.call(this, this.config.selectors.inputs.volume), + volume: getElement.call( + this, + this.config.selectors.inputs.volume, + ), }; // Display this.elements.display = { - buffer: getElement.call(this, this.config.selectors.display.buffer), - currentTime: getElement.call(this, this.config.selectors.display.currentTime), - duration: getElement.call(this, this.config.selectors.display.duration), + buffer: getElement.call( + this, + this.config.selectors.display.buffer, + ), + currentTime: getElement.call( + this, + this.config.selectors.display.currentTime, + ), + duration: getElement.call( + this, + this.config.selectors.display.duration, + ), }; // Seek tooltip @@ -77,7 +124,10 @@ const controls = { return true; } catch (error) { // Log it - this.debug.warn('It looks like there is a problem with your custom controls HTML', error); + this.debug.warn( + 'It looks like there is a problem with your custom controls HTML', + error, + ); // Restore native video controls this.toggleNativeControls(true); @@ -90,7 +140,9 @@ const controls = { createIcon(type, attributes) { const namespace = 'http://www.w3.org/2000/svg'; const iconUrl = controls.getIconUrl.call(this); - const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; + const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${ + this.config.iconPrefix + }`; // Create const icon = document.createElementNS(namespace, 'svg'); @@ -112,7 +164,11 @@ const controls = { if ('href' in use) { use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); } else { - use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); + use.setAttributeNS( + 'http://www.w3.org/1999/xlink', + 'xlink:href', + path, + ); } // Add to @@ -131,7 +187,9 @@ const controls = { const text = universals[type] || i18n.get(type, this.config); const attributes = Object.assign({}, attr, { - class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '), + class: [attr.class, this.config.classNames.hidden] + .filter(Boolean) + .join(' '), }); return createElement('span', attributes, text); }, @@ -218,7 +276,9 @@ const controls = { break; case 'play-large': - attributes.class += ` ${this.config.classNames.control}--overlaid`; + attributes.class += ` ${ + this.config.classNames.control + }--overlaid`; type = 'play'; label = 'play'; icon = 'play'; @@ -232,19 +292,41 @@ const controls = { // Setup toggle icon and labels if (toggle) { // Icon - button.appendChild(controls.createIcon.call(this, iconPressed, { class: 'icon--pressed' })); - button.appendChild(controls.createIcon.call(this, icon, { class: 'icon--not-pressed' })); + button.appendChild( + controls.createIcon.call(this, iconPressed, { + class: 'icon--pressed', + }), + ); + button.appendChild( + controls.createIcon.call(this, icon, { + class: 'icon--not-pressed', + }), + ); // Label/Tooltip - button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' })); - button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' })); + button.appendChild( + controls.createLabel.call(this, labelPressed, { + class: 'label--pressed', + }), + ); + button.appendChild( + controls.createLabel.call(this, label, { + class: 'label--not-pressed', + }), + ); } else { button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createLabel.call(this, label)); } // Merge attributes - extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); + extend( + attributes, + getAttributesFromSelector( + this.config.selectors.buttons[type], + attributes, + ), + ); setAttributes(button, attributes); @@ -344,12 +426,16 @@ const controls = { // Create time display createTime(type) { - const attributes = getAttributesFromSelector(this.config.selectors.display[type]); + const attributes = getAttributesFromSelector( + this.config.selectors.display[type], + ); const container = createElement( 'div', extend(attributes, { - class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(), + class: `${this.config.classNames.display.time} ${ + attributes.class ? attributes.class : '' + }`.trim(), 'aria-label': i18n.get(type, this.config), }), '00:00', @@ -364,55 +450,82 @@ const controls = { // Bind keyboard shortcuts for a menu item bindMenuItemShortcuts(menuItem, type) { // Handle space or -> to open menu - on(menuItem, 'keydown', event => { - // We only care about space and ⬆️ ⬇️️ ➡️ - if (![32,38,39,40].includes(event.which)) { - return; - } - - // Prevent play / seek - event.preventDefault(); - event.stopPropagation(); + on( + menuItem, + 'keydown keyup', + event => { + // We only care about space and ⬆️ ⬇️️ ➡️ + if (![32, 38, 39, 40].includes(event.which)) { + return; + } - const isRadioButton = matches(menuItem, '[role="menuitemradio"]'); + // Prevent play / seek + event.preventDefault(); + event.stopPropagation(); - // Show the respective menu - if (!isRadioButton && [32,39].includes(event.which)) { - controls.showMenuPanel.call(this, type, true); - } else { - let target; + // We're just here to prevent the keydown bubbling + if (event.type === 'keydown') { + return; + } - if (event.which !== 32) { - if (event.which === 40 || isRadioButton && event.which === 39) { - target = menuItem.nextElementSibling; + const isRadioButton = matches( + menuItem, + '[role="menuitemradio"]', + ); - if (!is.element(target)) { - target = menuItem.parentNode.firstElementChild; + // Show the respective menu + if (!isRadioButton && [32, 39].includes(event.which)) { + controls.showMenuPanel.call(this, type, true); + } else { + let target; + + if (event.which !== 32) { + if ( + event.which === 40 || + (isRadioButton && event.which === 39) + ) { + target = menuItem.nextElementSibling; + + if (!is.element(target)) { + target = menuItem.parentNode.firstElementChild; + } + } else { + target = menuItem.previousElementSibling; + + if (!is.element(target)) { + target = menuItem.parentNode.lastElementChild; + } } - } else { - target = menuItem.previousElementSibling; - if (!is.element(target)) { - target = menuItem.parentNode.lastElementChild; - } + setFocus.call(this, target, true); } - - setFocus.call(this, target, true); } - } - }, false); + }, + false, + ); }, // Create a settings menu item - createMenuItem({ value, list, type, title, badge = null, checked = false }) { - const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); + createMenuItem({ + value, + list, + type, + title, + badge = null, + checked = false, + }) { + const attributes = getAttributesFromSelector( + this.config.selectors.inputs[type], + ); const menuItem = createElement( 'button', extend(attributes, { type: 'button', role: 'menuitemradio', - class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(), + class: `${this.config.classNames.control} ${ + attributes.class ? attributes.class : '' + }`.trim(), 'aria-checked': checked, value, }), @@ -440,18 +553,23 @@ const controls = { if (checked) { Array.from(menuItem.parentNode.children) .filter(node => matches(node, '[role="menuitemradio"]')) - .forEach(node => node.setAttribute('aria-checked', 'false')); + .forEach(node => + node.setAttribute('aria-checked', 'false'), + ); } - menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); + menuItem.setAttribute( + 'aria-checked', + checked ? 'true' : 'false', + ); }, }); this.listeners.bind( menuItem, - 'click keydown', + 'click keyup', event => { - if (event.type === 'keydown' && event.which !== 32) { + if (event.type === 'keyup' && event.which !== 32) { return; } @@ -477,7 +595,11 @@ const controls = { break; } - controls.showMenuPanel.call(this, 'home', event.type === 'keydown'); + controls.showMenuPanel.call( + this, + 'home', + event.type === 'keyup', + ); }, type, false, @@ -520,12 +642,17 @@ const controls = { // Update range if (is.element(this.elements.inputs.volume)) { - controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); + controls.setRange.call( + this, + this.elements.inputs.volume, + this.muted ? 0 : this.volume, + ); } // Update mute state if (is.element(this.elements.buttons.mute)) { - this.elements.buttons.mute.pressed = this.muted || this.volume === 0; + this.elements.buttons.mute.pressed = + this.muted || this.volume === 0; } }, @@ -552,7 +679,9 @@ const controls = { const setProgress = (target, input) => { const value = is.number(input) ? input : 0; - const progress = is.element(target) ? target : this.elements.display.buffer; + const progress = is.element(target) + ? target + : this.elements.display.buffer; // Update value and label if (is.element(progress)) { @@ -576,7 +705,11 @@ const controls = { // Set seek range value only if it's a 'natural' time event if (event.type === 'timeupdate') { - controls.setRange.call(this, this.elements.inputs.seek, value); + controls.setRange.call( + this, + this.elements.inputs.seek, + value, + ); } break; @@ -584,7 +717,10 @@ const controls = { // Check buffer status case 'playing': case 'progress': - setProgress(this.elements.display.buffer, this.buffered * 100); + setProgress( + this.elements.display.buffer, + this.buffered * 100, + ); break; @@ -612,7 +748,9 @@ const controls = { const format = i18n.get('seekLabel', this.config); range.setAttribute( 'aria-valuetext', - format.replace('{currentTime}', currentTime).replace('{duration}', duration), + format + .replace('{currentTime}', currentTime) + .replace('{duration}', duration), ); } else if (matches(range, this.config.selectors.inputs.volume)) { const percent = range.value * 100; @@ -662,7 +800,10 @@ const controls = { if (is.event(event)) { percent = 100 / clientRect.width * (event.pageX - clientRect.left); } else if (hasClass(this.elements.display.seekTooltip, visible)) { - percent = parseFloat(this.elements.display.seekTooltip.style.left, 10); + percent = parseFloat( + this.elements.display.seekTooltip.style.left, + 10, + ); } else { return; } @@ -675,14 +816,21 @@ const controls = { } // Display the time a click would seek to - controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); + controls.updateTimeDisplay.call( + this, + this.elements.display.seekTooltip, + this.duration / 100 * percent, + ); // Set position this.elements.display.seekTooltip.style.left = `${percent}%`; // Show/hide the tooltip // If the event is a moues in/out and percentage is inside bounds - if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) { + if ( + is.event(event) && + ['mouseenter', 'mouseleave'].includes(event.type) + ) { toggle(event.type === 'mouseenter'); } }, @@ -690,7 +838,9 @@ const controls = { // Handle time change event timeUpdate(event) { // Only invert if only one time element is displayed and used for both duration and currentTime - const invert = !is.element(this.elements.display.duration) && this.config.invertTime; + const invert = + !is.element(this.elements.display.duration) && + this.config.invertTime; // Duration controls.updateTimeDisplay.call( @@ -712,7 +862,10 @@ const controls = { // Show the duration on metadataloaded or durationchange events durationUpdate() { // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false - if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) { + if ( + !this.supported.ui || + (!this.config.invertTime && this.currentTime) + ) { return; } @@ -720,7 +873,7 @@ const controls = { // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415 // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062 // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338 - if (this.duration >= 2**32) { + if (this.duration >= 2 ** 32) { toggleHidden(this.elements.display.currentTime, true); toggleHidden(this.elements.progress, true); return; @@ -728,7 +881,10 @@ const controls = { // Update ARIA values if (is.element(this.elements.inputs.seek)) { - this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration); + this.elements.inputs.seek.setAttribute( + 'aria-valuemax', + this.duration, + ); } // If there's a spot to display duration @@ -736,12 +892,20 @@ const controls = { // If there's only one time display, display duration there if (!hasDuration && this.config.displayDuration && this.paused) { - controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); + controls.updateTimeDisplay.call( + this, + this.elements.display.currentTime, + this.duration, + ); } // If there's a duration element, update content if (hasDuration) { - controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); + controls.updateTimeDisplay.call( + this, + this.elements.display.duration, + this.duration, + ); } // Update the tooltip (if visible) @@ -770,8 +934,13 @@ const controls = { } // Unsupported value - if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { - this.debug.warn(`Unsupported value of '${value}' for ${setting}`); + if ( + !is.empty(this.options[setting]) && + !this.options[setting].includes(value) + ) { + this.debug.warn( + `Unsupported value of '${value}' for ${setting}`, + ); return; } @@ -793,7 +962,9 @@ const controls = { } // Update the label - const label = this.elements.settings.buttons[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); // Find the radio option and check it @@ -808,11 +979,16 @@ const controls = { getLabel(setting, value) { switch (setting) { case 'speed': - return value === 1 ? i18n.get('normal', this.config) : `${value}×`; + return value === 1 + ? i18n.get('normal', this.config) + : `${value}×`; case 'quality': if (is.number(value)) { - const label = i18n.get(`qualityLabel.${value}`, this.config); + const label = i18n.get( + `qualityLabel.${value}`, + this.config, + ); if (!label.length) { return `${value}p`; @@ -839,15 +1015,20 @@ const controls = { } const type = 'quality'; - const list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); + 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)); + 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; + const toggle = + !is.empty(this.options.quality) && this.options.quality.length > 1; controls.toggleMenuButton.call(this, type, toggle); // Empty the menu @@ -947,7 +1128,9 @@ const controls = { // TODO: Captions or language? Currently it's mixed const type = 'captions'; - const list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); + const list = this.elements.settings.panels.captions.querySelector( + '[role="menu"]', + ); const tracks = captions.getTracks.call(this); const toggle = Boolean(tracks.length); @@ -970,7 +1153,9 @@ const controls = { value, checked: this.captions.toggled && this.currentTrack === value, title: captions.getLabel.call(this, track), - badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), + badge: + track.language && + controls.createBadge.call(this, track.language.toUpperCase()), list, type: 'language', })); @@ -998,7 +1183,9 @@ const controls = { } const type = 'speed'; - const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); + const list = this.elements.settings.panels.speed.querySelector( + '[role="menu"]', + ); // Set the speed options if (is.array(options)) { @@ -1008,10 +1195,13 @@ const controls = { } // Set options if passed and filter based on config - this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed)); + this.options.speed = this.options.speed.filter(speed => + this.config.speed.options.includes(speed), + ); // 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.toggleMenuButton.call(this, type, toggle); // Empty the menu @@ -1041,7 +1231,9 @@ const controls = { // Check if we need to hide/show the settings menu checkMenu() { const { buttons } = this.elements.settings; - const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); + const visible = + !is.empty(buttons) && + Object.values(buttons).some(button => !button.hidden); toggleHidden(this.elements.settings.menu, !visible); }, @@ -1056,10 +1248,13 @@ const controls = { return; } - const show = is.boolean(input) ? input : is.element(popup) && popup.hasAttribute('hidden'); + const show = is.boolean(input) + ? input + : is.element(popup) && popup.hasAttribute('hidden'); if (is.event(input)) { - const isMenuItem = is.element(popup) && popup.contains(input.target); + const isMenuItem = + is.element(popup) && popup.contains(input.target); const isButton = input.target === this.elements.buttons.settings; // If the click was inside the form or if the click @@ -1083,12 +1278,20 @@ const controls = { // Show the actual popup if (is.element(popup)) { toggleHidden(popup, !show); - toggleClass(this.elements.container, this.config.classNames.menu.open, show); + + toggleClass( + this.elements.container, + this.config.classNames.menu.open, + show, + ); // Focus the first item if key interaction - if (show && is.event(input) && input.type === 'keydown') { - const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); + if (show && is.event(input) && input.type === 'keyup') { + const pane = Object.values(this.elements.settings.panels).find( + pane => !pane.hidden, + ); const firstItem = pane.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, true); } } @@ -1119,7 +1322,9 @@ const controls = { // Show a panel in the menu showMenuPanel(type = '', tabFocus = false) { - const target = document.getElementById(`plyr-settings-${this.id}-${type}`); + const target = document.getElementById( + `plyr-settings-${this.id}-${type}`, + ); // Nothing to show, bail if (!is.element(target)) { @@ -1128,7 +1333,9 @@ const controls = { // Hide all other panels const container = target.parentNode; - const current = Array.from(container.children).find(node => !node.hidden); + const current = Array.from(container.children).find( + node => !node.hidden, + ); // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -1142,7 +1349,10 @@ const controls = { // Restore auto height/width const restore = event => { // We're only bothered about height and width on the container - if (event.target !== container || !['width', 'height'].includes(event.propertyName)) { + if ( + event.target !== container || + !['width', 'height'].includes(event.propertyName) + ) { return; } @@ -1182,7 +1392,10 @@ const controls = { } // Create the container - const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); + const container = createElement( + 'div', + getAttributesFromSelector(this.config.selectors.controls.wrapper), + ); // Restart button if (this.config.controls.includes('restart')) { @@ -1201,12 +1414,17 @@ const controls = { // Fast forward button if (this.config.controls.includes('fast-forward')) { - container.appendChild(controls.createButton.call(this, 'fast-forward')); + container.appendChild( + controls.createButton.call(this, 'fast-forward'), + ); } // Progress if (this.config.controls.includes('progress')) { - const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); + const progress = createElement( + 'div', + getAttributesFromSelector(this.config.selectors.progress), + ); // Seek range slider progress.appendChild( @@ -1240,7 +1458,9 @@ const controls = { // Media current time display if (this.config.controls.includes('current-time')) { - container.appendChild(controls.createTime.call(this, 'currentTime')); + container.appendChild( + controls.createTime.call(this, 'currentTime'), + ); } // Media duration display @@ -1288,7 +1508,10 @@ const controls = { } // 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 control = createElement('div', { class: 'plyr__menu', hidden: '', @@ -1330,13 +1553,20 @@ const controls = { // TODO: bundle this with the createMenuItem helper and bindings const menuItem = createElement( 'button', - extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { - type: 'button', - class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, - role: 'menuitem', - 'aria-haspopup': true, - hidden: '', - }), + extend( + getAttributesFromSelector( + this.config.selectors.buttons.settings, + ), + { + type: 'button', + class: `${this.config.classNames.control} ${ + this.config.classNames.control + }--forward`, + role: 'menuitem', + 'aria-haspopup': true, + hidden: '', + }, + ), ); // Bind menu shortcuts for keyboard users @@ -1347,7 +1577,11 @@ const controls = { controls.showMenuPanel.call(this, type, false); }); - const flex = createElement('span', null, i18n.get(type, this.config)); + const flex = createElement( + 'span', + null, + i18n.get(type, this.config), + ); const value = createElement('span', { class: this.config.classNames.menu.value, @@ -1369,7 +1603,9 @@ const controls = { // Back button const backButton = createElement('button', { type: 'button', - class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, + class: `${this.config.classNames.control} ${ + this.config.classNames.control + }--back`, }); // Visible label @@ -1395,19 +1631,24 @@ const controls = { ); // Go back via keyboard - on(pane, 'keydown', event => { - // We only care about <- - if (event.which !== 37) { - return; - } + on( + pane, + 'keydown', + event => { + // We only care about <- + if (event.which !== 37) { + return; + } - // Prevent seek - event.preventDefault(); - event.stopPropagation(); + // Prevent seek + event.preventDefault(); + event.stopPropagation(); - // Show the respective menu - controls.showMenuPanel.call(this, 'home', true); - }, false); + // Show the respective menu + controls.showMenuPanel.call(this, 'home', true); + }, + false, + ); // Go back via button click on(backButton, 'click', () => { @@ -1450,18 +1691,25 @@ const controls = { // Toggle fullscreen button if (this.config.controls.includes('fullscreen')) { - container.appendChild(controls.createButton.call(this, 'fullscreen')); + container.appendChild( + controls.createButton.call(this, 'fullscreen'), + ); } // Larger overlaid play button if (this.config.controls.includes('play-large')) { - this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); + this.elements.container.appendChild( + controls.createButton.call(this, 'play-large'), + ); } this.elements.controls = container; if (this.isHTML5) { - controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); + controls.setQualityMenu.call( + this, + html5.getQualityOptions.call(this), + ); } controls.setSpeedMenu.call(this); @@ -1496,7 +1744,10 @@ const controls = { }; let update = true; - if (is.string(this.config.controls) || is.element(this.config.controls)) { + if ( + is.string(this.config.controls) || + is.element(this.config.controls) + ) { // String or HTMLElement passed as the option container = this.config.controls; } else if (is.function(this.config.controls)) { @@ -1542,7 +1793,9 @@ const controls = { // Inject to custom location if (is.string(this.config.selectors.controls.container)) { - target = document.querySelector(this.config.selectors.controls.container); + target = document.querySelector( + this.config.selectors.controls.container, + ); } // Inject into the container by default @@ -1551,7 +1804,9 @@ const controls = { } // Inject controls HTML (needs to be before captions, hence "afterbegin") - const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML'; + const insertMethod = is.element(container) + ? 'insertAdjacentElement' + : 'insertAdjacentHTML'; target[insertMethod]('afterbegin', container); // Find the elements if need be @@ -1567,7 +1822,9 @@ const controls = { // Setup tooltips if (this.config.tooltips.controls) { const { classNames, selectors } = this.config; - const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`; + const selector = `${selectors.controls.wrapper} ${ + selectors.labels + } .${classNames.hidden}`; const labels = getElements.call(this, selector); Array.from(labels).forEach(label => { diff --git a/src/js/listeners.js b/src/js/listeners.js index ca88f3a4..80c2e630 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -6,14 +6,7 @@ import controls from './controls'; import ui from './ui'; import { repaint } from './utils/animation'; import browser from './utils/browser'; -import { - getElement, - getElements, - hasClass, - matches, - toggleClass, - toggleHidden, -} from './utils/elements'; +import { getElement, getElements, hasClass, matches, toggleClass, toggleHidden } from './utils/elements'; import { on, once, toggleListener, triggerEvent } from './utils/events'; import is from './utils/is'; @@ -235,7 +228,7 @@ class Listeners { clearTimeout(this.focusTimer); // Ignore any key other than tab - if (event.type === 'keydown' && event.code !== 'Tab') { + if (event.type === 'keydown' && event.which !== 9) { return; } @@ -699,18 +692,20 @@ class Listeners { // Settings menu - keyboard toggle this.bind( player.elements.buttons.settings, - 'keydown', + 'keyup', event => { - // We only care about space - if (event.which !== 32) { + // We only care about space and return + if (event.which !== 32 && event.which !== 13) { return; } // Prevent scroll event.preventDefault(); - // Prevent playing video - event.stopPropagation(); + // Prevent playing video (Firefox) + if (event.which === 32) { + event.stopPropagation(); + } // Toggle menu controls.toggleMenu.call(player, event); diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index cfef1b3a..2b681465 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -41,7 +41,7 @@ display: none; } -// Audio styles +// Audio control .plyr--audio .plyr__control { &.plyr__tab-focus, &:hover, @@ -51,6 +51,21 @@ } } +// Video control +.plyr--video .plyr__control { + svg { + filter: drop-shadow(0 1px 1px rgba(#000, 0.15)); + } + + // Hover and tab focus + &.plyr__tab-focus, + &:hover, + &[aria-expanded='true'] { + background: $plyr-video-control-bg-hover; + color: $plyr-video-control-color-hover; + } +} + // Large play button (video only) .plyr__control--overlaid { background: rgba($plyr-video-control-bg-hover, 0.8); diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss index d06cb232..0991a9bf 100644 --- a/src/sass/components/controls.scss +++ b/src/sass/components/controls.scss @@ -32,6 +32,14 @@ margin-left: ($plyr-control-spacing / 2); } + &:empty { + display: none; + + ~ .plyr__captions { + transform: translateY(0); + } + } + @media (min-width: $plyr-bp-sm) { > .plyr__control, .plyr__progress, @@ -48,6 +56,14 @@ } } +// Audio controls +.plyr--audio .plyr__controls { + background: $plyr-audio-controls-bg; + border-radius: inherit; + color: $plyr-audio-control-color; + padding: $plyr-control-spacing; +} + // Video controls .plyr--video .plyr__controls { background: linear-gradient( @@ -64,32 +80,10 @@ position: absolute; right: 0; transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out; - z-index: 2; - - .plyr__control { - svg { - filter: drop-shadow(0 1px 1px rgba(#000, 0.15)); - } - - // Hover and tab focus - &.plyr__tab-focus, - &:hover, - &[aria-expanded='true'] { - background: $plyr-video-control-bg-hover; - color: $plyr-video-control-color-hover; - } - } -} - -// Audio controls -.plyr--audio .plyr__controls { - background: $plyr-audio-controls-bg; - border-radius: inherit; - color: $plyr-audio-control-color; - padding: $plyr-control-spacing; + z-index: 3; } -// Hide controls +// Hide video controls .plyr--video.plyr--hide-controls .plyr__controls { opacity: 0; pointer-events: none; @@ -109,11 +103,3 @@ .plyr--fullscreen-enabled [data-plyr='fullscreen'] { display: inline-block; } - -.plyr__controls:empty { - display: none; - - ~ .plyr__captions { - transform: translateY(0); - } -} diff --git a/src/sass/components/menus.scss b/src/sass/components/menus.scss index be354e46..b8c85284 100644 --- a/src/sass/components/menus.scss +++ b/src/sass/components/menus.scss @@ -39,7 +39,8 @@ > div { overflow: hidden; - transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1); + transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), + width 0.35s cubic-bezier(0.4, 0, 0.2, 1); } // Arrow @@ -73,7 +74,8 @@ color: $plyr-menu-color; display: flex; font-size: $plyr-font-size-menu; - padding: ceil($plyr-control-padding / 2) ceil($plyr-control-padding * 1.5); + padding: ceil($plyr-control-padding / 2) + ceil($plyr-control-padding * 1.5); user-select: none; width: 100%; -- cgit v1.2.3 From 748aa5179f4244b785cc293791710e13f8a7a468 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 00:38:19 +1000 Subject: Comments about keydown vs keyup for Firefox --- src/js/controls.js | 2 ++ src/js/listeners.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 4ee4c572..1abb8263 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -448,6 +448,8 @@ const controls = { }, // Bind keyboard shortcuts for a menu item + // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus + // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 bindMenuItemShortcuts(menuItem, type) { // Handle space or -> to open menu on( diff --git a/src/js/listeners.js b/src/js/listeners.js index 80c2e630..881393ba 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -6,7 +6,14 @@ import controls from './controls'; import ui from './ui'; import { repaint } from './utils/animation'; import browser from './utils/browser'; -import { getElement, getElements, hasClass, matches, toggleClass, toggleHidden } from './utils/elements'; +import { + getElement, + getElements, + hasClass, + matches, + toggleClass, + toggleHidden, +} from './utils/elements'; import { on, once, toggleListener, triggerEvent } from './utils/events'; import is from './utils/is'; @@ -690,6 +697,8 @@ class Listeners { }); // Settings menu - keyboard toggle + // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus + // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 this.bind( player.elements.buttons.settings, 'keyup', -- cgit v1.2.3 From 2371619486dbbbfdb0350923684e53963141a7af Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 00:56:44 +1000 Subject: Linting --- src/js/captions.js | 4 +- src/js/controls.js | 395 +++++++++++-------------------------------------- src/js/listeners.js | 378 ++++++++++++---------------------------------- src/js/plyr.js | 94 +++--------- src/js/support.js | 20 +-- src/js/ui.js | 82 ++-------- src/js/utils/events.js | 81 ++-------- 7 files changed, 233 insertions(+), 821 deletions(-) (limited to 'src') diff --git a/src/js/captions.js b/src/js/captions.js index 14f77a2e..4ad2a956 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -84,7 +84,9 @@ const captions = { // * toggled: The real captions state const languages = dedupe( - Array.from(navigator.languages || navigator.language || navigator.userLanguage).map(language => language.split('-')[0]), + Array.from(navigator.languages || navigator.language || navigator.userLanguage).map( + language => language.split('-')[0], + ), ); let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); diff --git a/src/js/controls.js b/src/js/controls.js index d0e6874d..e7779c71 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -23,9 +23,7 @@ const controls = { // Get icon URL getIconUrl() { const url = new URL(this.config.iconUrl, window.location); - const cors = - url.host !== window.location.host || - (browser.isIE && !window.svg4everybody); + const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody); return { url: this.config.iconUrl, @@ -36,82 +34,37 @@ const controls = { // Find the UI controls findElements() { try { - this.elements.controls = getElement.call( - this, - this.config.selectors.controls.wrapper, - ); + this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); // Buttons this.elements.buttons = { - play: getElements.call( - this, - this.config.selectors.buttons.play, - ), - pause: getElement.call( - this, - this.config.selectors.buttons.pause, - ), - restart: getElement.call( - this, - this.config.selectors.buttons.restart, - ), - rewind: getElement.call( - this, - this.config.selectors.buttons.rewind, - ), - fastForward: getElement.call( - this, - this.config.selectors.buttons.fastForward, - ), + play: getElements.call(this, this.config.selectors.buttons.play), + pause: getElement.call(this, this.config.selectors.buttons.pause), + restart: getElement.call(this, this.config.selectors.buttons.restart), + rewind: getElement.call(this, this.config.selectors.buttons.rewind), + fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), mute: getElement.call(this, this.config.selectors.buttons.mute), pip: getElement.call(this, this.config.selectors.buttons.pip), - airplay: getElement.call( - this, - this.config.selectors.buttons.airplay, - ), - settings: getElement.call( - this, - this.config.selectors.buttons.settings, - ), - captions: getElement.call( - this, - this.config.selectors.buttons.captions, - ), - fullscreen: getElement.call( - this, - this.config.selectors.buttons.fullscreen, - ), + airplay: getElement.call(this, this.config.selectors.buttons.airplay), + settings: getElement.call(this, this.config.selectors.buttons.settings), + captions: getElement.call(this, this.config.selectors.buttons.captions), + fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), }; // Progress - this.elements.progress = getElement.call( - this, - this.config.selectors.progress, - ); + this.elements.progress = getElement.call(this, this.config.selectors.progress); // Inputs this.elements.inputs = { seek: getElement.call(this, this.config.selectors.inputs.seek), - volume: getElement.call( - this, - this.config.selectors.inputs.volume, - ), + volume: getElement.call(this, this.config.selectors.inputs.volume), }; // Display this.elements.display = { - buffer: getElement.call( - this, - this.config.selectors.display.buffer, - ), - currentTime: getElement.call( - this, - this.config.selectors.display.currentTime, - ), - duration: getElement.call( - this, - this.config.selectors.display.duration, - ), + buffer: getElement.call(this, this.config.selectors.display.buffer), + currentTime: getElement.call(this, this.config.selectors.display.currentTime), + duration: getElement.call(this, this.config.selectors.display.duration), }; // Seek tooltip @@ -124,10 +77,7 @@ const controls = { return true; } catch (error) { // Log it - this.debug.warn( - 'It looks like there is a problem with your custom controls HTML', - error, - ); + this.debug.warn('It looks like there is a problem with your custom controls HTML', error); // Restore native video controls this.toggleNativeControls(true); @@ -140,9 +90,7 @@ const controls = { createIcon(type, attributes) { const namespace = 'http://www.w3.org/2000/svg'; const iconUrl = controls.getIconUrl.call(this); - const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${ - this.config.iconPrefix - }`; + const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; // Create const icon = document.createElementNS(namespace, 'svg'); @@ -164,11 +112,7 @@ const controls = { if ('href' in use) { use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); } else { - use.setAttributeNS( - 'http://www.w3.org/1999/xlink', - 'xlink:href', - path, - ); + use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); } // Add to @@ -187,9 +131,7 @@ const controls = { const text = universals[type] || i18n.get(type, this.config); const attributes = Object.assign({}, attr, { - class: [attr.class, this.config.classNames.hidden] - .filter(Boolean) - .join(' '), + class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '), }); return createElement('span', attributes, text); }, @@ -276,9 +218,7 @@ const controls = { break; case 'play-large': - attributes.class += ` ${ - this.config.classNames.control - }--overlaid`; + attributes.class += ` ${this.config.classNames.control}--overlaid`; type = 'play'; label = 'play'; icon = 'play'; @@ -320,13 +260,7 @@ const controls = { } // Merge attributes - extend( - attributes, - getAttributesFromSelector( - this.config.selectors.buttons[type], - attributes, - ), - ); + extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); setAttributes(button, attributes); @@ -426,16 +360,12 @@ const controls = { // Create time display createTime(type) { - const attributes = getAttributesFromSelector( - this.config.selectors.display[type], - ); + const attributes = getAttributesFromSelector(this.config.selectors.display[type]); const container = createElement( 'div', extend(attributes, { - class: `${this.config.classNames.display.time} ${ - attributes.class ? attributes.class : '' - }`.trim(), + class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(), 'aria-label': i18n.get(type, this.config), }), '00:00', @@ -470,10 +400,7 @@ const controls = { return; } - const isRadioButton = matches( - menuItem, - '[role="menuitemradio"]', - ); + const isRadioButton = matches(menuItem, '[role="menuitemradio"]'); // Show the respective menu if (!isRadioButton && [32, 39].includes(event.which)) { @@ -482,10 +409,7 @@ const controls = { let target; if (event.which !== 32) { - if ( - event.which === 40 || - (isRadioButton && event.which === 39) - ) { + if (event.which === 40 || (isRadioButton && event.which === 39)) { target = menuItem.nextElementSibling; if (!is.element(target)) { @@ -508,26 +432,15 @@ const controls = { }, // Create a settings menu item - createMenuItem({ - value, - list, - type, - title, - badge = null, - checked = false, - }) { - const attributes = getAttributesFromSelector( - this.config.selectors.inputs[type], - ); + createMenuItem({ value, list, type, title, badge = null, checked = false }) { + const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); const menuItem = createElement( 'button', extend(attributes, { type: 'button', role: 'menuitemradio', - class: `${this.config.classNames.control} ${ - attributes.class ? attributes.class : '' - }`.trim(), + class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(), 'aria-checked': checked, value, }), @@ -555,15 +468,10 @@ const controls = { if (checked) { Array.from(menuItem.parentNode.children) .filter(node => matches(node, '[role="menuitemradio"]')) - .forEach(node => - node.setAttribute('aria-checked', 'false'), - ); + .forEach(node => node.setAttribute('aria-checked', 'false')); } - menuItem.setAttribute( - 'aria-checked', - checked ? 'true' : 'false', - ); + menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); }, }); @@ -597,11 +505,7 @@ const controls = { break; } - controls.showMenuPanel.call( - this, - 'home', - event.type === 'keyup', - ); + controls.showMenuPanel.call(this, 'home', event.type === 'keyup'); }, type, false, @@ -644,17 +548,12 @@ const controls = { // Update range if (is.element(this.elements.inputs.volume)) { - controls.setRange.call( - this, - this.elements.inputs.volume, - this.muted ? 0 : this.volume, - ); + controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); } // Update mute state if (is.element(this.elements.buttons.mute)) { - this.elements.buttons.mute.pressed = - this.muted || this.volume === 0; + this.elements.buttons.mute.pressed = this.muted || this.volume === 0; } }, @@ -681,9 +580,7 @@ const controls = { const setProgress = (target, input) => { const value = is.number(input) ? input : 0; - const progress = is.element(target) - ? target - : this.elements.display.buffer; + const progress = is.element(target) ? target : this.elements.display.buffer; // Update value and label if (is.element(progress)) { @@ -707,11 +604,7 @@ const controls = { // Set seek range value only if it's a 'natural' time event if (event.type === 'timeupdate') { - controls.setRange.call( - this, - this.elements.inputs.seek, - value, - ); + controls.setRange.call(this, this.elements.inputs.seek, value); } break; @@ -719,10 +612,7 @@ const controls = { // Check buffer status case 'playing': case 'progress': - setProgress( - this.elements.display.buffer, - this.buffered * 100, - ); + setProgress(this.elements.display.buffer, this.buffered * 100); break; @@ -750,9 +640,7 @@ const controls = { const format = i18n.get('seekLabel', this.config); range.setAttribute( 'aria-valuetext', - format - .replace('{currentTime}', currentTime) - .replace('{duration}', duration), + format.replace('{currentTime}', currentTime).replace('{duration}', duration), ); } else if (matches(range, this.config.selectors.inputs.volume)) { const percent = range.value * 100; @@ -802,10 +690,7 @@ const controls = { if (is.event(event)) { percent = 100 / clientRect.width * (event.pageX - clientRect.left); } else if (hasClass(this.elements.display.seekTooltip, visible)) { - percent = parseFloat( - this.elements.display.seekTooltip.style.left, - 10, - ); + percent = parseFloat(this.elements.display.seekTooltip.style.left, 10); } else { return; } @@ -818,21 +703,14 @@ const controls = { } // Display the time a click would seek to - controls.updateTimeDisplay.call( - this, - this.elements.display.seekTooltip, - this.duration / 100 * percent, - ); + controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position this.elements.display.seekTooltip.style.left = `${percent}%`; // Show/hide the tooltip // If the event is a moues in/out and percentage is inside bounds - if ( - is.event(event) && - ['mouseenter', 'mouseleave'].includes(event.type) - ) { + if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) { toggle(event.type === 'mouseenter'); } }, @@ -840,9 +718,7 @@ const controls = { // Handle time change event timeUpdate(event) { // Only invert if only one time element is displayed and used for both duration and currentTime - const invert = - !is.element(this.elements.display.duration) && - this.config.invertTime; + const invert = !is.element(this.elements.display.duration) && this.config.invertTime; // Duration controls.updateTimeDisplay.call( @@ -864,10 +740,7 @@ const controls = { // Show the duration on metadataloaded or durationchange events durationUpdate() { // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false - if ( - !this.supported.ui || - (!this.config.invertTime && this.currentTime) - ) { + if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) { return; } @@ -883,10 +756,7 @@ const controls = { // Update ARIA values if (is.element(this.elements.inputs.seek)) { - this.elements.inputs.seek.setAttribute( - 'aria-valuemax', - this.duration, - ); + this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration); } // If there's a spot to display duration @@ -894,20 +764,12 @@ const controls = { // If there's only one time display, display duration there if (!hasDuration && this.config.displayDuration && this.paused) { - controls.updateTimeDisplay.call( - this, - this.elements.display.currentTime, - this.duration, - ); + controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); } // If there's a duration element, update content if (hasDuration) { - controls.updateTimeDisplay.call( - this, - this.elements.display.duration, - this.duration, - ); + controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); } // Update the tooltip (if visible) @@ -936,13 +798,8 @@ const controls = { } // Unsupported value - if ( - !is.empty(this.options[setting]) && - !this.options[setting].includes(value) - ) { - this.debug.warn( - `Unsupported value of '${value}' for ${setting}`, - ); + if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { + this.debug.warn(`Unsupported value of '${value}' for ${setting}`); return; } @@ -964,9 +821,7 @@ const controls = { } // Update the label - const label = this.elements.settings.buttons[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); // Find the radio option and check it @@ -981,16 +836,11 @@ const controls = { getLabel(setting, value) { switch (setting) { case 'speed': - return value === 1 - ? i18n.get('normal', this.config) - : `${value}×`; + return value === 1 ? i18n.get('normal', this.config) : `${value}×`; case 'quality': if (is.number(value)) { - const label = i18n.get( - `qualityLabel.${value}`, - this.config, - ); + const label = i18n.get(`qualityLabel.${value}`, this.config); if (!label.length) { return `${value}p`; @@ -1017,20 +867,15 @@ const controls = { } const type = 'quality'; - const list = this.elements.settings.panels.quality.querySelector( - '[role="menu"]', - ); + 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), - ); + 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; + const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; controls.toggleMenuButton.call(this, type, toggle); // Empty the menu @@ -1130,9 +975,7 @@ const controls = { // TODO: Captions or language? Currently it's mixed const type = 'captions'; - const list = this.elements.settings.panels.captions.querySelector( - '[role="menu"]', - ); + const list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); const tracks = captions.getTracks.call(this); const toggle = Boolean(tracks.length); @@ -1155,9 +998,7 @@ const controls = { value, checked: this.captions.toggled && this.currentTrack === value, title: captions.getLabel.call(this, track), - badge: - track.language && - controls.createBadge.call(this, track.language.toUpperCase()), + badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), list, type: 'language', })); @@ -1185,9 +1026,7 @@ const controls = { } const type = 'speed'; - const list = this.elements.settings.panels.speed.querySelector( - '[role="menu"]', - ); + const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options if (is.array(options)) { @@ -1197,13 +1036,10 @@ const controls = { } // Set options if passed and filter based on config - this.options.speed = this.options.speed.filter(speed => - this.config.speed.options.includes(speed), - ); + this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed)); // 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.toggleMenuButton.call(this, type, toggle); // Empty the menu @@ -1233,9 +1069,7 @@ const controls = { // Check if we need to hide/show the settings menu checkMenu() { const { buttons } = this.elements.settings; - const visible = - !is.empty(buttons) && - Object.values(buttons).some(button => !button.hidden); + const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); toggleHidden(this.elements.settings.menu, !visible); }, @@ -1250,13 +1084,10 @@ const controls = { return; } - const show = is.boolean(input) - ? input - : is.element(popup) && popup.hasAttribute('hidden'); + const show = is.boolean(input) ? input : is.element(popup) && popup.hasAttribute('hidden'); if (is.event(input)) { - const isMenuItem = - is.element(popup) && popup.contains(input.target); + const isMenuItem = is.element(popup) && popup.contains(input.target); const isButton = input.target === this.elements.buttons.settings; // If the click was inside the form or if the click @@ -1281,17 +1112,11 @@ const controls = { if (is.element(popup)) { toggleHidden(popup, !show); - toggleClass( - this.elements.container, - this.config.classNames.menu.open, - show, - ); + toggleClass(this.elements.container, this.config.classNames.menu.open, show); // Focus the first item if key interaction if (show && is.event(input) && input.type === 'keyup') { - const pane = Object.values(this.elements.settings.panels).find( - pane => !pane.hidden, - ); + const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); const firstItem = pane.querySelector('[role^="menuitem"]'); setFocus.call(this, firstItem, true); @@ -1324,9 +1149,7 @@ const controls = { // Show a panel in the menu showMenuPanel(type = '', tabFocus = false) { - const target = document.getElementById( - `plyr-settings-${this.id}-${type}`, - ); + const target = document.getElementById(`plyr-settings-${this.id}-${type}`); // Nothing to show, bail if (!is.element(target)) { @@ -1335,9 +1158,7 @@ const controls = { // Hide all other panels const container = target.parentNode; - const current = Array.from(container.children).find( - node => !node.hidden, - ); + const current = Array.from(container.children).find(node => !node.hidden); // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -1351,10 +1172,7 @@ const controls = { // Restore auto height/width const restore = event => { // We're only bothered about height and width on the container - if ( - event.target !== container || - !['width', 'height'].includes(event.propertyName) - ) { + if (event.target !== container || !['width', 'height'].includes(event.propertyName)) { return; } @@ -1389,10 +1207,7 @@ const controls = { // TODO: Set order based on order in the config.controls array? create(data) { // Create the container - const container = createElement( - 'div', - getAttributesFromSelector(this.config.selectors.controls.wrapper), - ); + const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); // Restart button if (this.config.controls.includes('restart')) { @@ -1411,17 +1226,12 @@ const controls = { // Fast forward button if (this.config.controls.includes('fast-forward')) { - container.appendChild( - controls.createButton.call(this, 'fast-forward'), - ); + container.appendChild(controls.createButton.call(this, 'fast-forward')); } // Progress if (this.config.controls.includes('progress')) { - const progress = createElement( - 'div', - getAttributesFromSelector(this.config.selectors.progress), - ); + const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); // Seek range slider progress.appendChild( @@ -1455,9 +1265,7 @@ const controls = { // Media current time display if (this.config.controls.includes('current-time')) { - container.appendChild( - controls.createTime.call(this, 'currentTime'), - ); + container.appendChild(controls.createTime.call(this, 'currentTime')); } // Media duration display @@ -1505,10 +1313,7 @@ const controls = { } // 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 control = createElement('div', { class: 'plyr__menu', hidden: '', @@ -1550,20 +1355,13 @@ const controls = { // TODO: bundle this with the createMenuItem helper and bindings const menuItem = createElement( 'button', - extend( - getAttributesFromSelector( - this.config.selectors.buttons.settings, - ), - { - type: 'button', - class: `${this.config.classNames.control} ${ - this.config.classNames.control - }--forward`, - role: 'menuitem', - 'aria-haspopup': true, - hidden: '', - }, - ), + extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { + type: 'button', + class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, + role: 'menuitem', + 'aria-haspopup': true, + hidden: '', + }), ); // Bind menu shortcuts for keyboard users @@ -1574,11 +1372,7 @@ const controls = { controls.showMenuPanel.call(this, type, false); }); - const flex = createElement( - 'span', - null, - i18n.get(type, this.config), - ); + const flex = createElement('span', null, i18n.get(type, this.config)); const value = createElement('span', { class: this.config.classNames.menu.value, @@ -1600,9 +1394,7 @@ const controls = { // Back button const backButton = createElement('button', { type: 'button', - class: `${this.config.classNames.control} ${ - this.config.classNames.control - }--back`, + class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, }); // Visible label @@ -1688,25 +1480,18 @@ const controls = { // Toggle fullscreen button if (this.config.controls.includes('fullscreen')) { - container.appendChild( - controls.createButton.call(this, 'fullscreen'), - ); + container.appendChild(controls.createButton.call(this, 'fullscreen')); } // Larger overlaid play button if (this.config.controls.includes('play-large')) { - this.elements.container.appendChild( - controls.createButton.call(this, 'play-large'), - ); + this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); } this.elements.controls = container; if (this.isHTML5) { - controls.setQualityMenu.call( - this, - html5.getQualityOptions.call(this), - ); + controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); } controls.setSpeedMenu.call(this); @@ -1793,9 +1578,7 @@ const controls = { // Inject to custom location if (is.string(this.config.selectors.controls.container)) { - target = document.querySelector( - this.config.selectors.controls.container, - ); + target = document.querySelector(this.config.selectors.controls.container); } // Inject into the container by default @@ -1804,9 +1587,7 @@ const controls = { } // Inject controls HTML (needs to be before captions, hence "afterbegin") - const insertMethod = is.element(container) - ? 'insertAdjacentElement' - : 'insertAdjacentHTML'; + const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML'; target[insertMethod]('afterbegin', container); // Find the elements if need be @@ -1822,9 +1603,7 @@ const controls = { // Setup tooltips if (this.config.tooltips.controls) { const { classNames, selectors } = this.config; - const selector = `${selectors.controls.wrapper} ${ - selectors.labels - } .${classNames.hidden}`; + const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`; const labels = getElements.call(this, selector); Array.from(labels).forEach(label => { diff --git a/src/js/listeners.js b/src/js/listeners.js index 881393ba..c1305bcd 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -6,14 +6,7 @@ import controls from './controls'; import ui from './ui'; import { repaint } from './utils/animation'; import browser from './utils/browser'; -import { - getElement, - getElements, - hasClass, - matches, - toggleClass, - toggleHidden, -} from './utils/elements'; +import { getElement, getElements, hasClass, matches, toggleClass, toggleHidden } from './utils/elements'; import { on, once, toggleListener, triggerEvent } from './utils/events'; import is from './utils/is'; @@ -69,38 +62,13 @@ class Listeners { return; } - if ( - event.which === 32 && - matches(focused, 'button, [role^="menuitem"]') - ) { + if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) { return; } } // Which keycodes should we prevent default - const preventDefault = [ - 32, - 37, - 38, - 39, - 40, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 56, - 57, - 67, - 70, - 73, - 75, - 76, - 77, - 79, - ]; + const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79]; // If the code is found prevent default (e.g. prevent scrolling for arrows) if (preventDefault.includes(code)) { @@ -195,11 +163,7 @@ class Listeners { // Escape is handle natively when in full screen // So we only need to worry about non native - if ( - !player.fullscreen.enabled && - player.fullscreen.active && - code === 27 - ) { + if (!player.fullscreen.enabled && player.fullscreen.active && code === 27) { player.fullscreen.toggle(); } @@ -222,11 +186,7 @@ class Listeners { player.touch = true; // Add touch class - toggleClass( - player.elements.container, - player.config.classNames.isTouch, - true, - ); + toggleClass(player.elements.container, player.config.classNames.isTouch, true); } setTabFocus(event) { @@ -272,11 +232,7 @@ class Listeners { return; } - toggleClass( - document.activeElement, - player.config.classNames.tabFocus, - true, - ); + toggleClass(document.activeElement, player.config.classNames.tabFocus, true); }, 10); } @@ -286,38 +242,17 @@ class Listeners { // Keyboard shortcuts if (player.config.keyboard.global) { - toggleListener.call( - player, - window, - 'keydown keyup', - this.handleKey, - toggle, - false, - ); + toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false); } // Click anywhere closes menu - toggleListener.call( - player, - document.body, - 'click', - this.toggleMenu, - toggle, - ); + toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection - toggleListener.call( - player, - document.body, - 'keydown focus blur', - this.setTabFocus, - toggle, - false, - true, - ); + toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); } // Container listeners @@ -326,13 +261,7 @@ class Listeners { // Keyboard shortcuts if (!player.config.keyboard.global && player.config.keyboard.focused) { - on.call( - player, - player.elements.container, - 'keydown keyup', - this.handleKey, - false, - ); + on.call(player, player.elements.container, 'keydown keyup', this.handleKey, false); } // Toggle controls on mouse events and entering fullscreen @@ -350,9 +279,7 @@ class Listeners { } // Show, then hide after a timeout unless another control event occurs - const show = ['touchstart', 'touchmove', 'mousemove'].includes( - event.type, - ); + const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); let delay = 0; @@ -366,10 +293,7 @@ class Listeners { clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking - player.timers.controls = setTimeout( - () => ui.toggleControls.call(player, false), - delay, - ); + player.timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay); }, ); } @@ -379,16 +303,11 @@ class Listeners { const { player } = this; // Time change on media - on.call(player, player.media, 'timeupdate seeking seeked', event => - controls.timeUpdate.call(player, event), - ); + on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event)); // Display duration - on.call( - player, - player.media, - 'durationchange loadeddata loadedmetadata', - event => controls.durationUpdate.call(player, event), + on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => + controls.durationUpdate.call(player, event), ); // Check for audio tracks on load @@ -408,30 +327,20 @@ class Listeners { }); // Check for buffer progress - on.call( - player, - player.media, - 'progress playing seeking seeked', - event => controls.updateProgress.call(player, event), + on.call(player, player.media, 'progress playing seeking seeked', event => + controls.updateProgress.call(player, event), ); // Handle volume changes - on.call(player, player.media, 'volumechange', event => - controls.updateVolume.call(player, event), - ); + on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event)); // Handle play/pause - on.call( - player, - player.media, - 'playing play pause ended emptied timeupdate', - event => ui.checkPlaying.call(player, event), + on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => + ui.checkPlaying.call(player, event), ); // Loading state - on.call(player, player.media, 'waiting canplay seeked playing', event => - ui.checkLoading.call(player, event), - ); + on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event)); // If autoplay, then load advertisement if required // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows @@ -443,23 +352,14 @@ class Listeners { // If ads are enabled, wait for them first if (player.ads.enabled && !player.ads.initialized) { // Wait for manager response - player.ads.managerPromise - .then(() => player.ads.play()) - .catch(() => player.play()); + player.ads.managerPromise.then(() => player.ads.play()).catch(() => player.play()); } }); // Click video - if ( - player.supported.ui && - player.config.clickToPlay && - !player.isAudio - ) { + if (player.supported.ui && player.config.clickToPlay && !player.isAudio) { // Re-fetch the wrapper - const wrapper = getElement.call( - player, - `.${player.config.classNames.video}`, - ); + const wrapper = getElement.call(player, `.${player.config.classNames.video}`); // Bail if there's no wrapper (this should never happen) if (!is.element(wrapper)) { @@ -467,42 +367,31 @@ class Listeners { } // On click play, pause ore restart - on.call( - player, - player.elements.container, - 'click touchstart', - event => { - const targets = [player.elements.container, wrapper]; - - // Ignore if click if not container or in video wrapper - if ( - !targets.includes(event.target) && - !wrapper.contains(event.target) - ) { - return; - } + on.call(player, player.elements.container, 'click touchstart', event => { + const targets = [player.elements.container, wrapper]; - // First touch on touch devices will just show controls (if we're hiding controls) - // If controls are shown then it'll toggle like a pointer device - if ( - player.config.hideControls && - player.touch && - hasClass( - player.elements.container, - player.config.classNames.hideControls, - ) - ) { - return; - } + // Ignore if click if not container or in video wrapper + if (!targets.includes(event.target) && !wrapper.contains(event.target)) { + return; + } - if (player.ended) { - player.restart(); - player.play(); - } else { - player.togglePlay(); - } - }, - ); + // First touch on touch devices will just show controls (if we're hiding controls) + // If controls are shown then it'll toggle like a pointer device + if ( + player.config.hideControls && + player.touch && + hasClass(player.elements.container, player.config.classNames.hideControls) + ) { + return; + } + + if (player.ended) { + player.restart(); + player.play(); + } else { + player.togglePlay(); + } + }); } // Disable right click @@ -545,19 +434,12 @@ class Listeners { // Quality change on.call(player, player.media, 'qualitychange', event => { // Update UI - controls.updateSetting.call( - player, - 'quality', - null, - event.detail.quality, - ); + controls.updateSetting.call(player, 'quality', null, event.detail.quality); }); // Proxy events to container // Bubble up key events for Edge - const proxyEvents = player.config.events - .concat(['keyup', 'keydown']) - .join(' '); + const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); on.call(player, player.media, proxyEvents, event => { let { detail = {} } = event; @@ -567,13 +449,7 @@ class Listeners { detail = player.media.error; } - triggerEvent.call( - player, - player.elements.container, - event.type, - true, - detail, - ); + triggerEvent.call(player, player.elements.container, event.type, true, detail); }); } @@ -625,28 +501,13 @@ class Listeners { } // Pause - this.bind( - player.elements.buttons.restart, - 'click', - player.restart, - 'restart', - ); + this.bind(player.elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind - this.bind( - player.elements.buttons.rewind, - 'click', - player.rewind, - 'rewind', - ); + this.bind(player.elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind - this.bind( - player.elements.buttons.fastForward, - 'click', - player.forward, - 'fastForward', - ); + this.bind(player.elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle this.bind( @@ -659,9 +520,7 @@ class Listeners { ); // Captions toggle - this.bind(player.elements.buttons.captions, 'click', () => - player.toggleCaptions(), - ); + this.bind(player.elements.buttons.captions, 'click', () => player.toggleCaptions()); // Fullscreen toggle this.bind( @@ -684,12 +543,7 @@ class Listeners { ); // Airplay - this.bind( - player.elements.buttons.airplay, - 'click', - player.airplay, - 'airplay', - ); + this.bind(player.elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle this.bind(player.elements.buttons.settings, 'click', event => { @@ -731,39 +585,30 @@ class Listeners { }); // Pause while seeking - this.bind( - player.elements.inputs.seek, - 'mousedown mouseup keydown keyup touchstart touchend', - event => { - const seek = event.currentTarget; - const code = event.keyCode ? event.keyCode : event.which; - const eventType = event.type; - const attribute = 'play-on-seeked'; - - if ( - (eventType === 'keydown' || eventType === 'keyup') && - (code !== 39 && code !== 37) - ) { - return; - } - // Was playing before? - const play = seek.hasAttribute(attribute); - - // Done seeking - const done = ['mouseup', 'touchend', 'keyup'].includes( - event.type, - ); + this.bind(player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { + const seek = event.currentTarget; + const code = event.keyCode ? event.keyCode : event.which; + const eventType = event.type; + const attribute = 'play-on-seeked'; - // If we're done seeking and it was playing, resume playback - if (play && done) { - seek.removeAttribute(attribute); - player.play(); - } else if (!done && player.playing) { - seek.setAttribute(attribute, ''); - player.pause(); - } - }, - ); + if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { + return; + } + // Was playing before? + const play = seek.hasAttribute(attribute); + + // Done seeking + const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); + + // If we're done seeking and it was playing, resume playback + if (play && done) { + seek.removeAttribute(attribute); + player.play(); + } else if (!done && player.playing) { + seek.setAttribute(attribute, ''); + player.pause(); + } + }); // Fix range inputs on iOS // Super weird iOS bug where after you interact with an
with attributes - this.provider = this.media.getAttribute( - this.config.attributes.embed.provider, - ); + this.provider = this.media.getAttribute(this.config.attributes.embed.provider); // Remove attribute - this.media.removeAttribute( - this.config.attributes.embed.provider, - ); + this.media.removeAttribute(this.config.attributes.embed.provider); } // Unsupported or missing provider - if ( - is.empty(this.provider) || - !Object.keys(providers).includes(this.provider) - ) { + if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { this.debug.error('Setup failed: Invalid provider'); return; } @@ -235,10 +222,7 @@ class Plyr { if (this.media.hasAttribute('autoplay')) { this.config.autoplay = true; } - if ( - this.media.hasAttribute('playsinline') || - this.media.hasAttribute('webkit-playsinline') - ) { + if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) { this.config.playsinline = true; } if (this.media.hasAttribute('muted')) { @@ -256,11 +240,7 @@ class Plyr { } // Check for support again but with type - this.supported = support.check( - this.type, - this.provider, - this.config.playsinline, - ); + this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail if (!this.supported.api) { @@ -293,14 +273,9 @@ class Plyr { // Listen for events if debugging if (this.config.debug) { - on.call( - this, - this.elements.container, - this.config.events.join(' '), - event => { - this.debug.log(`event: ${event.type}`); - }, - ); + on.call(this, this.elements.container, this.config.events.join(' '), event => { + this.debug.log(`event: ${event.type}`); + }); } // Setup interface @@ -450,9 +425,7 @@ class Plyr { * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime */ rewind(seekTime) { - this.currentTime = - this.currentTime - - (is.number(seekTime) ? seekTime : this.config.seekTime); + this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime); } /** @@ -460,9 +433,7 @@ class Plyr { * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime */ forward(seekTime) { - this.currentTime = - this.currentTime + - (is.number(seekTime) ? seekTime : this.config.seekTime); + this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime); } /** @@ -479,9 +450,7 @@ class Plyr { const inputIsValid = is.number(input) && input > 0; // Set - this.media.currentTime = inputIsValid - ? Math.min(input, this.duration) - : 0; + this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging this.debug.log(`Seeking to ${this.currentTime} seconds`); @@ -531,10 +500,7 @@ class Plyr { // Media duration can be NaN or Infinity before the media has loaded const realDuration = (this.media || {}).duration; - const duration = - !is.number(realDuration) || realDuration === Infinity - ? 0 - : realDuration; + const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; // If config duration is funky, use regular duration return fauxDuration || duration; @@ -728,9 +694,7 @@ class Plyr { if (!options.includes(quality)) { const value = closest(options, quality); - this.debug.warn( - `Unsupported quality option: ${quality}, using ${value} instead`, - ); + this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`); quality = value; } @@ -929,9 +893,7 @@ class Plyr { const toggle = is.boolean(input) ? input : this.pip === states.inline; // Toggle based on current state - this.media.webkitSetPresentationMode( - toggle ? states.pip : states.inline, - ); + this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); } /** @@ -964,27 +926,16 @@ class Plyr { // Don't toggle if missing UI support or if it's audio if (this.supported.ui && !this.isAudio) { // Get state before change - const isHidden = hasClass( - this.elements.container, - this.config.classNames.hideControls, - ); + const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); // Negate the argument if not undefined since adding the class to hides the controls const force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state - const hiding = toggleClass( - this.elements.container, - this.config.classNames.hideControls, - force, - ); + const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); // Close menu - if ( - hiding && - this.config.controls.includes('settings') && - !is.empty(this.config.settings) - ) { + if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { controls.toggleMenu.call(this, false); } @@ -1074,12 +1025,7 @@ class Plyr { replaceElement(this.elements.original, this.elements.container); // Event - triggerEvent.call( - this, - this.elements.original, - 'destroyed', - true, - ); + triggerEvent.call(this, this.elements.original, 'destroyed', true); // Callback if (is.function(callback)) { diff --git a/src/js/support.js b/src/js/support.js index 4681f5c7..6395293f 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -25,13 +25,9 @@ const support = { // Check for support // Basic functionality vs full UI check(type, provider, playsinline) { - const canPlayInline = - browser.isIPhone && playsinline && support.playsinline; + const canPlayInline = browser.isIPhone && playsinline && support.playsinline; const api = support[type] || provider !== 'html5'; - const ui = - api && - support.rangeInput && - (type !== 'video' || !browser.isIPhone || canPlayInline); + const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); return { api, @@ -41,9 +37,7 @@ const support = { // Picture-in-picture support // Safari only currently - pip: (() => - !browser.isIPhone && - is.function(createElement('video').webkitSetPresentationMode))(), + pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(), // Airplay support // Safari only currently @@ -75,9 +69,7 @@ const support = { } try { - return Boolean( - type && this.media.canPlayType(type).replace(/no/, ''), - ); + return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); } catch (err) { return false; } @@ -102,9 +94,7 @@ const support = { // Reduced motion iOS & MacOS setting // https://webkit.org/blog/7551/responsive-design-for-motion/ - reducedMotion: - 'matchMedia' in window && - window.matchMedia('(prefers-reduced-motion)').matches, + reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches, }; export default support; diff --git a/src/js/ui.js b/src/js/ui.js index 8c61d805..34fe7e82 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -14,16 +14,8 @@ import loadImage from './utils/loadImage'; const ui = { addStyleHook() { - toggleClass( - this.elements.container, - this.config.selectors.container.replace('.', ''), - true, - ); - toggleClass( - this.elements.container, - this.config.classNames.uiSupported, - this.supported.ui, - ); + toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); + toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); }, // Toggle native HTML5 media controls @@ -43,9 +35,7 @@ const ui = { // Don't setup interface if no support if (!this.supported.ui) { - this.debug.warn( - `Basic support only for ${this.provider} ${this.type}`, - ); + this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); // Restore native controls ui.toggleNativeControls.call(this, true); @@ -103,25 +93,13 @@ const ui = { ); // Check for airplay support - toggleClass( - this.elements.container, - this.config.classNames.airplay.supported, - support.airplay && this.isHTML5, - ); + toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class - toggleClass( - this.elements.container, - this.config.classNames.isIos, - browser.isIos, - ); + toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class - toggleClass( - this.elements.container, - this.config.classNames.isTouch, - this.touch, - ); + toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls this.ready = true; @@ -171,9 +149,7 @@ const ui = { } // Default to media type - const title = !is.empty(this.config.title) - ? this.config.title - : 'video'; + const title = !is.empty(this.config.title) ? this.config.title : 'video'; const format = i18n.get('frameTitle', this.config); iframe.setAttribute('title', format.replace('{title}', title)); @@ -182,11 +158,7 @@ const ui = { // Toggle poster togglePoster(enable) { - toggleClass( - this.elements.container, - this.config.classNames.posterEnabled, - enable, - ); + toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); }, // Set the poster image (async) @@ -217,9 +189,7 @@ const ui = { .then(() => { // Prevent race conditions if (poster !== this.poster) { - throw new Error( - 'setPoster cancelled by later call to setPoster', - ); + throw new Error('setPoster cancelled by later call to setPoster'); } }) .then(() => { @@ -237,21 +207,9 @@ const ui = { // Check playing state checkPlaying(event) { // Class hooks - toggleClass( - this.elements.container, - this.config.classNames.playing, - this.playing, - ); - toggleClass( - this.elements.container, - this.config.classNames.paused, - this.paused, - ); - toggleClass( - this.elements.container, - this.config.classNames.stopped, - this.stopped, - ); + toggleClass(this.elements.container, this.config.classNames.playing, this.playing); + toggleClass(this.elements.container, this.config.classNames.paused, this.paused); + toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set state Array.from(this.elements.buttons.play || []).forEach(target => { @@ -277,11 +235,7 @@ const ui = { // Timer to prevent flicker when seeking this.timers.loading = setTimeout(() => { // Update progress bar loading class state - toggleClass( - this.elements.container, - this.config.classNames.loading, - this.loading, - ); + toggleClass(this.elements.container, this.config.classNames.loading, this.loading); // Update controls visibility ui.toggleControls.call(this); @@ -294,15 +248,7 @@ const ui = { if (controls && this.config.hideControls) { // Show controls if force, loading, paused, or button interaction, otherwise hide - this.toggleControls( - Boolean( - force || - this.loading || - this.paused || - controls.pressed || - controls.hover, - ), - ); + this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover)); } }, }; diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 9820fcae..9f734f04 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -27,21 +27,9 @@ const supportsPassiveListeners = (() => { })(); // Toggle event listener -export function toggleListener( - element, - event, - callback, - toggle = false, - passive = true, - capture = false, -) { +export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { // Bail if no element, event, or callback - if ( - !element || - !('addEventListener' in element) || - is.empty(event) || - !is.function(callback) - ) { + if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { return; } @@ -69,74 +57,28 @@ export function toggleListener( this.eventListeners.push({ element, type, callback, options }); } - element[toggle ? 'addEventListener' : 'removeEventListener']( - type, - callback, - options, - ); + element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); }); } // Bind event handler -export function on( - element, - events = '', - callback, - passive = true, - capture = false, -) { - toggleListener.call( - this, - element, - events, - callback, - true, - passive, - capture, - ); +export function on(element, events = '', callback, passive = true, capture = false) { + toggleListener.call(this, element, events, callback, true, passive, capture); } // Unbind event handler -export function off( - element, - events = '', - callback, - passive = true, - capture = false, -) { - toggleListener.call( - this, - element, - events, - callback, - false, - passive, - capture, - ); +export function off(element, events = '', callback, passive = true, capture = false) { + toggleListener.call(this, element, events, callback, false, passive, capture); } // Bind once-only event handler -export function once( - element, - events = '', - callback, - passive = true, - capture = false, -) { +export function once(element, events = '', callback, passive = true, capture = false) { function onceCallback(...args) { off(element, events, onceCallback, passive, capture); callback.apply(this, args); } - toggleListener.call( - this, - element, - events, - onceCallback, - true, - passive, - capture, - ); + toggleListener.call(this, element, events, onceCallback, true, passive, capture); } // Trigger event @@ -173,9 +115,6 @@ export function unbindListeners() { // Run method when / if player is ready export function ready() { return new Promise( - resolve => - this.ready - ? setTimeout(resolve, 0) - : on.call(this, this.elements.container, 'ready', resolve), + resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)), ).then(() => {}); } -- cgit v1.2.3 From c8db1e55ddff51a1eb4ff08887cbed134116cd88 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 01:26:15 +1000 Subject: Escape closes menu --- src/js/controls.js | 47 +++++++++++---------- src/js/listeners.js | 118 +++++++++++++++++++++++++++++----------------------- src/js/utils/is.js | 2 + 3 files changed, 93 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index e7779c71..90a4560c 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -479,7 +479,7 @@ const controls = { menuItem, 'click keyup', event => { - if (event.type === 'keyup' && event.which !== 32) { + if (is.keyboardEvent(event) && event.which !== 32) { return; } @@ -505,7 +505,7 @@ const controls = { break; } - controls.showMenuPanel.call(this, 'home', event.type === 'keyup'); + controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event)); }, type, false, @@ -1084,13 +1084,18 @@ const controls = { return; } - const show = is.boolean(input) ? input : is.element(popup) && popup.hasAttribute('hidden'); + // True toggle by default + let show = is.element(popup) && popup.hasAttribute('hidden'); - if (is.event(input)) { - const isMenuItem = is.element(popup) && popup.contains(input.target); - const isButton = input.target === this.elements.buttons.settings; + if (is.boolean(input)) { + show = input; + } else if (is.keyboardEvent(input) && input.which === 27) { + show = false; + } else if (is.event(input)) { + const isMenuItem = popup.contains(input.target); + const isButton = input.target === button; - // If the click was inside the form or if the click + // If the click was inside the menu or if the click // wasn't the button or menu item and we're trying to // show the menu (a doc click shouldn't show the menu) if (isMenuItem || (!isMenuItem && !isButton && show)) { @@ -1103,24 +1108,24 @@ const controls = { } } - // Set form and button attributes - if (is.element(button)) { - button.setAttribute('aria-expanded', show); - } + // Set button attributes + button.setAttribute('aria-expanded', show); // Show the actual popup - if (is.element(popup)) { - toggleHidden(popup, !show); - - toggleClass(this.elements.container, this.config.classNames.menu.open, show); + toggleHidden(popup, !show); - // Focus the first item if key interaction - if (show && is.event(input) && input.type === 'keyup') { - const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); - const firstItem = pane.querySelector('[role^="menuitem"]'); + // Add class hook + toggleClass(this.elements.container, this.config.classNames.menu.open, show); - setFocus.call(this, firstItem, true); - } + // Focus the first item if key interaction + if (show && is.keyboardEvent(input)) { + const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); + const firstItem = pane.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, true); + } + // If closing, re-focus the button + else if (!show) { + setFocus.call(this, button, is.keyboardEvent(input)); } }, diff --git a/src/js/listeners.js b/src/js/listeners.js index c1305bcd..8176e9a3 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -26,6 +26,7 @@ class Listeners { // Handle key presses handleKey(event) { const { player } = this; + const { elements } = player; const code = event.keyCode ? event.keyCode : event.which; const pressed = event.type === 'keydown'; const repeat = pressed && code === this.lastKey; @@ -56,7 +57,7 @@ class Listeners { const focused = document.activeElement; if (is.element(focused)) { const { editable } = player.config.selectors; - const { seek } = player.elements.inputs; + const { seek } = elements.inputs; if (focused !== seek && matches(focused, editable)) { return; @@ -182,15 +183,17 @@ class Listeners { // Device is touch enabled firstTouch() { const { player } = this; + const { elements } = player; player.touch = true; // Add touch class - toggleClass(player.elements.container, player.config.classNames.isTouch, true); + toggleClass(elements.container, player.config.classNames.isTouch, true); } setTabFocus(event) { const { player } = this; + const { elements } = player; clearTimeout(this.focusTimer); @@ -228,7 +231,7 @@ class Listeners { const focused = document.activeElement; // Ignore if current focus element isn't inside the player - if (!player.elements.container.contains(focused)) { + if (!elements.container.contains(focused)) { return; } @@ -258,19 +261,20 @@ class Listeners { // Container listeners container() { const { player } = this; + const { elements } = player; // Keyboard shortcuts if (!player.config.keyboard.global && player.config.keyboard.focused) { - on.call(player, player.elements.container, 'keydown keyup', this.handleKey, false); + on.call(player, elements.container, 'keydown keyup', this.handleKey, false); } // Toggle controls on mouse events and entering fullscreen on.call( player, - player.elements.container, + elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { - const { controls } = player.elements; + const { controls } = elements; // Remove button states for fullscreen if (event.type === 'enterfullscreen') { @@ -301,6 +305,7 @@ class Listeners { // Listen for media events media() { const { player } = this; + const { elements } = player; // Time change on media on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event)); @@ -313,8 +318,8 @@ class Listeners { // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point on.call(player, player.media, 'canplay', () => { - toggleHidden(player.elements.volume, !player.hasAudio); - toggleHidden(player.elements.buttons.mute, !player.hasAudio); + toggleHidden(elements.volume, !player.hasAudio); + toggleHidden(elements.buttons.mute, !player.hasAudio); }); // Handle the media finishing @@ -367,8 +372,8 @@ class Listeners { } // On click play, pause ore restart - on.call(player, player.elements.container, 'click touchstart', event => { - const targets = [player.elements.container, wrapper]; + on.call(player, elements.container, 'click touchstart', event => { + const targets = [elements.container, wrapper]; // Ignore if click if not container or in video wrapper if (!targets.includes(event.target) && !wrapper.contains(event.target)) { @@ -380,7 +385,7 @@ class Listeners { if ( player.config.hideControls && player.touch && - hasClass(player.elements.container, player.config.classNames.hideControls) + hasClass(elements.container, player.config.classNames.hideControls) ) { return; } @@ -398,7 +403,7 @@ class Listeners { if (player.supported.ui && player.config.disableContextMenu) { on.call( player, - player.elements.wrapper, + elements.wrapper, 'contextmenu', event => { event.preventDefault(); @@ -449,7 +454,7 @@ class Listeners { detail = player.media.error; } - triggerEvent.call(player, player.elements.container, event.type, true, detail); + triggerEvent.call(player, elements.container, event.type, true, detail); }); } @@ -489,29 +494,30 @@ class Listeners { // Listen for control events controls() { const { player } = this; + const { elements } = player; // IE doesn't support input event, so we fallback to change const inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle - if (player.elements.buttons.play) { - Array.from(player.elements.buttons.play).forEach(button => { + if (elements.buttons.play) { + Array.from(elements.buttons.play).forEach(button => { this.bind(button, 'click', player.togglePlay, 'play'); }); } // Pause - this.bind(player.elements.buttons.restart, 'click', player.restart, 'restart'); + this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind - this.bind(player.elements.buttons.rewind, 'click', player.rewind, 'rewind'); + this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind - this.bind(player.elements.buttons.fastForward, 'click', player.forward, 'fastForward'); + this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle this.bind( - player.elements.buttons.mute, + elements.buttons.mute, 'click', () => { player.muted = !player.muted; @@ -520,11 +526,11 @@ class Listeners { ); // Captions toggle - this.bind(player.elements.buttons.captions, 'click', () => player.toggleCaptions()); + this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions()); // Fullscreen toggle this.bind( - player.elements.buttons.fullscreen, + elements.buttons.fullscreen, 'click', () => { player.fullscreen.toggle(); @@ -534,7 +540,7 @@ class Listeners { // Picture-in-Picture this.bind( - player.elements.buttons.pip, + elements.buttons.pip, 'click', () => { player.pip = 'toggle'; @@ -543,10 +549,10 @@ class Listeners { ); // Airplay - this.bind(player.elements.buttons.airplay, 'click', player.airplay, 'airplay'); + this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle - this.bind(player.elements.buttons.settings, 'click', event => { + this.bind(elements.buttons.settings, 'click', event => { controls.toggleMenu.call(player, event); }); @@ -554,7 +560,7 @@ class Listeners { // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 this.bind( - player.elements.buttons.settings, + elements.buttons.settings, 'keyup', event => { // We only care about space and return @@ -577,23 +583,30 @@ class Listeners { false, ); + // Escape closes menu + this.bind(elements.settings.menu, 'keydown', event => { + if (event.which === 27) { + controls.toggleMenu.call(player, event); + } + }); + // Set range input alternative "value", which matches the tooltip time (#954) - this.bind(player.elements.inputs.seek, 'mousedown mousemove', event => { - const rect = player.elements.progress.getBoundingClientRect(); + this.bind(elements.inputs.seek, 'mousedown mousemove', event => { + const rect = elements.progress.getBoundingClientRect(); const percent = 100 / rect.width * (event.pageX - rect.left); event.currentTarget.setAttribute('seek-value', percent); }); // Pause while seeking - this.bind(player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { + this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { const seek = event.currentTarget; const code = event.keyCode ? event.keyCode : event.which; - const eventType = event.type; const attribute = 'play-on-seeked'; - if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { + if (is.keyboardEvent(event) && (code !== 39 && code !== 37)) { return; } + // Was playing before? const play = seek.hasAttribute(attribute); @@ -615,13 +628,12 @@ class Listeners { // it takes over further interactions on the page. This is a hack if (browser.isIos) { const inputs = getElements.call(player, 'input[type="range"]'); - Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target))); } // Seek this.bind( - player.elements.inputs.seek, + elements.inputs.seek, inputEvent, event => { const seek = event.currentTarget; @@ -640,10 +652,22 @@ class Listeners { 'seek', ); + // Seek tooltip + this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => + controls.updateSeekTooltip.call(player, event), + ); + + // Polyfill for lower fill in for webkit + if (browser.isWebkit) { + Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => { + this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target)); + }); + } + // Current time invert // Only if one time element is used for both currentTime and duration - if (player.config.toggleInvert && !is.element(player.elements.display.duration)) { - this.bind(player.elements.display.currentTime, 'click', () => { + if (player.config.toggleInvert && !is.element(elements.display.duration)) { + this.bind(elements.display.currentTime, 'click', () => { // Do nothing if we're at the start if (player.currentTime === 0) { return; @@ -657,7 +681,7 @@ class Listeners { // Volume this.bind( - player.elements.inputs.volume, + elements.inputs.volume, inputEvent, event => { player.volume = event.target.value; @@ -665,30 +689,18 @@ class Listeners { 'volume', ); - // Polyfill for lower fill in for webkit - if (browser.isWebkit) { - Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => { - this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target)); - }); - } - - // Seek tooltip - this.bind(player.elements.progress, 'mouseenter mouseleave mousemove', event => - controls.updateSeekTooltip.call(player, event), - ); - // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) - this.bind(player.elements.controls, 'mouseenter mouseleave', event => { - player.elements.controls.hover = !player.touch && event.type === 'mouseenter'; + this.bind(elements.controls, 'mouseenter mouseleave', event => { + elements.controls.hover = !player.touch && event.type === 'mouseenter'; }); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) - this.bind(player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); + this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { + elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); }); // Focus in/out on controls - this.bind(player.elements.controls, 'focusin focusout', event => { + this.bind(elements.controls, 'focusin focusout', event => { const { config, elements, timers } = player; const isFocusIn = event.type === 'focusin'; @@ -718,7 +730,7 @@ class Listeners { // Mouse wheel for volume this.bind( - player.elements.inputs.volume, + elements.inputs.volume, 'wheel', event => { // Detect "natural" scroll - suppored on OS X Safari only diff --git a/src/js/utils/is.js b/src/js/utils/is.js index b4760da4..2952d486 100644 --- a/src/js/utils/is.js +++ b/src/js/utils/is.js @@ -16,6 +16,7 @@ const isNodeList = input => instanceOf(input, NodeList); const isElement = input => instanceOf(input, Element); const isTextNode = input => getConstructor(input) === Text; const isEvent = input => instanceOf(input, Event); +const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); @@ -56,6 +57,7 @@ export default { element: isElement, textNode: isTextNode, event: isEvent, + keyboardEvent: isKeyboardEvent, cue: isCue, track: isTrack, url: isUrl, -- cgit v1.2.3 From aaa56caa9cfea98fef06767e8c1bedd85fbf4199 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 01:38:57 +1000 Subject: =?UTF-8?q?Only=20focus=20button=20if=20menu=20wasn=E2=80=99t=20hi?= =?UTF-8?q?dden=20already?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/controls.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 90a4560c..fa03a36d 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1085,7 +1085,8 @@ const controls = { } // True toggle by default - let show = is.element(popup) && popup.hasAttribute('hidden'); + const hidden = popup.hasAttribute('hidden'); + let show = hidden; if (is.boolean(input)) { show = input; @@ -1124,7 +1125,7 @@ const controls = { setFocus.call(this, firstItem, true); } // If closing, re-focus the button - else if (!show) { + else if (!show && !hidden) { setFocus.call(this, button, is.keyboardEvent(input)); } }, -- cgit v1.2.3 From ba511b51c77d6e11cfd7dbe950c2741d24efb51d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 13:00:51 +1000 Subject: Box shadow fix for range track --- src/sass/lib/mixins.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index e6afe046..e015ffee 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -28,6 +28,7 @@ border: 0; border-radius: ($plyr-range-track-height / 2); height: $plyr-range-track-height; + transition: box-shadow 0.3s ease; user-select: none; } @@ -43,8 +44,7 @@ } @mixin plyr-range-thumb-active($color: rgba($plyr-range-thumb-bg, 0.5)) { - box-shadow: $plyr-range-thumb-shadow, - 0 0 0 $plyr-range-thumb-active-shadow-width $color; + box-shadow: $plyr-range-thumb-shadow, 0 0 0 $plyr-range-thumb-active-shadow-width $color; } // Fullscreen styles -- cgit v1.2.3 From 0892d69ba20f5d419655a8cacc450d7850abf336 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 1 Aug 2018 13:56:49 +1000 Subject: Handle race condition for ads lib loading after source change --- src/js/plugins/ads.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index f5727a1d..8b1aac8c 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -207,6 +207,11 @@ class Ads { * @param {Event} adsManagerLoadedEvent */ onAdsManagerLoaded(event) { + // Load could occur after a source change (race condition) + if (!this.enabled) { + return; + } + // Get the ads manager const settings = new google.ima.AdsRenderingSettings(); @@ -240,10 +245,6 @@ class Ads { }); } - // Get skippable state - // TODO: Skip button - // this.player.debug.warn(this.manager.getAdSkippableState()); - // Set volume to match player this.manager.setVolume(this.player.volume); -- cgit v1.2.3 From 7f4b74e2d43ed95a695f760f48e5b8e0792f9528 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 2 Aug 2018 00:47:03 +1000 Subject: Fix for hover over iframed players not showing controls --- src/sass/components/poster.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/sass/components/poster.scss b/src/sass/components/poster.scss index 15e87257..9b239d4f 100644 --- a/src/sass/components/poster.scss +++ b/src/sass/components/poster.scss @@ -7,16 +7,16 @@ background-position: 50% 50%; background-repeat: no-repeat; background-size: contain; - display: none; height: 100%; left: 0; - pointer-events: none; + opacity: 0; position: absolute; top: 0; + transition: opacity 0.2s ease; width: 100%; z-index: 1; } .plyr--stopped.plyr__poster-enabled .plyr__poster { - display: block; + opacity: 1; } -- cgit v1.2.3 From b96fcfc8ac51e3296faded796b6b285f260fa79e Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 2 Aug 2018 00:55:48 +1000 Subject: v3.4.0-beta.1 --- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/js/plyr.js b/src/js/plyr.js index b99df2ff..dfea59ea 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.23 +// plyr.js v3.4.0-beta.1 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 7553ee91..d8dd476b 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.23 +// plyr.js v3.4.0-beta.1 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From dbf768b1bd6adb48b7e8580bcea09386ce064566 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 3 Aug 2018 09:58:51 +1000 Subject: Remove 'video is playing' requirement for iosNative fullscreen --- src/js/fullscreen.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 7091fde1..44c7e1cf 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -177,9 +177,7 @@ class Fullscreen { // iOS native fullscreen doesn't need the request step if (browser.isIos && this.player.config.fullscreen.iosNative) { - if (this.player.playing) { - this.target.webkitEnterFullscreen(); - } + this.target.webkitEnterFullscreen(); } else if (!Fullscreen.native) { toggleFallback.call(this, true); } else if (!this.prefix) { -- cgit v1.2.3 From aacb1720170f46e8041517eadd79bf069c57d917 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 5 Aug 2018 21:48:21 +1000 Subject: Removed aria-labelled-by --- src/js/controls.js | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index fa03a36d..101b7e2a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1327,7 +1327,6 @@ const controls = { control.appendChild( controls.createButton.call(this, 'settings', { - id: `plyr-settings-toggle-${data.id}`, 'aria-haspopup': true, 'aria-controls': `plyr-settings-${data.id}`, 'aria-expanded': false, @@ -1338,7 +1337,6 @@ const controls = { class: 'plyr__menu__container', id: `plyr-settings-${data.id}`, hidden: '', - 'aria-labelled-by': `plyr-settings-toggle-${data.id}`, }); const inner = createElement('div'); -- cgit v1.2.3 From 4ea458e1a3c5b3dfeb88c2f6c015b3c06bf2bbc0 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 5 Aug 2018 21:48:42 +1000 Subject: Rounded aria-valuetext to 1 decimal place --- src/js/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 101b7e2a..acc9d011 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -645,7 +645,7 @@ const controls = { } else if (matches(range, this.config.selectors.inputs.volume)) { const percent = range.value * 100; range.setAttribute('aria-valuenow', percent); - range.setAttribute('aria-valuetext', `${percent}%`); + range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`); } else { range.setAttribute('aria-valuenow', range.value); } -- cgit v1.2.3 From 0bc6b1f1b3a55d28718995cd3bc257ea56d2122c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 5 Aug 2018 22:41:21 +1000 Subject: =?UTF-8?q?Fix=20issue=20where=20enter=20key=20wasn=E2=80=99t=20se?= =?UTF-8?q?tting=20focus=20correctly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/controls.js | 46 ++++++++++++++++++++++++++++++++-------------- src/js/listeners.js | 19 ++++++++++++++----- src/js/utils/elements.js | 22 ++++------------------ 3 files changed, 50 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index acc9d011..6265f2aa 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -381,7 +381,7 @@ const controls = { // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 bindMenuItemShortcuts(menuItem, type) { - // Handle space or -> to open menu + // Navigate through menus via arrow keys and space on( menuItem, 'keydown keyup', @@ -429,6 +429,16 @@ const controls = { }, false, ); + + // Enter will fire a `click` event but we still need to manage focus + // So we bind to keyup which fires after and set focus here + on(menuItem, 'keyup', event => { + if (event.which !== 13) { + return; + } + + controls.focusFirstMenuItem.call(this, null, true); + }); }, // Create a settings menu item @@ -1074,6 +1084,23 @@ const controls = { toggleHidden(this.elements.settings.menu, !visible); }, + // Focus the first menu item in a given (or visible) menu + focusFirstMenuItem(pane, tabFocus = false) { + if (this.elements.settings.popup.hidden) { + return; + } + + let target = pane; + + if (!is.element(target)) { + target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); + } + + const firstItem = target.querySelector('[role^="menuitem"]'); + + setFocus.call(this, firstItem, tabFocus); + }, + // Show/hide menu toggleMenu(input) { const { popup } = this.elements.settings; @@ -1085,7 +1112,7 @@ const controls = { } // True toggle by default - const hidden = popup.hasAttribute('hidden'); + const { hidden } = popup; let show = hidden; if (is.boolean(input)) { @@ -1094,19 +1121,13 @@ const controls = { show = false; } else if (is.event(input)) { const isMenuItem = popup.contains(input.target); - const isButton = input.target === button; // If the click was inside the menu or if the click // wasn't the button or menu item and we're trying to // show the menu (a doc click shouldn't show the menu) - if (isMenuItem || (!isMenuItem && !isButton && show)) { + if (isMenuItem || (!isMenuItem && input.target !== button && show)) { return; } - - // Prevent the toggle being caught by the doc listener - if (isButton) { - input.stopPropagation(); - } } // Set button attributes @@ -1120,9 +1141,7 @@ const controls = { // Focus the first item if key interaction if (show && is.keyboardEvent(input)) { - const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden); - const firstItem = pane.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, true); + controls.focusFirstMenuItem.call(this, null, true); } // If closing, re-focus the button else if (!show && !hidden) { @@ -1205,8 +1224,7 @@ const controls = { toggleHidden(target, false); // Focus the first item - const firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, tabFocus); + controls.focusFirstMenuItem.call(this, target, tabFocus); }, // Build the default HTML diff --git a/src/js/listeners.js b/src/js/listeners.js index 8176e9a3..c2b772b2 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -553,6 +553,9 @@ class Listeners { // Settings menu - click toggle this.bind(elements.buttons.settings, 'click', event => { + // Prevent the document click listener closing the menu + event.stopPropagation(); + controls.toggleMenu.call(player, event); }); @@ -563,8 +566,16 @@ class Listeners { elements.buttons.settings, 'keyup', event => { + const code = event.which; + // We only care about space and return - if (event.which !== 32 && event.which !== 13) { + if (![13, 32].includes(code)) { + return; + } + + // Because return triggers a click anyway, all we need to do is set focus + if (code === 13) { + controls.focusFirstMenuItem.call(player, null, true); return; } @@ -572,15 +583,13 @@ class Listeners { event.preventDefault(); // Prevent playing video (Firefox) - if (event.which === 32) { - event.stopPropagation(); - } + event.stopPropagation(); // Toggle menu controls.toggleMenu.call(player, event); }, null, - false, + false, // Can't be passive as we're preventing default ); // Escape closes menu diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 3a3dfcfd..a6722da2 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -116,11 +116,7 @@ export function emptyElement(element) { // Replace element export function replaceElement(newChild, oldChild) { - if ( - !is.element(oldChild) || - !is.element(oldChild.parentNode) || - !is.element(newChild) - ) { + if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { return null; } @@ -195,7 +191,7 @@ export function toggleHidden(element, hidden) { let hide = hidden; if (!is.boolean(hide)) { - hide = !element.hasAttribute('hidden'); + hide = !element.hidden; } if (hide) { @@ -263,10 +259,7 @@ export function trapFocus(element = null, toggle = false) { return; } - const focusable = getElements.call( - this, - 'button:not(:disabled), input:not(:disabled), [tabindex]', - ); + const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); const first = focusable[0]; const last = focusable[focusable.length - 1]; @@ -290,14 +283,7 @@ export function trapFocus(element = null, toggle = false) { } }; - toggleListener.call( - this, - this.elements.container, - 'keydown', - trap, - toggle, - false, - ); + toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } // Set focus and tab focus class -- cgit v1.2.3 From 3ad118c026d67c684eb90520982e8a802ab89241 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 5 Aug 2018 22:43:35 +1000 Subject: 3.4.0-beta.2 --- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/js/plyr.js b/src/js/plyr.js index dfea59ea..573b4d4c 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.4.0-beta.1 +// plyr.js v3.4.0-beta.2 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index d8dd476b..e9bdc14a 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.4.0-beta.1 +// plyr.js v3.4.0-beta.2 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3 From a4f1fdec5d2c7770316ff465ed27ccd8b08f64f7 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sun, 12 Aug 2018 20:05:52 +0200 Subject: Fix #1153: Captions language fallback --- src/js/captions.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/js/captions.js b/src/js/captions.js index 4ad2a956..f31b5305 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -83,11 +83,8 @@ const captions = { // * active: The state preferred by user settings or config // * toggled: The real captions state - const languages = dedupe( - Array.from(navigator.languages || navigator.language || navigator.userLanguage).map( - language => language.split('-')[0], - ), - ); + const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; + const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); -- cgit v1.2.3 From 297f297d181b694446e04f653da694660e9971b4 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 21:39:16 +1000 Subject: Moved i18n to utils --- src/js/captions.js | 2 +- src/js/controls.js | 2 +- src/js/i18n.js | 34 ---------------------------------- src/js/plugins/ads.js | 2 +- src/js/ui.js | 2 +- src/js/utils/i18n.js | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 src/js/i18n.js create mode 100644 src/js/utils/i18n.js (limited to 'src') diff --git a/src/js/captions.js b/src/js/captions.js index f31b5305..ae4642aa 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -4,7 +4,6 @@ // ========================================================================== import controls from './controls'; -import i18n from './i18n'; import support from './support'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; @@ -18,6 +17,7 @@ import { } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import fetch from './utils/fetch'; +import i18n from './utils/i18n'; import is from './utils/is'; import { getHTML } from './utils/strings'; import { parseUrl } from './utils/urls'; diff --git a/src/js/controls.js b/src/js/controls.js index 6265f2aa..a9957be9 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -5,13 +5,13 @@ import captions from './captions'; import html5 from './html5'; -import i18n from './i18n'; import support from './support'; import { repaint, transitionEndEvent } from './utils/animation'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements'; import { off, on } from './utils/events'; +import i18n from './utils/i18n'; import is from './utils/is'; import loadSprite from './utils/loadSprite'; import { extend } from './utils/objects'; diff --git a/src/js/i18n.js b/src/js/i18n.js deleted file mode 100644 index 5b0ebbab..00000000 --- a/src/js/i18n.js +++ /dev/null @@ -1,34 +0,0 @@ -// ========================================================================== -// Plyr internationalization -// ========================================================================== - -import is from './utils/is'; -import { getDeep } from './utils/objects'; -import { replaceAll } from './utils/strings'; - -const i18n = { - get(key = '', config = {}) { - if (is.empty(key) || is.empty(config)) { - return ''; - } - - let string = getDeep(config.i18n, key); - - if (is.empty(string)) { - return ''; - } - - const replace = { - '{seektime}': config.seekTime, - '{title}': config.title, - }; - - Object.entries(replace).forEach(([key, value]) => { - string = replaceAll(string, key, value); - }); - - return string; - }, -}; - -export default i18n; diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 8b1aac8c..375fdc13 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -6,9 +6,9 @@ /* global google */ -import i18n from '../i18n'; import { createElement } from '../utils/elements'; import { triggerEvent } from '../utils/events'; +import i18n from '../utils/i18n'; import is from '../utils/is'; import loadScript from '../utils/loadScript'; import { formatTime } from '../utils/time'; diff --git a/src/js/ui.js b/src/js/ui.js index 34fe7e82..f0c898bf 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -4,11 +4,11 @@ import captions from './captions'; import controls from './controls'; -import i18n from './i18n'; import support from './support'; import browser from './utils/browser'; import { getElement, toggleClass } from './utils/elements'; import { ready, triggerEvent } from './utils/events'; +import i18n from './utils/i18n'; import is from './utils/is'; import loadImage from './utils/loadImage'; diff --git a/src/js/utils/i18n.js b/src/js/utils/i18n.js new file mode 100644 index 00000000..f71e1a42 --- /dev/null +++ b/src/js/utils/i18n.js @@ -0,0 +1,34 @@ +// ========================================================================== +// Plyr internationalization +// ========================================================================== + +import is from './is'; +import { getDeep } from './objects'; +import { replaceAll } from './strings'; + +const i18n = { + get(key = '', config = {}) { + if (is.empty(key) || is.empty(config)) { + return ''; + } + + let string = getDeep(config.i18n, key); + + if (is.empty(string)) { + return ''; + } + + const replace = { + '{seektime}': config.seekTime, + '{title}': config.title, + }; + + Object.entries(replace).forEach(([key, value]) => { + string = replaceAll(string, key, value); + }); + + return string; + }, +}; + +export default i18n; -- cgit v1.2.3 From 6953a12e2af93cd4c7c45ce89ac9052da4725eac Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:01:56 +1000 Subject: Set background color for video --- src/sass/components/video.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/sass/components/video.scss b/src/sass/components/video.scss index 3caf866d..c3dc4152 100644 --- a/src/sass/components/video.scss +++ b/src/sass/components/video.scss @@ -3,6 +3,7 @@ // -------------------------------------------------------------- .plyr--video { + background: #000; overflow: hidden; // Menu open -- cgit v1.2.3 From 4e1df8677ff1d4c0e02ec78d4d23d161b80d468c Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:02:14 +1000 Subject: Fix tooltip alignment --- src/sass/components/tooltips.scss | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/sass/components/tooltips.scss b/src/sass/components/tooltips.scss index 537e2444..80603bb5 100644 --- a/src/sass/components/tooltips.scss +++ b/src/sass/components/tooltips.scss @@ -10,6 +10,7 @@ color: $plyr-tooltip-color; font-size: $plyr-font-size-small; font-weight: $plyr-font-weight-regular; + left: 50%; line-height: 1.3; margin-bottom: ($plyr-tooltip-padding * 2); opacity: 0; @@ -64,6 +65,7 @@ // Last tooltip .plyr__controls > .plyr__control:last-child .plyr__tooltip { + left: auto; right: 0; transform: translate(0, 10px) scale(0.8); transform-origin: 100% 100%; -- cgit v1.2.3 From 468b20d227f7b6c17aab90364e3d9699bae0ac9d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:42:12 +1000 Subject: Moved mute button inside plyr__volume --- src/js/controls.js | 57 ++++++++++++++++++++++------------------- src/sass/components/volume.scss | 7 +++-- 2 files changed, 35 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index a9957be9..9152f338 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1142,9 +1142,8 @@ const controls = { // Focus the first item if key interaction if (show && is.keyboardEvent(input)) { controls.focusFirstMenuItem.call(this, null, true); - } - // If closing, re-focus the button - else if (!show && !hidden) { + } else if (!show && !hidden) { + // If closing, re-focus the button setFocus.call(this, button, is.keyboardEvent(input)); } }, @@ -1297,36 +1296,39 @@ const controls = { container.appendChild(controls.createTime.call(this, 'duration')); } - // Toggle mute button - if (this.config.controls.includes('mute')) { - container.appendChild(controls.createButton.call(this, 'mute')); - } - - // Volume range control - if (this.config.controls.includes('volume')) { + // Volume controls + if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) { const volume = createElement('div', { class: 'plyr__volume', }); - // Set the attributes - const attributes = { - max: 1, - step: 0.05, - value: this.config.volume, - }; + // Toggle mute button + if (this.config.controls.includes('mute')) { + volume.appendChild(controls.createButton.call(this, 'mute')); + } - // Create the volume range slider - volume.appendChild( - controls.createRange.call( - this, - 'volume', - extend(attributes, { - id: `plyr-volume-${data.id}`, - }), - ), - ); + // Volume range control + if (this.config.controls.includes('volume')) { + // Set the attributes + const attributes = { + max: 1, + step: 0.05, + value: this.config.volume, + }; + + // Create the volume range slider + volume.appendChild( + controls.createRange.call( + this, + 'volume', + extend(attributes, { + id: `plyr-volume-${data.id}`, + }), + ), + ); - this.elements.volume = volume; + this.elements.volume = volume; + } container.appendChild(volume); } @@ -1512,6 +1514,7 @@ const controls = { this.elements.controls = container; + // Set available quality levels if (this.isHTML5) { controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); } diff --git a/src/sass/components/volume.scss b/src/sass/components/volume.scss index d22b7cba..82a6dd36 100644 --- a/src/sass/components/volume.scss +++ b/src/sass/components/volume.scss @@ -3,20 +3,23 @@ // -------------------------------------------------------------- .plyr__volume { + align-items: center; + display: flex; flex: 1; position: relative; input[type='range'] { + margin-left: ($plyr-control-spacing / 2); position: relative; z-index: 2; } @media (min-width: $plyr-bp-sm) { - max-width: 50px; + max-width: 90px; } @media (min-width: $plyr-bp-md) { - max-width: 80px; + max-width: 110px; } } -- cgit v1.2.3 From 211ad6c8f59c3d48648f8be0bc1cde85fd72ea95 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:43:08 +1000 Subject: Removed YouTube quality controls --- src/js/config/defaults.js | 17 +++------------ src/js/html5.js | 3 +++ src/js/listeners.js | 6 ------ src/js/plugins/youtube.js | 54 ----------------------------------------------- src/js/plyr.js | 5 ----- 5 files changed, 6 insertions(+), 79 deletions(-) (limited to 'src') diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index a898755d..e6e2d7c1 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -68,19 +68,7 @@ const defaults = { // Quality default quality: { default: 576, - options: [ - 4320, - 2880, - 2160, - 1440, - 1080, - 720, - 576, - 480, - 360, - 240, - 'default', // YouTube's "auto" - ], + options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], }, // Set loops @@ -268,8 +256,9 @@ const defaults = { // YouTube 'statechange', + + // Quality 'qualitychange', - 'qualityrequested', // Ads 'adsloaded', diff --git a/src/js/html5.js b/src/js/html5.js index 0876211a..fc8da8c0 100644 --- a/src/js/html5.js +++ b/src/js/html5.js @@ -82,6 +82,9 @@ const html5 = { triggerEvent.call(player, player.media, 'qualitychange', false, { quality: input, }); + + // Save to storage + player.storage.set({ quality: input }); }, }); }, diff --git a/src/js/listeners.js b/src/js/listeners.js index c2b772b2..a6db034a 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -430,12 +430,6 @@ class Listeners { player.storage.set({ speed: player.speed }); }); - // Quality request - on.call(player, player.media, 'qualityrequested', event => { - // Save to storage - player.storage.set({ quality: event.detail.quality }); - }); - // Quality change on.call(player, player.media, 'qualitychange', event => { // Update UI diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 66a73acf..73175c14 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -2,9 +2,7 @@ // YouTube plugin // ========================================================================== -import controls from '../controls'; import ui from '../ui'; -import { dedupe } from '../utils/arrays'; import { createElement, replaceElement, toggleClass } from '../utils/elements'; import { triggerEvent } from '../utils/events'; import fetch from '../utils/fetch'; @@ -23,37 +21,6 @@ function parseId(url) { return url.match(regex) ? RegExp.$2 : url; } -// Standardise YouTube quality unit -function mapQualityUnit(input) { - const qualities = { - hd2160: 2160, - hd1440: 1440, - hd1080: 1080, - hd720: 720, - large: 480, - medium: 360, - small: 240, - tiny: 144, - }; - - const entry = Object.entries(qualities).find(entry => entry.includes(input)); - - if (entry) { - // Get the match corresponding to the input - return entry.find(value => value !== input); - } - - return 'default'; -} - -function mapQualityUnits(levels) { - if (is.empty(levels)) { - return levels; - } - - return dedupe(levels.map(level => mapQualityUnit(level))); -} - // Set playback state and trigger change (only on actual change) function assurePlaybackState(play) { if (play && !this.embed.hasPlayed) { @@ -225,11 +192,6 @@ const youtube = { triggerEvent.call(player, player.media, 'error'); } }, - onPlaybackQualityChange() { - triggerEvent.call(player, player.media, 'qualitychange', false, { - quality: player.media.quality, - }); - }, onPlaybackRateChange(event) { // Get the instance const instance = event.target; @@ -299,16 +261,6 @@ const youtube = { }, }); - // Quality - Object.defineProperty(player.media, 'quality', { - get() { - return mapQualityUnit(instance.getPlaybackQuality()); - }, - set(input) { - instance.setPlaybackQuality(mapQualityUnit(input)); - }, - }); - // Volume let { volume } = player.config; Object.defineProperty(player.media, 'volume', { @@ -457,12 +409,6 @@ const youtube = { player.media.duration = instance.getDuration(); triggerEvent.call(player, player.media, 'durationchange'); } - - // Get quality - controls.setQualityMenu.call( - player, - mapQualityUnits(instance.getAvailableQualityLevels()), - ); } break; diff --git a/src/js/plyr.js b/src/js/plyr.js index 573b4d4c..44e41cb6 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -698,11 +698,6 @@ class Plyr { quality = value; } - // Trigger request event - triggerEvent.call(this, this.media, 'qualityrequested', false, { - quality, - }); - // Update config config.selected = quality; -- cgit v1.2.3 From 1aeef8128895a97ba7f2f4e8f289efae4db5e9a9 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:43:22 +1000 Subject: Controls spacing improvements --- src/sass/components/controls.scss | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss index bde8600b..41426e8b 100644 --- a/src/sass/components/controls.scss +++ b/src/sass/components/controls.scss @@ -18,18 +18,22 @@ > .plyr__control, .plyr__progress, .plyr__time, - .plyr__menu { + .plyr__menu, + .plyr__volume { margin-left: ($plyr-control-spacing / 2); + } - &:first-child, - &:first-child + [data-plyr='pause'] { - margin-left: 0; - margin-right: auto; - } + .plyr__menu + .plyr__control, + > .plyr__control + .plyr__menu, + > .plyr__control + .plyr__control, + .plyr__progress + .plyr__control { + margin-left: floor($plyr-control-spacing / 4); } - .plyr__volume { - margin-left: ($plyr-control-spacing / 2); + > .plyr__control:first-child, + > .plyr__control:first-child + [data-plyr='pause'] { + margin-left: 0; + margin-right: auto; } // Hide empty controls @@ -39,17 +43,12 @@ @media (min-width: $plyr-bp-sm) { > .plyr__control, + .plyr__menu, .plyr__progress, .plyr__time, - .plyr__menu { + .plyr__volume { margin-left: $plyr-control-spacing; } - - > .plyr__control + .plyr__control, - .plyr__menu + .plyr__control, - > .plyr__control + .plyr__menu { - margin-left: ($plyr-control-spacing / 2); - } } } @@ -72,12 +71,15 @@ bottom: 0; color: $plyr-video-control-color; left: 0; - padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing - $plyr-control-spacing; + padding: ($plyr-control-spacing * 2) ($plyr-control-spacing / 2) ($plyr-control-spacing / 2); position: absolute; right: 0; transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out; z-index: 3; + + @media (min-width: $plyr-bp-sm) { + padding: ($plyr-control-spacing * 3.5) $plyr-control-spacing $plyr-control-spacing; + } } // Hide video controls -- cgit v1.2.3 From 95f6fa2731bc251a1a1dd52aa0abfd55c2fd3b5e Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:46:58 +1000 Subject: Fix for setting pressed property of undefined (Fixes #1102) --- src/js/listeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index a6db034a..5fe20695 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -277,7 +277,7 @@ class Listeners { const { controls } = elements; // Remove button states for fullscreen - if (event.type === 'enterfullscreen') { + if (controls && event.type === 'enterfullscreen') { controls.pressed = false; controls.hover = false; } -- cgit v1.2.3 From 286d0d1794d4c9d4c4db151897b102b95c5799c0 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 13 Aug 2018 23:52:10 +1000 Subject: Fix for pressed property missing with custom controls (Fixes #1062) --- src/js/controls.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 9152f338..d8de2632 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -275,18 +275,6 @@ const controls = { this.elements.buttons[type] = button; } - // Toggle classname when pressed property is set - const className = this.config.classNames.controlPressed; - Object.defineProperty(button, 'pressed', { - enumerable: true, - get() { - return hasClass(button, className); - }, - set(pressed = false) { - toggleClass(button, className, pressed); - }, - }); - return button; }, @@ -1620,6 +1608,23 @@ const controls = { controls.findElements.call(this); } + // Add pressed property to buttons + if (!is.empty(this.elements.buttons)) { + // Toggle classname when pressed property is set + Object.values(this.elements.buttons).forEach(button => { + const className = this.config.classNames.controlPressed; + Object.defineProperty(button, 'pressed', { + enumerable: true, + get() { + return hasClass(button, className); + }, + set(pressed = false) { + toggleClass(button, className, pressed); + }, + }); + }); + } + // Edge sometimes doesn't finish the paint so force a redraw if (window.navigator.userAgent.includes('Edge')) { repaint(target); -- cgit v1.2.3