From 9a021dfd4f3eb230757a82eaf53e39d06c0795b9 Mon Sep 17 00:00:00 2001 From: jean rodriguez Date: Thu, 28 Feb 2019 15:52:24 -0500 Subject: [PATCH 01/45] component youtube --- app/components/youtube/bootstrap.yml | 14 +++ app/components/youtube/client.js | 118 ++++++++++++++++++++++ app/components/youtube/model.js | 95 +++++++++++++++++ app/components/youtube/schema.yml | 146 +++++++++++++++++++++++++++ app/components/youtube/template.hbs | 53 ++++++++++ 5 files changed, 426 insertions(+) create mode 100644 app/components/youtube/bootstrap.yml create mode 100644 app/components/youtube/client.js create mode 100644 app/components/youtube/model.js create mode 100644 app/components/youtube/schema.yml create mode 100644 app/components/youtube/template.hbs diff --git a/app/components/youtube/bootstrap.yml b/app/components/youtube/bootstrap.yml new file mode 100644 index 0000000..fb72cf0 --- /dev/null +++ b/app/components/youtube/bootstrap.yml @@ -0,0 +1,14 @@ +_components: + youtube: + videoId: '' + videoType: 'editorial' + videoLocation: 'article' + playerCaption: '' + autoPlay: false + autoPlayNextVideo: true + videoPlaylist: '' + playerBorderTopCTA: 'Watch' + playerBorderTop: false + playerBorderBottom: false + previousTypeRelated: false + customPlay: false diff --git a/app/components/youtube/client.js b/app/components/youtube/client.js new file mode 100644 index 0000000..037b3b3 --- /dev/null +++ b/app/components/youtube/client.js @@ -0,0 +1,118 @@ +'use strict'; + +const youtubeVideoPlayer = require('../../services/universal/youtube-video-player'), + $visibility = require('../../services/client/visibility'); + +DS.controller('youtube', ['$gtm', function ($gtm) { + function Constructor(el) { + var videoConfig = { + videoContainerId: el.getAttribute('data-element-id').trim(), + videoId: el.getAttribute('data-video-id').trim(), + // player variables and settings + playerParams: { + loop: 1, + listType: 'playlist', + list: el.getAttribute('data-playlist').trim(), + autoplay: el.getAttribute('data-autoplay-video') === 'true' ? 1 : 0, + controls: 1, + enablejsapi: 1, + modestbranding: 1, + rel: 0, + showinfo: 0, + wmode: 'transparent' + }, + customParams: { + autoPlayNextVideo: el.getAttribute('data-autoplay-next-video').trim(), + trackVideoType: el.getAttribute('data-track-video-type').trim(), + customPlayer: el.getAttribute('data-custom-play').trim(), + templateid: el.getAttribute('data-element-id').trim() + } + }, + analytics = getAnalyticsCustomDimensions(el); + + if (videoConfig.customParams.trackVideoType === 'Sponsored') { + videoConfig.playerParams.list = ''; + } + this.el = el; + this.visible = new $visibility.Visible(this.el, { preloadThreshold: 800 }); + + // when the video player element enters the viewport, load the video(s) + if (this.visible.preload && $visibility.isElementNotHidden(el)) { + // if the YouTube api is ready the videos(s) can be loaded + if (window.nymYTApiReady === true) { + youtubeVideoPlayer.init(videoConfig); + } else { + // wait and listen for the YouTube api to be ready before loading the video(s) + document.addEventListener('nym-youtube-event:youtube-api-ready', function () { + youtubeVideoPlayer.init(videoConfig); + }); + } + } else { + this.visible.on('preload', function () { + youtubeVideoPlayer.init(videoConfig); + }); + } + + /** + * Player ready event + * this fires when the player is initially loaded and pushes variables specific to the + * component into the data layer. Information about the video itself is captured from the + * native gtm.video trigger on play and finish + */ + document.addEventListener('player-ready-' + videoConfig.videoContainerId, function () { + $gtm.reportNow(Object.assign({ + youtubeAction: 'player ready' + }, analytics)); + }); + + /** + * Player start event + * + * we don't need to send an event here, updating the video id for posterity + * also might be nice to send an event if we see the video id changed? + */ + document.addEventListener('player-start-' + videoConfig.videoContainerId, function (evt) { + var hasChanged = el.getAttribute('data-video-id') !== evt.player.videoId; + + if (hasChanged) { + updateElementAttributes(el, evt.player); + // this will tell the gtm.video trigger to stop ignoring gtm.video events + // in the case that an external video was played initially then switched to + // an internal playlist + $gtm.reportNow(Object.assign({ + event: 'youtubeVideoReset', + youtubeVideoId: evt.player.videoId, + youtubeChannelName: 'New York Magazine' + })); + } + }); + } + + /** + * Updates Element attributes + * @param {Object} el - DOM node element + * @param {Object} config - Attributes values from player + */ + function updateElementAttributes(el, config) { + el.setAttribute('data-video-id', config.videoId); + } + + /** + * Gets analytics custom dimensions for video player + * @param {Object} el + * @returns {Object} analytics + */ + function getAnalyticsCustomDimensions(el) { + return { + event: 'youtubeVideo', + youtubeVideoId: el.getAttribute('data-video-id'), + youtubeVideoLocation: el.getAttribute('data-track-video-location'), + youtubeVideoType: el.getAttribute('data-track-video-type'), + youtubeVideoTitle: el.getAttribute('data-track-video-title'), + youtubeChannelName: el.getAttribute('data-track-channel-name'), + youtubeVideoDuration: el.getAttribute('data-track-video-duration') + }; + } + + return Constructor; +}]); diff --git a/app/components/youtube/model.js b/app/components/youtube/model.js new file mode 100644 index 0000000..477d54a --- /dev/null +++ b/app/components/youtube/model.js @@ -0,0 +1,95 @@ +'use strict'; +const _get = require('lodash/get'), + { getVideoDetails } = require('../../services/universal/youtube'), + defaultPlayerBorderTopCTA = 'Watch'; + +/** + * Override various settings by type of video + * @param {object} data + */ +function updateSettingsByType(data) { + switch (data.videoType) { + case 'related': + // By default, display borders and CTA when `related` type is first selected, afterwards accept user's selection + data.playerBorderTopCTA = !data.previousTypeRelated && !data.playerBorderTopCTA ? defaultPlayerBorderTopCTA : data.playerBorderTopCTA; + data.playerBorderTop = !data.previousTypeRelated ? true : data.playerBorderTop; + data.playerBorderBottom = !data.previousTypeRelated ? true : data.playerBorderBottom; + data.previousTypeRelated = true; + break; + case 'sponsored': + data.autoPlay = false; + data.autoPlayNextVideo = false; + default: + // Toggle borders off if user previously selected `related` type. `sponsored` and `editorial` types share defaults + data.playerBorderTop = data.previousTypeRelated ? false : data.playerBorderTop; + data.playerBorderBottom = data.previousTypeRelated ? false : data.playerBorderBottom; + data.previousTypeRelated = false; + } +} + +function clearVideoId(data) { + data.videoId = (data.videoId || '').split('&')[0]; + + return data; +} + +function setVideoDetails(data, videoDetails) { + var maxResThumb; + + if (!videoDetails.title) { + data.videoValid = false; + + return data; + } + + maxResThumb = _get(videoDetails, 'thumbnails.maxres.url'); + + data.videoValid = true; + data.channelName = videoDetails.channelTitle; + data.videoTitle = videoDetails.title; + data.videoThumbnail = maxResThumb ? maxResThumb : _get(videoDetails, 'thumbnails.high.url'); // get the maxres if available, otherwise get the high res which we know will be there + data.videoDuration = videoDetails.duration; + + return data; +} + +function getDefaultPlaylistBySite(data, locals) { + switch (locals.site.slug) { + case 'wwwthecut': + return 'PL4B448958847DA6FB'; + break; + case 'vulture': + return 'PLZQfnFyelTBOQ15kmHSgEbdjzLMWzZpL7'; + break; + case 'grubstreet': + return 'PLtmzdzCeRsyG_td56GV9JtS3yif177lfK'; + break; + case 'di': + return 'PLtmzdzCeRsyHbGTxOX4BZvSgXBh20n-_4'; + break; + case 'selectall': + return 'PLtmzdzCeRsyHh67c-VlEj8Nqpj5nL8pf6'; + break; + default: + return 'PLtmzdzCeRsyFQ64kOTZS7eBLQ1fH2feu7'; // if its a site without a default playlist, use the 'latest from new york' playlist + break; + } +} + +module.exports.save = (uri, data, locals) => { + clearVideoId(data); + updateSettingsByType(data); + + if (data.videoId && !data.videoPlaylist) { + data.videoPlaylist = getDefaultPlaylistBySite(data, locals); + } + + if (data.videoId) { + return getVideoDetails(data.videoId) + .then(videoDetails => setVideoDetails(data, videoDetails)); + } + + data.videoValid = true; // technically not an invalid video because no videoId so we don't want to show an error message in edit mode + + return data; +}; diff --git a/app/components/youtube/schema.yml b/app/components/youtube/schema.yml new file mode 100644 index 0000000..2aca8f1 --- /dev/null +++ b/app/components/youtube/schema.yml @@ -0,0 +1,146 @@ +_version: 1.1 + +_description: | + Youtube video embed for Clay. Can only be used for Youtube videos hosted by NYMag or Vulture. For externally hosted videos, use the Video component. + For technical reasons, YouTube embeds don't display a preview in edit mode. Preview the video by previewing the full page (from the Clay toolbar). Embeds can display a specific video ID. +videoId: + _label: YouTube Video ID + _publish: videoPlayerId + _has: + input: text + _subscribe: articleVideoId + +videoType: + _label: Type + _has: + input: radio + options: + - + name: Editorial (Original) + value: editorial + - + name: Editorial (External) + value: embedded + - + name: Related + value: related + - + name: Sponsored + value: sponsored + validate: + required: true + requiredMessage: Please select a video type + +videoSize: + _label: Size + _has: + input: radio + options: + - + name: Column Width + value: column-width + - + name: Break-out + value: break-out + _reveal: + field: videoType + operator: === + values: + - sponsored + - editorial + +playerHeadline: + _label: Video Headline + _has: + input: text + _reveal: + field: videoType + value: related + +playerCaption: + _label: Video Caption + _has: text + +autoPlay: + _label: Autoplay on Page Load + _has: checkbox + +autoPlayNextVideo: + _label: Automatically start next video + _has: checkbox + _reveal: + field: videoType + operator: '!==' + value: sponsored + +videoPlaylist: + _label: Youtube Playlist (defaults to current site) + _reveal: + field: autoPlayNextVideo + operator: truthy + _has: + input: text + validate: + required: + field: autoPlayNextVideo + operator: truthy + requiredMessage: Video Player should contain video playlist + help: Playlists are used for auto playing additional videos in the YouTube component after the assigned video is finished. Only required for videos that are selected to automatically start next video above + +playerBorderTopCTA: + _label: Top Border Title + _has: text + +playerBorderTop: + _label: Show Top Border + _has: checkbox + +playerBorderBottom: + _label: Show Bottom Border + _has: checkbox + +customPlay: + _subscribe: customPlay + +_groups: + settings: + fields: + - autoPlay (Autoplay) + - autoPlayNextVideo (Autoplay) + - videoPlaylist (Autoplay) + - playerBorderTopCTA (Borders and Border Title) + - playerBorderTop (Borders and Border Title) + - playerBorderBottom (Borders and Border Title) + inline: + fields: + - videoId + - videoType + - videoSize + - playerHeadline + - playerCaption + _label: Youtube Video Player + _placeholder: + text: Youtube Video Player + height: 100px + ifEmpty: videoId + +# set by bootstrap or first-run +videoLocation: + help: For analytics, to tell what kind of page the video is on + +# non-user-editable fields, set by model.js +previousTypeRelated: + help: Toggle for displaying convenience-based changes to player borders based on previously selected video type. Set by model.js +videoValid: + help: Used to decide whether or not to display a video in the template + _placeholder: + text: Invalid video id ${videoId}. This video will not display. + permanent: true +channelName: + help: Name of the channel the video comes from. Used to decide whether or not to listen to GTM triggers for this video +videoTitle: + help: Title of the video +videoThumbnail: + help: maxres video thumbnail +videoDuration: + help: video duration in seconds diff --git a/app/components/youtube/template.hbs b/app/components/youtube/template.hbs new file mode 100644 index 0000000..61467b9 --- /dev/null +++ b/app/components/youtube/template.hbs @@ -0,0 +1,53 @@ +{{ set 'uniquePlayerID' (randomString 'youtube-player-') }} +{{#ifAny @root.locals.edit videoValid}} +
+ {{#if playerBorderTopCTA }} + + {{/if}} + +
+ {{#unless @root.locals.edit}} +
+ {{!-- The