diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/captions.js | 246 | ||||
-rw-r--r-- | src/js/controls.js | 69 | ||||
-rw-r--r-- | src/js/defaults.js | 1 | ||||
-rw-r--r-- | src/js/listeners.js | 15 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 29 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 5 | ||||
-rw-r--r-- | src/js/plyr.js | 74 | ||||
-rw-r--r-- | src/js/utils.js | 84 | ||||
-rw-r--r-- | src/sass/components/captions.scss | 2 |
9 files changed, 281 insertions, 244 deletions
diff --git a/src/js/captions.js b/src/js/captions.js index 30c4bc74..bafcf87e 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -69,12 +69,18 @@ const captions = { ({ active } = this.config.captions); } - // Set toggled state - this.toggleCaptions(active); + // Get language from storage, fallback to config + let language = this.storage.get('language') || this.config.captions.language; + if (language === 'auto') { + [ language ] = (navigator.language || navigator.userLanguage).split('-'); + } + // Set language and show if active + captions.setLanguage.call(this, language, active); // Watch changes to textTracks and update captions menu - if (this.config.captions.update) { - utils.on(this.media.textTracks, 'addtrack removetrack', captions.update.bind(this)); + if (this.isHTML5) { + const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; + utils.on(this.media.textTracks, trackEvents, captions.update.bind(this)); } // Update available languages in list next tick (the event must not be triggered before the listeners) @@ -82,21 +88,39 @@ const captions = { }, update() { - // Update tracks - const tracks = captions.getTracks.call(this); - this.options.captions = tracks.map(({language}) => language); + const tracks = captions.getTracks.call(this, true); + // Get the wanted language + const { language, meta } = this.captions; - // Set language if it hasn't been set already - if (!this.language) { - let { language } = this.config.captions; - if (language === 'auto') { - [ language ] = (navigator.language || navigator.userLanguage).split('-'); - } - this.language = this.storage.get('language') || (language || '').toLowerCase(); + // Handle tracks (add event listener and "pseudo"-default) + if (this.isHTML5 && this.isVideo) { + tracks + .filter(track => !meta.get(track)) + .forEach(track => { + this.debug.log('Track added', track); + // Attempt to store if the original dom element was "default" + meta.set(track, { + default: track.mode === 'showing', + }); + + // Turn off native caption rendering to avoid double captions + track.mode = 'hidden'; + + // Add event listener for cue changes + utils.on(track, 'cuechange', () => captions.updateCues.call(this)); + }); + } + + const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode); + const firstMatch = this.language !== language && tracks.find(track => track.language === language); + + // Update language if removed or first matching track added + if (trackRemoved || firstMatch) { + captions.setLanguage.call(this, language, this.config.captions.active); } - // Toggle the class hooks - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); + // Enable or disable captions based on track length + utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks)); // Update available languages in list if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) { @@ -104,70 +128,94 @@ const captions = { } }, - // Set the captions language - setLanguage() { - // Setup HTML5 track rendering - if (this.isHTML5 && this.isVideo) { - captions.getTracks.call(this).forEach(track => { - // Show track - utils.on(track, 'cuechange', event => captions.setCue.call(this, event)); - - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line - track.mode = 'hidden'; - }); + set(index, setLanguage = true, show = true) { + const tracks = captions.getTracks.call(this); - // Get current track - const currentTrack = captions.getCurrentTrack.call(this); + // Disable captions if setting to -1 + if (index === -1) { + this.toggleCaptions(false); + return; + } - // Check if suported kind - if (utils.is.track(currentTrack)) { - // If we change the active track while a cue is already displayed we need to update it - if (Array.from(currentTrack.activeCues || []).length) { - captions.setCue.call(this, currentTrack); - } - } - } else if (this.isVimeo && this.captions.active) { - this.embed.enableTextTrack(this.language); + if (!utils.is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; } - }, - // Get the tracks - getTracks() { - // Return empty array at least - if (utils.is.nullOrUndefined(this.media)) { - return []; + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; } - // Only get accepted kinds - return Array.from(this.media.textTracks || []).filter(track => [ - 'captions', - 'subtitles', - ].includes(track.kind)); - }, + if (this.captions.currentTrack !== index) { + this.captions.currentTrack = index; + const track = captions.getCurrentTrack.call(this); + const { language } = track || {}; - // Get the current track for the current language - getCurrentTrack() { - const tracks = captions.getTracks.call(this); + // Store reference to node for invalidation on remove + this.captions.currentTrackNode = track; + + // Prevent setting language in some cases, since it can violate user's intentions + if (setLanguage) { + this.captions.language = language; + } - if (!tracks.length) { - return null; + // Handle Vimeo captions + if (this.isVimeo) { + this.embed.enableTextTrack(language); + } + + // Trigger event + utils.dispatchEvent.call(this, this.media, 'languagechange'); } - // Get track based on current language - let track = tracks.find(track => track.language.toLowerCase() === this.language); + if (this.isHTML5 && this.isVideo) { + // If we change the active track while a cue is already displayed we need to update it + captions.updateCues.call(this); + } - // Get the <track> with default attribute - if (!track) { - track = utils.getElement.call(this, 'track[default]'); + // Show captions + if (show) { + this.toggleCaptions(true); } + }, - // Get the first track - if (!track) { - [track] = tracks; + setLanguage(language, show = true) { + if (!utils.is.string(language)) { + this.debug.warn('Invalid language argument', language); + return; } + // Normalize + this.captions.language = language.toLowerCase(); - return track; + // Set currentTrack + const tracks = captions.getTracks.call(this); + const track = captions.getCurrentTrack.call(this, true); + captions.set.call(this, tracks.indexOf(track), false, show); + }, + + // Get current valid caption tracks + // If update is false it will also ignore tracks without metadata + // This is used to "freeze" the language options when captions.update is false + getTracks(update = false) { + // Handle media or textTracks missing or null + const tracks = Array.from((this.media || {}).textTracks || []); + // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) + // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) + return tracks + .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) + .filter(track => [ + 'captions', + 'subtitles', + ].includes(track.kind)); + }, + + // Get the current track for the current language + getCurrentTrack(fromLanguage = false) { + const tracks = captions.getTracks.call(this); + const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); + const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); + return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0]; }, // Get UI label for track @@ -193,56 +241,48 @@ const captions = { return i18n.get('disabled', this.config); }, - // Display active caption if it contains text - setCue(input) { - // Get the track from the event if needed - const track = utils.is.event(input) ? input.target : input; - const { activeCues } = track; - const active = activeCues.length && activeCues[0]; - const currentTrack = captions.getCurrentTrack.call(this); - - // Only display current track - if (track !== currentTrack) { + // Update captions using current track's active cues + // Also optional array argument in case there isn't any track (ex: vimeo) + updateCues(input) { + // Requires UI + if (!this.supported.ui) { return; } - // Display a cue, if there is one - if (utils.is.cue(active)) { - captions.setText.call(this, active.getCueAsHTML()); - } else { - captions.setText.call(this, null); + if (!utils.is.element(this.elements.captions)) { + this.debug.warn('No captions element to render to'); + return; } - utils.dispatchEvent.call(this, this.media, 'cuechange'); - }, - - // Set the current caption - setText(input) { - // Requires UI - if (!this.supported.ui) { + // Only accept array or empty input + if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) { + this.debug.warn('updateCues: Invalid input', input); return; } - if (utils.is.element(this.elements.captions)) { - const content = utils.createElement('span'); + let cues = input; - // Empty the container - utils.emptyElement(this.elements.captions); + // Get cues from track + if (!cues) { + const track = captions.getCurrentTrack.call(this); + cues = Array.from((track || {}).activeCues || []) + .map(cue => cue.getCueAsHTML()) + .map(utils.getHTML); + } - // Default to empty - const caption = !utils.is.nullOrUndefined(input) ? input : ''; + // Set new caption text + const content = cues.map(cueText => cueText.trim()).join('\n'); + const changed = content !== this.elements.captions.innerHTML; - // Set the span content - if (utils.is.string(caption)) { - content.innerText = caption.trim(); - } else { - content.appendChild(caption); - } + if (changed) { + // Empty the container and create a new child element + utils.emptyElement(this.elements.captions); + const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption)); + caption.innerHTML = content; + this.elements.captions.appendChild(caption); - // Set new caption text - this.elements.captions.appendChild(content); - } else { - this.debug.warn('No captions element to render to'); + // Trigger event + utils.dispatchEvent.call(this, this.media, 'cuechange'); } }, }; diff --git a/src/js/controls.js b/src/js/controls.js index 20518f9c..058e636f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -376,7 +376,7 @@ const controls = { }, // Create a settings menu item - createMenuItem(value, list, type, title, badge = null, checked = false) { + createMenuItem({value, list, type, title, badge = null, checked = false}) { const item = utils.createElement('li'); const label = utils.createElement('label', { @@ -680,8 +680,13 @@ const controls = { return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; }) .forEach(quality => { - const label = controls.getLabel.call(this, 'quality', quality); - controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality)); + controls.createMenuItem.call(this, { + value: quality, + list, + type, + title: controls.getLabel.call(this, 'quality', quality), + badge: getBadge(quality), + }); }); controls.updateSetting.call(this, type, list); @@ -722,16 +727,7 @@ const controls = { switch (setting) { case 'captions': - if (this.captions.active) { - if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) { - value = this.captions.language; - } else { - value = 'enabled'; - } - } else { - value = ''; - } - + value = this.currentTrack; break; default: @@ -831,10 +827,10 @@ const controls = { // TODO: Captions or language? Currently it's mixed const type = 'captions'; const list = this.elements.settings.panes.captions.querySelector('ul'); + const tracks = captions.getTracks.call(this); // Toggle the pane and tab - const toggle = captions.getTracks.call(this).length; - controls.toggleTab.call(this, type, toggle); + controls.toggleTab.call(this, type, tracks.length); // Empty the menu utils.emptyElement(list); @@ -843,34 +839,31 @@ const controls = { controls.checkMenu.call(this); // If there's no captions, bail - if (!toggle) { + if (!tracks.length) { return; } - // Re-map the tracks into just the data we need - const tracks = captions.getTracks.call(this).map(track => ({ - language: !utils.is.empty(track.language) ? track.language : 'enabled', - label: captions.getLabel.call(this, track), + // Generate options data + const options = tracks.map((track, value) => ({ + value, + checked: this.captions.active && this.currentTrack === value, + title: captions.getLabel.call(this, track), + badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), + list, + type: 'language', })); // Add the "Disabled" option to turn off captions - tracks.unshift({ - language: '', - label: i18n.get('disabled', this.config), + options.unshift({ + value: -1, + checked: !this.captions.active, + title: i18n.get('disabled', this.config), + list, + type: 'language', }); // Generate options - tracks.forEach(track => { - controls.createMenuItem.call( - this, - track.language, - list, - 'language', - track.label, - track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, - track.language.toLowerCase() === this.language, - ); - }); + options.forEach(controls.createMenuItem.bind(this)); controls.updateSetting.call(this, type, list); }, @@ -927,8 +920,12 @@ const controls = { // Create items this.options.speed.forEach(speed => { - const label = controls.getLabel.call(this, 'speed', speed); - controls.createMenuItem.call(this, speed, list, type, label); + controls.createMenuItem.call(this, { + value: speed, + list, + type, + title: controls.getLabel.call(this, 'speed', speed), + }); }); controls.updateSetting.call(this, type, list); diff --git a/src/js/defaults.js b/src/js/defaults.js index 37fcdeec..ff207017 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -328,6 +328,7 @@ const defaults = { }, progress: '.plyr__progress', captions: '.plyr__captions', + caption: '.plyr__caption', menu: { quality: '.js-plyr__menu__list--quality', }, diff --git a/src/js/listeners.js b/src/js/listeners.js index 86236fe3..c391ea4c 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -74,7 +74,10 @@ class Listeners { // and if the focused element is not editable (e.g. text input) // and any that accept key input http://webaim.org/techniques/keyboard/ const focused = utils.getFocusElement(); - if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) { + if (utils.is.element(focused) && ( + focused !== this.player.elements.inputs.seek && + utils.matches(focused, this.player.config.selectors.editable)) + ) { return; } @@ -411,7 +414,7 @@ class Listeners { 'keyup', 'keydown', ]).join(' '), event => { - let detail = {}; + let {detail = {}} = event; // Get error details from media if (event.type === 'error') { @@ -520,7 +523,7 @@ class Listeners { proxy( event, () => { - this.player.language = event.target.value; + this.player.currentTrack = Number(event.target.value); showHomeTab(); }, 'language', @@ -560,6 +563,12 @@ class Listeners { on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { const seek = event.currentTarget; + const code = event.keyCode ? event.keyCode : event.which; + const eventType = event.type; + + if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { + return; + } // Was playing before? const play = seek.hasAttribute('play-on-seeked'); diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 46d4f3f9..652c920c 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -9,6 +9,9 @@ import utils from './../utils'; // Set playback state and trigger change (only on actual change) function assurePlaybackState(play) { + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } if (this.media.paused === play) { this.media.paused = !play; utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); @@ -153,19 +156,20 @@ const vimeo = { // Get current paused state and volume etc const { embed, media, paused, volume } = player; + const restorePause = paused && !embed.hasPlayed; // Set seeking state and trigger event media.seeking = true; utils.dispatchEvent.call(player, media, 'seeking'); // If paused, mute until seek is complete - Promise.resolve(paused && embed.setVolume(0)) + Promise.resolve(restorePause && embed.setVolume(0)) // Seek .then(() => embed.setCurrentTime(time)) // Restore paused - .then(() => paused && embed.pause()) + .then(() => restorePause && embed.pause()) // Restore volume - .then(() => paused && embed.setVolume(volume)) + .then(() => restorePause && embed.setVolume(volume)) .catch(() => { // Do nothing }); @@ -301,17 +305,20 @@ const vimeo = { captions.setup.call(player); }); - player.embed.on('cuechange', data => { - let cue = null; - - if (data.cues.length) { - cue = utils.stripHTML(data.cues[0].text); - } - - captions.setText.call(player, cue); + player.embed.on('cuechange', ({ cues = [] }) => { + const strippedCues = cues.map(cue => utils.stripHTML(cue.text)); + captions.updateCues.call(player, strippedCues); }); player.embed.on('loaded', () => { + // Assure state and events are updated on autoplay + player.embed.getPaused().then(paused => { + assurePlaybackState.call(player, !paused); + if (!paused) { + utils.dispatchEvent.call(player, player.media, 'playing'); + } + }); + if (utils.is.element(player.embed.element) && player.supported.ui) { const frame = player.embed.element; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 67b8093e..9b067c8a 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -66,6 +66,9 @@ function mapQualityUnits(levels) { // Set playback state and trigger change (only on actual change) function assurePlaybackState(play) { + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } if (this.media.paused === play) { this.media.paused = !play; utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); @@ -469,7 +472,7 @@ const youtube = { case 1: // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) - if (player.media.paused) { + if (player.media.paused && !player.embed.hasPlayed) { player.media.pause(); } else { assurePlaybackState.call(player, true); diff --git a/src/js/plyr.js b/src/js/plyr.js index 90ddb8fa..752b3d3c 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -84,7 +84,8 @@ class Plyr { // Captions this.captions = { active: null, - currentTrack: null, + currentTrack: -1, + meta: new WeakMap(), }; // Fullscreen @@ -96,7 +97,6 @@ class Plyr { this.options = { speed: [], quality: [], - captions: [], }; // Debugging @@ -854,61 +854,35 @@ class Plyr { } /** - * Set the captions language - * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) + * Set the caption track by index + * @param {number} - Caption index */ - set language(input) { - // Nothing specified - if (!utils.is.string(input)) { - return; - } - - // If empty string is passed, assume disable captions - if (utils.is.empty(input)) { - this.toggleCaptions(false); - return; - } - - // Normalize - const language = input.toLowerCase(); - - // Check for support - if (!this.options.captions.includes(language)) { - this.debug.warn(`Unsupported language option: ${language}`); - return; - } - - // Ensure captions are enabled - this.toggleCaptions(true); - - // Enabled only - if (language === 'enabled') { - return; - } - - // If nothing to change, bail - if (this.language === language) { - return; - } - - // Update config - this.captions.language = language; - - // Clear caption - captions.setText.call(this, null); + set currentTrack(input) { + captions.set.call(this, input); + } - // Update captions - captions.setLanguage.call(this); + /** + * Get the current caption track index (-1 if disabled) + */ + get currentTrack() { + const { active, currentTrack } = this.captions; + return active ? currentTrack : -1; + } - // Trigger an event - utils.dispatchEvent.call(this, this.media, 'languagechange'); + /** + * Set the wanted language for captions + * Since tracks can be added later it won't update the actual caption track until there is a matching track + * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc) + */ + set language(input) { + captions.setLanguage.call(this, input); } /** - * Get the current captions language + * Get the current track's language */ get language() { - return this.captions.language; + return (captions.getCurrentTrack.call(this) || {}).language; } /** @@ -1159,7 +1133,7 @@ class Plyr { } else if (utils.is.nodeList(selector)) { targets = Array.from(selector); } else if (utils.is.array(selector)) { - targets = selector.filter(i => utils.is.element(i)); + targets = selector.filter(utils.is.element); } if (utils.is.empty(targets)) { diff --git a/src/js/utils.js b/src/js/utils.js index b6ba0941..c36763dd 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -11,63 +11,64 @@ const utils = { // Check variable types is: { object(input) { - return this.getConstructor(input) === Object; + return utils.getConstructor(input) === Object; }, number(input) { - return this.getConstructor(input) === Number && !Number.isNaN(input); + return utils.getConstructor(input) === Number && !Number.isNaN(input); }, string(input) { - return this.getConstructor(input) === String; + return utils.getConstructor(input) === String; }, boolean(input) { - return this.getConstructor(input) === Boolean; + return utils.getConstructor(input) === Boolean; }, function(input) { - return this.getConstructor(input) === Function; + return utils.getConstructor(input) === Function; }, array(input) { - return !this.nullOrUndefined(input) && Array.isArray(input); + return !utils.is.nullOrUndefined(input) && Array.isArray(input); }, weakMap(input) { - return this.instanceof(input, WeakMap); + return utils.is.instanceof(input, WeakMap); }, nodeList(input) { - return this.instanceof(input, NodeList); + return utils.is.instanceof(input, NodeList); }, element(input) { - return this.instanceof(input, Element); + return utils.is.instanceof(input, Element); }, textNode(input) { - return this.getConstructor(input) === Text; + return utils.getConstructor(input) === Text; }, event(input) { - return this.instanceof(input, Event); + return utils.is.instanceof(input, Event); }, cue(input) { - return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue); + return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue); }, track(input) { - return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind)); + return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind)); }, url(input) { - return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); + return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); }, nullOrUndefined(input) { return input === null || typeof input === 'undefined'; }, empty(input) { return ( - this.nullOrUndefined(input) || - ((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) || - (this.object(input) && !Object.keys(input).length) + utils.is.nullOrUndefined(input) || + ((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) || + (utils.is.object(input) && !Object.keys(input).length) ); }, instanceof(input, constructor) { return Boolean(input && constructor && input instanceof constructor); }, - getConstructor(input) { - return !this.nullOrUndefined(input) ? input.constructor : null; - }, + }, + + getConstructor(input) { + return !utils.is.nullOrUndefined(input) ? input.constructor : null; }, // Unfortunately, due to mixed support, UA sniffing is required @@ -151,24 +152,23 @@ const utils = { return; } - const prefix = 'cache-'; + const prefix = 'cache'; const hasId = utils.is.string(id); let isCached = false; - const exists = () => document.querySelectorAll(`#${id}`).length; + const exists = () => document.getElementById(id) !== null; + + const update = (container, data) => { + container.innerHTML = data; - function injectSprite(data) { // Check again incase of race condition if (hasId && exists()) { return; } - // Inject content - this.innerHTML = data; - // Inject the SVG to the body - document.body.insertBefore(this, document.body.childNodes[0]); - } + document.body.insertAdjacentElement('afterbegin', container); + }; // Only load once if ID set if (!hasId || !exists()) { @@ -184,13 +184,12 @@ const utils = { // Check in cache if (useStorage) { - const cached = window.localStorage.getItem(prefix + id); + const cached = window.localStorage.getItem(`${prefix}-${id}`); isCached = cached !== null; if (isCached) { const data = JSON.parse(cached); - injectSprite.call(container, data.content); - return; + update(container, data.content); } } @@ -204,14 +203,14 @@ const utils = { if (useStorage) { window.localStorage.setItem( - prefix + id, + `${prefix}-${id}`, JSON.stringify({ content: result, }), ); } - injectSprite.call(container, result); + update(container, result); }) .catch(() => {}); } @@ -627,16 +626,16 @@ const utils = { formatTime(time = 0, displayHours = false, inverted = false) { // Bail if the value isn't a number if (!utils.is.number(time)) { - return this.formatTime(null, displayHours, inverted); + return utils.formatTime(null, displayHours, inverted); } // Format time component to add leading zero const format = value => `0${value}`.slice(-2); // Breakdown to hours, mins, secs - let hours = this.getHours(time); - const mins = this.getMinutes(time); - const secs = this.getSeconds(time); + let hours = utils.getHours(time); + const mins = utils.getMinutes(time); + const secs = utils.getSeconds(time); // Do we need to display hours? if (displayHours || hours > 0) { @@ -794,10 +793,10 @@ const utils = { // Parse URL if needed if (input.startsWith('http://') || input.startsWith('https://')) { - ({ search } = this.parseUrl(input)); + ({ search } = utils.parseUrl(input)); } - if (this.is.empty(search)) { + if (utils.is.empty(search)) { return null; } @@ -833,6 +832,13 @@ const utils = { return fragment.firstChild.innerText; }, + // Like outerHTML, but also works for DocumentFragment + getHTML(element) { + const wrapper = document.createElement('div'); + wrapper.appendChild(element); + return wrapper.innerHTML; + }, + // Get aspect ratio for dimensions getAspectRatio(width, height) { const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); diff --git a/src/sass/components/captions.scss b/src/sass/components/captions.scss index 9dfc2be8..8fce581a 100644 --- a/src/sass/components/captions.scss +++ b/src/sass/components/captions.scss @@ -21,7 +21,7 @@ transition: transform 0.4s ease-in-out; width: 100%; - span { + .plyr__caption { background: $plyr-captions-bg; border-radius: 2px; box-decoration-break: clone; |