UNPKG

vuetube

Version:

A fast, lightweight, lazyload vue component acting as a thin layer over the YouTube IFrame Player API which renders fast

560 lines (508 loc) 15.6 kB
'use strict';Object.defineProperty(exports,'__esModule',{value:true});var Vue=require('vue');function _interopDefaultLegacy(e){return e&&typeof e==='object'&&'default'in e?e:{'default':e}}var Vue__default=/*#__PURE__*/_interopDefaultLegacy(Vue);/** * YouTube hosts */ var DEFAULT_HOST = 'https://www.youtube.com'; var NO_COOKIES_HOST = 'https://www.youtube-nocookie.com'; /** * Origins that are in the critical path */ var GOOGLE_ADS_URL = 'https://googleads.g.doubleclick.net'; var GOOGLE_FONTS_URL = 'https://fonts.gstatic.com'; var GOOGLE_URL = 'https://google.com'; var YTIMG_URL = 'https://i.ytimg.com'; var GSTATIC_URL = 'https://www.gstatic.com'; var YT3_URL = 'https://yt3.ggpht.com'; var YOUTUBE_API_URL = 'https://www.youtube.com/iframe_api'; /** * Add preconnect links to the head of the document * @link https://www.igvita.com/2015/08/17/eliminating-roundtrips-with-preconnect/ */ function createWarmLink(url, addCrossorigin) { var el = document.createElement('link'); el.rel = 'preconnect'; el.href = url; if (addCrossorigin) { el.crossOrigin = ''; } document.head.appendChild(el); }/** * A simple function to format object to GET parameters. */ function getStringifiedParams(object) { var keys = Object.keys(object); var params = keys.filter(function (key) { return object[key] !== undefined; }).map(function (key) { return "".concat(key, "=").concat(object[key]); }); return params.length > 0 ? "?".concat(params.join('&')) : ''; }/** * Load youtube API * @link https://developers.google.com/youtube/iframe_api_reference */ function loadYoutubeAPI() { var script = document.createElement('script'); script.src = YOUTUBE_API_URL; document.head.appendChild(script); }/** * Return typeof value */ function toType(value) { return typeof value; } /** * Check is typeof value is function */ function isFunction(value) { return toType(value) === 'function'; }/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types */ /** * Get normalized vue slot */ function getNormalizeSlot(name, props, $slots, $scopedSlots) { var slot = $scopedSlots[name] || $slots[name]; return isFunction(slot) ? slot(props) : slot; }var VueTube = /* #__PURE__ */Vue__default['default'].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; } });/** * Import vue components */ /** * Install function executed by Vue.use() */ var install = function installVueTube(Vue) { Vue.component(VueTube.name, VueTube); };exports.VueTube=VueTube;exports.default=install;