diff options
Diffstat (limited to 'src/js/plyr.js')
-rw-r--r-- | src/js/plyr.js | 1160 |
1 files changed, 749 insertions, 411 deletions
diff --git a/src/js/plyr.js b/src/js/plyr.js index 22f07788..db17ad00 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -16,7 +16,7 @@ } else { context[name] = definition(); } -}).call(this, 'Plyr', this, function() { +}.call(this, 'Plyr', this, function() { 'use strict'; /* global jQuery, console */ @@ -36,7 +36,6 @@ // Logging to console debug: false, - logPrefix: '', // Auto play (if supported) autoplay: false, @@ -135,12 +134,7 @@ 'airplay', 'fullscreen' ], - settings: [ - 'captions', - 'quality', - 'speed', - 'loop' - ], + settings: ['captions', 'quality', 'speed', 'loop'], // Localisation i18n: { @@ -168,13 +162,14 @@ end: 'End', all: 'All', reset: 'Reset', - none: 'None' + none: 'None', + disabled: 'Disabled' }, // URLs urls: { vimeo: { - api: 'https://player.vimeo.com/api/player.js', + api: 'https://player.vimeo.com/api/player.js' }, youtube: { api: 'https://www.youtube.com/iframe_api' @@ -227,19 +222,21 @@ 'seeked', 'emptied', 'ratechange', + 'cuechange', // Custom events 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', - 'qualitychange', - 'qualityrequested', + 'captionchange', 'controlshidden', 'controlsshown', // YouTube - 'statechange' + 'statechange', + 'qualitychange', + 'qualityrequested' ], // Selectors @@ -279,7 +276,7 @@ buffer: '.plyr__progress--buffer', played: '.plyr__progress--played', loop: '.plyr__progress--loop', - volume: '.plyr__volume--display', + volume: '.plyr__volume--display' }, progress: '.plyr__progress', captions: '.plyr__captions', @@ -289,7 +286,7 @@ }, // Class hooks added to the player in different states - classes: { + classNames: { video: 'plyr__video-wrapper', embed: 'plyr__video-embed', control: 'plyr__control', @@ -345,10 +342,17 @@ return input !== null && Array.isArray(input); }, number: function(input) { - return input !== null && (typeof input === 'number' && !isNaN(input - 0) || (typeof input === 'object' && input.constructor === Number)); + return ( + input !== null && + ((typeof input === 'number' && !isNaN(input - 0)) || + (typeof input === 'object' && input.constructor === Number)) + ); }, string: function(input) { - return input !== null && (typeof input === 'string' || (typeof input === 'object' && input.constructor === String)); + return ( + input !== null && + (typeof input === 'string' || (typeof input === 'object' && input.constructor === String)) + ); }, boolean: function(input) { return input !== null && typeof input === 'boolean'; @@ -369,13 +373,18 @@ return input !== null && (input instanceof window.TextTrackCue || input instanceof window.VTTCue); }, track: function(input) { - return input !== null && input instanceof window.TextTrack; + return input !== null && (input instanceof window.TextTrack || typeof input.kind === 'string'); }, undefined: function(input) { return input !== null && typeof input === 'undefined'; }, empty: function(input) { - return input === null || this.undefined(input) || ((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) || (this.object(input) && Object.keys(input).length === 0); + return ( + input === null || + this.undefined(input) || + ((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) || + (this.object(input) && Object.keys(input).length === 0) + ); } }, @@ -394,7 +403,7 @@ var isChrome = false; var isSafari = false; - if ((navigator.appVersion.indexOf('Windows NT') !== -1) && (navigator.appVersion.indexOf('rv:11') !== -1)) { + if (navigator.appVersion.indexOf('Windows NT') !== -1 && navigator.appVersion.indexOf('rv:11') !== -1) { // MSIE 11 isIE = true; name = 'IE'; @@ -453,7 +462,7 @@ name: name, version: majorVersion, isIE: isIE, - isOldIE: (isIE && majorVersion <= 9), + isOldIE: isIE && majorVersion <= 9, isFirefox: isFirefox, isChrome: isChrome, isSafari: isSafari, @@ -468,7 +477,7 @@ var basic = false; var full = false; var browser = utils.getBrowser(); - var playsInline = (browser.isIPhone && inline && support.inline); + var playsInline = browser.isIPhone && inline && support.inline; switch (type) { case 'video': @@ -489,12 +498,12 @@ case 'vimeo': case 'soundcloud': basic = true; - full = (!browser.isOldIE && !browser.isIos); + full = !browser.isOldIE && !browser.isIos; break; default: - basic = (support.audio && support.video); - full = (basic && !browser.isOldIE); + basic = support.audio && support.video; + full = basic && !browser.isOldIE; } return { @@ -546,7 +555,7 @@ // Loops backwards to prevent having to clone the wrapper on the // first element (see `child` below). for (var i = elements.length - 1; i >= 0; i--) { - var child = (i > 0) ? wrapper.cloneNode(true) : wrapper; + var child = i > 0 ? wrapper.cloneNode(true) : wrapper; var element = elements[i]; // Cache the current parent and sibling. @@ -572,19 +581,13 @@ // Remove an element removeElement: function(element) { - if (!utils.is.htmlElement(element) || - !utils.is.htmlElement(element.parentNode)) { + if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) { return; } element.parentNode.removeChild(element); }, - // Prepend child - prependChild: function(parent, element) { - parent.insertBefore(element, parent.firstChild); - }, - // Inaert an element after another insertAfter: function(element, target) { target.parentNode.insertBefore(element, target.nextSibling); @@ -611,16 +614,14 @@ // Insert a DocumentFragment insertElement: function(type, parent, attributes, text) { - // Create a new <element> - var element = utils.createElement(type, attributes, text); - - // Inject the new element - utils.prependChild(parent, element); + // Inject the new <element> + parent.appendChild(utils.createElement(type, attributes, text)); }, // Remove all child elements emptyElement: function(element) { var length = element.childNodes.length; + while (length--) { element.removeChild(element.lastChild); } @@ -706,7 +707,7 @@ element.className = name + (toggle ? ' ' + className : ''); } - return toggle && !contains || !toggle && contains; + return (toggle && !contains) || (!toggle && contains); } return null; @@ -728,7 +729,8 @@ matches: function(element, selector) { var prototype = Element.prototype; - var matches = prototype.matches || + var matches = + prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || @@ -754,12 +756,18 @@ // Bind along with custom handler proxy: function(element, eventName, customListener, defaultListener, passive, capture) { - utils.on(element, eventName, function(event) { - if (customListener) { - customListener.apply(element, [event]); - } - defaultListener.apply(element, [event]); - }, passive, capture); + utils.on( + element, + eventName, + function(event) { + if (customListener) { + customListener.apply(element, [event]); + } + defaultListener.apply(element, [event]); + }, + passive, + capture + ); }, // Toggle event listener @@ -832,7 +840,7 @@ }, // Trigger event - event: function(element, type, bubbles, properties) { + dispatchEvent: function(element, type, bubbles, properties) { // Bail if no element if (!element || !type) { return; @@ -882,7 +890,7 @@ } // Get state - state = (utils.is.boolean(state) ? state : !target.getAttribute('aria-pressed')); + state = utils.is.boolean(state) ? state : !target.getAttribute('aria-pressed'); // Set the attribute on target target.setAttribute('aria-pressed', state); @@ -895,7 +903,7 @@ if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) { return 0; } - return ((current / max) * 100).toFixed(2); + return (current / max * 100).toFixed(2); }, // Deep extend/merge destination object with N more objects @@ -1011,9 +1019,12 @@ // Once loaded, inject to container and body xhr.onload = function() { if (support.storage) { - window.localStorage.setItem(prefix + id, JSON.stringify({ - content: xhr.responseText - })); + window.localStorage.setItem( + prefix + id, + JSON.stringify({ + content: xhr.responseText + }) + ); } updateSprite(container, xhr.responseText); @@ -1073,7 +1084,7 @@ prefix: prefix, // Yet again Microsoft awesomeness, // Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes - eventType: (prefix === 'ms' ? 'MSFullscreenChange' : prefix + 'fullscreenchange'), + eventType: prefix === 'ms' ? 'MSFullscreenChange' : prefix + 'fullscreenchange', // Is an element fullscreen isFullScreen: function(element) { @@ -1105,14 +1116,18 @@ element = document.body; } - return !prefix.length ? element.requestFullScreen() : element[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); + return !prefix.length + ? element.requestFullScreen() + : element[prefix + (prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); }, cancelFullScreen: function() { if (!support.fullscreen) { return false; } - return !prefix.length ? document.cancelFullScreen() : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); + return !prefix.length + ? document.cancelFullScreen() + : document[prefix + (prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); }, element: function() { if (!support.fullscreen) { @@ -1255,18 +1270,25 @@ } // jQuery, NodeList or Array passed, use first element - if ((window.jQuery && player.media instanceof jQuery) || + if ( + (window.jQuery && player.media instanceof jQuery) || utils.is.nodeList(player.media) || - utils.is.array(player.media)) { + utils.is.array(player.media) + ) { player.media = player.media[0]; } // Set config - player.config = utils.extend({}, defaults, options, (function() { - try { - return JSON.parse(player.media.getAttribute('data-plyr')); - } catch (e) {} - })()); + player.config = utils.extend( + {}, + defaults, + options, + (function() { + try { + return JSON.parse(player.media.getAttribute('data-plyr')); + } catch (e) {} + })() + ); // Elements cache player.elements = { @@ -1285,9 +1307,8 @@ // Captions player.captions = { - enabled: false, - captions: [], - tracks: [], + enabled: null, + tracks: null, currentTrack: null }; @@ -1333,9 +1354,14 @@ // Trigger events, with plyr instance passed function trigger(element, type, bubbles, properties) { - utils.event(element, type, bubbles, utils.extend({}, properties, { - plyr: player - })); + utils.dispatchEvent( + element, + type, + bubbles, + utils.extend({}, properties, { + plyr: player + }) + ); } // Trap focus inside container @@ -1391,6 +1417,8 @@ src: attributes }); } else if (utils.is.array(attributes)) { + warn(attributes); + attributes.forEach(function(attribute) { utils.insertElement(type, player.media, attribute); }); @@ -1401,7 +1429,7 @@ function getIconUrl() { return { url: player.config.iconUrl, - absolute: (player.config.iconUrl.indexOf('http') === 0) || player.browser.isIE + absolute: player.config.iconUrl.indexOf('http') === 0 || player.browser.isIE }; } @@ -1413,9 +1441,12 @@ // Create <svg> var icon = document.createElementNS(namespace, 'svg'); - utils.setAttributes(icon, utils.extend(attributes, { - role: 'presentation' - })); + utils.setAttributes( + icon, + utils.extend(attributes, { + role: 'presentation' + }) + ); // Create the <use> to reference sprite var use = document.createElementNS(namespace, 'use'); @@ -1441,20 +1472,30 @@ break; } - return utils.createElement('span', { - class: player.config.classes.hidden - }, text); + return utils.createElement( + 'span', + { + class: player.config.classNames.hidden + }, + text + ); } // Create a badge function createBadge(text) { var badge = utils.createElement('span', { - class: player.config.classes.menu.value + class: player.config.classNames.menu.value }); - badge.appendChild(utils.createElement('span', { - class: player.config.classes.menu.badge - }, text)); + badge.appendChild( + utils.createElement( + 'span', + { + class: player.config.classNames.menu.badge + }, + text + ) + ); return badge; } @@ -1471,11 +1512,11 @@ } if ('class' in attributes) { - if (attributes.class.indexOf(player.config.classes.control) === -1) { - attributes.class += ' ' + player.config.classes.control; + if (attributes.class.indexOf(player.config.classNames.control) === -1) { + attributes.class += ' ' + player.config.classNames.control; } } else { - attributes.class = player.config.classes.control; + attributes.class = player.config.classNames.control; } // Large play button @@ -1511,13 +1552,18 @@ } // Merge attributes - utils.extend(attributes, utils.getAttributesFromSelector(player.config.selectors.buttons[type], attributes)); + utils.extend( + attributes, + utils.getAttributesFromSelector(player.config.selectors.buttons[type], attributes) + ); // Add toggle icon if needed if (utils.is.string(iconToggled)) { - button.appendChild(createIcon(iconToggled, { - class: 'icon--' + iconToggled - })); + button.appendChild( + createIcon(iconToggled, { + class: 'icon--' + iconToggled + }) + ); } button.appendChild(createIcon(iconDefault)); @@ -1533,20 +1579,31 @@ // Create an <input type='range'> function createRange(type, attributes) { // Seek label - var label = utils.createElement('label', { - for: attributes.id, - class: player.config.classes.hidden - }, player.config.i18n[type]); + var label = utils.createElement( + 'label', + { + for: attributes.id, + class: player.config.classNames.hidden + }, + player.config.i18n[type] + ); // Seek input - var input = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs[type]), { - type: 'range', - min: 0, - max: 100, - step: 0.1, - value: 0, - autocomplete: 'off' - }, attributes)); + var input = utils.createElement( + 'input', + utils.extend( + utils.getAttributesFromSelector(player.config.selectors.inputs[type]), + { + type: 'range', + min: 0, + max: 100, + step: 0.1, + value: 0, + autocomplete: 'off' + }, + attributes + ) + ); player.elements.inputs[type] = input; @@ -1558,11 +1615,18 @@ // Create a <progress> function createProgress(type, attributes) { - var progress = utils.createElement('progress', utils.extend(utils.getAttributesFromSelector(player.config.selectors.display[type]), { - min: 0, - max: 100, - value: 0 - }, attributes)); + var progress = utils.createElement( + 'progress', + utils.extend( + utils.getAttributesFromSelector(player.config.selectors.display[type]), + { + min: 0, + max: 100, + value: 0 + }, + attributes + ) + ); // Create the label inside if (type !== 'volume') { @@ -1593,11 +1657,23 @@ class: 'plyr__time' }); - container.appendChild(utils.createElement('span', { - class: player.config.classes.hidden - }, player.config.i18n[type])); - - container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(player.config.selectors.display[type]), '00:00')); + container.appendChild( + utils.createElement( + 'span', + { + class: player.config.classNames.hidden + }, + player.config.i18n[type] + ) + ); + + container.appendChild( + utils.createElement( + 'span', + utils.getAttributesFromSelector(player.config.selectors.display[type]), + '00:00' + ) + ); player.elements.display[type] = container; @@ -1613,7 +1689,10 @@ } // Create the container - var controls = utils.createElement('div', utils.getAttributesFromSelector(player.config.selectors.controls.wrapper)); + var controls = utils.createElement( + 'div', + utils.getAttributesFromSelector(player.config.selectors.controls.wrapper) + ); // Restart button if (utils.inArray(player.config.controls, 'restart')) { @@ -1639,7 +1718,10 @@ // Progress if (utils.inArray(player.config.controls, 'progress')) { - var container = utils.createElement('span', utils.getAttributesFromSelector(player.config.selectors.progress)); + var container = utils.createElement( + 'span', + utils.getAttributesFromSelector(player.config.selectors.progress) + ); // Seek range slider var seek = createRange('seek', { @@ -1658,10 +1740,14 @@ // Seek tooltip if (player.config.tooltips.seek) { - var tooltip = utils.createElement('span', { - role: 'tooltip', - class: player.config.classes.tooltip - }, '00:00'); + var tooltip = utils.createElement( + 'span', + { + role: 'tooltip', + class: player.config.classNames.tooltip + }, + '00:00' + ); container.appendChild(tooltip); player.elements.display.seekTooltip = tooltip; @@ -1700,9 +1786,12 @@ }; // Create the volume range slider - var range = createRange('volume', utils.extend(attributes, { - id: 'plyr-volume-' + data.id - })); + var range = createRange( + 'volume', + utils.extend(attributes, { + id: 'plyr-volume-' + data.id + }) + ); volume.appendChild(range.label); volume.appendChild(range.input); @@ -1724,12 +1813,14 @@ class: 'plyr__menu' }); - menu.appendChild(createButton('settings', { - id: 'plyr-settings-toggle-' + data.id, - 'aria-haspopup': true, - 'aria-controls': 'plyr-settings-' + data.id, - 'aria-expanded': false - })); + menu.appendChild( + createButton('settings', { + id: 'plyr-settings-toggle-' + data.id, + 'aria-haspopup': true, + 'aria-controls': 'plyr-settings-' + data.id, + 'aria-expanded': false + }) + ); var form = utils.createElement('form', { class: 'plyr__menu__container', @@ -1761,17 +1852,22 @@ hidden: '' }); - var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(player.config.selectors.buttons.settings), { - type: 'button', - class: player.config.classes.control + ' ' + player.config.classes.control + '--forward', - id: 'plyr-settings-' + data.id + '-' + type + '-tab', - 'aria-haspopup': true, - 'aria-controls': 'plyr-settings-' + data.id + '-' + type, - 'aria-expanded': false - }), player.config.i18n[type]); + var button = utils.createElement( + 'button', + utils.extend(utils.getAttributesFromSelector(player.config.selectors.buttons.settings), { + type: 'button', + class: + player.config.classNames.control + ' ' + player.config.classNames.control + '--forward', + id: 'plyr-settings-' + data.id + '-' + type + '-tab', + 'aria-haspopup': true, + 'aria-controls': 'plyr-settings-' + data.id + '-' + type, + 'aria-expanded': false + }), + player.config.i18n[type] + ); var value = utils.createElement('span', { - class: player.config.classes.menu.value + class: player.config.classNames.menu.value }); // Speed contains HTML entities @@ -1798,13 +1894,17 @@ hidden: '' }); - var back = utils.createElement('button', { - type: 'button', - class: player.config.classes.control + ' ' + player.config.classes.control + '--back', - 'aria-haspopup': true, - 'aria-controls': 'plyr-settings-' + data.id + '-home', - 'aria-expanded': false - }, player.config.i18n[type]); + var back = utils.createElement( + 'button', + { + type: 'button', + class: player.config.classNames.control + ' ' + player.config.classNames.control + '--back', + 'aria-haspopup': true, + 'aria-controls': 'plyr-settings-' + data.id + '-home', + 'aria-expanded': false + }, + player.config.i18n[type] + ); pane.appendChild(back); @@ -1847,7 +1947,7 @@ player.elements.controls = controls; - setLoopMenu(); + //setLoopMenu(); setSpeedMenu(); return controls; @@ -1936,14 +2036,17 @@ var item = utils.createElement('li'); var label = utils.createElement('label', { - class: player.config.classes.control + class: player.config.classNames.control }); - var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.quality), { - type: 'radio', - name: 'plyr-quality', - value: quality, - })); + var radio = utils.createElement( + 'input', + utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.quality), { + type: 'radio', + name: 'plyr-quality', + value: quality + }) + ); label.appendChild(radio); label.appendChild(document.createTextNode(getLabel('quality', quality))); @@ -1957,55 +2060,73 @@ list.appendChild(item); }); - setSelectedSetting('quality', list); + updateSetting('quality', list); } // Translate a value into a nice label // TODO: Localisation function getLabel(setting, value) { - if (setting === 'speed') { - if (value === 1) { - return 'Normal'; - } + switch (setting) { + case 'speed': + return value === 1 ? 'Normal' : value + '×'; + + case 'quality': + switch (value) { + case 'hd2160': + return '2160P'; + case 'hd1440': + return '1440P'; + case 'hd1080': + return '1080P'; + case 'hd720': + return '720P'; + case 'large': + return '480P'; + case 'medium': + return '360P'; + case 'small': + return '240P'; + case 'tiny': + return 'Tiny'; + case 'default': + return 'Auto'; + default: + return value; + } - return value + '×'; - } else if (setting === 'quality') { - switch (value) { - case 'hd2160': - return '2160P'; - case 'hd1440': - return '1440P'; - case 'hd1080': - return '1080P'; - case 'hd720': - return '720P'; - case 'large': - return '480P'; - case 'medium': - return '360P'; - case 'small': - return '240P'; - case 'tiny': - return 'Tiny'; - case 'default': - return 'Auto'; - default: - return value; - } + case 'captions': + return getLanguage(); } } // Update the selected setting - function setSelectedSetting(setting, list) { - var value = player[setting].selected; + function updateSetting(setting, list) { + var value = null; - if (utils.is.empty(value)) { - value = player.config[setting].default; - } + switch (setting) { + case 'captions': + value = player.captions.language; - // Unsupported quality - if (!utils.inArray(player[setting].options, value)) { - return; + if (!player.captions.enabled) { + value = ''; + } + + break; + + default: + value = player[setting].selected; + + if (utils.is.empty(value)) { + value = player.config[setting].default; + } + + // Unsupported value + if (!utils.inArray(player[setting].options, value)) { + warn('Unsupported option'); + return; + } + + break; } // Get the list if we need to @@ -2014,7 +2135,7 @@ } // Find the radio option - var target = list.querySelector('[value="' + value + '"]'); + var target = list.querySelector('input[value="' + value + '"]'); if (!utils.is.htmlElement(target)) { return; @@ -2024,12 +2145,12 @@ target.checked = true; // Find the label - var label = player.elements.settings.tabs[setting].querySelector('.' + player.config.classes.menu.value); + var label = player.elements.settings.tabs[setting].querySelector('.' + player.config.classNames.menu.value); label.innerHTML = getLabel(setting, value); } // Set the looping options - function setLoopMenu() { + /*function setLoopMenu() { var options = ['start', 'end', 'all', 'reset']; var list = player.elements.settings.panes.loop.querySelector('ul'); @@ -2037,17 +2158,25 @@ player.elements.settings.tabs.loop.removeAttribute('hidden'); player.elements.settings.panes.loop.removeAttribute('hidden'); + // Toggle the pane and tab + var toggle = !utils.is.empty(player.loop.options); + toggleTab('loop', toggle); + // Empty the menu utils.emptyElement(list); options.forEach(function(option) { var item = utils.createElement('li'); - var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(player.config.selectors.buttons.loop), { - type: 'button', - class: player.config.classes.control, - 'data-plyr-loop-action': option - }), player.config.i18n[option]); + var button = utils.createElement( + 'button', + utils.extend(utils.getAttributesFromSelector(player.config.selectors.buttons.loop), { + type: 'button', + class: player.config.classNames.control, + 'data-plyr-loop-action': option + }), + player.config.i18n[option] + ); if (utils.inArray(['start', 'end'], option)) { var badge = createBadge('00:00'); @@ -2057,15 +2186,15 @@ item.appendChild(button); list.appendChild(item); }); - } + }*/ // Set a list of available captions languages function setCaptionsMenu() { var list = player.elements.settings.panes.captions.querySelector('ul'); - // Show the pane and tab - player.elements.settings.tabs.captions.removeAttribute('hidden'); - player.elements.settings.panes.captions.removeAttribute('hidden'); + // Toggle the pane and tab + var toggle = !utils.is.empty(player.captions.tracks); + toggleTab('captions', toggle); // Empty the menu utils.emptyElement(list); @@ -2086,7 +2215,7 @@ // Add the "None" option to turn off captions tracks.unshift({ - language: 'off', + language: '', label: player.config.i18n.none }); @@ -2095,16 +2224,19 @@ var item = utils.createElement('li'); var label = utils.createElement('label', { - class: player.config.classes.control + class: player.config.classNames.control }); - var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.language), { - type: 'radio', - name: 'plyr-language', - value: track.language, - })); + var radio = utils.createElement( + 'input', + utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.language), { + type: 'radio', + name: 'plyr-language', + value: track.language + }) + ); - if (track.language.toLowerCase() === player.config.captions.language.toLowerCase()) { + if (track.language.toLowerCase() === player.captions.language.toLowerCase()) { radio.checked = true; } @@ -2118,6 +2250,8 @@ item.appendChild(label); list.appendChild(item); }); + + updateSetting('captions', list); } // Set a list of available captions languages @@ -2160,14 +2294,17 @@ var item = utils.createElement('li'); var label = utils.createElement('label', { - class: player.config.classes.control + class: player.config.classNames.control }); - var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.speed), { - type: 'radio', - name: 'plyr-speed', - value: speed, - })); + var radio = utils.createElement( + 'input', + utils.extend(utils.getAttributesFromSelector(player.config.selectors.inputs.speed), { + type: 'radio', + name: 'plyr-speed', + value: speed + }) + ); label.appendChild(radio); label.insertAdjacentHTML('beforeend', getLabel('speed', speed)); @@ -2175,7 +2312,7 @@ list.appendChild(item); }); - setSelectedSetting('speed', list); + updateSetting('speed', list); } // Setup fullscreen @@ -2192,7 +2329,7 @@ log((nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled'); // Add styling hook - utils.toggleClass(player.elements.container, player.config.classes.fullscreen.enabled, true); + utils.toggleClass(player.elements.container, player.config.classNames.fullscreen.enabled, true); } else { log('Fullscreen not supported and fallback disabled'); } @@ -2208,23 +2345,53 @@ } // Setup captions - function setupCaptions(tracks) { + function setupCaptions() { + // Set default language if not set + if (!utils.is.empty(player.storage.language)) { + player.captions.language = player.storage.language; + } else if (utils.is.empty(player.captions.language)) { + player.captions.language = player.config.captions.language.toLowerCase(); + } + + // Set captions enabled state if not set + if (!utils.is.boolean(player.captions.enabled)) { + if (!utils.is.empty(player.storage.language)) { + player.captions.enabled = player.storage.captions; + } else { + player.captions.enabled = player.config.captions.active; + } + } + // Only Vimeo and HTML5 video supported at this point if (!utils.inArray(['video', 'vimeo'], player.type) || (player.type === 'video' && !support.textTracks)) { + player.captions.tracks = null; + + // Clear menu and hide + setCaptionsMenu(); + return; } // Inject the container if (!utils.is.htmlElement(player.elements.captions)) { - player.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(player.config.selectors.captions)); + player.elements.captions = utils.createElement( + 'div', + utils.getAttributesFromSelector(player.config.selectors.captions) + ); utils.insertAfter(player.elements.captions, player.elements.wrapper); } // Get tracks - player.captions.tracks = utils.is.array(tracks) ? tracks : player.media.textTracks; + if (player.type === 'video') { + player.captions.tracks = player.media.textTracks; + } // Set the class hook - utils.toggleClass(player.elements.container, player.config.classes.captions.enabled, !utils.is.empty(player.captions.tracks)); + utils.toggleClass( + player.elements.container, + player.config.classNames.captions.enabled, + !utils.is.empty(player.captions.tracks) + ); // If no caption file exists, hide container for caption text if (utils.is.empty(player.captions.tracks)) { @@ -2234,9 +2401,43 @@ // Enable UI showCaptions(); - if (player.type === 'video') { - var language = player.config.captions.language.toLowerCase(); + // Get a track + function setCurrentTrack() { + // Reset by default + player.captions.currentTrack = null; + + // Filter doesn't seem to work for a TextTrackList :-( + [].forEach.call(player.captions.tracks, function(track) { + if (track.language === player.captions.language.toLowerCase()) { + player.captions.currentTrack = track; + } + }); + } + + // Get current track + setCurrentTrack(); + + // If we couldn't get the requested language, revert to default + if (!utils.is.track(player.captions.currentTrack)) { + var language = player.config.captions.language; + + // Reset to default + // We don't update user storage as the selected language could become available + player.captions.language = language; + + // Get fallback track + setCurrentTrack(); + + // If no match, disable captions + if (!utils.is.track(player.captions.currentTrack)) { + player.toggleCaptions(false); + } + + updateSetting('captions'); + } + // Setup HTML5 track rendering + if (player.type === 'video') { // Turn off native caption rendering to avoid double captions [].forEach.call(player.captions.tracks, function(track) { // Remove previous bindings (if we've changed source or language) @@ -2244,29 +2445,21 @@ // Hide captions track.mode = 'hidden'; - - // If language matches, it's the selected track - if (track.language === language) { - player.captions.currentTrack = track; - } }); - // If we couldn't get the requested language, we get the first - if (!utils.is.track(player.captions.currentTrack)) { - warn('No language found to match ' + language + ' in tracks'); - player.captions.currentTrack = player.captions.tracks[0]; - } + // Check if suported kind + var supported = utils.inArray(['captions', 'subtitles'], player.captions.currentTrack.kind); - // If it's a caption or subtitle, render it - var track = player.captions.currentTrack; - if (utils.is.track(track) && utils.inArray(['captions', 'subtitles'], track.kind)) { - utils.on(track, 'cuechange', setActiveCue); + if (utils.is.track(player.captions.currentTrack) && supported) { + utils.on(player.captions.currentTrack, 'cuechange', setActiveCue); // If we change the active track while a cue is already displayed we need to update it - if (track.activeCues && track.activeCues.length > 0) { - setActiveCue(track); + if (player.captions.currentTrack.activeCues && player.captions.currentTrack.activeCues.length > 0) { + setActiveCue(player.captions.currentTrack); } } + } else if (player.type === 'vimeo' && player.captions.active) { + player.embed.enableTextTrack(player.captions.language); } // Set available languages in list @@ -2276,13 +2469,13 @@ // Get current selected caption language function getLanguage() { if (!support.textTracks || utils.is.empty(player.captions.tracks)) { - return 'None'; + return player.config.i18n.none; } if (player.captions.enabled) { return player.captions.currentTrack.label; } else { - return 'Disabled'; + return player.config.i18n.disabled; } } @@ -2301,6 +2494,8 @@ } else { setCaption(); } + + trigger(player.media, 'cuechange'); } // Set the current caption @@ -2342,13 +2537,13 @@ // Otherwise fall back to the default config if (!utils.is.boolean(active)) { - active = player.config.captions.active; + active = player.captions.active; } else { - player.config.captions.active = active; + player.captions.active = active; } if (active) { - utils.toggleClass(player.elements.container, player.config.classes.captions.active, true); + utils.toggleClass(player.elements.container, player.config.classNames.captions.active, true); utils.toggleState(player.elements.buttons.captions, true); } } @@ -2377,17 +2572,15 @@ // HTML passed as the option if (utils.is.string(player.config.controls)) { controls = player.config.controls; - } - // A custom function to build controls - // The function can return a HTMLElement or String - else if (utils.is.function(player.config.controls)) { + } else if (utils.is.function(player.config.controls)) { + // A custom function to build controls + // The function can return a HTMLElement or String controls = player.config.controls({ id: player.id, seektime: player.config.seekTime }); - } - // Create controls - else { + } else { + // Create controls controls = createControls({ id: player.id, seektime: player.config.seekTime, @@ -2427,13 +2620,21 @@ // Setup tooltips if (player.config.tooltips.controls) { - var labels = getElements([player.config.selectors.controls.wrapper, ' ', player.config.selectors.labels, ' .', player.config.classes.hidden].join('')); + var labels = getElements( + [ + player.config.selectors.controls.wrapper, + ' ', + player.config.selectors.labels, + ' .', + player.config.classNames.hidden + ].join('') + ); for (var i = labels.length - 1; i >= 0; i--) { var label = labels[i]; - utils.toggleClass(label, player.config.classes.hidden, false); - utils.toggleClass(label, player.config.classes.tooltip, true); + utils.toggleClass(label, player.config.classNames.hidden, false); + utils.toggleClass(label, player.config.classNames.tooltip, true); } } } @@ -2465,7 +2666,7 @@ // Inputs player.elements.inputs = { seek: getElement(player.config.selectors.inputs.seek), - volume: getElement(player.config.selectors.inputs.volume), + volume: getElement(player.config.selectors.inputs.volume) }; // Display @@ -2474,12 +2675,14 @@ played: getElement(player.config.selectors.display.played), volume: getElement(player.config.selectors.display.volume), duration: getElement(player.config.selectors.display.duration), - currentTime: getElement(player.config.selectors.display.currentTime), + currentTime: getElement(player.config.selectors.display.currentTime) }; // Seek tooltip if (utils.is.htmlElement(player.elements.progress)) { - player.elements.display.seekTooltip = player.elements.progress.querySelector('.' + player.config.classes.tooltip); + player.elements.display.seekTooltip = player.elements.progress.querySelector( + '.' + player.config.classNames.tooltip + ); } return true; @@ -2496,7 +2699,11 @@ // Toggle style hook function toggleStyleHook() { - utils.toggleClass(player.elements.container, player.config.selectors.container.replace('.', ''), player.supported.full); + utils.toggleClass( + player.elements.container, + player.config.selectors.container.replace('.', ''), + player.supported.full + ); } // Toggle native HTML5 media controls @@ -2534,7 +2741,10 @@ // Set iframe title // https://github.com/sampotts/plyr/issues/124 if (utils.is.htmlElement(iframe)) { - var title = utils.is.string(player.config.title) && !utils.is.empty(player.config.title) ? player.config.title : 'video'; + var title = + utils.is.string(player.config.title) && !utils.is.empty(player.config.title) + ? player.config.title + : 'video'; iframe.setAttribute('title', player.config.i18n.frameTitle.replace('{title}', title)); } } @@ -2596,35 +2806,51 @@ if (player.supported.full) { // Add type class - utils.toggleClass(player.elements.container, player.config.classes.type.replace('{0}', player.type), true); + utils.toggleClass( + player.elements.container, + player.config.classNames.type.replace('{0}', player.type), + true + ); // Add video class for embeds // This will require changes if audio embeds are added if (utils.inArray(types.embed, player.type)) { - utils.toggleClass(player.elements.container, player.config.classes.type.replace('{0}', 'video'), true); + utils.toggleClass( + player.elements.container, + player.config.classNames.type.replace('{0}', 'video'), + true + ); } // Check for picture-in-picture support - utils.toggleClass(player.elements.container, player.config.classes.pip.enabled, support.pip && player.type === 'video'); + utils.toggleClass( + player.elements.container, + player.config.classNames.pip.enabled, + support.pip && player.type === 'video' + ); // Check for airplay support - utils.toggleClass(player.elements.container, player.config.classes.airplay.enabled, support.airplay && utils.inArray(types.html5, player.type)); + utils.toggleClass( + player.elements.container, + player.config.classNames.airplay.enabled, + support.airplay && utils.inArray(types.html5, player.type) + ); // If there's no autoplay attribute, assume the video is stopped and add state class - utils.toggleClass(player.elements.container, player.config.classes.stopped, player.config.autoplay); + utils.toggleClass(player.elements.container, player.config.classNames.stopped, player.config.autoplay); // Add iOS class - utils.toggleClass(player.elements.container, player.config.classes.isIos, player.browser.isIos); + utils.toggleClass(player.elements.container, player.config.classNames.isIos, player.browser.isIos); // Add touch class - utils.toggleClass(player.elements.container, player.config.classes.isTouch, support.touch); + utils.toggleClass(player.elements.container, player.config.classNames.isTouch, support.touch); } // Inject the player wrapper if (utils.inArray(['video', 'youtube', 'vimeo'], player.type)) { // Create the wrapper div player.elements.wrapper = utils.createElement('div', { - class: player.config.classes.video + class: player.config.classNames.video }); // Wrap the video in a container @@ -2640,7 +2866,7 @@ // Setup YouTube/Vimeo function setupEmbed() { var mediaId; - var id = player.type + '-' + Math.floor(Math.random() * (10000)); + var id = player.type + '-' + Math.floor(Math.random() * 10000); // Parse IDs from URLs if supplied switch (player.type) { @@ -2659,7 +2885,7 @@ } // Add embed class for responsive - utils.toggleClass(player.elements.wrapper, player.config.classes.embed, true); + utils.toggleClass(player.elements.wrapper, player.config.classNames.embed, true); if (player.type === 'youtube') { // Set ID @@ -2717,8 +2943,8 @@ }); utils.setAttributes(soundCloud, { - 'src': 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/' + mediaId, - 'id': id + src: 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/' + mediaId, + id: id }); player.media.appendChild(soundCloud); @@ -2757,8 +2983,8 @@ player.embed = new window.YT.Player(player.media.id, { videoId: videoId, playerVars: { - autoplay: (player.config.autoplay ? 1 : 0), // Autoplay - controls: (player.supported.full ? 0 : 1), // Only show controls if not fully supported + autoplay: player.config.autoplay ? 1 : 0, // Autoplay + controls: player.supported.full ? 0 : 1, // Only show controls if not fully supported rel: 0, // No related vids showinfo: 0, // Hide info iv_load_policy: 3, // Hide annotations @@ -2768,20 +2994,20 @@ // Tracking for stats origin: window.location.hostname, - widget_referrer: window.location.href, + widget_referrer: window.location.href // Captions is flaky on YouTube - //cc_load_policy: (player.config.captions.active ? 1 : 0), + //cc_load_policy: (player.captions.active ? 1 : 0), //cc_lang_pref: 'en', }, events: { - 'onError': function(event) { + onError: function(event) { trigger(player.elements.container, 'error', true, { code: event.data, embed: event.target }); }, - 'onPlaybackQualityChange': function(event) { + onPlaybackQualityChange: function(event) { // Get the instance var instance = event.target; @@ -2791,7 +3017,7 @@ // Trigger timeupdate trigger(player.media, 'qualitychange'); }, - 'onPlaybackRateChange': function(event) { + onPlaybackRateChange: function(event) { // Get the instance var instance = event.target; @@ -2801,7 +3027,7 @@ // Trigger timeupdate trigger(player.media, 'ratechange'); }, - 'onReady': function(event) { + onReady: function(event) { // Get the instance var instance = event.target; @@ -2852,7 +3078,10 @@ player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something - if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { + if ( + player.media.lastBuffered === null || + player.media.lastBuffered < player.media.buffered + ) { trigger(player.media, 'progress'); } @@ -2868,7 +3097,7 @@ } }, 200); }, - 'onStateChange': function(event) { + onStateChange: function(event) { // Get the instance var instance = event.target; @@ -2999,13 +3228,9 @@ // Get captions player.embed.getTextTracks().then(function(tracks) { - // tracks = an array of track objects - setupCaptions(tracks); + player.captions.tracks = tracks; - // TODO: Captions - if (player.config.captions.active) { - player.embed.enableTextTrack(player.config.captions.language.toLowerCase()); - } + setupCaptions(); }); player.embed.on('cuechange', function(data) { @@ -3140,9 +3365,9 @@ // Check playing state function checkPlaying() { - utils.toggleClass(player.elements.container, player.config.classes.playing, !player.media.paused); + utils.toggleClass(player.elements.container, player.config.classNames.playing, !player.media.paused); - utils.toggleClass(player.elements.container, player.config.classes.stopped, player.media.paused); + utils.toggleClass(player.elements.container, player.config.classNames.stopped, player.media.paused); player.toggleControls(player.media.paused); } @@ -3237,7 +3462,9 @@ var container = current.parentNode; // Set other toggles to be expanded false - [].forEach.call(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]'), function(toggle) { + [].forEach.call(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]'), function( + toggle + ) { toggle.setAttribute('aria-expanded', false); }); @@ -3253,8 +3480,7 @@ // Restore auto height/width var restore = function(event) { // We're only bothered about height and width on the container - if (event.target !== container || - !utils.inArray(['width', 'height'], event.propertyName)) { + if (event.target !== container || !utils.inArray(['width', 'height'], event.propertyName)) { return; } @@ -3299,12 +3525,12 @@ } // Update the volume in storage - player.core.updateStorage({ + updateStorage({ volume: player.media.volume }); // Toggle class if muted - utils.toggleClass(player.elements.container, player.config.classes.muted, player.media.muted); + utils.toggleClass(player.elements.container, player.config.classNames.muted, player.media.muted); // Update checkbox for mute state if (player.supported.full && player.elements.buttons.mute) { @@ -3314,7 +3540,7 @@ // Check if media is loading function checkLoading(event) { - player.loading = (event.type === 'waiting'); + player.loading = event.type === 'waiting'; // Clear timer clearTimeout(timers.loading); @@ -3322,11 +3548,11 @@ // Timer to prevent flicker when seeking timers.loading = setTimeout(function() { // Toggle container class hook - utils.toggleClass(player.elements.container, player.config.classes.loading, player.loading); + utils.toggleClass(player.elements.container, player.config.classNames.loading, player.loading); // Show controls if loading, hide if done player.toggleControls(player.loading); - }, (player.loading ? 250 : 0)); + }, player.loading ? 250 : 0); } // Update <progress> elements @@ -3344,10 +3570,6 @@ // Video playing case 'timeupdate': case 'seeking': - if (player.elements.controls.pressed) { - return; - } - value = utils.getPercentage(player.media.currentTime, duration); // Set seek range value only if it's a 'natural' time event @@ -3357,7 +3579,7 @@ break; - // Check buffer status + // Check buffer status case 'playing': case 'progress': progress = player.elements.display.buffer; @@ -3369,7 +3591,7 @@ return utils.getPercentage(buffered.end(0), duration); } else if (utils.is.number(buffered)) { // YouTube returns between 0 and 1 - return (buffered * 100); + return buffered * 100; } return 0; @@ -3433,11 +3655,11 @@ var secs = parseInt(time % 60); var mins = parseInt((time / 60) % 60); - var hours = parseInt(((time / 60) / 60) % 60); + var hours = parseInt((time / 60 / 60) % 60); var duration = player.getDuration(); // Do we need to display hours? - var displayHours = (parseInt(((duration / 60) / 60) % 60) > 0); + var displayHours = parseInt((duration / 60 / 60) % 60) > 0; // Ensure it's two digits. For example, 03 rather than 3. secs = ('0' + secs).slice(-2); @@ -3516,18 +3738,23 @@ var duration = player.getDuration(); // Bail if setting not true - if (!player.config.tooltips.seek || !utils.is.htmlElement(player.elements.inputs.seek) || !utils.is.htmlElement(player.elements.display.seekTooltip) || duration === 0) { + if ( + !player.config.tooltips.seek || + !utils.is.htmlElement(player.elements.inputs.seek) || + !utils.is.htmlElement(player.elements.display.seekTooltip) || + duration === 0 + ) { return; } // Calculate percentage var clientRect = player.elements.inputs.seek.getBoundingClientRect(); var percent = 0; - var visible = player.config.classes.tooltip + '--visible'; + var visible = player.config.classNames.tooltip + '--visible'; // Determine percentage, if already visible if (utils.is.event(event)) { - percent = ((100 / clientRect.width) * (event.pageX - clientRect.left)); + percent = 100 / clientRect.width * (event.pageX - clientRect.left); } else { if (utils.hasClass(player.elements.display.seekTooltip, visible)) { percent = player.elements.display.seekTooltip.style.left.replace('%', ''); @@ -3544,7 +3771,7 @@ } // Display the time a click would seek to - updateTimeDisplay(((duration / 100) * percent), player.elements.display.seekTooltip); + updateTimeDisplay(duration / 100 * percent, player.elements.display.seekTooltip); // Set position player.elements.display.seekTooltip.style.left = percent + '%'; @@ -3552,7 +3779,7 @@ // Show/hide the tooltip // If the event is a moues in/out and percentage is inside bounds if (utils.is.event(event) && utils.inArray(['mouseenter', 'mouseleave'], event.type)) { - utils.toggleClass(player.elements.display.seekTooltip, visible, (event.type === 'mouseenter')); + utils.toggleClass(player.elements.display.seekTooltip, visible, event.type === 'mouseenter'); } } @@ -3621,7 +3848,7 @@ } // Inject the new element - utils.prependChild(player.elements.container, player.media); + player.elements.container.appendChild(player.media); // Autoplay the new source? if (utils.is.boolean(source.autoplay)) { @@ -3651,8 +3878,16 @@ } // Restore class hooks - utils.toggleClass(player.elements.container, player.config.classes.fullscreen.active, player.fullscreen.active); - utils.toggleClass(player.elements.container, player.config.classes.captions.active, player.captions.enabled); + utils.toggleClass( + player.elements.container, + player.config.classNames.fullscreen.active, + player.fullscreen.active + ); + utils.toggleClass( + player.elements.container, + player.config.classNames.captions.active, + player.captions.enabled + ); toggleStyleHook(); // Set new sources for html5 @@ -3675,7 +3910,10 @@ } // If HTML5 or embed but not fully supported, setupInterface and call ready now - if (utils.inArray(types.html5, player.type) || (utils.inArray(types.embed, player.type) && !player.supported.full)) { + if ( + utils.inArray(types.html5, player.type) || + (utils.inArray(types.embed, player.type) && !player.supported.full) + ) { // Setup interface setupInterface(); @@ -3692,7 +3930,7 @@ // Listen for control events function listeners() { // IE doesn't support input event, so we fallback to change - var inputEvent = (player.browser.isIE ? 'change' : 'input'); + var inputEvent = player.browser.isIE ? 'change' : 'input'; // Click play/pause helper function togglePlay() { @@ -3704,7 +3942,7 @@ // Setup focus and tab focus if (target) { - var hadTabFocus = utils.hasClass(trigger, player.config.classes.tabFocus); + var hadTabFocus = utils.hasClass(trigger, player.config.classNames.tabFocus); setTimeout(function() { if (utils.is.htmlElement(target)) { @@ -3712,8 +3950,8 @@ } if (hadTabFocus) { - utils.toggleClass(trigger, player.config.classes.tabFocus, false); - utils.toggleClass(target, player.config.classes.tabFocus, true); + utils.toggleClass(trigger, player.config.classNames.tabFocus, false); + utils.toggleClass(target, player.config.classNames.tabFocus, true); } }, 100); } @@ -3726,10 +3964,14 @@ // Detect tab focus function checkTabFocus(focused) { - utils.toggleClass(getElements('.' + player.config.classes.tabFocus), player.config.classes.tabFocus, false); + utils.toggleClass( + getElements('.' + player.config.classNames.tabFocus), + player.config.classNames.tabFocus, + false + ); if (player.elements.container.contains(focused)) { - utils.toggleClass(focused, player.config.classes.tabFocus, true); + utils.toggleClass(focused, player.config.classNames.tabFocus, true); } } @@ -3739,18 +3981,27 @@ // Handle global presses if (player.config.keyboard.global) { - utils.on(window, 'keydown keyup', function(event) { - var code = getKeyCode(event); - var focused = utils.getFocusElement(); - var allowed = [48, 49, 50, 51, 52, 53, 54, 56, 57, 75, 77, 70, 67, 73, 76, 79]; - - // Only handle global key press if key is in the allowed keys - // and if the focused element is not editable (e.g. text input) - // and any that accept key input http://webaim.org/techniques/keyboard/ - if (utils.inArray(allowed, code) && (!utils.is.htmlElement(focused) || !utils.matches(focused, player.config.selectors.editable))) { - handleKey(event); - } - }, false); + utils.on( + window, + 'keydown keyup', + function(event) { + var code = getKeyCode(event); + var focused = utils.getFocusElement(); + var allowed = [48, 49, 50, 51, 52, 53, 54, 56, 57, 75, 77, 70, 67, 73, 76, 79]; + + // Only handle global key press if key is in the allowed keys + // and if the focused element is not editable (e.g. text input) + // and any that accept key input http://webaim.org/techniques/keyboard/ + if ( + utils.inArray(allowed, code) && + (!utils.is.htmlElement(focused) || + !utils.matches(focused, player.config.selectors.editable)) + ) { + handleKey(event); + } + }, + false + ); } // Handle presses on focused @@ -3779,14 +4030,36 @@ } // Divide the max duration into 10th's and times by the number value - player.seek((duration / 10) * (code - 48)); + player.seek(duration / 10 * (code - 48)); } // Handle the key on keydown // Reset on keyup if (pressed) { // Which keycodes should we prevent default - var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67, 73, 76, 79]; + var preventDefault = [ + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 56, + 57, + 32, + 75, + 38, + 40, + 77, + 39, + 37, + 70, + 67, + 73, + 76, + 79 + ]; var checkFocus = [38, 40]; if (utils.inArray(checkFocus, code)) { @@ -3904,7 +4177,11 @@ }); utils.on(document.body, 'click', function() { - utils.toggleClass(getElement('.' + player.config.classes.tabFocus), player.config.classes.tabFocus, false); + utils.toggleClass( + getElement('.' + player.config.classNames.tabFocus), + player.config.classNames.tabFocus, + false + ); }); for (var button in player.elements.buttons) { @@ -3958,7 +4235,9 @@ }); // Fullscreen - utils.proxy(player.elements.buttons.fullscreen, 'click', player.config.listeners.fullscreen, function(event) { + utils.proxy(player.elements.buttons.fullscreen, 'click', player.config.listeners.fullscreen, function( + event + ) { player.toggleFullscreen(event); }); @@ -3986,27 +4265,23 @@ // Settings - Language if (utils.matches(event.target, player.config.selectors.inputs.language)) { handlerProxy.call(this, event, player.config.listeners.language, function() { + player.toggleCaptions(true); + player.setLanguage(event.target.value.toLowerCase()); }); - } - - // Settings - Quality - else if (utils.matches(event.target, player.config.selectors.inputs.quality)) { + } else if (utils.matches(event.target, player.config.selectors.inputs.quality)) { + // Settings - Quality handlerProxy.call(this, event, player.config.listeners.quality, function() { player.setQuality(event.target.value); }); - } - - // Settings - Speed - else if (utils.matches(event.target, player.config.selectors.inputs.speed)) { + } else if (utils.matches(event.target, player.config.selectors.inputs.speed)) { + // Settings - Speed handlerProxy.call(this, event, player.config.listeners.speed, function() { player.setSpeed(parseFloat(event.target.value)); }); - } - - // Settings - Looping - // TODO: use toggle buttons - else if (utils.matches(event.target, player.config.selectors.buttons.loop)) { + } else if (utils.matches(event.target, player.config.selectors.buttons.loop)) { + // Settings - Looping + // TODO: use toggle buttons handlerProxy.call(this, event, player.config.listeners.loop, function() { // TODO: This should be done in the method itself I think // var value = event.target.getAttribute('data-loop__value') || event.target.getAttribute('data-loop__type'); @@ -4023,7 +4298,7 @@ // Seek utils.proxy(player.elements.inputs.seek, inputEvent, player.config.listeners.seek, function(event) { var duration = player.getDuration(); - player.seek((event.target.value / event.target.max) * duration); + player.seek(event.target.value / event.target.max * duration); }); // Seek @@ -4037,9 +4312,13 @@ // Toggle controls visibility based on mouse movement if (player.config.hideControls) { // Toggle controls on mouse events and entering fullscreen - utils.on(player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', function(event) { - player.toggleControls(event); - }); + utils.on( + player.elements.container, + 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', + function(event) { + player.toggleControls(event); + } + ); // Watch for cursor over controls so they don't hide when trying to interact utils.on(player.elements.controls, 'mouseenter mouseleave', function(event) { @@ -4047,53 +4326,66 @@ }); // Watch for cursor over controls so they don't hide when trying to interact - utils.on(player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function(event) { + utils.on(player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function( + event + ) { player.elements.controls.pressed = utils.inArray(['mousedown', 'touchstart'], event.type); }); // Focus in/out on controls // TODO: Check we need capture here - utils.on(player.elements.controls, 'focus blur', function(event) { - player.toggleControls(event); - }, true, true); + utils.on( + player.elements.controls, + 'focus blur', + function(event) { + player.toggleControls(event); + }, + true, + true + ); } // Mouse wheel for volume - utils.proxy(player.elements.inputs.volume, 'wheel', player.config.listeners.volume, 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; - var step = (1 / 5); - var direction = 0; - - // Scroll down (or up on natural) to decrease - if (event.deltaY < 0 || event.deltaX > 0) { - if (inverted) { - player.decreaseVolume(step); - direction = -1; - } else { - player.increaseVolume(step); - direction = 1; + utils.proxy( + player.elements.inputs.volume, + 'wheel', + player.config.listeners.volume, + 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; + var step = 1 / 50; + var direction = 0; + + // Scroll down (or up on natural) to decrease + if (event.deltaY < 0 || event.deltaX > 0) { + if (inverted) { + player.decreaseVolume(step); + direction = -1; + } else { + player.increaseVolume(step); + direction = 1; + } } - } - // Scroll up (or down on natural) to increase - if (event.deltaY > 0 || event.deltaX < 0) { - if (inverted) { - player.increaseVolume(step); - direction = 1; - } else { - player.decreaseVolume(step); - direction = -1; + // Scroll up (or down on natural) to increase + if (event.deltaY > 0 || event.deltaX < 0) { + if (inverted) { + player.increaseVolume(step); + direction = 1; + } else { + player.decreaseVolume(step); + direction = -1; + } } - } - // Don't break page scrolling at max and min - if ((direction === 1 && player.media.volume < 1) || - (direction === -1 && player.media.volume > 0)) { - event.preventDefault(); - } - }, false); + // Don't break page scrolling at max and min + if ((direction === 1 && player.media.volume < 1) || (direction === -1 && player.media.volume > 0)) { + event.preventDefault(); + } + }, + false + ); // Handle user exiting fullscreen by escaping etc if (support.fullscreen) { @@ -4143,7 +4435,7 @@ // Click video if (player.config.clickToPlay && player.type !== 'audio') { // Re-fetch the wrapper - var wrapper = getElement('.' + player.config.classes.video); + var wrapper = getElement('.' + player.config.classNames.video); // Bail if there's no wrapper (this should never happen) if (!wrapper) { @@ -4173,9 +4465,14 @@ // Disable right click if (player.config.disableContextMenu) { - utils.on(player.media, 'contextmenu', function(event) { - event.preventDefault(); - }, false); + utils.on( + player.media, + 'contextmenu', + function(event) { + event.preventDefault(); + }, + false + ); } // Speed change @@ -4184,7 +4481,7 @@ player.speed.selected = player.media.playbackRate; // Update UI - setSelectedSetting('speed'); + updateSetting('speed'); // Save speed to localStorage updateStorage({ @@ -4198,7 +4495,7 @@ player.quality.selected = player.media.quality; // Update UI - setSelectedSetting('quality'); + updateSetting('quality'); // Save speed to localStorage updateStorage({ @@ -4206,6 +4503,25 @@ }); }); + // Caption language change + utils.on(player.media, 'captionchange', function() { + // Save speed to localStorage + updateStorage({ + language: player.captions.language + }); + }); + + // Captions toggle + utils.on(player.media, 'captionsenabled captionsdisabled', function() { + // Update UI + updateSetting('captions'); + + // Save speed to localStorage + updateStorage({ + captions: player.captions.enabled + }); + }); + // Proxy events to container // Bubble up key events for Edge utils.on(player.media, player.config.events.concat(['keyup', 'keydown']).join(' '), function(event) { @@ -4445,7 +4761,10 @@ // Setup interface // If embed but not fully supported, setupInterface (to avoid flash of controls) and call ready now - if (utils.inArray(types.html5, player.type) || (utils.inArray(types.embed, player.type) && !player.supported.full)) { + if ( + utils.inArray(types.html5, player.type) || + (utils.inArray(types.embed, player.type) && !player.supported.full) + ) { // Setup UI setupInterface(); @@ -4468,7 +4787,7 @@ updateTimeDisplay: updateTimeDisplay, updateSeekDisplay: updateSeekDisplay, updateSource: updateSource, - updateStorage: updateStorage, + toggleMenu: toggleMenu, timers: timers, support: support, @@ -4999,7 +5318,12 @@ // If the method is called without parameter, toggle based on current value if (!utils.is.boolean(show)) { - show = (player.elements.container.className.indexOf(player.config.classes.captions.active) === -1); + show = player.elements.container.className.indexOf(player.config.classNames.captions.active) === -1; + } + + // Nothing to change... + if (player.captions.enabled === show) { + return player; } // Set global @@ -5009,16 +5333,11 @@ utils.toggleState(player.elements.buttons.captions, player.captions.enabled); // Add class hook - utils.toggleClass(player.elements.container, player.config.classes.captions.active, player.captions.enabled); + utils.toggleClass(player.elements.container, player.config.classNames.captions.active, player.captions.enabled); // Trigger an event player.core.trigger(player.media, player.captions.enabled ? 'captionsenabled' : 'captionsdisabled'); - // Save captions state to localStorage - player.core.updateStorage({ - captions: player.captions.enabled - }); - // Allow chaining return player; }; @@ -5028,13 +5347,27 @@ var player = this; // Nothing specified - if (!utils.is.string(language)) { - player.core.warn('Language is required'); - return; + if (utils.is.empty(language)) { + player.toggleCaptions(false); + return player; } + // Normalize + language = language.toLowerCase(); + + // If nothing to change, bail + if (player.captions.language === language) { + return player; + } + + // Reset UI + player.toggleCaptions(true); + // Update config - player.config.captions.language = language.toLowerCase(); + player.captions.language = language; + + // Trigger an event + player.core.trigger(player.media, 'captionchange'); // Clear caption player.core.setCaption(); @@ -5048,7 +5381,7 @@ // Get current language Plyr.prototype.getLanguage = function() { - return this.config.captions.language; + return this.captions.language; }; // Toggle fullscreen @@ -5103,7 +5436,11 @@ } // Set class hook - utils.toggleClass(player.elements.container, player.config.classes.fullscreen.active, player.fullscreen.active); + utils.toggleClass( + player.elements.container, + player.config.classNames.fullscreen.active, + player.fullscreen.active + ); // Set button state if (player.elements.buttons && player.elements.buttons.fullscreen) { @@ -5178,13 +5515,13 @@ var delay = 0; var show = toggle; var isEnterFullscreen = false; - var loading = utils.hasClass(player.elements.container, player.config.classes.loading); + var loading = utils.hasClass(player.elements.container, player.config.classNames.loading); // Default to false if no boolean if (!utils.is.boolean(toggle)) { if (utils.is.event(toggle)) { // Is the enter fullscreen event - isEnterFullscreen = (toggle.type === 'enterfullscreen'); + isEnterFullscreen = toggle.type === 'enterfullscreen'; // Whether to show controls show = utils.inArray(['mousemove', 'touchstart', 'mouseenter', 'focus'], toggle.type); @@ -5199,7 +5536,7 @@ delay = 3000; } } else { - show = utils.hasClass(player.elements.container, player.config.classes.hideControls); + show = utils.hasClass(player.elements.container, player.config.classNames.hideControls); } } @@ -5209,7 +5546,7 @@ // If the mouse is not over the controls, set a timeout to hide them if (show || player.media.paused || loading) { // Check if controls toggled - var toggled = utils.toggleClass(player.elements.container, player.config.classes.hideControls, false); + var toggled = utils.toggleClass(player.elements.container, player.config.classNames.hideControls, false); // Trigger event if (toggled) { @@ -5237,11 +5574,12 @@ } // Check if controls toggled - var toggled = utils.toggleClass(player.elements.container, player.config.classes.hideControls, true); + var toggled = utils.toggleClass(player.elements.container, player.config.classNames.hideControls, true); - // Trigger event + // Trigger event and close menu if (toggled) { player.core.trigger(player.media, 'controlshidden'); + player.core.toggleMenu(false); } }, delay); } @@ -5370,8 +5708,8 @@ } // If custom duration is funky, use regular duration - return (isNaN(duration) ? mediaDuration : duration); + return isNaN(duration) ? mediaDuration : duration; }; return Plyr; -});
\ No newline at end of file +})); |