UNPKG

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
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;