diff options
Diffstat (limited to 'dist/plyr.js')
-rw-r--r-- | dist/plyr.js | 1267 |
1 files changed, 753 insertions, 514 deletions
diff --git a/dist/plyr.js b/dist/plyr.js index 197c8b06..f66f7bda 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -414,12 +414,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 <element> + if (!is.element(parent)) { + return; + } + parent.appendChild(createElement(type, attributes, text)); } @@ -439,6 +446,10 @@ typeof navigator === "object" && (function (global, factory) { // Remove all child elements function emptyElement(element) { + if (!is.element(element)) { + return; + } + var length = element.childNodes.length; @@ -537,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') { @@ -547,7 +564,7 @@ typeof navigator === "object" && (function (global, factory) { return element.classList.contains(className); } - return null; + return false; } // Has class name @@ -578,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; @@ -611,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 @@ -627,6 +631,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 () { @@ -649,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); } @@ -1539,12 +1565,20 @@ typeof navigator === "object" && (function (global, factory) { // Setup toggle icon and labels if (toggle) { // Icon - button.appendChild(controls.createIcon.call(this, iconPressed, { class: 'icon--pressed' })); - button.appendChild(controls.createIcon.call(this, icon, { class: 'icon--not-pressed' })); + button.appendChild(controls.createIcon.call(this, iconPressed, { + class: 'icon--pressed' + })); + button.appendChild(controls.createIcon.call(this, icon, { + class: 'icon--not-pressed' + })); // Label/Tooltip - button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' })); - button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' })); + button.appendChild(controls.createLabel.call(this, labelPressed, { + class: 'label--pressed' + })); + button.appendChild(controls.createLabel.call(this, label, { + class: 'label--not-pressed' + })); } else { button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createLabel.call(this, label)); @@ -1645,7 +1679,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'); @@ -1656,8 +1690,62 @@ typeof navigator === "object" && (function (global, factory) { }, + // Bind keyboard shortcuts for a menu item + // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus + // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 + bindMenuItemShortcuts: function bindMenuItemShortcuts(menuItem, type) { + var _this = this; + + // Handle space or -> to open menu + on(menuItem, 'keydown keyup', function (event) { + // We only care about space and ⬆️ ⬇️️ ➡️ + if (![32, 38, 39, 40].includes(event.which)) { + return; + } + + // Prevent play / seek + event.preventDefault(); + event.stopPropagation(); + + // We're just here to prevent the keydown bubbling + if (event.type === 'keydown') { + return; + } + + var isRadioButton = matches(menuItem, '[role="menuitemradio"]'); + + // Show the respective menu + if (!isRadioButton && [32, 39].includes(event.which)) { + controls.showMenuPanel.call(_this, type, true); + } 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 _this2 = this; + var value = _ref.value, list = _ref.list, type = _ref.type, @@ -1667,32 +1755,80 @@ typeof navigator === "object" && (function (global, factory) { _ref$checked = _ref.checked, checked = _ref$checked === undefined ? false : _ref$checked; - var item = createElement('li'); + var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); - var label = createElement('label', { - class: this.config.classNames.control - }); - - var radio = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { - type: 'radio', - name: 'plyr-' + type, - value: value, - checked: checked, - class: 'plyr__sr-only' + var menuItem = createElement('button', extend(attributes, { + type: 'button', + role: 'menuitemradio', + class: (this.config.classNames.control + ' ' + (attributes.class ? attributes.class : '')).trim(), + 'aria-checked': checked, + value: value })); - var faux = createElement('span', { hidden: '' }); + var flex = createElement('span'); - label.appendChild(radio); - label.appendChild(faux); - label.insertAdjacentHTML('beforeend', title); + // We have to set as HTML incase of special characters + flex.innerHTML = title; if (is.element(badge)) { - label.appendChild(badge); + flex.appendChild(badge); } - item.appendChild(label); - list.appendChild(item); + menuItem.appendChild(flex); + + // Replicate radio button behaviour + Object.defineProperty(menuItem, 'checked', { + enumerable: true, + get: function get$$1() { + return menuItem.getAttribute('aria-checked') === 'true'; + }, + set: function set$$1(checked) { + // Ensure exclusivity + if (checked) { + Array.from(menuItem.parentNode.children).filter(function (node) { + return matches(node, '[role="menuitemradio"]'); + }).forEach(function (node) { + return node.setAttribute('aria-checked', 'false'); + }); + } + + menuItem.setAttribute('aria-checked', checked ? 'true' : 'false'); + } + }); + + this.listeners.bind(menuItem, 'click keyup', function (event) { + if (event.type === 'keyup' && event.which !== 32) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + menuItem.checked = true; + + switch (type) { + case 'language': + _this2.currentTrack = Number(value); + break; + + case 'quality': + _this2.quality = value; + break; + + case 'speed': + _this2.speed = parseFloat(value); + break; + + default: + break; + } + + controls.showMenuPanel.call(_this2, 'home', event.type === 'keyup'); + }, type, false); + + controls.bindMenuItemShortcuts.call(this, menuItem, type); + + list.appendChild(menuItem); }, @@ -1765,7 +1901,7 @@ typeof navigator === "object" && (function (global, factory) { // Update <progress> elements updateProgress: function updateProgress(event) { - var _this = this; + var _this3 = this; if (!this.supported.ui || !is.event(event)) { return; @@ -1775,7 +1911,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 : _this3.elements.display.buffer; // Update value and label if (is.element(progress)) { @@ -1855,7 +1991,7 @@ typeof navigator === "object" && (function (global, factory) { // Update hover tooltip for seeking updateSeekTooltip: function updateSeekTooltip(event) { - var _this2 = 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) { @@ -1868,7 +2004,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(_this4.elements.display.seekTooltip, visible, _toggle); }; // Hide on touch @@ -1966,71 +2102,60 @@ 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); }, - // 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.panes.quality)) { - return; - } + if (setting === 'captions') { + value = this.currentTrack; + } else { + value = !is.empty(input) ? input : this[setting]; - var type = 'quality'; - var list = this.elements.settings.panes.quality.querySelector('ul'); + // 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 - var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; - controls.toggleTab.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; + } }, @@ -2062,72 +2187,83 @@ typeof navigator === "object" && (function (global, factory) { }, - // Update the selected setting - updateSetting: function updateSetting(setting, container, input) { - var pane = this.elements.settings.panes[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 _this5 = 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 _this5.config.quality.options.includes(quality); + }); } - // Get the list if we need to - if (!is.element(list)) { - list = pane && pane.querySelector('ul'); - } + // 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.tabs[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, _this5.config); - // Find the radio option and check it - var target = list && list.querySelector('input[value="' + value + '"]'); + if (!label.length) { + return null; + } - if (is.element(target)) { - target.checked = true; - } + return controls.createBadge.call(_this5, label); + }; + + // Sort options by the config and then render options + this.options.quality.sort(function (a, b) { + var sorting = _this5.config.quality.options; + return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; + }).forEach(function (quality) { + controls.createMenuItem.call(_this5, { + value: quality, + list: list, + type: type, + title: controls.getLabel.call(_this5, 'quality', quality), + badge: getBadge(quality) + }); + }); + + controls.updateSetting.call(this, type, list); }, // Set the looping options /* setLoopMenu() { // Menu required - if (!is.element(this.elements.settings.panes.loop)) { + if (!is.element(this.elements.settings.panels.loop)) { return; } const options = ['start', 'end', 'all', 'reset']; - const list = this.elements.settings.panes.loop.querySelector('ul'); + const list = this.elements.settings.panels.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.panels.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 => { @@ -2155,15 +2291,21 @@ typeof navigator === "object" && (function (global, factory) { // Set a list of available captions languages setCaptionsMenu: function setCaptionsMenu() { - var _this4 = this; + var _this6 = 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.panes.captions.querySelector('ul'); + 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.toggleTab.call(this, type, tracks.length); + controls.toggleMenuButton.call(this, type, toggle); // Empty the menu emptyElement(list); @@ -2172,7 +2314,7 @@ typeof navigator === "object" && (function (global, factory) { controls.checkMenu.call(this); // If there's no captions, bail - if (!tracks.length) { + if (!toggle) { return; } @@ -2180,9 +2322,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: _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' }; @@ -2206,19 +2348,15 @@ 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 _this7 = this; // Menu required - if (!is.element(this.elements.settings.panes.speed)) { + if (!is.element(this.elements.settings.panels.speed)) { return; } var type = 'speed'; + var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options if (is.array(options)) { @@ -2229,12 +2367,15 @@ 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 _this7.config.speed.options.includes(speed); }); // 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); + + // Empty the menu + emptyElement(list); // Check if we need to toggle the parent controls.checkMenu.call(this); @@ -2244,19 +2385,13 @@ typeof navigator === "object" && (function (global, factory) { return; } - // Get the list to populate - var list = this.elements.settings.panes.speed.querySelector('ul'); - - // Empty the menu - emptyElement(list); - // Create items this.options.speed.forEach(function (speed) { - controls.createMenuItem.call(_this5, { + controls.createMenuItem.call(_this7, { value: speed, list: list, type: type, - title: controls.getLabel.call(_this5, 'speed', speed) + title: controls.getLabel.call(_this7, 'speed', speed) }); }); @@ -2266,10 +2401,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); @@ -2277,21 +2412,21 @@ typeof navigator === "object" && (function (global, factory) { // Show/hide menu - toggleMenu: function toggleMenu(event) { - var form = this.elements.settings.form; + toggleMenu: function toggleMenu(input) { + 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(input) ? input : is.element(popup) && popup.hasAttribute('hidden'); - if (is.event(event)) { - var isMenuItem = is.element(form) && form.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 @@ -2302,7 +2437,7 @@ typeof navigator === "object" && (function (global, factory) { // Prevent the toggle being caught by the doc listener if (isButton) { - event.stopPropagation(); + input.stopPropagation(); } } @@ -2311,32 +2446,32 @@ typeof navigator === "object" && (function (global, factory) { button.setAttribute('aria-expanded', show); } - if (is.element(form)) { - toggleHidden(form, !show); + // Show the actual popup + if (is.element(popup)) { + toggleHidden(popup, !show); + toggleClass(this.elements.container, this.config.classNames.menu.open, show); - if (show) { - form.removeAttribute('tabindex'); - } else { - form.setAttribute('tabindex', -1); + // Focus the first item if key interaction + if (show && is.event(input) && input.type === 'keyup') { + 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); } } }, - // 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(function (input) { - var name = input.getAttribute('name'); - input.setAttribute('name', name + '-clone'); - }); - // Append to parent so we get the "real" size tab.parentNode.appendChild(clone); @@ -2354,34 +2489,24 @@ typeof navigator === "object" && (function (global, factory) { }, - // Toggle Menu - showTab: function showTab() { - var _this6 = this; + // Show a panel in the menu + showMenuPanel: function showMenuPanel() { + var _this8 = this; - var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - var menu = this.elements.settings.menu; + var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - var pane = document.getElementById(target); + var target = document.getElementById('plyr-settings-' + this.id + '-' + type); // Nothing to show, bail - if (!is.element(pane)) { - return; - } - - // Are we targeting a tab? If not, bail - var isTab = pane.getAttribute('role') === 'tabpanel'; - if (!isTab) { + if (!is.element(target)) { return; } - // Hide all other tabs - // Get other tabs - var current = menu.querySelector('[role="tabpanel"]: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) { - 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 @@ -2391,12 +2516,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; } @@ -2405,7 +2530,7 @@ typeof navigator === "object" && (function (global, factory) { container.style.height = ''; // Only listen once - off.call(_this6, container, transitionEndEvent, restore); + off.call(_this8, container, transitionEndEvent, restore); }; // Listen for the transition finishing and restore auto height/width @@ -2418,26 +2543,20 @@ typeof navigator === "object" && (function (global, factory) { // Set attributes on current tab toggleHidden(current, true); - 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) { - 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(); + var firstItem = target.querySelector('[role^="menuitem"]'); + setFocus.call(this, firstItem, tabFocus); }, // Build the default HTML // TODO: Set order based on order in the config.controls array? create: function create(data) { - var _this7 = this; + var _this9 = this; // Do nothing if we want no controls if (is.empty(this.config.controls)) { @@ -2540,107 +2659,134 @@ 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 + home.appendChild(menu); + inner.appendChild(home); + this.elements.settings.panels.home = home; + + // Build the menu items this.config.settings.forEach(function (type) { - var tab = createElement('li', { - role: 'tab', + // TODO: bundle this with the createMenuItem helper and bindings + var menuItem = createElement('button', extend(getAttributesFromSelector(_this9.config.selectors.buttons.settings), { + type: 'button', + class: _this9.config.classNames.control + ' ' + _this9.config.classNames.control + '--forward', + role: 'menuitem', + 'aria-haspopup': true, hidden: '' + })); + + // Bind menu shortcuts for keyboard users + controls.bindMenuItemShortcuts.call(_this9, menuItem, type); + + // Show menu on click + on(menuItem, 'click', function () { + controls.showMenuPanel.call(_this9, type, false); }); - var button = 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)); + var flex = createElement('span', null, i18n.get(type, _this9.config)); var value = createElement('span', { - class: _this7.config.classNames.menu.value + class: _this9.config.classNames.menu.value }); // 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 + var backButton = 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)); + class: _this9.config.classNames.control + ' ' + _this9.config.classNames.control + '--back' + }); + + // Visible label + backButton.appendChild(createElement('span', { + 'aria-hidden': true + }, i18n.get(type, _this9.config))); + + // Screen reader label + backButton.appendChild(createElement('span', { + class: _this9.config.classNames.hidden + }, i18n.get('menuBack', _this9.config))); + + // Go back via keyboard + on(pane, 'keydown', function (event) { + // We only care about <- + if (event.which !== 37) { + return; + } + + // Prevent seek + event.preventDefault(); + event.stopPropagation(); - pane.appendChild(back); + // Show the respective menu + controls.showMenuPanel.call(_this9, 'home', true); + }, false); - var options = createElement('ul'); + // Go back via button click + on(backButton, 'click', function () { + controls.showMenuPanel.call(_this9, 'home', false); + }); + + // Add to pane + pane.appendChild(backButton); + + // Menu + pane.appendChild(createElement('div', { + role: 'menu' + })); - pane.appendChild(options); inner.appendChild(pane); - _this7.elements.settings.panes[type] = pane; + _this9.elements.settings.buttons[type] = menuItem; + _this9.elements.settings.panels[type] = pane; }); - form.appendChild(inner); - menu.appendChild(form); - container.appendChild(menu); + 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 @@ -2677,7 +2823,7 @@ typeof navigator === "object" && (function (global, factory) { // Insert controls inject: function inject() { - var _this8 = this; + var _this10 = this; // Sprite if (this.config.loadSprite) { @@ -2786,8 +2932,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, _this10.config.classNames.hidden, false); + toggleClass(label, _this10.config.classNames.tooltip, true); }); } } @@ -3537,6 +3683,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', @@ -4174,9 +4323,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); } @@ -4186,7 +4338,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'; @@ -4206,23 +4358,33 @@ 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 // 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 = player.config.selectors.editable; + var seek = 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(); @@ -4250,52 +4412,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: @@ -4314,8 +4476,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 @@ -4338,10 +4500,62 @@ 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 player = this.player; + + + clearTimeout(this.focusTimer); + + // Ignore any key other than tab + if (event.type === 'keydown' && event.which !== 9) { + return; + } + + // Store reference to event timeStamp + if (event.type === 'keydown') { + this.lastKeyDown = event.timeStamp; + } + + // Remove current classes + var removeCurrent = function removeCurrent() { + var className = player.config.classNames.tabFocus; + var current = getElements.call(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 (!player.elements.container.contains(focused)) { + return; + } + + toggleClass(document.activeElement, player.config.classNames.tabFocus, true); + }, 10); } // Global window & document listeners @@ -4350,17 +4564,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(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); } // Container listeners @@ -4368,34 +4587,17 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'container', value: function container() { - var _this2 = 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); - } - // 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); - }); + 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 = _this2.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 @@ -4410,16 +4612,17 @@ typeof navigator === "object" && (function (global, factory) { var delay = 0; if (show) { - ui.toggleControls.call(_this2.player, true); + ui.toggleControls.call(player, true); // Use longer timeout for touch devices - delay = _this2.player.touch ? 3000 : 2000; + delay = player.touch ? 3000 : 2000; } // Clear timer - clearTimeout(_this2.player.timers.controls); - // Timer to prevent flicker when seeking - _this2.player.timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this2.player, false); + clearTimeout(player.timers.controls); + + // Set new timer to prevent flicker when seeking + player.timers.controls = setTimeout(function () { + return ui.toggleControls.call(player, false); }, delay); }); } @@ -4429,76 +4632,77 @@ typeof navigator === "object" && (function (global, factory) { }, { key: 'media', value: function media() { - var _this3 = 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(_this3.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(_this3.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(_this3.player.elements.volume, !_this3.player.hasAudio); - toggleHidden(_this3.player.elements.buttons.mute, !_this3.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 (_this3.player.isHTML5 && _this3.player.isVideo && _this3.player.config.resetOnEnd) { + if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { // Restart - _this3.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(_this3.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(_this3.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(_this3.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(_this3.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 (!_this3.player.ads) { + on.call(player, player.media, 'playing', function () { + if (!player.ads) { return; } // If ads are enabled, wait for them first - if (_this3.player.ads.enabled && !_this3.player.ads.initialized) { + if (player.ads.enabled && !player.ads.initialized) { // Wait for manager response - _this3.player.ads.managerPromise.then(function () { - return _this3.player.ads.play(); + player.ads.managerPromise.then(function () { + return player.ads.play(); }).catch(function () { - return _this3.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)) { @@ -4506,224 +4710,250 @@ 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) { + 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 (_this3.player.paused) { - _this3.player.play(); - } else if (_this3.player.ended) { - _this3.player.restart(); - _this3.player.play(); + if (player.ended) { + player.restart(); + player.play(); } else { - _this3.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 - _this3.player.storage.set({ volume: _this3.player.volume, muted: _this3.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(_this3.player, 'speed'); + controls.updateSetting.call(player, 'speed'); // Save to storage - _this3.player.storage.set({ speed: _this3.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 - _this3.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(_this3.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(' '); - on.call(this.player, this.player.media, proxyEvents, function (event) { + var proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); + + 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 = _this3.player.media.error; + detail = player.media.error; } - triggerEvent.call(_this3.player, _this3.player.elements.container, event.type, true, detail); + triggerEvent.call(player, player.elements.container, event.type, true, detail); }); } - // 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 player = this.player; - // IE doesn't support input event, so we fallback to change - var inputEvent = browser.isIE ? 'change' : 'input'; + var customHandler = player.config.listeners[customHandlerKey]; + var hasCustomHandler = is.function(customHandler); + var returned = true; - // 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; + // Execute custom handler + if (hasCustomHandler) { + returned = customHandler.call(player, event); + } - // Execute custom handler - if (hasCustomHandler) { - returned = customHandler.call(_this4.player, event); - } + // Only call default handler if not prevented in custom handler + if (returned && is.function(defaultHandler)) { + defaultHandler.call(player, event); + } + } - // Only call default handler if not prevented in custom handler - if (returned && is.function(defaultHandler)) { - defaultHandler.call(_this4.player, event); - } - }; + // Trigger custom and default handlers - // Trigger custom and default handlers - var bind = function bind(element, type, defaultHandler, customHandlerKey) { - var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + }, { + key: 'bind', + value: function bind(element, type, defaultHandler, customHandlerKey) { + var _this = this; - var customHandler = _this4.player.config.listeners[customHandlerKey]; - var hasCustomHandler = is.function(customHandler); + var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + var player = this.player; - on.call(_this4.player, element, type, function (event) { - return proxy(event, defaultHandler, customHandlerKey); - }, passive && !hasCustomHandler); - }; + var customHandler = player.config.listeners[customHandlerKey]; + var hasCustomHandler = is.function(customHandler); + + on.call(player, element, type, function (event) { + return _this.proxy(event, defaultHandler, customHandlerKey); + }, passive && !hasCustomHandler); + } + + // Listen for control events + + }, { + key: 'controls', + value: function controls$$1() { + 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) { - bind(button, 'click', _this4.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 - bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); + this.bind(player.elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind - bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); + this.bind(player.elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind - bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); + this.bind(player.elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle - bind(this.player.elements.buttons.mute, 'click', function () { - _this4.player.muted = !_this4.player.muted; + this.bind(player.elements.buttons.mute, 'click', function () { + player.muted = !player.muted; }, 'mute'); // Captions toggle - bind(this.player.elements.buttons.captions, 'click', function () { - return _this4.player.toggleCaptions(); + this.bind(player.elements.buttons.captions, 'click', function () { + return player.toggleCaptions(); }); // Fullscreen toggle - bind(this.player.elements.buttons.fullscreen, 'click', function () { - _this4.player.fullscreen.toggle(); + this.bind(player.elements.buttons.fullscreen, 'click', function () { + player.fullscreen.toggle(); }, 'fullscreen'); // Picture-in-Picture - bind(this.player.elements.buttons.pip, 'click', function () { - _this4.player.pip = 'toggle'; + this.bind(player.elements.buttons.pip, 'click', function () { + player.pip = 'toggle'; }, 'pip'); // Airplay - bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); + this.bind(player.elements.buttons.airplay, 'click', player.airplay, 'airplay'); - // Settings menu - bind(this.player.elements.buttons.settings, 'click', function (event) { - controls.toggleMenu.call(_this4.player, event); + // Settings menu - click toggle + this.bind(player.elements.buttons.settings, 'click', function (event) { + controls.toggleMenu.call(player, event); }); - // Settings menu - bind(this.player.elements.settings.form, '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); - }; - - // 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'); - } else { - var tab = event.target; - controls.showTab.call(_this4.player, tab.getAttribute('aria-controls')); + // Settings menu - keyboard toggle + // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus + // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 + this.bind(player.elements.buttons.settings, 'keyup', function (event) { + // We only care about space and return + if (event.which !== 32 && event.which !== 13) { + return; } - }); + + // Prevent scroll + event.preventDefault(); + + // Prevent playing video (Firefox) + if (event.which === 32) { + event.stopPropagation(); + } + + // Toggle menu + controls.toggleMenu.call(player, event); + }, null, false); // 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(); - 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 - 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'); - _this4.player.play(); - } else if (!done && _this4.player.playing) { - seek.setAttribute('play-on-seeked', ''); - _this4.player.pause(); + seek.removeAttribute(attribute); + player.play(); + } else if (!done && player.playing) { + seek.setAttribute(attribute, ''); + player.pause(); } }); + // Fix range inputs on iOS + // Super weird iOS bug where after you interact with an <input type="range">, + // it takes over further interactions on the page. This is a hack + if (browser.isIos) { + var inputs = getElements.call(player, 'input[type="range"]'); + + Array.from(inputs).forEach(function (input) { + return _this2.bind(input, inputEvent, function (event) { + return repaint(event.target); + }); + }); + } + // Seek - 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) @@ -4735,88 +4965,89 @@ typeof navigator === "object" && (function (global, factory) { seek.removeAttribute('seek-value'); - _this4.player.currentTime = seekTo / seek.max * _this4.player.duration; + 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)) { - 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 (_this4.player.currentTime === 0) { + if (player.currentTime === 0) { return; } - _this4.player.config.invertTime = !_this4.player.config.invertTime; + player.config.invertTime = !player.config.invertTime; - controls.timeUpdate.call(_this4.player); + controls.timeUpdate.call(player); }); } // Volume - bind(this.player.elements.inputs.volume, inputEvent, function (event) { - _this4.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 <input type="range"> 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); + 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 - bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) { - return controls.updateSeekTooltip.call(_this4.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) - bind(this.player.elements.controls, 'mouseenter mouseleave', function (event) { - _this4.player.elements.controls.hover = !_this4.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) - bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { - _this4.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 - bind(this.player.elements.controls, 'focusin focusout', function (event) { - var _player = _this4.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(_this4.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 = _this4.touch ? 3000 : 4000; + var delay = _this2.touch ? 3000 : 4000; // Clear timer clearTimeout(timers.controls); + // Hide timers.controls = setTimeout(function () { - return ui.toggleControls.call(_this4.player, false); + return ui.toggleControls.call(player, false); }, delay); } }); // Mouse wheel for volume - 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; @@ -4836,10 +5067,10 @@ typeof navigator === "object" && (function (global, factory) { var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2% - _this4.player.increaseVolume(direction / 50); + player.increaseVolume(direction / 50); // Don't break page scrolling at max and min - var volume = _this4.player.media.volume; + var volume = player.media.volume; if (direction === 1 && volume < 1 || direction === -1 && volume > 0) { event.preventDefault(); @@ -6993,16 +7224,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 + panels: {}, + buttons: {} + } }; // Captions @@ -7103,7 +7335,7 @@ typeof navigator === "object" && (function (global, factory) { // YouTube requires the playsinline in the URL if (this.isYouTube) { this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); - this.config.hl = url.searchParams.get('hl'); + this.config.hl = url.searchParams.get('hl'); // TODO: Should this be setting language? } else { this.config.playsinline = true; } @@ -7139,7 +7371,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')) { @@ -7211,7 +7443,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) { @@ -7417,13 +7651,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; } @@ -7913,7 +8150,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; |