aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/captions.js246
-rw-r--r--src/js/controls.js69
-rw-r--r--src/js/defaults.js1
-rw-r--r--src/js/listeners.js15
-rw-r--r--src/js/plugins/vimeo.js29
-rw-r--r--src/js/plugins/youtube.js5
-rw-r--r--src/js/plyr.js74
-rw-r--r--src/js/utils.js84
-rw-r--r--src/sass/components/captions.scss2
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;