vuetube
Version:
A fast, lightweight, lazyload vue component acting as a thin layer over the YouTube IFrame Player API which renders fast
491 lines (444 loc) • 13.7 kB
JavaScript
import { DEFAULT_HOST, NO_COOKIES_HOST, YOUTUBE_API_URL, YTIMG_URL, GOOGLE_FONTS_URL, GOOGLE_ADS_URL, GOOGLE_URL, YT3_URL, GSTATIC_URL } from '../../utils/constants.js';
import { createWarmLink } from '../../helpers/createWarmLink.js';
import { getStringifiedParams } from '../../helpers/getStringifiedParams.js';
import { loadYoutubeAPI } from '../../helpers/loadYoutubeAPI.js';
import { getNormalizeSlot } from '../../helpers/getNormalizedSlot.js';
import { isFunction } from '../../helpers/inspect.js';
import Vue from 'vue';
var VueTube = /* #__PURE__ */Vue.extend({
name: 'VueTube',
props: {
/**
* The ID of YouTube video
*/
videoId: {
type: String,
required: true
},
/**
* Should embed a playlist of several videos
*/
isPlaylist: {
type: Boolean,
default: false
},
/**
* The aspect ratio for iframe
*/
aspectRatio: {
type: Number,
default: 16 / 9
},
/**
* Change video host to www.youtube.com
* By default, video loaded from https://www.youtube-nocookie.com
*/
enableCookies: {
type: Boolean,
default: false
},
/**
* Parameters that are available in the YouTube embedded player.
* @link https://developers.google.com/youtube/player_parameters#Parameters
*/
playerVars: {
type: Object,
default: function _default() {
return {};
}
},
/**
* Disable warming up connections to origins that are in the critical path
*/
disableWarming: {
type: Boolean,
default: false
},
/**
* Disable webp thumbnail
*/
disableWebp: {
type: Boolean,
default: false
},
/**
* Alt attribute for image
*/
imageAlt: {
type: String,
default: ''
},
/**
* Loading attribute for image
* @link https://caniuse.com/loading-lazy-attr
*/
imageLoading: {
type: String,
default: 'lazy',
validator: function validator(value) {
return ['lazy', 'eager', 'auto'].indexOf(value) !== -1;
}
},
/**
* Thumbnail resolution from YouTube API
* @link https://stackoverflow.com/a/18400445/13374604
*/
resolution: {
type: String,
default: 'sddefault',
validator: function validator(value) {
return ['maxresdefault', 'sddefault', 'hqdefault', 'mqdefault', 'default'].indexOf(value) !== -1;
}
},
/**
* Aria-label attribute for button
*/
buttonLabel: {
type: String,
default: 'Play video'
},
/**
* Title attribute for iframe
*/
iframeTitle: {
type: String,
default: undefined
},
/**
* Allow attribute for iframe
*/
iframeAllow: {
type: String,
default: 'accelerometer;autoplay;encrypted-media;gyroscope;picture-in-picture'
}
},
data: function data() {
return {
/**
* Are preconnect links already appended to the head
*/
isConnectionWarmed: false,
/**
* Is video played
*/
isPlayed: false,
/**
* Is video loaded
*/
isLoaded: false,
/**
* Player instance
*/
player: null
};
},
/**
* Clear out the reference to the destroyed player
*/
beforeDestroy: function beforeDestroy() {
var player = this.player;
if (player !== null && typeof player.destroy === 'function') {
player.destroy();
this.player = null;
}
},
computed: {
/**
* Calculated video host
*/
host: function host() {
var enableCookies = this.enableCookies;
return enableCookies ? DEFAULT_HOST : NO_COOKIES_HOST;
},
/**
* Calculate iframe url with params
*/
iframeUrl: function iframeUrl() {
var playerVars = this.playerVars,
host = this.host,
videoId = this.videoId,
isPlaylist = this.isPlaylist;
var DEFAULT_PARAMS = {
autoplay: 1
};
var CONCAT_PARAMS = Object.assign({}, DEFAULT_PARAMS, playerVars);
if (isPlaylist) {
var STRINGIFIED_PLAYLIST_PARAMS = getStringifiedParams(Object.assign({}, CONCAT_PARAMS, {
list: videoId
}));
return "".concat(host, "/embed/videoseries").concat(STRINGIFIED_PLAYLIST_PARAMS);
}
var STRINGIFIED_SINGLE_VIDEO_PARAMS = getStringifiedParams(CONCAT_PARAMS);
return "".concat(host, "/embed/").concat(videoId).concat(STRINGIFIED_SINGLE_VIDEO_PARAMS);
},
/**
* Calculate padding for aspect ratio
* @link https://css-tricks.com/aspect-ratio-boxes/
*/
calculatedAspectRatioPadding: function calculatedAspectRatioPadding() {
var aspectRatio = this.aspectRatio;
return "".concat(100 / aspectRatio, "%");
},
/**
* Wrapper component
*/
boxComponent: function boxComponent() {
var $createElement = this.$createElement,
calculatedAspectRatioPadding = this.calculatedAspectRatioPadding,
isPlayed = this.isPlayed,
iframeComponent = this.iframeComponent,
thumbnailComponent = this.thumbnailComponent,
playBtnComponent = this.playBtnComponent,
warmConnections = this.warmConnections,
playVideo = this.playVideo;
var BOX_COMPONENT = $createElement('div', {
on: {
'~pointerover': warmConnections,
'~click': playVideo
},
class: ['vuetube', {
'vuetube--played': isPlayed
}]
}, [$createElement('div', {
class: 'vuetube__box',
style: {
'padding-bottom': calculatedAspectRatioPadding
}
}, [isPlayed ? iframeComponent : thumbnailComponent, playBtnComponent])]);
return BOX_COMPONENT;
},
/**
* Picture component
*/
thumbnailComponent: function thumbnailComponent() {
var $slots = this.$slots,
$scopedSlots = this.$scopedSlots,
$createElement = this.$createElement,
resolution = this.resolution,
videoId = this.videoId,
disableWebp = this.disableWebp,
imageAlt = this.imageAlt,
imageLoading = this.imageLoading;
var webp = "".concat(YTIMG_URL, "/vi_webp/").concat(videoId, "/").concat(resolution, ".webp");
var jpg = "".concat(YTIMG_URL, "/vi/").concat(videoId, "/").concat(resolution, ".jpg");
var THUMBNAIL_COMPONENT = $createElement('picture', {
class: ['vuetube__thumbnail']
}, [!disableWebp && $createElement('source', {
attrs: {
srcset: webp,
type: 'image/webp'
}
}), $createElement('source', {
attrs: {
srcset: jpg,
type: 'image/jpeg'
}
}), $createElement('img', {
class: ['vuetube__image'],
attrs: {
src: jpg,
alt: imageAlt,
loading: imageLoading
}
})]);
return getNormalizeSlot('thumbnail', {}, $slots, $scopedSlots) || THUMBNAIL_COMPONENT;
},
/**
* Button component
*/
playBtnComponent: function playBtnComponent() {
var $createElement = this.$createElement,
buttonLabel = this.buttonLabel,
playVideo = this.playVideo,
playBtnIconComponent = this.playBtnIconComponent;
var PLAY_BTN_COMPONENT = $createElement('button', {
class: ['vuetube__button'],
attrs: {
type: 'button',
'aria-label': buttonLabel
},
on: {
'~click': playVideo
}
}, [playBtnIconComponent]);
return PLAY_BTN_COMPONENT;
},
/**
* Icon component
*/
playBtnIconComponent: function playBtnIconComponent() {
var $createElement = this.$createElement,
$slots = this.$slots,
$scopedSlots = this.$scopedSlots;
var PLAY_BTN_ICON_COMPONENT = $createElement('svg', {
attrs: {
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 68 48',
class: 'vuetube__icon',
'aria-hidden': true,
focusable: 'false'
}
}, [$createElement('path', {
attrs: {
class: 'vuetube__icon-bg',
d: 'M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z'
}
}), $createElement('path', {
attrs: {
class: 'vuetube__icon-triangle',
d: 'M45 24L27 14v20'
}
})]);
return getNormalizeSlot('icon', {}, $slots, $scopedSlots) || PLAY_BTN_ICON_COMPONENT;
},
/**
* Iframe component
*/
iframeComponent: function iframeComponent() {
var $createElement = this.$createElement,
iframeTitle = this.iframeTitle,
onIframeLoad = this.onIframeLoad,
iframeAllow = this.iframeAllow,
iframeUrl = this.iframeUrl;
var IFRAME_COMPONENT = $createElement('iframe', {
ref: 'iframe',
class: ['vuetube__iframe'],
attrs: {
src: iframeUrl,
allow: iframeAllow,
title: iframeTitle,
allowfullscreen: true
},
on: {
load: onIframeLoad
}
});
return IFRAME_COMPONENT;
}
},
methods: {
/**
* Add preconnect links
*/
warmConnections: function warmConnections() {
var disableWarming = this.disableWarming,
enableCookies = this.enableCookies,
isConnectionWarmed = this.isConnectionWarmed;
if (disableWarming || isConnectionWarmed) {
return;
}
var DEFAULT_PRECONNECTS = [DEFAULT_HOST, GOOGLE_ADS_URL];
var NO_COOKIES_PRECONNECTS = [NO_COOKIES_HOST];
var COMMON_PRECONNECTS = [GOOGLE_FONTS_URL, YTIMG_URL, GOOGLE_URL, YT3_URL, GSTATIC_URL];
var FINAL_PRECONNECTS = [];
var PRECONNECTS = Array.from(document.querySelectorAll('link[rel=preconnect]'));
var PRECONNECTED_URLS = PRECONNECTS.map(function (link) {
return link.href;
});
if (enableCookies) {
FINAL_PRECONNECTS = DEFAULT_PRECONNECTS.concat(COMMON_PRECONNECTS);
} else {
FINAL_PRECONNECTS = NO_COOKIES_PRECONNECTS.concat(COMMON_PRECONNECTS);
}
var FILTERED_PRECONNECTS = FINAL_PRECONNECTS.filter(function (preconnect) {
return PRECONNECTED_URLS.indexOf("".concat(preconnect, "/")) === -1;
});
FILTERED_PRECONNECTS.forEach(function (preconnect) {
createWarmLink(preconnect, preconnect === GOOGLE_FONTS_URL);
});
this.isConnectionWarmed = true;
},
/**
* Run video
*/
playVideo: function playVideo() {
this.isPlayed = true;
this.$emit('player:play');
},
/**
* Run after iframe has been loaded
*/
onIframeLoad: function onIframeLoad() {
var $refs = this.$refs,
playerVars = this.playerVars,
initAPI = this.initAPI;
this.isLoaded = true;
var el = $refs.iframe;
el.focus(); // @ts-expect-error check user vars
var SHOULD_LOAD_API = playerVars.enablejsapi === 1;
if (SHOULD_LOAD_API) {
initAPI();
}
this.$emit('player:load');
},
/**
* Init YouTube API
* @link https://developers.google.com/youtube/iframe_api_reference
*/
initAPI: function initAPI() {
var _this = this;
if (window.YT && isFunction(window.YT.Player)) {
this.initAPIPlayer();
} else {
var prevOnYouTubeIframeAPIReady = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = function () {
if (isFunction(prevOnYouTubeIframeAPIReady)) {
prevOnYouTubeIframeAPIReady();
}
_this.initAPIPlayer();
};
var scripts = Array.from(document.getElementsByTagName('script'));
var isLoaded = scripts.some(function (script) {
return script.src === YOUTUBE_API_URL;
});
if (!isLoaded) {
loadYoutubeAPI();
}
}
},
/**
* Build player with YouTube API
* @link https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player
*/
initAPIPlayer: function initAPIPlayer() {
var _this2 = this;
var $refs = this.$refs,
videoId = this.videoId,
playerVars = this.playerVars;
var el = $refs.iframe;
var player = new window.YT.Player(el, {
videoId: videoId,
playerVars: playerVars,
events: {
onReady: function onReady(e) {
return _this2.$emit('player:ready', e);
},
onStateChange: function onStateChange(e) {
return _this2.$emit('player:statechange', e);
},
onPlaybackQualityChange: function onPlaybackQualityChange(e) {
return _this2.$emit('player:playbackqualitychange', e);
},
onPlaybackRateChange: function onPlaybackRateChange(e) {
return _this2.$emit('player:playbackratechange', e);
},
onError: function onError(e) {
return _this2.$emit('player:error', e);
},
onApiChange: function onApiChange(e) {
return _this2.$emit('player:apichange', e);
}
}
});
this.player = player;
}
},
/**
* Render component
*/
render: function render() {
var boxComponent = this.boxComponent;
return boxComponent;
}
});
export default VueTube;