diff options
Diffstat (limited to 'src/js')
-rw-r--r-- | src/js/controls.js | 16 | ||||
-rw-r--r-- | src/js/defaults.js | 19 | ||||
-rw-r--r-- | src/js/listeners.js | 357 | ||||
-rw-r--r-- | src/js/media.js | 5 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 75 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 15 | ||||
-rw-r--r-- | src/js/plyr.js | 67 | ||||
-rw-r--r-- | src/js/ui.js | 6 | ||||
-rw-r--r-- | src/js/utils.js | 137 |
9 files changed, 396 insertions, 301 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index d40165e1..ac7ba2b6 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -6,11 +6,14 @@ import support from './support'; import utils from './utils'; import ui from './ui'; +// Sniff out the browser +const browser = utils.getBrowser(); + const controls = { // Webkit polyfill for lower fill range updateRangeFill(target) { // WebKit only - if (!this.browser.isWebkit) { + if (!browser.isWebkit) { return; } @@ -49,7 +52,7 @@ const controls = { getIconUrl() { return { url: this.config.iconUrl, - absolute: this.config.iconUrl.indexOf('http') === 0 || (this.browser.isIE && !window.svg4everybody), + absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody), }; }, @@ -1139,14 +1142,11 @@ const controls = { inject() { // Sprite if (this.config.loadSprite) { - const iconUrl = controls.getIconUrl.call(this); + const icon = controls.getIconUrl.call(this); // Only load external sprite using AJAX - if (iconUrl.absolute) { - this.log(`AJAX loading absolute SVG sprite ${this.browser.isIE ? '(due to IE)' : ''}`); - utils.loadSprite(iconUrl.url, 'sprite-plyr'); - } else { - this.log('Sprite will be used as external resource directly'); + if (icon.absolute) { + utils.loadSprite(icon.url, 'sprite-plyr'); } } diff --git a/src/js/defaults.js b/src/js/defaults.js index 837b981b..ee863066 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -19,9 +19,18 @@ const defaults = { volume: 1, muted: false, + // Pass a custom duration + duration: null, + // Display the media duration displayDuration: true, + // Aspect ratio (for embeds) + ratio: '16:9', + + // Looping + loop: false, + // Click video to play clickToPlay: true, @@ -42,22 +51,12 @@ const defaults = { // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', - // Pass a custom duration - duration: null, - // Quality default quality: { default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'], }, - // Set loops - loop: { - active: false, - start: null, - end: null, - }, - // Speed default and options to display speed: { default: 1, diff --git a/src/js/listeners.js b/src/js/listeners.js index 9b84a729..7a455c13 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -9,162 +9,14 @@ import fullscreen from './fullscreen'; import storage from './storage'; import ui from './ui'; -const listeners = { - // Listen for media events - media() { - // Time change on media - utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event)); - - // Display duration - utils.on(this.media, 'durationchange loadedmetadata', event => ui.displayDuration.call(this, event)); - - // Handle the media finishing - utils.on(this.media, 'ended', () => { - // Show poster on end - if (this.type === 'video' && this.config.showPosterOnEnd) { - // Restart - this.restart(); - - // Re-load media - this.media.load(); - } - }); - - // Check for buffer progress - utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event)); - - // Handle native mute - utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event)); - - // Handle native play/pause - utils.on(this.media, 'play pause ended', event => ui.checkPlaying.call(this, event)); - - // Loading - utils.on(this.media, 'waiting canplay seeked', event => ui.checkLoading.call(this, event)); - - // Click video - if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') { - // Re-fetch the wrapper - const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`); - - // Bail if there's no wrapper (this should never happen) - if (!wrapper) { - return; - } - - // Set cursor - wrapper.style.cursor = 'pointer'; - - // On click play, pause ore restart - utils.on(wrapper, 'click', () => { - // Touch devices will just show controls (if we're hiding controls) - if (this.config.hideControls && support.touch && !this.media.paused) { - return; - } - - if (this.media.paused) { - this.play(); - } else if (this.media.ended) { - this.restart(); - this.play(); - } else { - this.pause(); - } - }); - } - - // Disable right click - if (this.config.disableContextMenu) { - utils.on( - this.media, - 'contextmenu', - event => { - event.preventDefault(); - }, - false - ); - } - - // Speed change - utils.on(this.media, 'ratechange', () => { - // Update UI - controls.updateSetting.call(this, 'speed'); - - // Save speed to localStorage - storage.set.call(this, { speed: this.speed }); - }); - - // Quality change - utils.on(this.media, 'qualitychange', () => { - // Update UI - controls.updateSetting.call(this, 'quality'); - - // Save speed to localStorage - storage.set.call(this, { quality: this.quality }); - }); - - // Caption language change - utils.on(this.media, 'captionchange', () => { - // Save speed to localStorage - storage.set.call(this, { language: this.language }); - }); +// Sniff out the browser +const browser = utils.getBrowser(); - // Volume change - utils.on(this.media, 'volumechange', () => { - // Save speed to localStorage - storage.set.call(this, { volume: this.volume }); - }); - - // Captions toggle - utils.on(this.media, 'captionsenabled captionsdisabled', () => { - // Update UI - controls.updateSetting.call(this, 'captions'); - - // Save speed to localStorage - storage.set.call(this, { captions: this.captions.enabled }); - }); - - // Proxy events to container - // Bubble up key events for Edge - utils.on(this.media, this.config.events.concat(['keyup', 'keydown']).join(' '), event => { - utils.dispatchEvent.call(this, this.elements.container, event.type, true); - }); - }, - - // Listen for control events - controls() { - // IE doesn't support input event, so we fallback to change - const inputEvent = this.browser.isIE ? 'change' : 'input'; +const listeners = { + // Global listeners + global() { let last = null; - // Trigger custom and default handlers - const proxy = (event, handlerKey, defaultHandler) => { - const customHandler = this.config.listeners[handlerKey]; - - // Execute custom handler - if (utils.is.function(customHandler)) { - customHandler.call(this, event); - } - - // Only call default handler if not prevented in custom handler - if (!event.defaultPrevented && utils.is.function(defaultHandler)) { - defaultHandler.call(this, event); - } - }; - - // Click play/pause helper - const togglePlay = () => { - const play = this.togglePlay(); - - // Determine which buttons - const target = this.elements.buttons[play ? 'pause' : 'play']; - - // Transfer focus - if (utils.is.htmlElement(target)) { - target.focus(); - } - }; - // Get the key code for an event const getKeyCode = event => (event.keyCode ? event.keyCode : event.which); @@ -249,6 +101,7 @@ const listeners = { case 75: // Space and K key if (!held) { + this.warn('togglePlay', event.type); this.togglePlay(); } break; @@ -322,10 +175,10 @@ const listeners = { }; // Keyboard shortcuts - if (this.config.keyboard.focused) { - utils.on(this.elements.container, 'keydown keyup', handleKey, false); - } else if (this.config.keyboard.global) { + if (this.config.keyboard.global) { utils.on(window, 'keydown keyup', handleKey, false); + } else if (this.config.keyboard.focused) { + utils.on(this.elements.container, 'keydown keyup', handleKey, false); } // Detect tab focus @@ -347,6 +200,180 @@ const listeners = { }, 0); }); + // Toggle controls visibility based on mouse movement + if (this.config.hideControls) { + // Toggle controls on mouse events and entering fullscreen + utils.on( + this.elements.container, + 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', + event => { + this.toggleControls(event); + } + ); + } + + // Handle user exiting fullscreen by escaping etc + if (fullscreen.enabled) { + utils.on(document, fullscreen.eventType, event => { + this.toggleFullscreen(event); + }); + } + }, + + // Listen for media events + media() { + // Time change on media + utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event)); + + // Display duration + utils.on(this.media, 'durationchange loadedmetadata', event => ui.displayDuration.call(this, event)); + + // Handle the media finishing + utils.on(this.media, 'ended', () => { + // Show poster on end + if (this.type === 'video' && this.config.showPosterOnEnd) { + // Restart + this.restart(); + + // Re-load media + this.media.load(); + } + }); + + // Check for buffer progress + utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event)); + + // Handle native mute + utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event)); + + // Handle native play/pause + utils.on(this.media, 'play pause ended', event => ui.checkPlaying.call(this, event)); + + // Loading + utils.on(this.media, 'waiting canplay seeked', event => ui.checkLoading.call(this, event)); + + // Click video + if (this.supported.ui && this.config.clickToPlay && this.type !== 'audio') { + // Re-fetch the wrapper + const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`); + + // Bail if there's no wrapper (this should never happen) + if (!wrapper) { + return; + } + + // Set cursor + wrapper.style.cursor = 'pointer'; + + // On click play, pause ore restart + utils.on(wrapper, 'click', () => { + // Touch devices will just show controls (if we're hiding controls) + if (this.config.hideControls && support.touch && !this.media.paused) { + return; + } + + if (this.media.paused) { + this.play(); + } else if (this.media.ended) { + this.restart(); + this.play(); + } else { + this.pause(); + } + }); + } + + // Disable right click + if (this.config.disableContextMenu) { + utils.on( + this.media, + 'contextmenu', + event => { + event.preventDefault(); + }, + false + ); + } + + // Speed change + utils.on(this.media, 'ratechange', () => { + // Update UI + controls.updateSetting.call(this, 'speed'); + + // Save speed to localStorage + storage.set.call(this, { speed: this.speed }); + }); + + // Quality change + utils.on(this.media, 'qualitychange', () => { + // Update UI + controls.updateSetting.call(this, 'quality'); + + // Save speed to localStorage + storage.set.call(this, { quality: this.quality }); + }); + + // Caption language change + utils.on(this.media, 'captionchange', () => { + // Save speed to localStorage + storage.set.call(this, { language: this.language }); + }); + + // Volume change + utils.on(this.media, 'volumechange', () => { + // Save speed to localStorage + storage.set.call(this, { volume: this.volume }); + }); + + // Captions toggle + utils.on(this.media, 'captionsenabled captionsdisabled', () => { + // Update UI + controls.updateSetting.call(this, 'captions'); + + // Save speed to localStorage + storage.set.call(this, { captions: this.captions.enabled }); + }); + + // Proxy events to container + // Bubble up key events for Edge + utils.on(this.media, this.config.events.concat(['keyup', 'keydown']).join(' '), event => { + utils.dispatchEvent.call(this, this.elements.container, event.type, true); + }); + }, + + // Listen for control events + controls() { + // IE doesn't support input event, so we fallback to change + const inputEvent = browser.isIE ? 'change' : 'input'; + + // Trigger custom and default handlers + const proxy = (event, handlerKey, defaultHandler) => { + const customHandler = this.config.listeners[handlerKey]; + + // Execute custom handler + if (utils.is.function(customHandler)) { + customHandler.call(this, event); + } + + // Only call default handler if not prevented in custom handler + if (!event.defaultPrevented && utils.is.function(defaultHandler)) { + defaultHandler.call(this, event); + } + }; + + // Click play/pause helper + const togglePlay = () => { + const play = this.togglePlay(); + + // Determine which buttons + const target = this.elements.buttons[play ? 'pause' : 'play']; + + // Transfer focus + if (utils.is.htmlElement(target)) { + target.focus(); + } + }; + // Play utils.on(this.elements.buttons.play, 'click', event => proxy(event, 'play', togglePlay)); @@ -468,7 +495,7 @@ const listeners = { ); // Polyfill for lower fill in <input type="range"> for webkit - if (this.browser.isWebkit) { + if (browser.isWebkit) { utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', event => { controls.updateRangeFill.call(this, event.target); }); @@ -481,15 +508,6 @@ const listeners = { // Toggle controls visibility based on mouse movement if (this.config.hideControls) { - // Toggle controls on mouse events and entering fullscreen - utils.on( - this.elements.container, - 'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', - event => { - this.toggleControls(event); - } - ); - // Watch for cursor over controls so they don't hide when trying to interact utils.on(this.elements.controls, 'mouseenter mouseleave', event => { this.elements.controls.hover = event.type === 'mouseenter'; @@ -553,13 +571,6 @@ const listeners = { }), false ); - - // Handle user exiting fullscreen by escaping etc - if (fullscreen.enabled) { - utils.on(document, fullscreen.eventType, event => { - this.toggleFullscreen(event); - }); - } }, }; diff --git a/src/js/media.js b/src/js/media.js index 9e53f5fc..46e6bec6 100644 --- a/src/js/media.js +++ b/src/js/media.js @@ -8,6 +8,9 @@ import youtube from './plugins/youtube'; import vimeo from './plugins/vimeo'; import ui from './ui'; +// Sniff out the browser +const browser = utils.getBrowser(); + const media = { // Setup media setup() { @@ -45,7 +48,7 @@ const media = { utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay); // Add iOS class - utils.toggleClass(this.elements.container, this.config.classNames.isIos, this.browser.isIos); + utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch); diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 0b815fa5..83b6d942 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -4,6 +4,7 @@ import utils from './../utils'; import captions from './../captions'; +import controls from './../controls'; import ui from './../ui'; const vimeo = { @@ -15,6 +16,9 @@ const vimeo = { // Add embed class for responsive utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + // Set intial ratio + vimeo.setAspectRatio.call(this); + // Set ID this.media.setAttribute('id', utils.generateId(this.type)); @@ -33,21 +37,31 @@ const vimeo = { } }, + // Set aspect ratio + setAspectRatio(input) { + const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); + const padding = 100 / ratio[0] * ratio[1]; + const offset = (300 - padding) / 6; + this.elements.wrapper.style.paddingBottom = `${padding}%`; + this.media.style.transform = `translateY(-${offset}%)`; + }, + // API Ready ready() { const player = this; // Get Vimeo params for the iframe const options = { - loop: this.config.loop.active, - autoplay: this.config.autoplay, + loop: player.config.loop.active, + autoplay: player.config.autoplay, byline: false, portrait: false, title: false, + speed: true, transparent: 0, }; const params = utils.buildUrlParameters(options); - const id = utils.parseVimeoId(this.embedId); + const id = utils.parseVimeoId(player.embedId); // Build an iframe const iframe = utils.createElement('iframe'); @@ -57,7 +71,7 @@ const vimeo = { player.media.appendChild(iframe); // Setup instance - // https://github.com/vimeo/this.js + // https://github.com/vimeo/player.js player.embed = new window.Vimeo.Player(iframe); // Create a faux HTML5 API using the Vimeo API @@ -99,18 +113,22 @@ const vimeo = { // Restore pause state if (paused) { - this.pause(); + player.pause(); } }, }); // Playback speed - // Not currently supported in Vimeo + let { playbackRate } = player.media; Object.defineProperty(player.media, 'playbackRate', { get() { - return null; + return playbackRate; + }, + set(input) { + playbackRate = input; + player.embed.setPlaybackRate(input); + utils.dispatchEvent.call(player, player.media, 'ratechange'); }, - set() {}, }); // Volume @@ -148,6 +166,17 @@ const vimeo = { }, }); + // Set aspect ratio based on video size + Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { + const ratio = utils.getAspectRatio(dimensions[0], dimensions[1]); + vimeo.setAspectRatio.call(this, ratio); + }); + + // Get available speeds + if (player.config.controls.includes('settings') && player.config.settings.includes('speed')) { + controls.setSpeedMenu.call(player); + } + // Get title player.embed.getVideoTitle().then(title => { player.config.title = title; @@ -156,7 +185,7 @@ const vimeo = { // Get current time player.embed.getCurrentTime().then(value => { currentTime = value; - utils.dispatchEvent.call(this, this.media, 'timeupdate'); + utils.dispatchEvent.call(player, player.media, 'timeupdate'); }); // Get duration @@ -202,31 +231,31 @@ const vimeo = { utils.dispatchEvent.call(player, player.media, 'pause'); }); - this.embed.on('timeupdate', data => { - this.media.seeking = false; + player.embed.on('timeupdate', data => { + player.media.seeking = false; currentTime = data.seconds; - utils.dispatchEvent.call(this, this.media, 'timeupdate'); + utils.dispatchEvent.call(player, player.media, 'timeupdate'); }); - this.embed.on('progress', data => { - this.media.buffered = data.percent; - utils.dispatchEvent.call(this, this.media, 'progress'); + player.embed.on('progress', data => { + player.media.buffered = data.percent; + utils.dispatchEvent.call(player, player.media, 'progress'); if (parseInt(data.percent, 10) === 1) { // Trigger event - utils.dispatchEvent.call(this, this.media, 'canplaythrough'); + utils.dispatchEvent.call(player, player.media, 'canplaythrough'); } }); - this.embed.on('seeked', () => { - this.media.seeking = false; - utils.dispatchEvent.call(this, this.media, 'seeked'); - utils.dispatchEvent.call(this, this.media, 'play'); + player.embed.on('seeked', () => { + player.media.seeking = false; + utils.dispatchEvent.call(player, player.media, 'seeked'); + utils.dispatchEvent.call(player, player.media, 'play'); }); - this.embed.on('ended', () => { - this.media.paused = true; - utils.dispatchEvent.call(this, this.media, 'ended'); + player.embed.on('ended', () => { + player.media.paused = true; + utils.dispatchEvent.call(player, player.media, 'ended'); }); // Rebuild UI diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 38f649a5..84d16488 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -17,6 +17,9 @@ const youtube = { // Add embed class for responsive utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + // Set aspect ratio + youtube.setAspectRatio.call(this); + // Set ID this.media.setAttribute('id', utils.generateId(this.type)); @@ -44,6 +47,12 @@ const youtube = { } }, + // Set aspect ratio + setAspectRatio() { + const ratio = this.config.ratio.split(':'); + this.elements.wrapper.style.paddingBottom = `${100 / ratio[0] * ratio[1]}%`; + }, + // API ready ready(videoId) { const player = this; @@ -66,9 +75,9 @@ const youtube = { origin: window && window.location.hostname, widget_referrer: window && window.location.href, - // Captions is flaky on YouTube - // cc_load_policy: (this.captions.active ? 1 : 0), - // cc_lang_pref: 'en', + // Captions are flaky on YouTube + cc_load_policy: (this.captions.active ? 1 : 0), + cc_lang_pref: this.config.captions.language, }, events: { onError(event) { diff --git a/src/js/plyr.js b/src/js/plyr.js index 5c28887e..355fe5cb 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -13,6 +13,7 @@ import utils from './utils'; import captions from './captions'; import controls from './controls'; import fullscreen from './fullscreen'; +import listeners from './listeners'; import media from './media'; import storage from './storage'; import source from './source'; @@ -78,7 +79,6 @@ class Plyr { }; // Captions - // TODO: captions.enabled should be in config? this.captions = { enabled: null, tracks: null, @@ -192,10 +192,7 @@ class Plyr { return; } - // Sniff out the browser - this.browser = utils.getBrowser(); - - // Load saved settings from localStorage + // Setup local storage for user settings storage.setup.call(this); // Check for support again but with type @@ -217,6 +214,9 @@ class Plyr { // Allow focus to be captured this.elements.container.setAttribute('tabindex', 0); + // Global listeners + listeners.global.call(this); + // Add style hook ui.addStyleHook.call(this); @@ -237,17 +237,27 @@ class Plyr { } } + // --------------------------------------- // API // --------------------------------------- + /** + * If the player is HTML5 + */ get isHTML5() { return types.html5.includes(this.type); } + + /** + * If the player is an embed - e.g. YouTube or Vimeo + */ get isEmbed() { return types.embed.includes(this.type); } - // Play + /** + * Play the media + */ play() { if ('play' in this.media) { this.media.play(); @@ -257,7 +267,9 @@ class Plyr { return this; } - // Pause + /** + * Pause the media + */ pause() { if ('pause' in this.media) { this.media.pause(); @@ -267,7 +279,10 @@ class Plyr { return this; } - // Toggle playback + /** + * Toggle playback based on current status + * @param {boolean} toggle + */ togglePlay(toggle) { // True toggle if nothing passed if ((!utils.is.boolean(toggle) && this.media.paused) || toggle) { @@ -277,31 +292,43 @@ class Plyr { return this.pause(); } - // Stop + /** + * Stop playback + */ stop() { return this.restart().pause(); } - // Restart + /** + * Restart playback + */ restart() { this.currentTime = 0; return this; } - // Rewind + /** + * Rewind + * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime + */ rewind(seekTime) { this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime); return this; } - // Fast forward + /** + * Fast forward + * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime + */ forward(seekTime) { this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime); return this; } - // Seek to time - // The input parameter can be an event or a number + /** + * Seek to a time + * @param {number} input - where to seek to in seconds. Defaults to 0 (the start) + */ set currentTime(input) { let targetTime = 0; @@ -327,7 +354,9 @@ class Plyr { return Number(this.media.currentTime); } - // Duration + /** + * Get the duration of the current media + */ get duration() { // Faux duration set via config const fauxDuration = parseInt(this.config.duration, 10); @@ -339,7 +368,10 @@ class Plyr { return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration; } - // Volume + /** + * Set the player volume + * @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage + */ set volume(value) { let volume = value; const max = 1; @@ -377,6 +409,9 @@ class Plyr { } } + /** + * Get the current player volume + */ get volume() { return this.media.volume; } diff --git a/src/js/ui.js b/src/js/ui.js index c0448054..aa579d8d 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -84,15 +84,17 @@ const ui = { // Update the UI ui.checkPlaying.call(this); + // Ready for API calls this.ready = true; // Ready event at end of execution stack utils.dispatchEvent.call(this, this.media, 'ready'); // Autoplay - if (this.config.autoplay) { + // TODO: check we still need this? + /* if (this.isEmbed && this.config.autoplay) { this.play(); - } + } */ }, // Show the duration on metadataloaded diff --git a/src/js/utils.js b/src/js/utils.js index 4296f345..1c3d6ed8 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -89,6 +89,73 @@ const utils = { firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); }, + // Load an external SVG sprite + loadSprite(url, id) { + if (typeof url !== 'string') { + return; + } + + const prefix = 'cache-'; + const hasId = typeof id === 'string'; + let isCached = false; + + function updateSprite(data) { + // Inject content + this.innerHTML = data; + + // Inject the SVG to the body + document.body.insertBefore(this, document.body.childNodes[0]); + } + + // Only load once + if (!hasId || !document.querySelectorAll(`#${id}`).length) { + // Create container + const container = document.createElement('div'); + container.setAttribute('hidden', ''); + + if (hasId) { + container.setAttribute('id', id); + } + + // Check in cache + if (support.storage) { + const cached = window.localStorage.getItem(prefix + id); + isCached = cached !== null; + + if (isCached) { + const data = JSON.parse(cached); + updateSprite.call(container, data.content); + } + } + + // ReSharper disable once InconsistentNaming + const xhr = new XMLHttpRequest(); + + // XHR for Chrome/Firefox/Opera/Safari + if ('withCredentials' in xhr) { + xhr.open('GET', url, true); + } else { + return; + } + + // Once loaded, inject to container and body + xhr.onload = () => { + if (support.storage) { + window.localStorage.setItem( + prefix + id, + JSON.stringify({ + content: xhr.responseText, + }) + ); + } + + updateSprite.call(container, xhr.responseText); + }; + + xhr.send(); + } + }, + // Generate a random ID generateId(prefix) { return `${prefix}-${Math.floor(Math.random() * 10000)}`; @@ -564,71 +631,11 @@ const utils = { return fragment.firstChild.innerText; }, - // Load an SVG sprite - loadSprite(url, id) { - if (typeof url !== 'string') { - return; - } - - const prefix = 'cache-'; - const hasId = typeof id === 'string'; - let isCached = false; - - function updateSprite(data) { - // Inject content - this.innerHTML = data; - - // Inject the SVG to the body - document.body.insertBefore(this, document.body.childNodes[0]); - } - - // Only load once - if (!hasId || !document.querySelectorAll(`#${id}`).length) { - // Create container - const container = document.createElement('div'); - container.setAttribute('hidden', ''); - - if (hasId) { - container.setAttribute('id', id); - } - - // Check in cache - if (support.storage) { - const cached = window.localStorage.getItem(prefix + id); - isCached = cached !== null; - - if (isCached) { - const data = JSON.parse(cached); - updateSprite.call(container, data.content); - } - } - - // ReSharper disable once InconsistentNaming - const xhr = new XMLHttpRequest(); - - // XHR for Chrome/Firefox/Opera/Safari - if ('withCredentials' in xhr) { - xhr.open('GET', url, true); - } else { - return; - } - - // Once loaded, inject to container and body - xhr.onload = () => { - if (support.storage) { - window.localStorage.setItem( - prefix + id, - JSON.stringify({ - content: xhr.responseText, - }) - ); - } - - updateSprite.call(container, xhr.responseText); - }; - - xhr.send(); - } + // Get aspect ratio for dimensions + getAspectRatio(width, height) { + const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h)); + const ratio = getRatio(width, height); + return `${width / ratio}:${height / ratio}`; }, // Get the transition end event |