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 --- dist/plyr.js | 249 ++++++++++++++++++++++++++++------------------------------- 1 file changed, 116 insertions(+), 133 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 62b32344..883e7a18 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -382,12 +382,19 @@ typeof navigator === "object" && (function (global, factory) { // Inaert an element after another function insertAfter(element, target) { + if (!is.element(element) || !is.element(target)) { + return; + } + target.parentNode.insertBefore(element, target.nextSibling); } // Insert a DocumentFragment function insertElement(type, parent, attributes, text) { - // Inject the new + if (!is.element(parent)) { + return; + } + parent.appendChild(createElement(type, attributes, text)); } @@ -407,6 +414,10 @@ typeof navigator === "object" && (function (global, factory) { // Remove all child elements function emptyElement(element) { + if (!is.element(element)) { + return; + } + var length = element.childNodes.length; @@ -1590,31 +1601,16 @@ typeof navigator === "object" && (function (global, factory) { _ref$checked = _ref.checked, checked = _ref$checked === undefined ? false : _ref$checked; - var item = createElement('li'); - - var label = createElement('label', { - class: this.config.classNames.control - }); - - var radio = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { - type: 'radio', - name: 'plyr-' + type, + var item = createElement('button', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { + type: 'button', value: value, - checked: checked, - class: 'plyr__sr-only' - })); - - var faux = createElement('span', { hidden: '' }); - - label.appendChild(radio); - label.appendChild(faux); - label.insertAdjacentHTML('beforeend', title); + 'aria-checked': checked + }), title); if (is.element(badge)) { - label.appendChild(badge); + item.appendChild(badge); } - item.appendChild(label); list.appendChild(item); }, @@ -1879,8 +1875,8 @@ typeof navigator === "object" && (function (global, factory) { // Hide/show a tab - toggleTab: function toggleTab(setting, toggle) { - toggleHidden(this.elements.settings.tabs[setting], !toggle); + toggleMenuButton: function toggleMenuButton(setting, toggle) { + toggleHidden(this.elements.settings.buttons[setting], !toggle); }, @@ -1889,12 +1885,12 @@ typeof navigator === "object" && (function (global, factory) { var _this3 = this; // Menu required - if (!is.element(this.elements.settings.panes.quality)) { + if (!is.element(this.elements.settings.menus.quality)) { return; } var type = 'quality'; - var list = this.elements.settings.panes.quality.querySelector('ul'); + var list = this.elements.settings.menus.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config if (is.array(options)) { @@ -1905,7 +1901,7 @@ typeof navigator === "object" && (function (global, factory) { // Toggle the pane and tab var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; - controls.toggleTab.call(this, type, toggle); + controls.toggleMenuButton.call(this, type, toggle); // Check if we need to toggle the parent controls.checkMenu.call(this); @@ -1977,7 +1973,7 @@ typeof navigator === "object" && (function (global, factory) { // Update the selected setting updateSetting: function updateSetting(setting, container, input) { - var pane = this.elements.settings.panes[setting]; + var pane = this.elements.settings.menus[setting]; var value = null; var list = container; @@ -2006,7 +2002,7 @@ typeof navigator === "object" && (function (global, factory) { // 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... @@ -2015,14 +2011,14 @@ typeof navigator === "object" && (function (global, factory) { } // Update the label - var label = this.elements.settings.tabs[setting].querySelector('.' + this.config.classNames.menu.value); + var label = this.elements.settings.buttons[setting].querySelector('.' + this.config.classNames.menu.value); label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it - var target = list && list.querySelector('input[value="' + value + '"]'); + var target = list && list.querySelector('button[value="' + value + '"]'); if (is.element(target)) { - target.checked = true; + target.setAttribute('aria-checked', true); } }, @@ -2030,17 +2026,17 @@ typeof navigator === "object" && (function (global, factory) { // 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); options.forEach(option => { @@ -2072,11 +2068,11 @@ typeof navigator === "object" && (function (global, factory) { // TODO: Captions or language? Currently it's mixed var type = 'captions'; - var list = this.elements.settings.panes.captions.querySelector('ul'); + var list = this.elements.settings.menus.captions.querySelector('[role="menu"]'); var tracks = captions.getTracks.call(this); // Toggle the pane and tab - controls.toggleTab.call(this, type, tracks.length); + controls.toggleMenuButton.call(this, type, tracks.length); // Empty the menu emptyElement(list); @@ -2127,7 +2123,7 @@ typeof navigator === "object" && (function (global, factory) { } // Menu required - if (!is.element(this.elements.settings.panes.speed)) { + if (!is.element(this.elements.settings.menus.speed)) { return; } @@ -2147,7 +2143,7 @@ typeof navigator === "object" && (function (global, factory) { // Toggle the pane and tab var toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; - controls.toggleTab.call(this, type, toggle); + controls.toggleMenuButton.call(this, type, toggle); // Check if we need to toggle the parent controls.checkMenu.call(this); @@ -2158,7 +2154,7 @@ typeof navigator === "object" && (function (global, factory) { } // Get the list to populate - var list = this.elements.settings.panes.speed.querySelector('ul'); + var list = this.elements.settings.menus.speed.querySelector('[role="menu"]'); // Empty the menu emptyElement(list); @@ -2179,10 +2175,10 @@ typeof navigator === "object" && (function (global, factory) { // Check if we need to hide/show the settings menu checkMenu: function checkMenu() { - var tabs = this.elements.settings.tabs; + var buttons = this.elements.settings.buttons; - var visible = !is.empty(tabs) && Object.values(tabs).some(function (tab) { - return !tab.hidden; + var visible = !is.empty(buttons) && Object.values(buttons).some(function (button) { + return !button.hidden; }); toggleHidden(this.elements.settings.menu, !visible); @@ -2191,19 +2187,19 @@ typeof navigator === "object" && (function (global, factory) { // Show/hide menu toggleMenu: function toggleMenu(event) { - var form = this.elements.settings.form; + var popup = this.elements.settings.popup; var 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; } - var show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden'); + var show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden'); if (is.event(event)) { - var isMenuItem = is.element(form) && form.contains(event.target); + var isMenuItem = is.element(popup) && popup.contains(event.target); var isButton = event.target === this.elements.buttons.settings; // If the click was inside the form or if the click @@ -2224,14 +2220,14 @@ typeof navigator === "object" && (function (global, factory) { button.setAttribute('aria-expanded', show); } - 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); } } }, @@ -2245,10 +2241,10 @@ typeof navigator === "object" && (function (global, factory) { clone.removeAttribute('hidden'); // Prevent input's being unchecked due to the name being identical - Array.from(clone.querySelectorAll('input[name]')).forEach(function (input) { - var name = input.getAttribute('name'); - input.setAttribute('name', name + '-clone'); - }); + /* 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); @@ -2268,34 +2264,37 @@ typeof navigator === "object" && (function (global, factory) { // Toggle Menu - showTab: function showTab() { + showMenu: function showMenu() { var _this6 = this; - var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var menu = this.elements.settings.menu; - var pane = document.getElementById(target); + var 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 - var isTab = pane.getAttribute('role') === 'tabpanel'; + /* const isTab = pane.getAttribute('role') === 'tabpanel'; if (!isTab) { return; - } + } */ // Hide all other tabs // Get other tabs - var current = menu.querySelector('[role="tabpanel"]:not([hidden])'); + var current = menu.querySelector('[id^=plyr-settings-' + this.id + ']:not([hidden])'); var container = current.parentNode; // Set other toggles to be expanded false - Array.from(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]')).forEach(function (toggle) { + /* Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => { toggle.setAttribute('aria-expanded', false); - }); + }); */ // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -2331,16 +2330,16 @@ typeof navigator === "object" && (function (global, factory) { // Set attributes on current tab toggleHidden(current, true); - current.setAttribute('tabindex', -1); + // current.setAttribute('tabindex', -1); // Set attributes on target toggleHidden(pane, false); - var tabs = getElements.call(this, '[aria-controls="' + target + '"]'); - Array.from(tabs).forEach(function (tab) { + /* 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(); @@ -2453,55 +2452,46 @@ typeof navigator === "object" && (function (global, factory) { // Settings button / menu if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { - var menu = createElement('div', { + var control = createElement('div', { class: 'plyr__menu', hidden: '' }); - menu.appendChild(controls.createButton.call(this, 'settings', { + 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 })); - var form = createElement('form', { + var 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 + 'aria-labelled-by': 'plyr-settings-toggle-' + data.id }); var inner = createElement('div'); var home = createElement('div', { - id: 'plyr-settings-' + data.id + '-home', - 'aria-labelled-by': 'plyr-settings-toggle-' + data.id, - role: 'tabpanel' + id: 'plyr-settings-' + data.id + '-home' }); - // Create the tab list - var tabs = createElement('ul', { - role: 'tablist' + // Create the menu + var menu = createElement('div', { + role: 'menu' }); - // Build the tabs + // Build the menu items this.config.settings.forEach(function (type) { - var tab = createElement('li', { - role: 'tab', - hidden: '' - }); - - var button = createElement('button', extend(getAttributesFromSelector(_this7.config.selectors.buttons.settings), { + var menuItem = createElement('button', extend(getAttributesFromSelector(_this7.config.selectors.buttons.settings), { type: 'button', class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--forward', - id: 'plyr-settings-' + data.id + '-' + type + '-tab', - 'aria-haspopup': true, - 'aria-controls': 'plyr-settings-' + data.id + '-' + type, - 'aria-expanded': false - }), i18n.get(type, _this7.config)); + 'role': 'menuitem', + 'aria-haspopup': true + })); + + var flex = createElement('span', null, i18n.get(type, _this7.config)); var value = createElement('span', { class: _this7.config.classNames.menu.value @@ -2510,50 +2500,46 @@ typeof navigator === "object" && (function (global, factory) { // 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); - _this7.elements.settings.tabs[type] = tab; - }); - - home.appendChild(tabs); - inner.appendChild(home); - - // Build the panes - this.config.settings.forEach(function (type) { + // Build the panes var pane = createElement('div', { id: 'plyr-settings-' + data.id + '-' + type, - hidden: '', - 'aria-labelled-by': 'plyr-settings-' + data.id + '-' + type + '-tab', - role: 'tabpanel', - tabindex: -1 + hidden: '' }); - var back = createElement('button', { + // Back button + pane.appendChild(createElement('button', { type: 'button', - class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--back', - 'aria-haspopup': true, - 'aria-controls': 'plyr-settings-' + data.id + '-home', - 'aria-expanded': false - }, i18n.get(type, _this7.config)); - - pane.appendChild(back); + class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--back' + }, i18n.get(type, _this7.config))); - var options = createElement('ul'); + // Menu + pane.appendChild(createElement('div', { + role: 'menu' + })); - pane.appendChild(options); inner.appendChild(pane); - _this7.elements.settings.panes[type] = pane; + menuItem.addEventListener('click', function () { + controls.showMenu.call(_this7, type); + }); + + _this7.elements.settings.buttons[type] = menuItem; + _this7.elements.settings.menus[type] = pane; }); - form.appendChild(inner); - 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 @@ -4617,13 +4603,12 @@ typeof navigator === "object" && (function (global, factory) { }); // Settings menu - bind(this.player.elements.settings.form, 'click', function (event) { + bind(this.player.elements.settings.popup, 'click', function (event) { event.stopPropagation(); // Go back to home tab on click var showHomeTab = function showHomeTab() { - var id = 'plyr-settings-' + _this4.player.id + '-home'; - controls.showTab.call(_this4.player, id); + controls.showMenu.call(_this4.player, 'home'); }; // Settings menu items - use event delegation as items are added/removed @@ -4642,9 +4627,6 @@ typeof navigator === "object" && (function (global, factory) { _this4.player.speed = parseFloat(event.target.value); showHomeTab(); }, 'speed'); - } else { - var tab = event.target; - controls.showTab.call(_this4.player, tab.getAttribute('aria-controls')); } }); @@ -6952,16 +6934,17 @@ typeof navigator === "object" && (function (global, factory) { // Elements cache this.elements = { container: null, + captions: null, buttons: {}, display: {}, progress: {}, inputs: {}, settings: { + popup: null, menu: null, - panes: {}, - tabs: {} - }, - captions: null + menus: {}, + buttons: {} + } }; // Captions -- 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 --- dist/plyr.js | 133 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 70 insertions(+), 63 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 804d7dd5..66ea1d3f 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -1624,7 +1624,7 @@ typeof navigator === "object" && (function (global, factory) { var attributes = getAttributesFromSelector(this.config.selectors.display[type]); var 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'); @@ -1646,16 +1646,42 @@ typeof navigator === "object" && (function (global, factory) { _ref$checked = _ref.checked, checked = _ref$checked === undefined ? false : _ref$checked; - var item = createElement('button', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { + var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); + + var item = createElement('button', extend(attributes, { type: 'button', + role: 'menuitemradio', + class: (this.config.classNames.control + ' ' + (attributes.class ? attributes.class : '')).trim(), value: 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: function get$$1() { + return item.getAttribute('aria-checked') === 'true'; + }, + set: function set$$1(checked) { + // Ensure exclusivity + if (checked) { + Array.from(item.parentNode.children).filter(function (node) { + return matches(node, '[role="menuitemradio"]'); + }).forEach(function (node) { + return node.setAttribute('aria-checked', 'false'); + }); + } + + item.setAttribute('aria-checked', checked ? 'true' : 'false'); + } + }); + list.appendChild(item); }, @@ -1930,12 +1956,12 @@ typeof navigator === "object" && (function (global, factory) { var _this3 = this; // Menu required - if (!is.element(this.elements.settings.menus.quality)) { + if (!is.element(this.elements.settings.panels.quality)) { return; } var type = 'quality'; - var list = this.elements.settings.menus.quality.querySelector('[role="menu"]'); + var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config if (is.array(options)) { @@ -2018,7 +2044,7 @@ typeof navigator === "object" && (function (global, factory) { // Update the selected setting updateSetting: function updateSetting(setting, container, input) { - var pane = this.elements.settings.menus[setting]; + var pane = this.elements.settings.panels[setting]; var value = null; var list = container; @@ -2060,10 +2086,10 @@ typeof navigator === "object" && (function (global, factory) { label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it - var target = list && list.querySelector('button[value="' + value + '"]'); + var target = list && list.querySelector('[value="' + value + '"]'); if (is.element(target)) { - target.setAttribute('aria-checked', true); + target.checked = true; } }, @@ -2071,14 +2097,14 @@ typeof navigator === "object" && (function (global, factory) { // 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); controls.toggleMenuButton.call(this, 'loop', toggle); @@ -2113,7 +2139,7 @@ typeof navigator === "object" && (function (global, factory) { // TODO: Captions or language? Currently it's mixed var type = 'captions'; - var list = this.elements.settings.menus.captions.querySelector('[role="menu"]'); + var list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); var tracks = captions.getTracks.call(this); // Toggle the pane and tab @@ -2168,7 +2194,7 @@ typeof navigator === "object" && (function (global, factory) { } // Menu required - if (!is.element(this.elements.settings.menus.speed)) { + if (!is.element(this.elements.settings.panels.speed)) { return; } @@ -2199,7 +2225,7 @@ typeof navigator === "object" && (function (global, factory) { } // Get the list to populate - var list = this.elements.settings.menus.speed.querySelector('[role="menu"]'); + var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Empty the menu emptyElement(list); @@ -2278,19 +2304,13 @@ typeof navigator === "object" && (function (global, factory) { }, - // Get the natural size of a tab - getTabSize: function getTabSize(tab) { + // Get the natural size of a menu panel + getMenuSize: function getMenuSize(tab) { var 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); @@ -2308,38 +2328,24 @@ typeof navigator === "object" && (function (global, factory) { }, - // Toggle Menu - showMenu: function showMenu() { + // Show a panel in the menu + showMenuPanel: function showMenuPanel() { var _this6 = this; var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - var menu = this.elements.settings.menu; - - var pane = document.getElementById('plyr-settings-' + this.id + '-' + type); - console.warn('plyr-settings-' + this.id + '-' + type); + var 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 - var current = menu.querySelector('[id^=plyr-settings-' + this.id + ']:not([hidden])'); - var 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 + var container = target.parentNode; + var current = Array.from(container.children).find(function (node) { + return !node.hidden; + }); // If we can do fancy animations, we'll animate the height/width if (support.transitions && !support.reducedMotion) { @@ -2348,12 +2354,12 @@ typeof navigator === "object" && (function (global, factory) { container.style.height = current.scrollHeight + 'px'; // Get potential sizes - var size = controls.getTabSize.call(this, pane); + var size = controls.getMenuSize.call(this, target); // Restore auto height/width - var restore = function restore(e) { + var restore = function 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; } @@ -2378,16 +2384,10 @@ typeof navigator === "object" && (function (global, factory) { // 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(); }, @@ -2532,7 +2532,7 @@ typeof navigator === "object" && (function (global, factory) { var menuItem = createElement('button', extend(getAttributesFromSelector(_this7.config.selectors.buttons.settings), { type: 'button', class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--forward', - 'role': 'menuitem', + role: 'menuitem', 'aria-haspopup': true })); @@ -2556,10 +2556,14 @@ typeof navigator === "object" && (function (global, factory) { }); // Back button - pane.appendChild(createElement('button', { + var back = createElement('button', { type: 'button', class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--back' - }, i18n.get(type, _this7.config))); + }, i18n.get(type, _this7.config)); + back.addEventListener('click', function () { + controls.showMenuPanel.call(_this7, 'home'); + }); + pane.appendChild(back); // Menu pane.appendChild(createElement('div', { @@ -2569,11 +2573,11 @@ typeof navigator === "object" && (function (global, factory) { inner.appendChild(pane); menuItem.addEventListener('click', function () { - controls.showMenu.call(_this7, type); + controls.showMenuPanel.call(_this7, type); }); _this7.elements.settings.buttons[type] = menuItem; - _this7.elements.settings.menus[type] = pane; + _this7.elements.settings.panels[type] = pane; }); home.appendChild(menu); @@ -3482,6 +3486,9 @@ typeof navigator === "object" && (function (global, factory) { isTouch: 'plyr--is-touch', uiSupported: 'plyr--full-ui', noTransition: 'plyr--no-transition', + display: { + time: 'plyr__time' + }, menu: { value: 'plyr__menu__value', badge: 'plyr__badge', @@ -4608,7 +4615,7 @@ typeof navigator === "object" && (function (global, factory) { // Go back to home tab on click var showHomeTab = function showHomeTab() { - controls.showMenu.call(_this4.player, 'home'); + controls.showMenuPanel.call(_this4.player, 'home'); }; // Settings menu items - use event delegation as items are added/removed @@ -6942,7 +6949,7 @@ typeof navigator === "object" && (function (global, factory) { settings: { popup: null, menu: null, - menus: {}, + panels: {}, buttons: {} } }; -- 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 --- dist/plyr.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 6bbbad13..f2ed3969 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -1957,6 +1957,7 @@ typeof navigator === "object" && (function (global, factory) { // Menu required if (!is.element(this.elements.settings.panels.quality)) { + console.warn('Not an element'); return; } @@ -1971,6 +1972,7 @@ typeof navigator === "object" && (function (global, factory) { } // Toggle the pane and tab + console.warn(this.options.quality); var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; controls.toggleMenuButton.call(this, type, toggle); @@ -2387,7 +2389,10 @@ typeof navigator === "object" && (function (global, factory) { toggleHidden(target, false); // Focus the first item - target.querySelectorAll('[role^="menuitem"]')[0].focus(); + var firstItem = target.querySelector('[role^="menuitem"]'); + if (firstItem) { + firstItem.focus(); + } }, @@ -3990,11 +3995,9 @@ typeof navigator === "object" && (function (global, factory) { } // If there's a play button, set label - if (is.nodeList(this.elements.buttons.play)) { - Array.from(this.elements.buttons.play).forEach(function (button) { - button.setAttribute('aria-label', label); - }); - } + Array.from(this.elements.buttons.play || []).forEach(function (button) { + button.setAttribute('aria-label', label); + }); // Set iframe title // https://github.com/sampotts/plyr/issues/124 @@ -4074,11 +4077,9 @@ typeof navigator === "object" && (function (global, factory) { toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set state - if (is.nodeList(this.elements.buttons.play)) { - Array.from(this.elements.buttons.play).forEach(function (target) { - target.pressed = _this3.playing; - }); - } + Array.from(this.elements.buttons.play || []).forEach(function (target) { + target.pressed = _this3.playing; + }); // Only update controls on non timeupdate events if (is.event(event) && event.type === 'timeupdate') { -- 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 --- dist/plyr.js | 621 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 330 insertions(+), 291 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 775c971d..2f2c192d 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -11,62 +11,94 @@ typeof navigator === "object" && (function (global, factory) { var getConstructor = function getConstructor(input) { return input !== null && typeof input !== 'undefined' ? input.constructor : null; }; - var instanceOf = function instanceOf(input, constructor) { return Boolean(input && constructor && input instanceof constructor); }; + var isNullOrUndefined = function isNullOrUndefined(input) { + return input === null || typeof input === 'undefined'; + }; + var isObject = function isObject(input) { + return getConstructor(input) === Object; + }; + var isNumber = function isNumber(input) { + return getConstructor(input) === Number && !Number.isNaN(input); + }; + var isString = function isString(input) { + return getConstructor(input) === String; + }; + var isBoolean = function isBoolean(input) { + return getConstructor(input) === Boolean; + }; + var isFunction = function isFunction(input) { + return getConstructor(input) === Function; + }; + var isArray = function isArray(input) { + return Array.isArray(input); + }; + var isWeakMap = function isWeakMap(input) { + return instanceOf(input, WeakMap); + }; + var isNodeList = function isNodeList(input) { + return instanceOf(input, NodeList); + }; + var isElement = function isElement(input) { + return instanceOf(input, Element); + }; + var isTextNode = function isTextNode(input) { + return getConstructor(input) === Text; + }; + var isEvent = function isEvent(input) { + return instanceOf(input, Event); + }; + var isCue = function isCue(input) { + return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); + }; + var isTrack = function isTrack(input) { + return instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind); + }; - var is = { - object: function object(input) { - return getConstructor(input) === Object; - }, - number: function number(input) { - return getConstructor(input) === Number && !Number.isNaN(input); - }, - string: function string(input) { - return getConstructor(input) === String; - }, - boolean: function boolean(input) { - return getConstructor(input) === Boolean; - }, - function: function _function(input) { - return getConstructor(input) === Function; - }, - array: function array(input) { - return !is.nullOrUndefined(input) && Array.isArray(input); - }, - weakMap: function weakMap(input) { - return instanceOf(input, WeakMap); - }, - nodeList: function nodeList(input) { - return instanceOf(input, NodeList); - }, - element: function element(input) { - return instanceOf(input, Element); - }, - textNode: function textNode(input) { - return getConstructor(input) === Text; - }, - event: function event(input) { - return instanceOf(input, Event); - }, - cue: function cue(input) { - return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); - }, - track: function track(input) { - return instanceOf(input, TextTrack) || !is.nullOrUndefined(input) && is.string(input.kind); - }, - url: function url(input) { - return !is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); - }, - nullOrUndefined: function nullOrUndefined(input) { - return input === null || typeof input === 'undefined'; - }, - empty: function empty(input) { - return is.nullOrUndefined(input) || (is.string(input) || is.array(input) || is.nodeList(input)) && !input.length || is.object(input) && !Object.keys(input).length; + var isEmpty = function isEmpty(input) { + return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length; + }; + + var isUrl = function isUrl(input) { + // Accept a URL object + if (instanceOf(input, window.URL)) { + return true; + } + + // Add the protocol if required + var string = input; + if (!input.startsWith('http://') || !input.startsWith('https://')) { + string = 'http://' + input; + } + + try { + return !isEmpty(new URL(string).hostname); + } catch (e) { + return false; } }; + var is = { + nullOrUndefined: isNullOrUndefined, + object: isObject, + number: isNumber, + string: isString, + boolean: isBoolean, + function: isFunction, + array: isArray, + weakMap: isWeakMap, + nodeList: isNodeList, + element: isElement, + textNode: isTextNode, + event: isEvent, + cue: isCue, + track: isTrack, + url: isUrl, + empty: isEmpty + }; + // ========================================================================== // Check for passive event listener support @@ -1637,6 +1669,8 @@ typeof navigator === "object" && (function (global, factory) { // Create a settings menu item createMenuItem: function createMenuItem(_ref) { + var _this = this; + var value = _ref.value, list = _ref.list, type = _ref.type, @@ -1652,17 +1686,21 @@ typeof navigator === "object" && (function (global, factory) { type: 'button', role: 'menuitemradio', class: (this.config.classNames.control + ' ' + (attributes.class ? attributes.class : '')).trim(), - value: value, - 'aria-checked': checked + 'aria-checked': checked, + value: value })); + var 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: function get$$1() { @@ -1682,6 +1720,29 @@ typeof navigator === "object" && (function (global, factory) { } }); + this.listeners.bind(item, 'click', function () { + 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); }, @@ -1755,7 +1816,7 @@ typeof navigator === "object" && (function (global, factory) { // Update elements updateProgress: function updateProgress(event) { - var _this = this; + var _this2 = this; if (!this.supported.ui || !is.event(event)) { return; @@ -1765,7 +1826,7 @@ typeof navigator === "object" && (function (global, factory) { var setProgress = function setProgress(target, input) { var value = is.number(input) ? input : 0; - var progress = is.element(target) ? target : _this.elements.display.buffer; + var progress = is.element(target) ? target : _this2.elements.display.buffer; // Update value and label if (is.element(progress)) { @@ -1845,7 +1906,7 @@ typeof navigator === "object" && (function (global, factory) { // Update hover tooltip for seeking updateSeekTooltip: function updateSeekTooltip(event) { - var _this2 = this; + var _this3 = this; // Bail if setting not true if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) { @@ -1858,7 +1919,7 @@ typeof navigator === "object" && (function (global, factory) { var visible = this.config.classNames.tooltip + '--visible'; var toggle = function toggle(_toggle) { - toggleClass(_this2.elements.display.seekTooltip, visible, _toggle); + toggleClass(_this3.elements.display.seekTooltip, visible, _toggle); }; // Hide on touch @@ -1951,68 +2012,55 @@ typeof navigator === "object" && (function (global, factory) { }, - // Set the quality menu - setQualityMenu: function setQualityMenu(options) { - var _this3 = this; + // Update the selected setting + updateSetting: function updateSetting(setting, container, input) { + var pane = this.elements.settings.panels[setting]; + var value = null; + var list = container; - // Menu required - if (!is.element(this.elements.settings.panels.quality)) { - console.warn('Not an element'); - return; - } + if (setting === 'captions') { + value = this.currentTrack; + } else { + value = !is.empty(input) ? input : this[setting]; - var type = 'quality'; - var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); + // Get default + if (is.empty(value)) { + value = this.config[setting].default; + } - // Set options if passed and filter based on uniqueness and config - if (is.array(options)) { - this.options.quality = dedupe(options).filter(function (quality) { - return _this3.config.quality.options.includes(quality); - }); - } + // Unsupported value + if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { + this.debug.warn('Unsupported value of \'' + value + '\' for ' + setting); + return; + } - // Toggle the pane and tab - console.warn(this.options.quality); - var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; - controls.toggleMenuButton.call(this, type, toggle); + // Disabled value + if (!this.config[setting].options.includes(value)) { + this.debug.warn('Disabled value of \'' + value + '\' for ' + setting); + return; + } + } - // Check if we need to toggle the parent - controls.checkMenu.call(this); + // Get the list if we need to + if (!is.element(list)) { + list = pane && pane.querySelector('[role="menu"]'); + } - // If we're hiding, nothing more to do - if (!toggle) { + // If there's no list it means it's not been rendered... + if (!is.element(list)) { return; } - // Empty the menu - emptyElement(list); - - // Get the badge HTML for HD, 4K etc - var getBadge = function getBadge(quality) { - var label = i18n.get('qualityBadge.' + quality, _this3.config); - - if (!label.length) { - return null; - } - - return controls.createBadge.call(_this3, label); - }; + // Update the label + var label = this.elements.settings.buttons[setting].querySelector('.' + this.config.classNames.menu.value); + label.innerHTML = controls.getLabel.call(this, setting, value); - // Sort options by the config and then render options - this.options.quality.sort(function (a, b) { - var sorting = _this3.config.quality.options; - return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; - }).forEach(function (quality) { - controls.createMenuItem.call(_this3, { - value: quality, - list: list, - type: type, - title: controls.getLabel.call(_this3, 'quality', quality), - badge: getBadge(quality) - }); - }); + // Find the radio option and check it + var target = list && list.querySelector('[value="' + value + '"]'); - controls.updateSetting.call(this, type, list); + if (is.element(target)) { + target.checked = true; + } }, @@ -2044,55 +2092,66 @@ typeof navigator === "object" && (function (global, factory) { }, - // Update the selected setting - updateSetting: function updateSetting(setting, container, input) { - var pane = this.elements.settings.panels[setting]; - var value = null; - var list = container; - - if (setting === 'captions') { - value = this.currentTrack; - } else { - value = !is.empty(input) ? input : this[setting]; + // Set the quality menu + setQualityMenu: function setQualityMenu(options) { + var _this4 = this; - // Get default - if (is.empty(value)) { - value = this.config[setting].default; - } + // Menu required + if (!is.element(this.elements.settings.panels.quality)) { + return; + } - // Unsupported value - if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { - this.debug.warn('Unsupported value of \'' + value + '\' for ' + setting); - return; - } + var type = 'quality'; + var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); - // Disabled value - if (!this.config[setting].options.includes(value)) { - this.debug.warn('Disabled value of \'' + value + '\' for ' + setting); - return; - } + // Set options if passed and filter based on uniqueness and config + if (is.array(options)) { + this.options.quality = dedupe(options).filter(function (quality) { + return _this4.config.quality.options.includes(quality); + }); } - // Get the list if we need to - if (!is.element(list)) { - list = pane && pane.querySelector('[role="menu"]'); - } + // Toggle the pane and tab + var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; + controls.toggleMenuButton.call(this, type, toggle); - // If there's no list it means it's not been rendered... - if (!is.element(list)) { + // Empty the menu + emptyElement(list); + + // Check if we need to toggle the parent + controls.checkMenu.call(this); + + // If we're hiding, nothing more to do + if (!toggle) { return; } - // Update the label - var label = this.elements.settings.buttons[setting].querySelector('.' + this.config.classNames.menu.value); - label.innerHTML = controls.getLabel.call(this, setting, value); + // Get the badge HTML for HD, 4K etc + var getBadge = function getBadge(quality) { + var label = i18n.get('qualityBadge.' + quality, _this4.config); - // Find the radio option and check it - var target = list && list.querySelector('[value="' + value + '"]'); + if (!label.length) { + return null; + } - if (is.element(target)) { - target.checked = true; - } + return controls.createBadge.call(_this4, label); + }; + + // Sort options by the config and then render options + this.options.quality.sort(function (a, b) { + var sorting = _this4.config.quality.options; + return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; + }).forEach(function (quality) { + controls.createMenuItem.call(_this4, { + value: quality, + list: list, + type: type, + title: controls.getLabel.call(_this4, 'quality', quality), + badge: getBadge(quality) + }); + }); + + controls.updateSetting.call(this, type, list); }, @@ -2137,15 +2196,21 @@ typeof navigator === "object" && (function (global, factory) { // Set a list of available captions languages setCaptionsMenu: function setCaptionsMenu() { - var _this4 = this; + var _this5 = this; + + // Menu required + if (!is.element(this.elements.settings.panels.captions)) { + return; + } // TODO: Captions or language? Currently it's mixed var type = 'captions'; var list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); var tracks = captions.getTracks.call(this); + var 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); @@ -2154,7 +2219,7 @@ typeof navigator === "object" && (function (global, factory) { controls.checkMenu.call(this); // If there's no captions, bail - if (!tracks.length) { + if (!toggle) { return; } @@ -2162,9 +2227,9 @@ typeof navigator === "object" && (function (global, factory) { var options = tracks.map(function (track, value) { return { value: value, - checked: _this4.captions.toggled && _this4.currentTrack === value, - title: captions.getLabel.call(_this4, track), - badge: track.language && controls.createBadge.call(_this4, track.language.toUpperCase()), + checked: _this5.captions.toggled && _this5.currentTrack === value, + title: captions.getLabel.call(_this5, track), + badge: track.language && controls.createBadge.call(_this5, track.language.toUpperCase()), list: list, type: 'language' }; @@ -2188,12 +2253,7 @@ typeof navigator === "object" && (function (global, factory) { // Set a list of available captions languages setSpeedMenu: function setSpeedMenu(options) { - var _this5 = this; - - // Do nothing if not selected - if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) { - return; - } + var _this6 = this; // Menu required if (!is.element(this.elements.settings.panels.speed)) { @@ -2201,6 +2261,7 @@ typeof navigator === "object" && (function (global, factory) { } var type = 'speed'; + var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options if (is.array(options)) { @@ -2211,13 +2272,16 @@ typeof navigator === "object" && (function (global, factory) { // Set options if passed and filter based on config this.options.speed = this.options.speed.filter(function (speed) { - return _this5.config.speed.options.includes(speed); + return _this6.config.speed.options.includes(speed); }); // Toggle the pane and tab var 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); @@ -2226,19 +2290,13 @@ typeof navigator === "object" && (function (global, factory) { return; } - // Get the list to populate - var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); - - // Empty the menu - emptyElement(list); - // Create items this.options.speed.forEach(function (speed) { - controls.createMenuItem.call(_this5, { + controls.createMenuItem.call(_this6, { value: speed, list: list, type: type, - title: controls.getLabel.call(_this5, 'speed', speed) + title: controls.getLabel.call(_this6, 'speed', speed) }); }); @@ -2332,7 +2390,7 @@ typeof navigator === "object" && (function (global, factory) { // Show a panel in the menu showMenuPanel: function showMenuPanel() { - var _this6 = this; + var _this7 = this; var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; @@ -2370,7 +2428,7 @@ typeof navigator === "object" && (function (global, factory) { container.style.height = ''; // Only listen once - off.call(_this6, container, transitionEndEvent, restore); + off.call(_this7, container, transitionEndEvent, restore); }; // Listen for the transition finishing and restore auto height/width @@ -2383,7 +2441,6 @@ typeof navigator === "object" && (function (global, factory) { // Set attributes on current tab toggleHidden(current, true); - // current.setAttribute('tabindex', -1); // Set attributes on target toggleHidden(target, false); @@ -2399,7 +2456,7 @@ typeof navigator === "object" && (function (global, factory) { // Build the default HTML // TODO: Set order based on order in the config.controls array? create: function create(data) { - var _this7 = this; + var _this8 = this; // Do nothing if we want no controls if (is.empty(this.config.controls)) { @@ -2534,17 +2591,18 @@ typeof navigator === "object" && (function (global, factory) { // Build the menu items this.config.settings.forEach(function (type) { - var menuItem = createElement('button', extend(getAttributesFromSelector(_this7.config.selectors.buttons.settings), { + var menuItem = createElement('button', extend(getAttributesFromSelector(_this8.config.selectors.buttons.settings), { type: 'button', - class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--forward', + class: _this8.config.classNames.control + ' ' + _this8.config.classNames.control + '--forward', role: 'menuitem', - 'aria-haspopup': true + 'aria-haspopup': true, + hidden: '' })); - var flex = createElement('span', null, i18n.get(type, _this7.config)); + var flex = createElement('span', null, i18n.get(type, _this8.config)); var value = createElement('span', { - class: _this7.config.classNames.menu.value + class: _this8.config.classNames.menu.value }); // Speed contains HTML entities @@ -2563,10 +2621,10 @@ typeof navigator === "object" && (function (global, factory) { // Back button var back = createElement('button', { type: 'button', - class: _this7.config.classNames.control + ' ' + _this7.config.classNames.control + '--back' - }, i18n.get(type, _this7.config)); + class: _this8.config.classNames.control + ' ' + _this8.config.classNames.control + '--back' + }, i18n.get(type, _this8.config)); back.addEventListener('click', function () { - controls.showMenuPanel.call(_this7, 'home'); + controls.showMenuPanel.call(_this8, 'home'); }); pane.appendChild(back); @@ -2578,11 +2636,11 @@ typeof navigator === "object" && (function (global, factory) { inner.appendChild(pane); menuItem.addEventListener('click', function () { - controls.showMenuPanel.call(_this7, type); + controls.showMenuPanel.call(_this8, type); }); - _this7.elements.settings.buttons[type] = menuItem; - _this7.elements.settings.panels[type] = pane; + _this8.elements.settings.buttons[type] = menuItem; + _this8.elements.settings.panels[type] = pane; }); home.appendChild(menu); @@ -2630,7 +2688,7 @@ typeof navigator === "object" && (function (global, factory) { // Insert controls inject: function inject() { - var _this8 = this; + var _this9 = this; // Sprite if (this.config.loadSprite) { @@ -2742,8 +2800,8 @@ typeof navigator === "object" && (function (global, factory) { var labels = getElements.call(this, selector); Array.from(labels).forEach(function (label) { - toggleClass(label, _this8.config.classNames.hidden, false); - toggleClass(label, _this8.config.classNames.tooltip, true); + toggleClass(label, _this9.config.classNames.hidden, false); + toggleClass(label, _this9.config.classNames.tooltip, true); }); } } @@ -4374,7 +4432,8 @@ typeof navigator === "object" && (function (global, factory) { // Clear timer clearTimeout(_this2.player.timers.controls); - // Timer to prevent flicker when seeking + + // Set new timer to prevent flicker when seeking _this2.player.timers.controls = setTimeout(function () { return ui.toggleControls.call(_this2.player, false); }, delay); @@ -4531,124 +4590,104 @@ typeof navigator === "object" && (function (global, factory) { }); } - // Listen for control events + // Run default and custom handlers }, { - key: 'controls', - value: function controls$$1() { - var _this4 = this; + key: 'proxy', + value: function proxy(event, defaultHandler, customHandlerKey) { + var customHandler = this.player.config.listeners[customHandlerKey]; + var hasCustomHandler = is.function(customHandler); + var returned = true; - // IE doesn't support input event, so we fallback to change - var inputEvent = browser.isIE ? 'change' : 'input'; + // Execute custom handler + if (hasCustomHandler) { + returned = customHandler.call(this.player, event); + } - // Run default and custom handlers - var proxy = function proxy(event, defaultHandler, customHandlerKey) { - var customHandler = _this4.player.config.listeners[customHandlerKey]; - var hasCustomHandler = is.function(customHandler); - var returned = true; + // Only call default handler if not prevented in custom handler + if (returned && is.function(defaultHandler)) { + defaultHandler.call(this.player, event); + } + } - // Execute custom handler - if (hasCustomHandler) { - returned = customHandler.call(_this4.player, event); - } + // Trigger custom and default handlers - // Only call default handler if not prevented in custom handler - if (returned && is.function(defaultHandler)) { - defaultHandler.call(_this4.player, event); - } - }; + }, { + key: 'bind', + value: function bind(element, type, defaultHandler, customHandlerKey) { + var _this4 = this; - // Trigger custom and default handlers - var bind = function bind(element, type, defaultHandler, customHandlerKey) { - var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; - var customHandler = _this4.player.config.listeners[customHandlerKey]; - var hasCustomHandler = is.function(customHandler); + var customHandler = this.player.config.listeners[customHandlerKey]; + var hasCustomHandler = is.function(customHandler); - on.call(_this4.player, element, type, function (event) { - return proxy(event, defaultHandler, customHandlerKey); - }, passive && !hasCustomHandler); - }; + on.call(this.player, element, type, function (event) { + return _this4.proxy(event, defaultHandler, customHandlerKey); + }, passive && !hasCustomHandler); + } + + // Listen for control events + + }, { + key: 'controls', + value: function controls$$1() { + var _this5 = this; + + // IE doesn't support input event, so we fallback to change + var inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle Array.from(this.player.elements.buttons.play).forEach(function (button) { - bind(button, 'click', _this4.player.togglePlay, 'play'); + _this5.bind(button, 'click', _this5.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.player.elements.buttons.mute, 'click', function () { - _this4.player.muted = !_this4.player.muted; + this.bind(this.player.elements.buttons.mute, 'click', function () { + _this5.player.muted = !_this5.player.muted; }, 'mute'); // Captions toggle - bind(this.player.elements.buttons.captions, 'click', function () { - return _this4.player.toggleCaptions(); + this.bind(this.player.elements.buttons.captions, 'click', function () { + return _this5.player.toggleCaptions(); }); // Fullscreen toggle - bind(this.player.elements.buttons.fullscreen, 'click', function () { - _this4.player.fullscreen.toggle(); + this.bind(this.player.elements.buttons.fullscreen, 'click', function () { + _this5.player.fullscreen.toggle(); }, 'fullscreen'); // Picture-in-Picture - bind(this.player.elements.buttons.pip, 'click', function () { - _this4.player.pip = 'toggle'; + this.bind(this.player.elements.buttons.pip, 'click', function () { + _this5.player.pip = 'toggle'; }, 'pip'); // Airplay - bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); - - // Settings menu - bind(this.player.elements.buttons.settings, 'click', function (event) { - controls.toggleMenu.call(_this4.player, event); - }); + this.bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); // Settings menu - bind(this.player.elements.settings.popup, 'click', function (event) { - event.stopPropagation(); - - // Go back to home tab on click - var showHomeTab = function showHomeTab() { - controls.showMenuPanel.call(_this4.player, 'home'); - }; - - // Settings menu items - use event delegation as items are added/removed - if (matches(event.target, _this4.player.config.selectors.inputs.language)) { - proxy(event, function () { - _this4.player.currentTrack = Number(event.target.value); - showHomeTab(); - }, 'language'); - } else if (matches(event.target, _this4.player.config.selectors.inputs.quality)) { - proxy(event, function () { - _this4.player.quality = event.target.value; - showHomeTab(); - }, 'quality'); - } else if (matches(event.target, _this4.player.config.selectors.inputs.speed)) { - proxy(event, function () { - _this4.player.speed = parseFloat(event.target.value); - showHomeTab(); - }, 'speed'); - } + this.bind(this.player.elements.buttons.settings, 'click', function (event) { + controls.toggleMenu.call(_this5.player, event); }); // Set range input alternative "value", which matches the tooltip time (#954) - bind(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) { - var clientRect = _this4.player.elements.progress.getBoundingClientRect(); + this.bind(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) { + var clientRect = _this5.player.elements.progress.getBoundingClientRect(); var 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', function (event) { + this.bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) { var seek = event.currentTarget; var code = event.keyCode ? event.keyCode : event.which; @@ -4666,15 +4705,15 @@ typeof navigator === "object" && (function (global, factory) { // If we're done seeking and it was playing, resume playback if (play && done) { seek.removeAttribute('play-on-seeked'); - _this4.player.play(); - } else if (!done && _this4.player.playing) { + _this5.player.play(); + } else if (!done && _this5.player.playing) { seek.setAttribute('play-on-seeked', ''); - _this4.player.pause(); + _this5.player.pause(); } }); // Seek - bind(this.player.elements.inputs.seek, inputEvent, function (event) { + this.bind(this.player.elements.inputs.seek, inputEvent, function (event) { var seek = event.currentTarget; // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) @@ -4686,56 +4725,56 @@ typeof navigator === "object" && (function (global, factory) { seek.removeAttribute('seek-value'); - _this4.player.currentTime = seekTo / seek.max * _this4.player.duration; + _this5.player.currentTime = seekTo / seek.max * _this5.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)) { - bind(this.player.elements.display.currentTime, 'click', function () { + this.bind(this.player.elements.display.currentTime, 'click', function () { // Do nothing if we're at the start - if (_this4.player.currentTime === 0) { + if (_this5.player.currentTime === 0) { return; } - _this4.player.config.invertTime = !_this4.player.config.invertTime; + _this5.player.config.invertTime = !_this5.player.config.invertTime; - controls.timeUpdate.call(_this4.player); + controls.timeUpdate.call(_this5.player); }); } // Volume - bind(this.player.elements.inputs.volume, inputEvent, function (event) { - _this4.player.volume = event.target.value; + this.bind(this.player.elements.inputs.volume, inputEvent, function (event) { + _this5.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(function (element) { - bind(element, 'input', function (event) { - return controls.updateRangeFill.call(_this4.player, event.target); + _this5.bind(element, 'input', function (event) { + return controls.updateRangeFill.call(_this5.player, event.target); }); }); } // Seek tooltip - bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { - return controls.updateSeekTooltip.call(_this4.player, event); + this.bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { + return controls.updateSeekTooltip.call(_this5.player, event); }); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) - bind(this.player.elements.controls, 'mouseenter mouseleave', function (event) { - _this4.player.elements.controls.hover = !_this4.player.touch && event.type === 'mouseenter'; + this.bind(this.player.elements.controls, 'mouseenter mouseleave', function (event) { + _this5.player.elements.controls.hover = !_this5.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', function (event) { - _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); + this.bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { + _this5.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); }); // Focus in/out on controls - bind(this.player.elements.controls, 'focusin focusout', function (event) { - var _player = _this4.player, + this.bind(this.player.elements.controls, 'focusin focusout', function (event) { + var _player = _this5.player, config = _player.config, elements = _player.elements, timers = _player.timers; @@ -4745,7 +4784,7 @@ typeof navigator === "object" && (function (global, factory) { toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); // Toggle - ui.toggleControls.call(_this4.player, event.type === 'focusin'); + ui.toggleControls.call(_this5.player, event.type === 'focusin'); // If focusin, hide again after delay if (event.type === 'focusin') { @@ -4755,19 +4794,19 @@ typeof navigator === "object" && (function (global, factory) { }, 0); // Delay a little more for keyboard users - var delay = _this4.touch ? 3000 : 4000; + var delay = _this5.touch ? 3000 : 4000; // Clear timer clearTimeout(timers.controls); // Hide timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this4.player, false); + return ui.toggleControls.call(_this5.player, false); }, delay); } }); // Mouse wheel for volume - bind(this.player.elements.inputs.volume, 'wheel', function (event) { + this.bind(this.player.elements.inputs.volume, 'wheel', function (event) { // Detect "natural" scroll - suppored on OS X Safari only // Other browsers on OS X will be inverted until support improves var inverted = event.webkitDirectionInvertedFromDevice; @@ -4777,10 +4816,10 @@ typeof navigator === "object" && (function (global, factory) { // Scroll down (or up on natural) to decrease if (event.deltaY < 0 || event.deltaX > 0) { if (inverted) { - _this4.player.decreaseVolume(step); + _this5.player.decreaseVolume(step); direction = -1; } else { - _this4.player.increaseVolume(step); + _this5.player.increaseVolume(step); direction = 1; } } @@ -4788,16 +4827,16 @@ typeof navigator === "object" && (function (global, factory) { // Scroll up (or down on natural) to increase if (event.deltaY > 0 || event.deltaX < 0) { if (inverted) { - _this4.player.increaseVolume(step); + _this5.player.increaseVolume(step); direction = 1; } else { - _this4.player.decreaseVolume(step); + _this5.player.decreaseVolume(step); direction = -1; } } // Don't break page scrolling at max and min - if (direction === 1 && _this4.player.media.volume < 1 || direction === -1 && _this4.player.media.volume > 0) { + if (direction === 1 && _this5.player.media.volume < 1 || direction === -1 && _this5.player.media.volume > 0) { event.preventDefault(); } }, 'volume', false); -- 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 --- dist/plyr.js | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 15 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 0dc672b0..819b32e2 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -638,6 +638,24 @@ typeof navigator === "object" && (function (global, factory) { toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } + // Set focus and tab focus class + function setFocus() { + var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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); + } + } + // ========================================================================== var transitionEndEvent = function () { @@ -2351,12 +2369,22 @@ typeof navigator === "object" && (function (global, factory) { 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') { + var pane = Object.values(this.elements.settings.panels).find(function (pane) { + return !pane.hidden; + }); + var firstItem = pane.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, true); + } } else { popup.setAttribute('tabindex', -1); } @@ -2447,9 +2475,7 @@ typeof navigator === "object" && (function (global, factory) { // Focus the first item var firstItem = target.querySelector('[role^="menuitem"]'); - if (firstItem) { - firstItem.focus(); - } + setFocus.call(this, firstItem, true); }, @@ -2589,6 +2615,9 @@ typeof navigator === "object" && (function (global, factory) { role: 'menu' }); + home.appendChild(menu); + inner.appendChild(home); + // Build the menu items this.config.settings.forEach(function (type) { var menuItem = createElement('button', extend(getAttributesFromSelector(_this8.config.selectors.buttons.settings), { @@ -2599,6 +2628,26 @@ typeof navigator === "object" && (function (global, factory) { hidden: '' })); + // Handle space or -> to open menu + on(menuItem, 'keydown', function (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(_this8, type); + }, false); + + // Show menu on click + on(menuItem, 'click', function () { + controls.showMenuPanel.call(_this8, type); + }); + var flex = createElement('span', null, i18n.get(type, _this8.config)); var value = createElement('span', { @@ -2619,14 +2668,43 @@ typeof navigator === "object" && (function (global, factory) { }); // Back button - var back = createElement('button', { + var backButton = createElement('button', { type: 'button', class: _this8.config.classNames.control + ' ' + _this8.config.classNames.control + '--back' - }, i18n.get(type, _this8.config)); - back.addEventListener('click', function () { + }); + + // Visible label + backButton.appendChild(createElement('span', { + 'aria-hidden': true + }, i18n.get(type, _this8.config))); + + // Screen reader label + backButton.appendChild(createElement('span', { + class: _this8.config.classNames.hidden + }, i18n.get('menuBack', _this8.config))); + + // Handle space or -> to open menu + on(backButton, 'keydown', function (event) { + // We only care about <- + if (event.which !== 37) { + return; + } + + // Prevent seek + event.preventDefault(); + event.stopPropagation(); + + // Show the respective menu + controls.showMenuPanel.call(_this8, 'home'); + }, false); + + // Go back + on(backButton, 'click', function () { controls.showMenuPanel.call(_this8, 'home'); }); - pane.appendChild(back); + + // Add to pane + pane.appendChild(backButton); // Menu pane.appendChild(createElement('div', { @@ -2635,17 +2713,10 @@ typeof navigator === "object" && (function (global, factory) { inner.appendChild(pane); - menuItem.addEventListener('click', function () { - controls.showMenuPanel.call(_this8, type); - }); - _this8.elements.settings.buttons[type] = menuItem; _this8.elements.settings.panels[type] = pane; }); - home.appendChild(menu); - inner.appendChild(home); - popup.appendChild(inner); control.appendChild(popup); container.appendChild(control); @@ -4678,11 +4749,28 @@ typeof navigator === "object" && (function (global, factory) { // 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', function (event) { controls.toggleMenu.call(_this5.player, event); }); + // Settings menu - keyboard toggle + this.bind(this.player.elements.buttons.settings, 'keydown', function (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(_this5.player, event); + }, null, false); + // Set range input alternative "value", which matches the tooltip time (#954) this.bind(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) { var clientRect = _this5.player.elements.progress.getBoundingClientRect(); -- 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 --- dist/plyr.js | 474 +++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 279 insertions(+), 195 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 35daacd5..697dd12f 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -548,6 +548,12 @@ typeof navigator === "object" && (function (global, factory) { // Mirror Element.classList.toggle, with IE compatibility for "force" argument function toggleClass(element, className, force) { + if (is.nodeList(element)) { + return Array.from(element).map(function (e) { + return toggleClass(e, className, force); + }); + } + if (is.element(element)) { var method = 'toggle'; if (typeof force !== 'undefined') { @@ -558,7 +564,7 @@ typeof navigator === "object" && (function (global, factory) { return element.classList.contains(className); } - return null; + return false; } // Has class name @@ -589,19 +595,6 @@ typeof navigator === "object" && (function (global, factory) { return this.elements.container.querySelector(selector); } - // Get the focused element - function getFocusElement() { - var focused = document.activeElement; - - if (!focused || focused === document.body) { - focused = null; - } else { - focused = document.querySelector(':focus'); - } - - return focused; - } - // Trap focus inside container function trapFocus() { var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; @@ -622,7 +615,7 @@ typeof navigator === "object" && (function (global, factory) { } // Get the current focused element - var focused = getFocusElement(); + var focused = document.activeElement; if (focused === last && !event.shiftKey) { // Move focus to first element that can be tabbed if Shift isn't used @@ -1685,9 +1678,54 @@ typeof navigator === "object" && (function (global, factory) { }, + // Bind keyboard shortcuts for a menu item + bindMenuItemShortcuts: function bindMenuItemShortcuts(menuItem, type) { + var _this = this; + + // Handle space or -> to open menu + on(menuItem, 'keydown', function (event) { + // We only care about space and ⬆️ ⬇️️ ➡️ + if (![32, 38, 39, 40].includes(event.which)) { + return; + } + + // Prevent play / seek + event.preventDefault(); + event.stopPropagation(); + + var isRadioButton = matches(menuItem, '[role="menuitemradio"]'); + + // Show the respective menu + if (!isRadioButton && [32, 39].includes(event.which)) { + controls.showMenuPanel.call(_this, type); + } else { + var target = void 0; + + 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: function createMenuItem(_ref) { - var _this = this; + var _this2 = this; var value = _ref.value, list = _ref.list, @@ -1700,7 +1738,7 @@ typeof navigator === "object" && (function (global, factory) { var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); - var item = createElement('button', extend(attributes, { + var menuItem = createElement('button', extend(attributes, { type: 'button', role: 'menuitemradio', class: (this.config.classNames.control + ' ' + (attributes.class ? attributes.class : '')).trim(), @@ -1717,51 +1755,61 @@ typeof navigator === "object" && (function (global, factory) { flex.appendChild(badge); } - item.appendChild(flex); + menuItem.appendChild(flex); - Object.defineProperty(item, 'checked', { + // Replicate radio button behaviour + Object.defineProperty(menuItem, 'checked', { enumerable: true, get: function get$$1() { - return item.getAttribute('aria-checked') === 'true'; + return menuItem.getAttribute('aria-checked') === 'true'; }, set: function set$$1(checked) { // Ensure exclusivity if (checked) { - Array.from(item.parentNode.children).filter(function (node) { + Array.from(menuItem.parentNode.children).filter(function (node) { return matches(node, '[role="menuitemradio"]'); }).forEach(function (node) { return node.setAttribute('aria-checked', 'false'); }); } - item.setAttribute('aria-checked', checked ? 'true' : 'false'); + menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); } }); - this.listeners.bind(item, 'click', function () { - item.checked = true; + this.listeners.bind(menuItem, 'click keydown', function (event) { + if (event.type === 'keydown' && event.which !== 32) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + menuItem.checked = true; switch (type) { case 'language': - _this.currentTrack = Number(value); + _this2.currentTrack = Number(value); break; case 'quality': - _this.quality = value; + _this2.quality = value; break; case 'speed': - _this.speed = parseFloat(value); + _this2.speed = parseFloat(value); break; default: break; } - controls.showMenuPanel.call(_this, 'home'); - }, type); + controls.showMenuPanel.call(_this2, 'home'); + }, type, false); - list.appendChild(item); + controls.bindMenuItemShortcuts.call(this, menuItem, type); + + list.appendChild(menuItem); }, @@ -1834,7 +1882,7 @@ typeof navigator === "object" && (function (global, factory) { // Update elements updateProgress: function updateProgress(event) { - var _this2 = this; + var _this3 = this; if (!this.supported.ui || !is.event(event)) { return; @@ -1844,7 +1892,7 @@ typeof navigator === "object" && (function (global, factory) { var setProgress = function setProgress(target, input) { var value = is.number(input) ? input : 0; - var progress = is.element(target) ? target : _this2.elements.display.buffer; + var progress = is.element(target) ? target : _this3.elements.display.buffer; // Update value and label if (is.element(progress)) { @@ -1924,7 +1972,7 @@ typeof navigator === "object" && (function (global, factory) { // Update hover tooltip for seeking updateSeekTooltip: function updateSeekTooltip(event) { - var _this3 = this; + var _this4 = this; // Bail if setting not true if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) { @@ -1937,7 +1985,7 @@ typeof navigator === "object" && (function (global, factory) { var visible = this.config.classNames.tooltip + '--visible'; var toggle = function toggle(_toggle) { - toggleClass(_this3.elements.display.seekTooltip, visible, _toggle); + toggleClass(_this4.elements.display.seekTooltip, visible, _toggle); }; // Hide on touch @@ -2122,7 +2170,7 @@ typeof navigator === "object" && (function (global, factory) { // Set the quality menu setQualityMenu: function setQualityMenu(options) { - var _this4 = this; + var _this5 = this; // Menu required if (!is.element(this.elements.settings.panels.quality)) { @@ -2135,7 +2183,7 @@ typeof navigator === "object" && (function (global, factory) { // Set options if passed and filter based on uniqueness and config if (is.array(options)) { this.options.quality = dedupe(options).filter(function (quality) { - return _this4.config.quality.options.includes(quality); + return _this5.config.quality.options.includes(quality); }); } @@ -2156,25 +2204,25 @@ typeof navigator === "object" && (function (global, factory) { // Get the badge HTML for HD, 4K etc var getBadge = function getBadge(quality) { - var label = i18n.get('qualityBadge.' + quality, _this4.config); + var label = i18n.get('qualityBadge.' + quality, _this5.config); if (!label.length) { return null; } - return controls.createBadge.call(_this4, label); + return controls.createBadge.call(_this5, label); }; // Sort options by the config and then render options this.options.quality.sort(function (a, b) { - var sorting = _this4.config.quality.options; + var sorting = _this5.config.quality.options; return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; }).forEach(function (quality) { - controls.createMenuItem.call(_this4, { + controls.createMenuItem.call(_this5, { value: quality, list: list, type: type, - title: controls.getLabel.call(_this4, 'quality', quality), + title: controls.getLabel.call(_this5, 'quality', quality), badge: getBadge(quality) }); }); @@ -2224,7 +2272,7 @@ typeof navigator === "object" && (function (global, factory) { // Set a list of available captions languages setCaptionsMenu: function setCaptionsMenu() { - var _this5 = this; + var _this6 = this; // Menu required if (!is.element(this.elements.settings.panels.captions)) { @@ -2255,9 +2303,9 @@ typeof navigator === "object" && (function (global, factory) { var options = tracks.map(function (track, value) { return { value: value, - checked: _this5.captions.toggled && _this5.currentTrack === value, - title: captions.getLabel.call(_this5, track), - badge: track.language && controls.createBadge.call(_this5, track.language.toUpperCase()), + checked: _this6.captions.toggled && _this6.currentTrack === value, + title: captions.getLabel.call(_this6, track), + badge: track.language && controls.createBadge.call(_this6, track.language.toUpperCase()), list: list, type: 'language' }; @@ -2281,7 +2329,7 @@ typeof navigator === "object" && (function (global, factory) { // Set a list of available captions languages setSpeedMenu: function setSpeedMenu(options) { - var _this6 = this; + var _this7 = this; // Menu required if (!is.element(this.elements.settings.panels.speed)) { @@ -2300,7 +2348,7 @@ typeof navigator === "object" && (function (global, factory) { // Set options if passed and filter based on config this.options.speed = this.options.speed.filter(function (speed) { - return _this6.config.speed.options.includes(speed); + return _this7.config.speed.options.includes(speed); }); // Toggle the pane and tab @@ -2320,11 +2368,11 @@ typeof navigator === "object" && (function (global, factory) { // Create items this.options.speed.forEach(function (speed) { - controls.createMenuItem.call(_this6, { + controls.createMenuItem.call(_this7, { value: speed, list: list, type: type, - title: controls.getLabel.call(_this6, 'speed', speed) + title: controls.getLabel.call(_this7, 'speed', speed) }); }); @@ -2345,7 +2393,7 @@ typeof navigator === "object" && (function (global, factory) { // Show/hide menu - toggleMenu: function toggleMenu(event) { + toggleMenu: function toggleMenu(input) { var popup = this.elements.settings.popup; var button = this.elements.buttons.settings; @@ -2355,11 +2403,11 @@ typeof navigator === "object" && (function (global, factory) { return; } - var show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden'); + var show = is.boolean(input) ? input : is.element(popup) && popup.hasAttribute('hidden'); - if (is.event(event)) { - var isMenuItem = is.element(popup) && popup.contains(event.target); - var isButton = event.target === this.elements.buttons.settings; + if (is.event(input)) { + var isMenuItem = is.element(popup) && popup.contains(input.target); + var 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 @@ -2370,7 +2418,7 @@ typeof navigator === "object" && (function (global, factory) { // Prevent the toggle being caught by the doc listener if (isButton) { - event.stopPropagation(); + input.stopPropagation(); } } @@ -2384,19 +2432,13 @@ typeof navigator === "object" && (function (global, factory) { 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') { - var pane = Object.values(this.elements.settings.panels).find(function (pane) { - return !pane.hidden; - }); - var 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') { + var pane = Object.values(this.elements.settings.panels).find(function (pane) { + return !pane.hidden; + }); + var firstItem = pane.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, true); } } }, @@ -2428,7 +2470,7 @@ typeof navigator === "object" && (function (global, factory) { // Show a panel in the menu showMenuPanel: function showMenuPanel() { - var _this7 = this; + var _this8 = this; var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; @@ -2466,7 +2508,7 @@ typeof navigator === "object" && (function (global, factory) { container.style.height = ''; // Only listen once - off.call(_this7, container, transitionEndEvent, restore); + off.call(_this8, container, transitionEndEvent, restore); }; // Listen for the transition finishing and restore auto height/width @@ -2492,7 +2534,7 @@ typeof navigator === "object" && (function (global, factory) { // Build the default HTML // TODO: Set order based on order in the config.controls array? create: function create(data) { - var _this8 = this; + var _this9 = this; // Do nothing if we want no controls if (is.empty(this.config.controls)) { @@ -2627,41 +2669,31 @@ typeof navigator === "object" && (function (global, factory) { home.appendChild(menu); inner.appendChild(home); + this.elements.settings.panels.home = home; // Build the menu items this.config.settings.forEach(function (type) { - var menuItem = createElement('button', extend(getAttributesFromSelector(_this8.config.selectors.buttons.settings), { + // TODO: bundle this with the createMenuItem helper and bindings + var menuItem = createElement('button', extend(getAttributesFromSelector(_this9.config.selectors.buttons.settings), { type: 'button', - class: _this8.config.classNames.control + ' ' + _this8.config.classNames.control + '--forward', + class: _this9.config.classNames.control + ' ' + _this9.config.classNames.control + '--forward', role: 'menuitem', 'aria-haspopup': true, hidden: '' })); - // Handle space or -> to open menu - on(menuItem, 'keydown', function (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(_this8, type); - }, false); + // Bind menu shortcuts for keyboard users + controls.bindMenuItemShortcuts.call(_this9, menuItem, type); // Show menu on click on(menuItem, 'click', function () { - controls.showMenuPanel.call(_this8, type); + controls.showMenuPanel.call(_this9, type); }); - var flex = createElement('span', null, i18n.get(type, _this8.config)); + var flex = createElement('span', null, i18n.get(type, _this9.config)); var value = createElement('span', { - class: _this8.config.classNames.menu.value + class: _this9.config.classNames.menu.value }); // Speed contains HTML entities @@ -2680,21 +2712,21 @@ typeof navigator === "object" && (function (global, factory) { // Back button var backButton = createElement('button', { type: 'button', - class: _this8.config.classNames.control + ' ' + _this8.config.classNames.control + '--back' + class: _this9.config.classNames.control + ' ' + _this9.config.classNames.control + '--back' }); // Visible label backButton.appendChild(createElement('span', { 'aria-hidden': true - }, i18n.get(type, _this8.config))); + }, i18n.get(type, _this9.config))); // Screen reader label backButton.appendChild(createElement('span', { - class: _this8.config.classNames.hidden - }, i18n.get('menuBack', _this8.config))); + class: _this9.config.classNames.hidden + }, i18n.get('menuBack', _this9.config))); - // Handle space or -> to open menu - on(backButton, 'keydown', function (event) { + // Go back via keyboard + on(pane, 'keydown', function (event) { // We only care about <- if (event.which !== 37) { return; @@ -2705,12 +2737,12 @@ typeof navigator === "object" && (function (global, factory) { event.stopPropagation(); // Show the respective menu - controls.showMenuPanel.call(_this8, 'home'); + controls.showMenuPanel.call(_this9, 'home'); }, false); - // Go back + // Go back via button click on(backButton, 'click', function () { - controls.showMenuPanel.call(_this8, 'home'); + controls.showMenuPanel.call(_this9, 'home'); }); // Add to pane @@ -2723,8 +2755,8 @@ typeof navigator === "object" && (function (global, factory) { inner.appendChild(pane); - _this8.elements.settings.buttons[type] = menuItem; - _this8.elements.settings.panels[type] = pane; + _this9.elements.settings.buttons[type] = menuItem; + _this9.elements.settings.panels[type] = pane; }); popup.appendChild(inner); @@ -2769,7 +2801,7 @@ typeof navigator === "object" && (function (global, factory) { // Insert controls inject: function inject() { - var _this9 = this; + var _this10 = this; // Sprite if (this.config.loadSprite) { @@ -2881,8 +2913,8 @@ typeof navigator === "object" && (function (global, factory) { var labels = getElements.call(this, selector); Array.from(labels).forEach(function (label) { - toggleClass(label, _this9.config.classNames.hidden, false); - toggleClass(label, _this9.config.classNames.tooltip, true); + toggleClass(label, _this10.config.classNames.hidden, false); + toggleClass(label, _this10.config.classNames.tooltip, true); }); } } @@ -4272,9 +4304,12 @@ typeof navigator === "object" && (function (global, factory) { 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); } @@ -4310,17 +4345,27 @@ typeof navigator === "object" && (function (global, factory) { // Handle the key on keydown // Reset on keyup if (pressed) { - // Which keycodes should we prevent default - var 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/ - var focused = getFocusElement(); - if (is.element(focused) && focused !== this.player.elements.inputs.seek && matches(focused, this.player.config.selectors.editable)) { - return; + var focused = document.activeElement; + if (is.element(focused)) { + var editable = this.player.config.selectors.editable; + var seek = this.player.elements.inputs.seek; + + + if (focused !== seek && matches(focused, editable)) { + return; + } + + if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) { + return; + } } + // Which keycodes should we prevent default + var 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(); @@ -4441,6 +4486,55 @@ typeof navigator === "object" && (function (global, factory) { // Add touch class toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); } + }, { + key: 'setTabFocus', + value: function setTabFocus(event) { + var _this2 = this; + + 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 + var removeCurrent = function removeCurrent() { + var className = _this2.player.config.classNames.tabFocus; + var current = getElements.call(_this2.player, '.' + className); + toggleClass(current, className, false); + }; + + // Determine if a key was pressed to trigger this event + var 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(function () { + var focused = document.activeElement; + + // Ignore if current focus element isn't inside the player + if (!_this2.player.elements.container.contains(focused)) { + return; + } + + toggleClass(document.activeElement, _this2.player.config.classNames.tabFocus, true); + }, 10); + } // Global window & document listeners @@ -4459,6 +4553,9 @@ typeof navigator === "object" && (function (global, factory) { // 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 @@ -4466,34 +4563,16 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'container', value: function container() { - var _this2 = this; + var _this3 = this; // 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); } - // Detect tab focus - // Remove class on blur/focusout - on.call(this.player, this.player.elements.container, 'focusout', function (event) { - toggleClass(event.target, _this2.player.config.classNames.tabFocus, false); - }); - // Add classname to tabbed elements - on.call(this.player, this.player.elements.container, 'keydown', function (event) { - if (event.keyCode !== 9) { - return; - } - - // Delay the adding of classname until the focus has changed - // This event fires before the focusin event - setTimeout(function () { - toggleClass(getFocusElement(), _this2.player.config.classNames.tabFocus, true); - }, 0); - }); - // Toggle controls on mouse events and entering fullscreen on.call(this.player, this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) { - var controls$$1 = _this2.player.elements.controls; + var controls$$1 = _this3.player.elements.controls; // Remove button states for fullscreen @@ -4508,17 +4587,17 @@ typeof navigator === "object" && (function (global, factory) { var delay = 0; if (show) { - ui.toggleControls.call(_this2.player, true); + ui.toggleControls.call(_this3.player, true); // Use longer timeout for touch devices - delay = _this2.player.touch ? 3000 : 2000; + delay = _this3.player.touch ? 3000 : 2000; } // Clear timer - clearTimeout(_this2.player.timers.controls); + clearTimeout(_this3.player.timers.controls); // Set new timer to prevent flicker when seeking - _this2.player.timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this2.player, false); + _this3.player.timers.controls = setTimeout(function () { + return ui.toggleControls.call(_this3.player, false); }, delay); }); } @@ -4528,68 +4607,68 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'media', value: function media() { - var _this3 = this; + var _this4 = this; // Time change on media on.call(this.player, this.player.media, 'timeupdate seeking seeked', function (event) { - return controls.timeUpdate.call(_this3.player, event); + return controls.timeUpdate.call(_this4.player, event); }); // Display duration on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', function (event) { - return controls.durationUpdate.call(_this3.player, event); + return controls.durationUpdate.call(_this4.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', function () { - toggleHidden(_this3.player.elements.volume, !_this3.player.hasAudio); - toggleHidden(_this3.player.elements.buttons.mute, !_this3.player.hasAudio); + toggleHidden(_this4.player.elements.volume, !_this4.player.hasAudio); + toggleHidden(_this4.player.elements.buttons.mute, !_this4.player.hasAudio); }); // Handle the media finishing on.call(this.player, this.player.media, 'ended', function () { // Show poster on end - if (_this3.player.isHTML5 && _this3.player.isVideo && _this3.player.config.resetOnEnd) { + if (_this4.player.isHTML5 && _this4.player.isVideo && _this4.player.config.resetOnEnd) { // Restart - _this3.player.restart(); + _this4.player.restart(); } }); // Check for buffer progress on.call(this.player, this.player.media, 'progress playing seeking seeked', function (event) { - return controls.updateProgress.call(_this3.player, event); + return controls.updateProgress.call(_this4.player, event); }); // Handle volume changes on.call(this.player, this.player.media, 'volumechange', function (event) { - return controls.updateVolume.call(_this3.player, event); + return controls.updateVolume.call(_this4.player, event); }); // Handle play/pause on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', function (event) { - return ui.checkPlaying.call(_this3.player, event); + return ui.checkPlaying.call(_this4.player, event); }); // Loading state on.call(this.player, this.player.media, 'waiting canplay seeked playing', function (event) { - return ui.checkLoading.call(_this3.player, event); + return ui.checkLoading.call(_this4.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', function () { - if (!_this3.player.ads) { + if (!_this4.player.ads) { return; } // If ads are enabled, wait for them first - if (_this3.player.ads.enabled && !_this3.player.ads.initialized) { + if (_this4.player.ads.enabled && !_this4.player.ads.initialized) { // Wait for manager response - _this3.player.ads.managerPromise.then(function () { - return _this3.player.ads.play(); + _this4.player.ads.managerPromise.then(function () { + return _this4.player.ads.play(); }).catch(function () { - return _this3.player.play(); + return _this4.player.play(); }); } }); @@ -4607,17 +4686,17 @@ typeof navigator === "object" && (function (global, factory) { // On click play, pause ore restart on.call(this.player, wrapper, 'click', function () { // Touch devices will just show controls (if we're hiding controls) - if (_this3.player.config.hideControls && _this3.player.touch && !_this3.player.paused) { + if (_this4.player.config.hideControls && _this4.player.touch && !_this4.player.paused) { return; } - if (_this3.player.paused) { - _this3.player.play(); - } else if (_this3.player.ended) { - _this3.player.restart(); - _this3.player.play(); + if (_this4.player.paused) { + _this4.player.play(); + } else if (_this4.player.ended) { + _this4.player.restart(); + _this4.player.play(); } else { - _this3.player.pause(); + _this4.player.pause(); } }); } @@ -4632,33 +4711,37 @@ typeof navigator === "object" && (function (global, factory) { // Volume change on.call(this.player, this.player.media, 'volumechange', function () { // Save to storage - _this3.player.storage.set({ volume: _this3.player.volume, muted: _this3.player.muted }); + _this4.player.storage.set({ + volume: _this4.player.volume, + muted: _this4.player.muted + }); }); // Speed change on.call(this.player, this.player.media, 'ratechange', function () { // Update UI - controls.updateSetting.call(_this3.player, 'speed'); + controls.updateSetting.call(_this4.player, 'speed'); // Save to storage - _this3.player.storage.set({ speed: _this3.player.speed }); + _this4.player.storage.set({ speed: _this4.player.speed }); }); // Quality request on.call(this.player, this.player.media, 'qualityrequested', function (event) { // Save to storage - _this3.player.storage.set({ quality: event.detail.quality }); + _this4.player.storage.set({ quality: event.detail.quality }); }); // Quality change on.call(this.player, this.player.media, 'qualitychange', function (event) { // Update UI - controls.updateSetting.call(_this3.player, 'quality', null, event.detail.quality); + controls.updateSetting.call(_this4.player, 'quality', null, event.detail.quality); }); // Proxy events to container // Bubble up key events for Edge var proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' '); + on.call(this.player, this.player.media, proxyEvents, function (event) { var _event$detail = event.detail, detail = _event$detail === undefined ? {} : _event$detail; @@ -4666,10 +4749,10 @@ typeof navigator === "object" && (function (global, factory) { // Get error details from media if (event.type === 'error') { - detail = _this3.player.media.error; + detail = _this4.player.media.error; } - triggerEvent.call(_this3.player, _this3.player.elements.container, event.type, true, detail); + triggerEvent.call(_this4.player, _this4.player.elements.container, event.type, true, detail); }); } @@ -4698,7 +4781,7 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'bind', value: function bind(element, type, defaultHandler, customHandlerKey) { - var _this4 = this; + var _this5 = this; var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; @@ -4706,7 +4789,7 @@ typeof navigator === "object" && (function (global, factory) { var hasCustomHandler = is.function(customHandler); on.call(this.player, element, type, function (event) { - return _this4.proxy(event, defaultHandler, customHandlerKey); + return _this5.proxy(event, defaultHandler, customHandlerKey); }, passive && !hasCustomHandler); } @@ -4715,7 +4798,7 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'controls', value: function controls$$1() { - var _this5 = this; + var _this6 = this; // IE doesn't support input event, so we fallback to change var inputEvent = browser.isIE ? 'change' : 'input'; @@ -4723,7 +4806,7 @@ typeof navigator === "object" && (function (global, factory) { // Play/pause toggle if (this.player.elements.buttons.play) { Array.from(this.player.elements.buttons.play).forEach(function (button) { - _this5.bind(button, 'click', _this5.player.togglePlay, 'play'); + _this6.bind(button, 'click', _this6.player.togglePlay, 'play'); }); } @@ -4738,22 +4821,22 @@ typeof navigator === "object" && (function (global, factory) { // Mute toggle this.bind(this.player.elements.buttons.mute, 'click', function () { - _this5.player.muted = !_this5.player.muted; + _this6.player.muted = !_this6.player.muted; }, 'mute'); // Captions toggle this.bind(this.player.elements.buttons.captions, 'click', function () { - return _this5.player.toggleCaptions(); + return _this6.player.toggleCaptions(); }); // Fullscreen toggle this.bind(this.player.elements.buttons.fullscreen, 'click', function () { - _this5.player.fullscreen.toggle(); + _this6.player.fullscreen.toggle(); }, 'fullscreen'); // Picture-in-Picture this.bind(this.player.elements.buttons.pip, 'click', function () { - _this5.player.pip = 'toggle'; + _this6.player.pip = 'toggle'; }, 'pip'); // Airplay @@ -4761,7 +4844,7 @@ typeof navigator === "object" && (function (global, factory) { // Settings menu - click toggle this.bind(this.player.elements.buttons.settings, 'click', function (event) { - controls.toggleMenu.call(_this5.player, event); + controls.toggleMenu.call(_this6.player, event); }); // Settings menu - keyboard toggle @@ -4778,12 +4861,12 @@ typeof navigator === "object" && (function (global, factory) { event.stopPropagation(); // Toggle menu - controls.toggleMenu.call(_this5.player, event); + controls.toggleMenu.call(_this6.player, event); }, null, false); // Set range input alternative "value", which matches the tooltip time (#954) this.bind(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) { - var clientRect = _this5.player.elements.progress.getBoundingClientRect(); + var clientRect = _this6.player.elements.progress.getBoundingClientRect(); var percent = 100 / clientRect.width * (event.pageX - clientRect.left); event.currentTarget.setAttribute('seek-value', percent); }); @@ -4807,10 +4890,10 @@ typeof navigator === "object" && (function (global, factory) { // If we're done seeking and it was playing, resume playback if (play && done) { seek.removeAttribute('play-on-seeked'); - _this5.player.play(); - } else if (!done && _this5.player.playing) { + _this6.player.play(); + } else if (!done && _this6.player.playing) { seek.setAttribute('play-on-seeked', ''); - _this5.player.pause(); + _this6.player.pause(); } }); @@ -4827,7 +4910,7 @@ typeof navigator === "object" && (function (global, factory) { seek.removeAttribute('seek-value'); - _this5.player.currentTime = seekTo / seek.max * _this5.player.duration; + _this6.player.currentTime = seekTo / seek.max * _this6.player.duration; }, 'seek'); // Current time invert @@ -4835,48 +4918,48 @@ typeof navigator === "object" && (function (global, factory) { if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) { this.bind(this.player.elements.display.currentTime, 'click', function () { // Do nothing if we're at the start - if (_this5.player.currentTime === 0) { + if (_this6.player.currentTime === 0) { return; } - _this5.player.config.invertTime = !_this5.player.config.invertTime; + _this6.player.config.invertTime = !_this6.player.config.invertTime; - controls.timeUpdate.call(_this5.player); + controls.timeUpdate.call(_this6.player); }); } // Volume this.bind(this.player.elements.inputs.volume, inputEvent, function (event) { - _this5.player.volume = event.target.value; + _this6.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(function (element) { - _this5.bind(element, 'input', function (event) { - return controls.updateRangeFill.call(_this5.player, event.target); + _this6.bind(element, 'input', function (event) { + return controls.updateRangeFill.call(_this6.player, event.target); }); }); } // Seek tooltip this.bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { - return controls.updateSeekTooltip.call(_this5.player, event); + return controls.updateSeekTooltip.call(_this6.player, event); }); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) this.bind(this.player.elements.controls, 'mouseenter mouseleave', function (event) { - _this5.player.elements.controls.hover = !_this5.player.touch && event.type === 'mouseenter'; + _this6.player.elements.controls.hover = !_this6.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', function (event) { - _this5.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); + _this6.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); }); // Focus in/out on controls this.bind(this.player.elements.controls, 'focusin focusout', function (event) { - var _player = _this5.player, + var _player = _this6.player, config = _player.config, elements = _player.elements, timers = _player.timers; @@ -4886,7 +4969,7 @@ typeof navigator === "object" && (function (global, factory) { toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); // Toggle - ui.toggleControls.call(_this5.player, event.type === 'focusin'); + ui.toggleControls.call(_this6.player, event.type === 'focusin'); // If focusin, hide again after delay if (event.type === 'focusin') { @@ -4896,13 +4979,14 @@ typeof navigator === "object" && (function (global, factory) { }, 0); // Delay a little more for keyboard users - var delay = _this5.touch ? 3000 : 4000; + var delay = _this6.touch ? 3000 : 4000; // Clear timer clearTimeout(timers.controls); + // Hide timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this5.player, false); + return ui.toggleControls.call(_this6.player, false); }, delay); } }); @@ -4928,10 +5012,10 @@ typeof navigator === "object" && (function (global, factory) { var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2% - _this5.player.increaseVolume(direction / 50); + _this6.player.increaseVolume(direction / 50); // Don't break page scrolling at max and min - var volume = _this5.player.media.volume; + var volume = _this6.player.media.volume; if (direction === 1 && volume < 1 || direction === -1 && volume > 0) { event.preventDefault(); -- 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 --- dist/plyr.js | 383 ++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 210 insertions(+), 173 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index 413e2d2f..10f2d300 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -671,9 +671,13 @@ typeof navigator === "object" && (function (global, factory) { // Force repaint of element function repaint(element) { setTimeout(function () { - 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); } @@ -1697,7 +1701,7 @@ typeof navigator === "object" && (function (global, factory) { // Show the respective menu if (!isRadioButton && [32, 39].includes(event.which)) { - controls.showMenuPanel.call(_this, type); + controls.showMenuPanel.call(_this, type, true); } else { var target = void 0; @@ -1804,7 +1808,7 @@ typeof navigator === "object" && (function (global, factory) { break; } - controls.showMenuPanel.call(_this2, 'home'); + controls.showMenuPanel.call(_this2, 'home', event.type === 'keydown'); }, type, false); controls.bindMenuItemShortcuts.call(this, menuItem, type); @@ -2473,6 +2477,7 @@ typeof navigator === "object" && (function (global, factory) { var _this8 = this; var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var target = document.getElementById('plyr-settings-' + this.id + '-' + type); @@ -2527,7 +2532,7 @@ typeof navigator === "object" && (function (global, factory) { // Focus the first item var firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, true); + setFocus.call(this, firstItem, tabFocus); }, @@ -2687,7 +2692,7 @@ typeof navigator === "object" && (function (global, factory) { // Show menu on click on(menuItem, 'click', function () { - controls.showMenuPanel.call(_this9, type); + controls.showMenuPanel.call(_this9, type, false); }); var flex = createElement('span', null, i18n.get(type, _this9.config)); @@ -2737,12 +2742,12 @@ typeof navigator === "object" && (function (global, factory) { event.stopPropagation(); // Show the respective menu - controls.showMenuPanel.call(_this9, 'home'); + controls.showMenuPanel.call(_this9, 'home', true); }, false); // Go back via button click on(backButton, 'click', function () { - controls.showMenuPanel.call(_this9, 'home'); + controls.showMenuPanel.call(_this9, 'home', false); }); // Add to pane @@ -4319,7 +4324,7 @@ typeof navigator === "object" && (function (global, factory) { createClass(Listeners, [{ key: 'handleKey', value: function handleKey(event) { - var _this = this; + var player = this.player; var code = event.keyCode ? event.keyCode : event.which; var pressed = event.type === 'keydown'; @@ -4339,7 +4344,7 @@ typeof navigator === "object" && (function (global, factory) { // Seek by the number keys var seekByKey = function 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 @@ -4350,8 +4355,8 @@ typeof navigator === "object" && (function (global, factory) { // and any that accept key input http://webaim.org/techniques/keyboard/ var focused = document.activeElement; if (is.element(focused)) { - var editable = this.player.config.selectors.editable; - var seek = this.player.elements.inputs.seek; + var editable = player.config.selectors.editable; + var seek = player.elements.inputs.seek; if (focused !== seek && matches(focused, editable)) { @@ -4393,52 +4398,52 @@ typeof navigator === "object" && (function (global, factory) { 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: @@ -4457,8 +4462,8 @@ typeof navigator === "object" && (function (global, factory) { // 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) { - this.player.fullscreen.toggle(); + if (!player.fullscreen.enabled && player.fullscreen.active && code === 27) { + player.fullscreen.toggle(); } // Store last code for next cycle @@ -4481,15 +4486,19 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'firstTouch', value: function firstTouch() { - this.player.touch = true; + var player = this.player; + + + player.touch = true; // Add touch class - toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); + toggleClass(player.elements.container, player.config.classNames.isTouch, true); } }, { key: 'setTabFocus', value: function setTabFocus(event) { - var _this2 = this; + var player = this.player; + clearTimeout(this.focusTimer); @@ -4505,8 +4514,8 @@ typeof navigator === "object" && (function (global, factory) { // Remove current classes var removeCurrent = function removeCurrent() { - var className = _this2.player.config.classNames.tabFocus; - var current = getElements.call(_this2.player, '.' + className); + var className = player.config.classNames.tabFocus; + var current = getElements.call(player, '.' + className); toggleClass(current, className, false); }; @@ -4523,16 +4532,15 @@ typeof navigator === "object" && (function (global, factory) { // Delay the adding of classname until the focus has changed // This event fires before the focusin event - this.focusTimer = setTimeout(function () { var focused = document.activeElement; // Ignore if current focus element isn't inside the player - if (!_this2.player.elements.container.contains(focused)) { + if (!player.elements.container.contains(focused)) { return; } - toggleClass(document.activeElement, _this2.player.config.classNames.tabFocus, true); + toggleClass(document.activeElement, player.config.classNames.tabFocus, true); }, 10); } @@ -4542,20 +4550,22 @@ typeof navigator === "object" && (function (global, factory) { key: 'global', value: function global() { var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; + var player = this.player; // Keyboard shortcuts - if (this.player.config.keyboard.global) { - toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); + + if (player.config.keyboard.global) { + toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false); } // Click anywhere closes menu - toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle); + toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); // 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, 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 @@ -4563,16 +4573,17 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'container', value: function container() { - var _this3 = this; + var player = this.player; // 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 (!player.config.keyboard.global && player.config.keyboard.focused) { + on.call(player, player.elements.container, 'keydown keyup', this.handleKey, false); } // Toggle controls on mouse events and entering fullscreen - on.call(this.player, this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) { - var controls$$1 = _this3.player.elements.controls; + on.call(player, player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) { + var controls$$1 = player.elements.controls; // Remove button states for fullscreen @@ -4587,17 +4598,17 @@ typeof navigator === "object" && (function (global, factory) { var delay = 0; if (show) { - ui.toggleControls.call(_this3.player, true); + ui.toggleControls.call(player, true); // Use longer timeout for touch devices - delay = _this3.player.touch ? 3000 : 2000; + delay = player.touch ? 3000 : 2000; } // Clear timer - clearTimeout(_this3.player.timers.controls); + clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking - _this3.player.timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this3.player, false); + player.timers.controls = setTimeout(function () { + return ui.toggleControls.call(player, false); }, delay); }); } @@ -4607,76 +4618,77 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'media', value: function media() { - var _this4 = this; + var player = this.player; // Time change on media - on.call(this.player, this.player.media, 'timeupdate seeking seeked', function (event) { - return controls.timeUpdate.call(_this4.player, event); + + on.call(player, player.media, 'timeupdate seeking seeked', function (event) { + return controls.timeUpdate.call(player, event); }); // Display duration - on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', function (event) { - return controls.durationUpdate.call(_this4.player, event); + on.call(player, player.media, 'durationchange loadeddata loadedmetadata', function (event) { + return 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', function () { - toggleHidden(_this4.player.elements.volume, !_this4.player.hasAudio); - toggleHidden(_this4.player.elements.buttons.mute, !_this4.player.hasAudio); + on.call(player, player.media, 'canplay', function () { + 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', function () { + on.call(player, player.media, 'ended', function () { // Show poster on end - if (_this4.player.isHTML5 && _this4.player.isVideo && _this4.player.config.resetOnEnd) { + if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { // Restart - _this4.player.restart(); + player.restart(); } }); // Check for buffer progress - on.call(this.player, this.player.media, 'progress playing seeking seeked', function (event) { - return controls.updateProgress.call(_this4.player, event); + on.call(player, player.media, 'progress playing seeking seeked', function (event) { + return controls.updateProgress.call(player, event); }); // Handle volume changes - on.call(this.player, this.player.media, 'volumechange', function (event) { - return controls.updateVolume.call(_this4.player, event); + on.call(player, player.media, 'volumechange', function (event) { + return controls.updateVolume.call(player, event); }); // Handle play/pause - on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', function (event) { - return ui.checkPlaying.call(_this4.player, event); + on.call(player, player.media, 'playing play pause ended emptied timeupdate', function (event) { + return ui.checkPlaying.call(player, event); }); // Loading state - on.call(this.player, this.player.media, 'waiting canplay seeked playing', function (event) { - return ui.checkLoading.call(_this4.player, event); + on.call(player, player.media, 'waiting canplay seeked playing', function (event) { + return 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', function () { - if (!_this4.player.ads) { + on.call(player, player.media, 'playing', function () { + if (!player.ads) { return; } // If ads are enabled, wait for them first - if (_this4.player.ads.enabled && !_this4.player.ads.initialized) { + if (player.ads.enabled && !player.ads.initialized) { // Wait for manager response - _this4.player.ads.managerPromise.then(function () { - return _this4.player.ads.play(); + player.ads.managerPromise.then(function () { + return player.ads.play(); }).catch(function () { - return _this4.player.play(); + return player.play(); }); } }); // Click video - if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { + if (player.supported.ui && player.config.clickToPlay && !player.isAudio) { // Re-fetch the wrapper - var wrapper = getElement.call(this.player, '.' + this.player.config.classNames.video); + var wrapper = getElement.call(player, '.' + player.config.classNames.video); // Bail if there's no wrapper (this should never happen) if (!is.element(wrapper)) { @@ -4684,75 +4696,81 @@ typeof navigator === "object" && (function (global, factory) { } // On click play, pause ore restart - on.call(this.player, wrapper, 'click', function () { - // Touch devices will just show controls (if we're hiding controls) - if (_this4.player.config.hideControls && _this4.player.touch && !_this4.player.paused) { + on.call(player, player.elements.container, 'click touchstart', function (event) { + var 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; + } + + // 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 (_this4.player.paused) { - _this4.player.play(); - } else if (_this4.player.ended) { - _this4.player.restart(); - _this4.player.play(); + if (player.ended) { + player.restart(); + player.play(); } else { - _this4.player.pause(); + player.togglePlay(); } }); } // Disable right click - if (this.player.supported.ui && this.player.config.disableContextMenu) { - on.call(this.player, this.player.elements.wrapper, 'contextmenu', function (event) { + if (player.supported.ui && player.config.disableContextMenu) { + on.call(player, player.elements.wrapper, 'contextmenu', function (event) { event.preventDefault(); }, false); } // Volume change - on.call(this.player, this.player.media, 'volumechange', function () { + on.call(player, player.media, 'volumechange', function () { // Save to storage - _this4.player.storage.set({ - volume: _this4.player.volume, - muted: _this4.player.muted + player.storage.set({ + volume: player.volume, + muted: player.muted }); }); // Speed change - on.call(this.player, this.player.media, 'ratechange', function () { + on.call(player, player.media, 'ratechange', function () { // Update UI - controls.updateSetting.call(_this4.player, 'speed'); + controls.updateSetting.call(player, 'speed'); // Save to storage - _this4.player.storage.set({ speed: _this4.player.speed }); + player.storage.set({ speed: player.speed }); }); // Quality request - on.call(this.player, this.player.media, 'qualityrequested', function (event) { + on.call(player, player.media, 'qualityrequested', function (event) { // Save to storage - _this4.player.storage.set({ quality: event.detail.quality }); + player.storage.set({ quality: event.detail.quality }); }); // Quality change - on.call(this.player, this.player.media, 'qualitychange', function (event) { + on.call(player, player.media, 'qualitychange', function (event) { // Update UI - controls.updateSetting.call(_this4.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 - var proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' '); + var proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); - on.call(this.player, this.player.media, proxyEvents, function (event) { + on.call(player, player.media, proxyEvents, function (event) { var _event$detail = event.detail, detail = _event$detail === undefined ? {} : _event$detail; // Get error details from media if (event.type === 'error') { - detail = _this4.player.media.error; + detail = player.media.error; } - triggerEvent.call(_this4.player, _this4.player.elements.container, event.type, true, detail); + triggerEvent.call(player, player.elements.container, event.type, true, detail); }); } @@ -4761,18 +4779,20 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'proxy', value: function proxy(event, defaultHandler, customHandlerKey) { - var customHandler = this.player.config.listeners[customHandlerKey]; + var player = this.player; + + var customHandler = player.config.listeners[customHandlerKey]; var hasCustomHandler = is.function(customHandler); var 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); } } @@ -4781,15 +4801,16 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'bind', value: function bind(element, type, defaultHandler, customHandlerKey) { - var _this5 = this; + var _this = this; var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + var player = this.player; - var customHandler = this.player.config.listeners[customHandlerKey]; + var customHandler = player.config.listeners[customHandlerKey]; var hasCustomHandler = is.function(customHandler); - on.call(this.player, element, type, function (event) { - return _this5.proxy(event, defaultHandler, customHandlerKey); + on.call(player, element, type, function (event) { + return _this.proxy(event, defaultHandler, customHandlerKey); }, passive && !hasCustomHandler); } @@ -4798,57 +4819,60 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'controls', value: function controls$$1() { - var _this6 = this; + var _this2 = this; + + var player = this.player; // IE doesn't support input event, so we fallback to change + var inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle - if (this.player.elements.buttons.play) { - Array.from(this.player.elements.buttons.play).forEach(function (button) { - _this6.bind(button, 'click', _this6.player.togglePlay, 'play'); + if (player.elements.buttons.play) { + Array.from(player.elements.buttons.play).forEach(function (button) { + _this2.bind(button, 'click', player.togglePlay, 'play'); }); } // Pause - this.bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); + this.bind(player.elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind - this.bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); + this.bind(player.elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind - this.bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); + this.bind(player.elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle - this.bind(this.player.elements.buttons.mute, 'click', function () { - _this6.player.muted = !_this6.player.muted; + this.bind(player.elements.buttons.mute, 'click', function () { + player.muted = !player.muted; }, 'mute'); // Captions toggle - this.bind(this.player.elements.buttons.captions, 'click', function () { - return _this6.player.toggleCaptions(); + this.bind(player.elements.buttons.captions, 'click', function () { + return player.toggleCaptions(); }); // Fullscreen toggle - this.bind(this.player.elements.buttons.fullscreen, 'click', function () { - _this6.player.fullscreen.toggle(); + this.bind(player.elements.buttons.fullscreen, 'click', function () { + player.fullscreen.toggle(); }, 'fullscreen'); // Picture-in-Picture - this.bind(this.player.elements.buttons.pip, 'click', function () { - _this6.player.pip = 'toggle'; + this.bind(player.elements.buttons.pip, 'click', function () { + player.pip = 'toggle'; }, 'pip'); // Airplay - this.bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); + this.bind(player.elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle - this.bind(this.player.elements.buttons.settings, 'click', function (event) { - controls.toggleMenu.call(_this6.player, event); + this.bind(player.elements.buttons.settings, 'click', function (event) { + controls.toggleMenu.call(player, event); }); // Settings menu - keyboard toggle - this.bind(this.player.elements.buttons.settings, 'keydown', function (event) { + this.bind(player.elements.buttons.settings, 'keydown', function (event) { // We only care about space if (event.which !== 32) { return; @@ -4861,44 +4885,44 @@ typeof navigator === "object" && (function (global, factory) { event.stopPropagation(); // Toggle menu - controls.toggleMenu.call(_this6.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', function (event) { - var clientRect = _this6.player.elements.progress.getBoundingClientRect(); - var percent = 100 / clientRect.width * (event.pageX - clientRect.left); + this.bind(player.elements.inputs.seek, 'mousedown mousemove', function (event) { + var rect = player.elements.progress.getBoundingClientRect(); + var percent = 100 / rect.width * (event.pageX - rect.left); event.currentTarget.setAttribute('seek-value', percent); }); // Pause while seeking - this.bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) { + this.bind(player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) { var seek = event.currentTarget; - var code = event.keyCode ? event.keyCode : event.which; var eventType = event.type; + var attribute = 'play-on-seeked'; if ((eventType === 'keydown' || eventType === 'keyup') && code !== 39 && code !== 37) { return; } // Was playing before? - var play = seek.hasAttribute('play-on-seeked'); + var play = seek.hasAttribute(attribute); // Done seeking var 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'); - _this6.player.play(); - } else if (!done && _this6.player.playing) { - seek.setAttribute('play-on-seeked', ''); - _this6.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, inputEvent, function (event) { + this.bind(player.elements.inputs.seek, inputEvent, function (event) { var seek = event.currentTarget; // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) @@ -4910,89 +4934,95 @@ typeof navigator === "object" && (function (global, factory) { seek.removeAttribute('seek-value'); - _this6.player.currentTime = seekTo / seek.max * _this6.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'); // 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)) { - this.bind(this.player.elements.display.currentTime, 'click', function () { + if (player.config.toggleInvert && !is.element(player.elements.display.duration)) { + this.bind(player.elements.display.currentTime, 'click', function () { // Do nothing if we're at the start - if (_this6.player.currentTime === 0) { + if (player.currentTime === 0) { return; } - _this6.player.config.invertTime = !_this6.player.config.invertTime; + player.config.invertTime = !player.config.invertTime; - controls.timeUpdate.call(_this6.player); + controls.timeUpdate.call(player); }); } // Volume - this.bind(this.player.elements.inputs.volume, inputEvent, function (event) { - _this6.player.volume = event.target.value; + this.bind(player.elements.inputs.volume, inputEvent, function (event) { + 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(function (element) { - _this6.bind(element, 'input', function (event) { - return controls.updateRangeFill.call(_this6.player, event.target); + Array.from(getElements.call(player, 'input[type="range"]')).forEach(function (element) { + _this2.bind(element, 'input', function (event) { + return controls.updateRangeFill.call(player, event.target); }); }); } // Seek tooltip - this.bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { - return controls.updateSeekTooltip.call(_this6.player, event); + this.bind(player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { + return 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', function (event) { - _this6.player.elements.controls.hover = !_this6.player.touch && event.type === 'mouseenter'; + this.bind(player.elements.controls, 'mouseenter mouseleave', function (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, 'mousedown mouseup touchstart touchend touchcancel', function (event) { - _this6.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); + this.bind(player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { + player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); }); // Focus in/out on controls - this.bind(this.player.elements.controls, 'focusin focusout', function (event) { - var _player = _this6.player, - config = _player.config, - elements = _player.elements, - timers = _player.timers; + this.bind(player.elements.controls, 'focusin focusout', function (event) { + var config = player.config, + elements = player.elements, + timers = player.timers; - // Skip transition to prevent focus from scrolling the parent element + var isFocusIn = event.type === 'focusin'; - toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); + // Skip transition to prevent focus from scrolling the parent element + toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); // Toggle - ui.toggleControls.call(_this6.player, event.type === 'focusin'); + ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay - if (event.type === 'focusin') { + if (isFocusIn) { // Restore transition setTimeout(function () { toggleClass(elements.controls, config.classNames.noTransition, false); }, 0); // Delay a little more for keyboard users - var delay = _this6.touch ? 3000 : 4000; + var delay = _this2.touch ? 3000 : 4000; // Clear timer clearTimeout(timers.controls); // Hide timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this6.player, false); + return ui.toggleControls.call(player, false); }, delay); } }); // Mouse wheel for volume - this.bind(this.player.elements.inputs.volume, 'wheel', function (event) { + this.bind(player.elements.inputs.volume, 'wheel', function (event) { // Detect "natural" scroll - suppored on OS X Safari only // Other browsers on OS X will be inverted until support improves var inverted = event.webkitDirectionInvertedFromDevice; @@ -5012,10 +5042,10 @@ typeof navigator === "object" && (function (global, factory) { var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2% - _this6.player.increaseVolume(direction / 50); + player.increaseVolume(direction / 50); // Don't break page scrolling at max and min - var volume = _this6.player.media.volume; + var volume = player.media.volume; if (direction === 1 && volume < 1 || direction === -1 && volume > 0) { event.preventDefault(); @@ -7310,7 +7340,7 @@ typeof navigator === "object" && (function (global, factory) { 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')) { @@ -7382,7 +7412,9 @@ typeof navigator === "object" && (function (global, factory) { 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) { @@ -7588,13 +7620,16 @@ typeof navigator === "object" && (function (global, factory) { if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { controls.toggleMenu.call(this, false); } + // Trigger event on change if (hiding !== isHidden) { var eventName = hiding ? 'controlshidden' : 'controlsshown'; triggerEvent.call(this, this.media, eventName); } + return !hiding; } + return false; } @@ -8084,7 +8119,9 @@ typeof navigator === "object" && (function (global, factory) { } // Trigger request event - triggerEvent.call(this, this.media, 'qualityrequested', false, { quality: quality }); + triggerEvent.call(this, this.media, 'qualityrequested', false, { + quality: quality + }); // Update config config.selected = quality; -- 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 --- dist/plyr.js | 8409 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 8409 insertions(+) create mode 100644 dist/plyr.js (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js new file mode 100644 index 00000000..bf61dad3 --- /dev/null +++ b/dist/plyr.js @@ -0,0 +1,8409 @@ +typeof navigator === "object" && (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define('Plyr', factory) : + (global.Plyr = factory()); +}(this, (function () { 'use strict'; + + // ========================================================================== + // Type checking utils + // ========================================================================== + + var getConstructor = function getConstructor(input) { + return input !== null && typeof input !== 'undefined' ? input.constructor : null; + }; + var instanceOf = function instanceOf(input, constructor) { + return Boolean(input && constructor && input instanceof constructor); + }; + var isNullOrUndefined = function isNullOrUndefined(input) { + return input === null || typeof input === 'undefined'; + }; + var isObject = function isObject(input) { + return getConstructor(input) === Object; + }; + var isNumber = function isNumber(input) { + return getConstructor(input) === Number && !Number.isNaN(input); + }; + var isString = function isString(input) { + return getConstructor(input) === String; + }; + var isBoolean = function isBoolean(input) { + return getConstructor(input) === Boolean; + }; + var isFunction = function isFunction(input) { + return getConstructor(input) === Function; + }; + var isArray = function isArray(input) { + return Array.isArray(input); + }; + var isWeakMap = function isWeakMap(input) { + return instanceOf(input, WeakMap); + }; + var isNodeList = function isNodeList(input) { + return instanceOf(input, NodeList); + }; + var isElement = function isElement(input) { + return instanceOf(input, Element); + }; + var isTextNode = function isTextNode(input) { + return getConstructor(input) === Text; + }; + var isEvent = function isEvent(input) { + return instanceOf(input, Event); + }; + var isCue = function isCue(input) { + return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); + }; + var isTrack = function isTrack(input) { + return instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind); + }; + + var isEmpty = function isEmpty(input) { + return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length; + }; + + var isUrl = function isUrl(input) { + // Accept a URL object + if (instanceOf(input, window.URL)) { + return true; + } + + // Add the protocol if required + var string = input; + if (!input.startsWith('http://') || !input.startsWith('https://')) { + string = 'http://' + input; + } + + try { + return !isEmpty(new URL(string).hostname); + } catch (e) { + return false; + } + }; + + var is = { + nullOrUndefined: isNullOrUndefined, + object: isObject, + number: isNumber, + string: isString, + boolean: isBoolean, + function: isFunction, + array: isArray, + weakMap: isWeakMap, + nodeList: isNodeList, + element: isElement, + textNode: isTextNode, + event: isEvent, + cue: isCue, + track: isTrack, + url: isUrl, + empty: isEmpty + }; + + // ========================================================================== + + // Check for passive event listener support + // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md + // https://www.youtube.com/watch?v=NPM6172J22g + var supportsPassiveListeners = function () { + // Test via a getter in the options object to see if the passive property is accessed + var supported = false; + try { + var options = Object.defineProperty({}, 'passive', { + get: function get() { + supported = true; + return null; + } + }); + window.addEventListener('test', null, options); + window.removeEventListener('test', null, options); + } catch (e) { + // Do nothing + } + + return supported; + }(); + + // Toggle event listener + function toggleListener(element, event, callback) { + var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + + var _this = this; + + var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false; + + // Bail if no element, event, or callback + if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { + return; + } + + // Allow multiple events + var events = event.split(' '); + + // Build options + // Default to just the capture boolean for browsers with no passive listener support + var options = capture; + + // If passive events listeners are supported + if (supportsPassiveListeners) { + options = { + // Whether the listener can be passive (i.e. default never prevented) + passive: passive, + // Whether the listener is a capturing listener or not + capture: capture + }; + } + + // If a single node is passed, bind the event listener + events.forEach(function (type) { + if (_this && _this.eventListeners && toggle) { + // Cache event listener + _this.eventListeners.push({ element: element, type: type, callback: callback, options: options }); + } + + element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); + }); + } + + // Bind event handler + function on(element) { + var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + var callback = arguments[2]; + var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; + var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + + toggleListener.call(this, element, events, callback, true, passive, capture); + } + + // Unbind event handler + function off(element) { + var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + var callback = arguments[2]; + var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; + var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + + toggleListener.call(this, element, events, callback, false, passive, capture); + } + + // Bind once-only event handler + function once(element) { + var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + var callback = arguments[2]; + var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; + var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + + function onceCallback() { + off(element, events, onceCallback, passive, capture); + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + callback.apply(this, args); + } + + toggleListener.call(this, element, events, onceCallback, true, passive, capture); + } + + // Trigger event + function triggerEvent(element) { + var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + var bubbles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + var detail = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + // Bail if no element + if (!is.element(element) || is.empty(type)) { + return; + } + + // Create and dispatch the event + var event = new CustomEvent(type, { + bubbles: bubbles, + detail: Object.assign({}, detail, { + plyr: this + }) + }); + + // Dispatch the event + element.dispatchEvent(event); + } + + // Unbind all cached event listeners + function unbindListeners() { + if (this && this.eventListeners) { + this.eventListeners.forEach(function (item) { + var element = item.element, + type = item.type, + callback = item.callback, + options = item.options; + + element.removeEventListener(type, callback, options); + }); + + this.eventListeners = []; + } + } + + // Run method when / if player is ready + function ready() { + var _this2 = this; + + return new Promise(function (resolve) { + return _this2.ready ? setTimeout(resolve, 0) : on.call(_this2, _this2.elements.container, 'ready', resolve); + }).then(function () {}); + } + + var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + + var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + var defineProperty = function (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + }; + + var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; + }(); + + var toConsumableArray = function (arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } else { + return Array.from(arr); + } + }; + + // ========================================================================== + + // Wrap an element + function wrap(elements, wrapper) { + // Convert `elements` to an array, if necessary. + var targets = elements.length ? elements : [elements]; + + // Loops backwards to prevent having to clone the wrapper on the + // first element (see `child` below). + Array.from(targets).reverse().forEach(function (element, index) { + var child = index > 0 ? wrapper.cloneNode(true) : wrapper; + + // Cache the current parent and sibling. + var parent = element.parentNode; + var sibling = element.nextSibling; + + // Wrap the element (is automatically removed from its current + // parent). + child.appendChild(element); + + // If the element had a sibling, insert the wrapper before + // the sibling to maintain the HTML structure; otherwise, just + // append it to the parent. + if (sibling) { + parent.insertBefore(child, sibling); + } else { + parent.appendChild(child); + } + }); + } + + // Set attributes + function setAttributes(element, attributes) { + if (!is.element(element) || is.empty(attributes)) { + return; + } + + // Assume null and undefined attributes should be left out, + // Setting them would otherwise convert them to "null" and "undefined" + Object.entries(attributes).filter(function (_ref) { + var _ref2 = slicedToArray(_ref, 2), + value = _ref2[1]; + + return !is.nullOrUndefined(value); + }).forEach(function (_ref3) { + var _ref4 = slicedToArray(_ref3, 2), + key = _ref4[0], + value = _ref4[1]; + + return element.setAttribute(key, value); + }); + } + + // Create a DocumentFragment + function createElement(type, attributes, text) { + // Create a new + var element = document.createElement(type); + + // Set all passed attributes + if (is.object(attributes)) { + setAttributes(element, attributes); + } + + // Add text node + if (is.string(text)) { + element.innerText = text; + } + + // Return built element + return element; + } + + // Inaert an element after another + function insertAfter(element, target) { + if (!is.element(element) || !is.element(target)) { + return; + } + + target.parentNode.insertBefore(element, target.nextSibling); + } + + // Insert a DocumentFragment + function insertElement(type, parent, attributes, text) { + if (!is.element(parent)) { + return; + } + + parent.appendChild(createElement(type, attributes, text)); + } + + // Remove element(s) + function removeElement(element) { + if (is.nodeList(element) || is.array(element)) { + Array.from(element).forEach(removeElement); + return; + } + + if (!is.element(element) || !is.element(element.parentNode)) { + return; + } + + element.parentNode.removeChild(element); + } + + // Remove all child elements + function emptyElement(element) { + if (!is.element(element)) { + return; + } + + var length = element.childNodes.length; + + + while (length > 0) { + element.removeChild(element.lastChild); + length -= 1; + } + } + + // Replace element + function replaceElement(newChild, oldChild) { + if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { + return null; + } + + oldChild.parentNode.replaceChild(newChild, oldChild); + + return newChild; + } + + // Get an attribute object from a string selector + function getAttributesFromSelector(sel, existingAttributes) { + // For example: + // '.test' to { class: 'test' } + // '#test' to { id: 'test' } + // '[data-test="test"]' to { 'data-test': 'test' } + + if (!is.string(sel) || is.empty(sel)) { + return {}; + } + + var attributes = {}; + var existing = existingAttributes; + + sel.split(',').forEach(function (s) { + // Remove whitespace + var selector = s.trim(); + var className = selector.replace('.', ''); + var stripped = selector.replace(/[[\]]/g, ''); + + // Get the parts and value + var parts = stripped.split('='); + var key = parts[0]; + var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; + + // Get the first character + var start = selector.charAt(0); + + switch (start) { + case '.': + // Add to existing classname + if (is.object(existing) && is.string(existing.class)) { + existing.class += ' ' + className; + } + + attributes.class = className; + break; + + case '#': + // ID selector + attributes.id = selector.replace('#', ''); + break; + + case '[': + // Attribute selector + attributes[key] = value; + + break; + + default: + break; + } + }); + + return attributes; + } + + // Toggle hidden + function toggleHidden(element, hidden) { + if (!is.element(element)) { + return; + } + + var hide = hidden; + + if (!is.boolean(hide)) { + hide = !element.hasAttribute('hidden'); + } + + if (hide) { + element.setAttribute('hidden', ''); + } else { + element.removeAttribute('hidden'); + } + } + + // Mirror Element.classList.toggle, with IE compatibility for "force" argument + function toggleClass(element, className, force) { + if (is.nodeList(element)) { + return Array.from(element).map(function (e) { + return toggleClass(e, className, force); + }); + } + + if (is.element(element)) { + var method = 'toggle'; + if (typeof force !== 'undefined') { + method = force ? 'add' : 'remove'; + } + + element.classList[method](className); + return element.classList.contains(className); + } + + return false; + } + + // Has class name + function hasClass(element, className) { + return is.element(element) && element.classList.contains(className); + } + + // Element matches selector + function matches(element, selector) { + var prototype = { Element: Element }; + + function match() { + return Array.from(document.querySelectorAll(selector)).includes(this); + } + + var matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + + return matches.call(element, selector); + } + + // Find all elements + function getElements(selector) { + return this.elements.container.querySelectorAll(selector); + } + + // Find a single element + function getElement(selector) { + return this.elements.container.querySelector(selector); + } + + // Trap focus inside container + function trapFocus() { + var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + if (!is.element(element)) { + return; + } + + var focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); + var first = focusable[0]; + var last = focusable[focusable.length - 1]; + + var trap = function trap(event) { + // Bail if not tab key or not fullscreen + if (event.key !== 'Tab' || event.keyCode !== 9) { + return; + } + + // Get the current focused element + var focused = document.activeElement; + + if (focused === last && !event.shiftKey) { + // Move focus to first element that can be tabbed if Shift isn't used + first.focus(); + event.preventDefault(); + } else if (focused === first && event.shiftKey) { + // Move focus to last element that can be tabbed if Shift is used + last.focus(); + event.preventDefault(); + } + }; + + toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); + } + + // Set focus and tab focus class + function setFocus() { + var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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); + } + } + + // ========================================================================== + + var transitionEndEvent = function () { + var element = document.createElement('span'); + + var events = { + WebkitTransition: 'webkitTransitionEnd', + MozTransition: 'transitionend', + OTransition: 'oTransitionEnd otransitionend', + transition: 'transitionend' + }; + + var type = Object.keys(events).find(function (event) { + return element.style[event] !== undefined; + }); + + return is.string(type) ? events[type] : false; + }(); + + // Force repaint of element + function repaint(element) { + setTimeout(function () { + try { + toggleHidden(element, true); + element.offsetHeight; // eslint-disable-line + toggleHidden(element, false); + } catch (e) { + // Do nothing + } + }, 0); + } + + // ========================================================================== + // Browser sniffing + // Unfortunately, due to mixed support, UA sniffing is required + // ========================================================================== + + var browser = { + isIE: /* @cc_on!@ */!!document.documentMode, + isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent), + isIPhone: /(iPhone|iPod)/gi.test(navigator.platform), + isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform) + }; + + // ========================================================================== + + // Default codecs for checking mimetype support + var defaultCodecs = { + 'audio/ogg': 'vorbis', + 'audio/wav': '1', + 'video/webm': 'vp8, vorbis', + 'video/mp4': 'avc1.42E01E, mp4a.40.2', + 'video/ogg': 'theora' + }; + + // Check for feature support + var support = { + // Basic support + audio: 'canPlayType' in document.createElement('audio'), + video: 'canPlayType' in document.createElement('video'), + + // Check for support + // Basic functionality vs full UI + check: function check(type, provider, playsinline) { + var canPlayInline = browser.isIPhone && playsinline && support.playsinline; + var api = support[type] || provider !== 'html5'; + var ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); + + return { + api: api, + ui: ui + }; + }, + + + // Picture-in-picture support + // Safari only currently + pip: function () { + return !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode); + }(), + + // Airplay support + // Safari only currently + airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent), + + // Inline playback support + // https://webkit.org/blog/6784/new-video-policies-for-ios/ + playsinline: 'playsInline' in document.createElement('video'), + + // Check for mime type support against a player instance + // Credits: http://diveintohtml5.info/everything.html + // Related: http://www.leanbackplayer.com/test/h5mt.html + mime: function mime(inputType) { + var _inputType$split = inputType.split('/'), + _inputType$split2 = slicedToArray(_inputType$split, 1), + mediaType = _inputType$split2[0]; + + if (!this.isHTML5 || mediaType !== this.type) { + return false; + } + + var type = void 0; + if (inputType && inputType.includes('codecs=')) { + // Use input directly + type = inputType; + } else if (inputType === 'audio/mpeg') { + // Skip codec + type = 'audio/mpeg;'; + } else if (inputType in defaultCodecs) { + // Use codec + type = inputType + '; codecs="' + defaultCodecs[inputType] + '"'; + } + + try { + return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); + } catch (err) { + return false; + } + }, + + + // Check for textTracks support + textTracks: 'textTracks' in document.createElement('video'), + + // Sliders + rangeInput: function () { + var range = document.createElement('input'); + range.type = 'range'; + return range.type === 'range'; + }(), + + // Touch + // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event + touch: 'ontouchstart' in document.documentElement, + + // Detect transitions support + transitions: transitionEndEvent !== false, + + // Reduced motion iOS & MacOS setting + // https://webkit.org/blog/7551/responsive-design-for-motion/ + reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches + }; + + // ========================================================================== + + var html5 = { + getSources: function getSources() { + var _this = this; + + if (!this.isHTML5) { + return []; + } + + var sources = Array.from(this.media.querySelectorAll('source')); + + // Filter out unsupported sources + return sources.filter(function (source) { + return support.mime.call(_this, source.getAttribute('type')); + }); + }, + + + // Get quality levels + getQualityOptions: function getQualityOptions() { + // Get sizes from elements + return html5.getSources.call(this).map(function (source) { + return Number(source.getAttribute('size')); + }).filter(Boolean); + }, + extend: function extend() { + if (!this.isHTML5) { + return; + } + + var player = this; + + // Quality + Object.defineProperty(player.media, 'quality', { + get: function get() { + // Get sources + var sources = html5.getSources.call(player); + var source = sources.find(function (source) { + return source.getAttribute('src') === player.source; + }); + + // Return size, if match is found + return source && Number(source.getAttribute('size')); + }, + set: function set(input) { + // Get sources + var sources = html5.getSources.call(player); + + // Get first match for requested size + var source = sources.find(function (source) { + return Number(source.getAttribute('size')) === input; + }); + + // No matching source found + if (!source) { + return; + } + + // Get current state + var _player$media = player.media, + currentTime = _player$media.currentTime, + paused = _player$media.paused, + preload = _player$media.preload, + readyState = _player$media.readyState; + + // Set new source + + player.media.src = source.getAttribute('src'); + + // Prevent loading if preload="none" and the current source isn't loaded (#1044) + if (preload !== 'none' || readyState) { + // Restore time + player.once('loadedmetadata', function () { + player.currentTime = currentTime; + + // Resume playing + if (!paused) { + player.play(); + } + }); + + // Load new source + player.media.load(); + } + + // Trigger change event + triggerEvent.call(player, player.media, 'qualitychange', false, { + quality: input + }); + } + }); + }, + + + // Cancel current network requests + // See https://github.com/sampotts/plyr/issues/174 + cancelRequests: function cancelRequests() { + if (!this.isHTML5) { + return; + } + + // Remove child sources + removeElement(html5.getSources.call(this)); + + // Set blank video src attribute + // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error + // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection + this.media.setAttribute('src', this.config.blankVideo); + + // Load the new empty source + // This will cancel existing requests + // See https://github.com/sampotts/plyr/issues/174 + this.media.load(); + + // Debugging + this.debug.log('Cancelled network requests'); + } + }; + + // ========================================================================== + + // Clone nested objects + function cloneDeep(object) { + return JSON.parse(JSON.stringify(object)); + } + + // Get a nested value in an object + function getDeep(object, path) { + return path.split('.').reduce(function (obj, key) { + return obj && obj[key]; + }, object); + } + + // Deep extend destination object with N more objects + function extend() { + var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + sources[_key - 1] = arguments[_key]; + } + + if (!sources.length) { + return target; + } + + var source = sources.shift(); + + if (!is.object(source)) { + return target; + } + + Object.keys(source).forEach(function (key) { + if (is.object(source[key])) { + if (!Object.keys(target).includes(key)) { + Object.assign(target, defineProperty({}, key, {})); + } + + extend(target[key], source[key]); + } else { + Object.assign(target, defineProperty({}, key, source[key])); + } + }); + + return extend.apply(undefined, [target].concat(sources)); + } + + // ========================================================================== + + // Generate a random ID + function generateId(prefix) { + return prefix + '-' + Math.floor(Math.random() * 10000); + } + + // Format string + function format(input) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + if (is.empty(input)) { + return input; + } + + return input.toString().replace(/{(\d+)}/g, function (match, i) { + return args[i].toString(); + }); + } + + // Get percentage + function getPercentage(current, max) { + if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { + return 0; + } + + return (current / max * 100).toFixed(2); + } + + // Replace all occurances of a string in a string + function replaceAll() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; + + return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); + } + + // Convert to title case + function toTitleCase() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + + return input.toString().replace(/\w\S*/g, function (text) { + return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); + }); + } + + // Convert string to pascalCase + function toPascalCase() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + + var string = input.toString(); + + // Convert kebab case + string = replaceAll(string, '-', ' '); + + // Convert snake case + string = replaceAll(string, '_', ' '); + + // Convert to title case + string = toTitleCase(string); + + // Convert to pascal case + return replaceAll(string, ' ', ''); + } + + // Convert string to pascalCase + function toCamelCase() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + + var string = input.toString(); + + // Convert to pascal case + string = toPascalCase(string); + + // Convert first character to lowercase + return string.charAt(0).toLowerCase() + string.slice(1); + } + + // Remove HTML from a string + function stripHTML(source) { + var fragment = document.createDocumentFragment(); + var element = document.createElement('div'); + fragment.appendChild(element); + element.innerHTML = source; + return fragment.firstChild.innerText; + } + + // Like outerHTML, but also works for DocumentFragment + function getHTML(element) { + var wrapper = document.createElement('div'); + wrapper.appendChild(element); + return wrapper.innerHTML; + } + + // ========================================================================== + + var i18n = { + get: function get$$1() { + var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + if (is.empty(key) || is.empty(config)) { + return ''; + } + + var string = getDeep(config.i18n, key); + + if (is.empty(string)) { + return ''; + } + + var replace = { + '{seektime}': config.seekTime, + '{title}': config.title + }; + + Object.entries(replace).forEach(function (_ref) { + var _ref2 = slicedToArray(_ref, 2), + key = _ref2[0], + value = _ref2[1]; + + string = replaceAll(string, key, value); + }); + + return string; + } + }; + + // ========================================================================== + + // Remove duplicates in an array + function dedupe(array) { + if (!is.array(array)) { + return array; + } + + return array.filter(function (item, index) { + return array.indexOf(item) === index; + }); + } + + // Get the closest value in an array + function closest(array, value) { + if (!is.array(array) || !array.length) { + return null; + } + + return array.reduce(function (prev, curr) { + return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; + }); + } + + // ========================================================================== + + var Storage = function () { + function Storage(player) { + classCallCheck(this, Storage); + + this.enabled = player.config.storage.enabled; + this.key = player.config.storage.key; + } + + // Check for actual support (see if we can use it) + + + createClass(Storage, [{ + key: 'get', + value: function get$$1(key) { + if (!Storage.supported || !this.enabled) { + return null; + } + + var store = window.localStorage.getItem(this.key); + + if (is.empty(store)) { + return null; + } + + var json = JSON.parse(store); + + return is.string(key) && key.length ? json[key] : json; + } + }, { + key: 'set', + value: function set$$1(object) { + // Bail if we don't have localStorage support or it's disabled + if (!Storage.supported || !this.enabled) { + return; + } + + // Can only store objectst + if (!is.object(object)) { + return; + } + + // Get current storage + var storage = this.get(); + + // Default to empty object + if (is.empty(storage)) { + storage = {}; + } + + // Update the working copy of the values + extend(storage, object); + + // Update storage + window.localStorage.setItem(this.key, JSON.stringify(storage)); + } + }], [{ + key: 'supported', + get: function get$$1() { + try { + if (!('localStorage' in window)) { + return false; + } + + var test = '___test'; + + // Try to use it (it might be disabled, e.g. user is in private mode) + // see: https://github.com/sampotts/plyr/issues/131 + window.localStorage.setItem(test, test); + window.localStorage.removeItem(test); + + return true; + } catch (e) { + return false; + } + } + }]); + return Storage; + }(); + + // ========================================================================== + // Fetch wrapper + // Using XHR to avoid issues with older browsers + // ========================================================================== + + function fetch(url) { + var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text'; + + return new Promise(function (resolve, reject) { + try { + var request = new XMLHttpRequest(); + + // Check for CORS support + if (!('withCredentials' in request)) { + return; + } + + request.addEventListener('load', function () { + if (responseType === 'text') { + try { + resolve(JSON.parse(request.responseText)); + } catch (e) { + resolve(request.responseText); + } + } else { + resolve(request.response); + } + }); + + request.addEventListener('error', function () { + throw new Error(request.status); + }); + + request.open('GET', url, true); + + // Set the required response type + request.responseType = responseType; + + request.send(); + } catch (e) { + reject(e); + } + }); + } + + // ========================================================================== + + // Load an external SVG sprite + function loadSprite(url, id) { + if (!is.string(url)) { + return; + } + + var prefix = 'cache'; + var hasId = is.string(id); + var isCached = false; + + var exists = function exists() { + return document.getElementById(id) !== null; + }; + + var update = function update(container, data) { + container.innerHTML = data; + + // Check again incase of race condition + if (hasId && exists()) { + return; + } + + // Inject the SVG to the body + document.body.insertAdjacentElement('afterbegin', container); + }; + + // Only load once if ID set + if (!hasId || !exists()) { + var useStorage = Storage.supported; + + // Create container + var container = document.createElement('div'); + container.setAttribute('hidden', ''); + + if (hasId) { + container.setAttribute('id', id); + } + + // Check in cache + if (useStorage) { + var cached = window.localStorage.getItem(prefix + '-' + id); + isCached = cached !== null; + + if (isCached) { + var data = JSON.parse(cached); + update(container, data.content); + } + } + + // Get the sprite + fetch(url).then(function (result) { + if (is.empty(result)) { + return; + } + + if (useStorage) { + window.localStorage.setItem(prefix + '-' + id, JSON.stringify({ + content: result + })); + } + + update(container, result); + }).catch(function () {}); + } + } + + // ========================================================================== + + // Time helpers + var getHours = function getHours(value) { + return parseInt(value / 60 / 60 % 60, 10); + }; + var getMinutes = function getMinutes(value) { + return parseInt(value / 60 % 60, 10); + }; + var getSeconds = function getSeconds(value) { + return parseInt(value % 60, 10); + }; + + // Format time to UI friendly string + function formatTime() { + var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var displayHours = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + // Bail if the value isn't a number + if (!is.number(time)) { + return formatTime(null, displayHours, inverted); + } + + // Format time component to add leading zero + var format = function format(value) { + return ('0' + value).slice(-2); + }; + + // Breakdown to hours, mins, secs + var hours = getHours(time); + var mins = getMinutes(time); + var secs = getSeconds(time); + + // Do we need to display hours? + if (displayHours || hours > 0) { + hours = hours + ':'; + } else { + hours = ''; + } + + // Render + return '' + (inverted && time > 0 ? '-' : '') + hours + format(mins) + ':' + format(secs); + } + + // ========================================================================== + + // TODO: Don't export a massive object - break down and create class + var controls = { + // Get icon URL + getIconUrl: function getIconUrl() { + var url = new URL(this.config.iconUrl, window.location); + var cors = url.host !== window.location.host || browser.isIE && !window.svg4everybody; + + return { + url: this.config.iconUrl, + cors: cors + }; + }, + + + // Find the UI controls + findElements: function findElements() { + try { + 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), + 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) + }; + + // 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) + }; + + // 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) + }; + + // Seek tooltip + if (is.element(this.elements.progress)) { + this.elements.display.seekTooltip = this.elements.progress.querySelector('.' + this.config.classNames.tooltip); + } + + return true; + } catch (error) { + // Log it + this.debug.warn('It looks like there is a problem with your custom controls HTML', error); + + // Restore native video controls + this.toggleNativeControls(true); + + return false; + } + }, + + + // Create icon + createIcon: function createIcon(type, attributes) { + var namespace = 'http://www.w3.org/2000/svg'; + var iconUrl = controls.getIconUrl.call(this); + var iconPath = (!iconUrl.cors ? iconUrl.url : '') + '#' + this.config.iconPrefix; + + // Create + var icon = document.createElementNS(namespace, 'svg'); + setAttributes(icon, extend(attributes, { + role: 'presentation', + focusable: 'false' + })); + + // Create the to reference sprite + var use = document.createElementNS(namespace, 'use'); + var path = iconPath + '-' + type; + + // Set `href` attributes + // https://github.com/sampotts/plyr/issues/460 + // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href + 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); + } + + // Add to + icon.appendChild(use); + + return icon; + }, + + + // Create hidden text label + createLabel: function createLabel(type) { + var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + // Skip i18n for abbreviations and brand names + var universals = { + pip: 'PIP', + airplay: 'AirPlay' + }; + var text = universals[type] || i18n.get(type, this.config); + + var attributes = Object.assign({}, attr, { + class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') + }); + return createElement('span', attributes, text); + }, + + + // Create a badge + createBadge: function createBadge(text) { + if (is.empty(text)) { + return null; + } + + var badge = createElement('span', { + class: this.config.classNames.menu.value + }); + + badge.appendChild(createElement('span', { + class: this.config.classNames.menu.badge + }, text)); + + return badge; + }, + + + // Create a
to hide the standard controls and UI + setAspectRatio: function setAspectRatio(input) { + var _split = (is.string(input) ? input : this.config.ratio).split(':'), + _split2 = slicedToArray(_split, 2), + x = _split2[0], + y = _split2[1]; + + var padding = 100 / x * y; + this.elements.wrapper.style.paddingBottom = padding + '%'; + + if (this.supported.ui) { + var height = 240; + var offset = (height - padding) / (height / 50); + + this.media.style.transform = 'translateY(-' + offset + '%)'; + } + }, + + + // API Ready + ready: function ready$$1() { + var _this2 = this; + + var player = this; + + // Get Vimeo params for the iframe + var options = { + loop: player.config.loop.active, + autoplay: player.autoplay, + // muted: player.muted, + byline: false, + portrait: false, + title: false, + speed: true, + transparent: 0, + gesture: 'media', + playsinline: !this.config.fullscreen.iosNative + }; + var params = buildUrlParams(options); + + // Get the source URL or ID + var source = player.media.getAttribute('src'); + + // Get from
if needed + if (is.empty(source)) { + source = player.media.getAttribute(player.config.attributes.embed.id); + } + + var id = parseId(source); + + // Build an iframe + var iframe = createElement('iframe'); + var src = format(player.config.urls.vimeo.iframe, id, params); + iframe.setAttribute('src', src); + iframe.setAttribute('allowfullscreen', ''); + iframe.setAttribute('allowtransparency', ''); + iframe.setAttribute('allow', 'autoplay'); + + // Get poster, if already set + var poster = player.poster; + + // Inject the package + + var wrapper = createElement('div', { poster: poster, class: player.config.classNames.embedContainer }); + wrapper.appendChild(iframe); + player.media = replaceElement(wrapper, player.media); + + // Get poster image + fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) { + if (is.empty(response)) { + return; + } + + // Get the URL for thumbnail + var url = new URL(response[0].thumbnail_large); + + // Get original image + url.pathname = url.pathname.split('_')[0] + '.jpg'; + + // Set and show poster + ui.setPoster.call(player, url.href).catch(function () {}); + }); + + // Setup instance + // https://github.com/vimeo/player.js + player.embed = new window.Vimeo.Player(iframe, { + autopause: player.config.autopause, + muted: player.muted + }); + + player.media.paused = true; + player.media.currentTime = 0; + + // Disable native text track rendering + if (player.supported.ui) { + player.embed.disableTextTrack(); + } + + // Create a faux HTML5 API using the Vimeo API + player.media.play = function () { + assurePlaybackState.call(player, true); + return player.embed.play(); + }; + + player.media.pause = function () { + assurePlaybackState.call(player, false); + return player.embed.pause(); + }; + + player.media.stop = function () { + player.pause(); + player.currentTime = 0; + }; + + // Seeking + var currentTime = player.media.currentTime; + + Object.defineProperty(player.media, 'currentTime', { + get: function get$$1() { + return currentTime; + }, + set: function set$$1(time) { + // Vimeo will automatically play on seek if the video hasn't been played before + + // Get current paused state and volume etc + var embed = player.embed, + media = player.media, + paused = player.paused, + volume = player.volume; + + var restorePause = paused && !embed.hasPlayed; + + // Set seeking state and trigger event + media.seeking = true; + triggerEvent.call(player, media, 'seeking'); + + // If paused, mute until seek is complete + Promise.resolve(restorePause && embed.setVolume(0)) + // Seek + .then(function () { + return embed.setCurrentTime(time); + }) + // Restore paused + .then(function () { + return restorePause && embed.pause(); + }) + // Restore volume + .then(function () { + return restorePause && embed.setVolume(volume); + }).catch(function () { + // Do nothing + }); + } + }); + + // Playback speed + var speed = player.config.speed.selected; + Object.defineProperty(player.media, 'playbackRate', { + get: function get$$1() { + return speed; + }, + set: function set$$1(input) { + player.embed.setPlaybackRate(input).then(function () { + speed = input; + triggerEvent.call(player, player.media, 'ratechange'); + }).catch(function (error) { + // Hide menu item (and menu if empty) + if (error.name === 'Error') { + controls.setSpeedMenu.call(player, []); + } + }); + } + }); + + // Volume + var volume = player.config.volume; + + Object.defineProperty(player.media, 'volume', { + get: function get$$1() { + return volume; + }, + set: function set$$1(input) { + player.embed.setVolume(input).then(function () { + volume = input; + triggerEvent.call(player, player.media, 'volumechange'); + }); + } + }); + + // Muted + var muted = player.config.muted; + + Object.defineProperty(player.media, 'muted', { + get: function get$$1() { + return muted; + }, + set: function set$$1(input) { + var toggle = is.boolean(input) ? input : false; + + player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () { + muted = toggle; + triggerEvent.call(player, player.media, 'volumechange'); + }); + } + }); + + // Loop + var loop = player.config.loop; + + Object.defineProperty(player.media, 'loop', { + get: function get$$1() { + return loop; + }, + set: function set$$1(input) { + var toggle = is.boolean(input) ? input : player.config.loop.active; + + player.embed.setLoop(toggle).then(function () { + loop = toggle; + }); + } + }); + + // Source + var currentSrc = void 0; + player.embed.getVideoUrl().then(function (value) { + currentSrc = value; + }).catch(function (error) { + _this2.debug.warn(error); + }); + + Object.defineProperty(player.media, 'currentSrc', { + get: function get$$1() { + return currentSrc; + } + }); + + // Ended + Object.defineProperty(player.media, 'ended', { + get: function get$$1() { + return player.currentTime === player.duration; + } + }); + + // Set aspect ratio based on video size + Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) { + var ratio = getAspectRatio(dimensions[0], dimensions[1]); + vimeo.setAspectRatio.call(_this2, ratio); + }); + + // Set autopause + player.embed.setAutopause(player.config.autopause).then(function (state) { + player.config.autopause = state; + }); + + // Get title + player.embed.getVideoTitle().then(function (title) { + player.config.title = title; + ui.setTitle.call(_this2); + }); + + // Get current time + player.embed.getCurrentTime().then(function (value) { + currentTime = value; + triggerEvent.call(player, player.media, 'timeupdate'); + }); + + // Get duration + player.embed.getDuration().then(function (value) { + player.media.duration = value; + triggerEvent.call(player, player.media, 'durationchange'); + }); + + // Get captions + player.embed.getTextTracks().then(function (tracks) { + player.media.textTracks = tracks; + captions.setup.call(player); + }); + + player.embed.on('cuechange', function (_ref) { + var _ref$cues = _ref.cues, + cues = _ref$cues === undefined ? [] : _ref$cues; + + var strippedCues = cues.map(function (cue) { + return stripHTML(cue.text); + }); + captions.updateCues.call(player, strippedCues); + }); + + player.embed.on('loaded', function () { + // Assure state and events are updated on autoplay + player.embed.getPaused().then(function (paused) { + assurePlaybackState.call(player, !paused); + if (!paused) { + triggerEvent.call(player, player.media, 'playing'); + } + }); + + if (is.element(player.embed.element) && player.supported.ui) { + var frame = player.embed.element; + + // Fix keyboard focus issues + // https://github.com/sampotts/plyr/issues/317 + frame.setAttribute('tabindex', -1); + } + }); + + player.embed.on('play', function () { + assurePlaybackState.call(player, true); + triggerEvent.call(player, player.media, 'playing'); + }); + + player.embed.on('pause', function () { + assurePlaybackState.call(player, false); + }); + + player.embed.on('timeupdate', function (data) { + player.media.seeking = false; + currentTime = data.seconds; + triggerEvent.call(player, player.media, 'timeupdate'); + }); + + player.embed.on('progress', function (data) { + player.media.buffered = data.percent; + triggerEvent.call(player, player.media, 'progress'); + + // Check all loaded + if (parseInt(data.percent, 10) === 1) { + triggerEvent.call(player, player.media, 'canplaythrough'); + } + + // Get duration as if we do it before load, it gives an incorrect value + // https://github.com/sampotts/plyr/issues/891 + player.embed.getDuration().then(function (value) { + if (value !== player.media.duration) { + player.media.duration = value; + triggerEvent.call(player, player.media, 'durationchange'); + } + }); + }); + + player.embed.on('seeked', function () { + player.media.seeking = false; + triggerEvent.call(player, player.media, 'seeked'); + }); + + player.embed.on('ended', function () { + player.media.paused = true; + triggerEvent.call(player, player.media, 'ended'); + }); + + player.embed.on('error', function (detail) { + player.media.error = detail; + triggerEvent.call(player, player.media, 'error'); + }); + + // Rebuild UI + setTimeout(function () { + return ui.build.call(player); + }, 0); + } + }; + + // ========================================================================== + + // Parse YouTube ID from URL + function parseId$1(url) { + if (is.empty(url)) { + return null; + } + + var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; + } + + // Standardise YouTube quality unit + function mapQualityUnit(input) { + var qualities = { + hd2160: 2160, + hd1440: 1440, + hd1080: 1080, + hd720: 720, + large: 480, + medium: 360, + small: 240, + tiny: 144 + }; + + var entry = Object.entries(qualities).find(function (entry) { + return entry.includes(input); + }); + + if (entry) { + // Get the match corresponding to the input + return entry.find(function (value) { + return value !== input; + }); + } + + return 'default'; + } + + function mapQualityUnits(levels) { + if (is.empty(levels)) { + return levels; + } + + return dedupe(levels.map(function (level) { + return mapQualityUnit(level); + })); + } + + // Set playback state and trigger change (only on actual change) + function assurePlaybackState$1(play) { + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } + if (this.media.paused === play) { + this.media.paused = !play; + triggerEvent.call(this, this.media, play ? 'play' : 'pause'); + } + } + + var youtube = { + setup: function setup() { + var _this = this; + + // Add embed class for responsive + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + + // Set aspect ratio + youtube.setAspectRatio.call(this); + + // Setup API + if (is.object(window.YT) && is.function(window.YT.Player)) { + youtube.ready.call(this); + } else { + // Load the API + loadScript(this.config.urls.youtube.sdk).catch(function (error) { + _this.debug.warn('YouTube API failed to load', error); + }); + + // Setup callback for the API + // YouTube has it's own system of course... + window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; + + // Add to queue + window.onYouTubeReadyCallbacks.push(function () { + youtube.ready.call(_this); + }); + + // Set callback to process queue + window.onYouTubeIframeAPIReady = function () { + window.onYouTubeReadyCallbacks.forEach(function (callback) { + callback(); + }); + }; + } + }, + + + // Get the media title + getTitle: function getTitle(videoId) { + var _this2 = this; + + // Try via undocumented API method first + // This method disappears now and then though... + // https://github.com/sampotts/plyr/issues/709 + if (is.function(this.embed.getVideoData)) { + var _embed$getVideoData = this.embed.getVideoData(), + title = _embed$getVideoData.title; + + if (is.empty(title)) { + this.config.title = title; + ui.setTitle.call(this); + return; + } + } + + // Or via Google API + var key = this.config.keys.google; + if (is.string(key) && !is.empty(key)) { + var url = format(this.config.urls.youtube.api, videoId, key); + + fetch(url).then(function (result) { + if (is.object(result)) { + _this2.config.title = result.items[0].snippet.title; + ui.setTitle.call(_this2); + } + }).catch(function () {}); + } + }, + + + // Set aspect ratio + setAspectRatio: function setAspectRatio() { + var ratio = this.config.ratio.split(':'); + this.elements.wrapper.style.paddingBottom = 100 / ratio[0] * ratio[1] + '%'; + }, + + + // API ready + ready: function ready$$1() { + var player = this; + + // Ignore already setup (race condition) + var currentId = player.media.getAttribute('id'); + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { + return; + } + + // Get the source URL or ID + var source = player.media.getAttribute('src'); + + // Get from
if needed + if (is.empty(source)) { + source = player.media.getAttribute(this.config.attributes.embed.id); + } + + // Replace the