ads/public/NativeAd.js

import customEvent from '../../utils/customEvent';
import updateIconPath from '../../utils/updateIconPath';

/**
 * Module for displaying native ads
 *
 * @module NativeAd
 * @prop {object} selectors - HTML element selectors
 * @prop {string} adUnitCss - css class name of an ad unit div
 * @prop {string} adContainerCss - css class name of an ad container div
 * @prop {object} templates - native ad templates
 */
const NativeAd = {
	selectors: {
		otfAd: '.c-ad--otf .c-ad__unit',
		collapseAdScript: '.c-ad--otf .c-ad__unit--loaded script:not([data-detected])',
		icon: '.c-icon use',
		posts: '.c-posts',
		toolTip: '[data-tip]',
		outbrainScript: '[data-outbrain-script]',
	},

	adUnitCss: 'c-ad__unit',

	adContainerCss: 'c-ad__container',

	hiddenCss: 'is-hidden',

	templates: {
		gnca_sponsored_most_popular: `
		<a href="{{data.url}}" title="{{data.title}}" class="c-ad__inner c-posts__inner">
			<div class="c-ad__media c-posts__media">
				<img src="{{data.image}}" class="c-ad__thumbnail c-posts__thumbnail" alt="">
			</div>
			<div class="c-ad__details c-posts__details">
				<div class="c-ad__headline c-posts__headline">{{data.title}}</div>
				<div class="c-ad__sponsor c-posts__sponsor">
					<span class="c-ad__sponsorLabel c-posts__sponsorLabel">Sponsored by</span>
					<img src="{{data.logo}}" class="c-ad__sponsorLogo c-posts__sponsorLogo" alt=""/>
				</div>
			</div>
		</a>
		`,

		post_tile: `
		<div class="c-posts__inner c-ad__inner">
			<a href="{{data.url}}" title="{{data.title}}" class="c-posts__media c-ad__media c-ad__link" target="_blank" rel="noreferrer">
				<img src="{{data.image}}" class="c-ad__thumbnail c-posts__thumbnail" alt="{{data.title}}" />
			</a>
			<div class="c-posts__details c-ad__details">
				<a href="{{data.url}}" title="{{data.title}}" class="c-posts__headline c-ad__headline c-ad__link" target="_blank" rel="noreferrer">
					{{data.title}}
				</a>
				<div class="c-ad__excerpt">{{data.excerpt}}</div>
				<div class="c-ad__sponsor c-sponsor__details">
					<a href="{{data.url}}" class="c-ad__sponsorLink c-ad__sponsorLogoLink" target="_blank" rel="noreferrer" title="{{data.sponsorLabel}}{{data.sponsor}}">
						<img src="{{data.logo}}" class="c-ad__sponsorLogo" alt="{{data.sponsor}}" />
					</a>
					<div class="c-sponsor__info c-ad__sponsorInfo">
						<span class="c-ad__sponsorLabel c-sponsor__label">{{data.sponsorLabel}}</span>
						<div class="c-ad__sponsorName c-sponsor__name">
							<a href="{{data.url}}" class="c-ad__sponsorLink" target="_blank" rel="noreferrer" title="{{data.sponsorLabel}}{{data.sponsor}}">
								{{data.sponsor}}</span>
							</a>
						</div>
					</div>
					<div class="c-ad__toolTip c-sponsor__button"
						data-tip
						data-tip-mobile-container=".c-sponsor__details"
						data-tip-title="What is this?"
						data-tip-content="Branded content is written by or on behalf of our sponsor and not by Global News' editorial staff."
						data-tip-link-label="If you'd like to learn more..."
						data-tip-link="/pages/sponsored-content-on-global-news/">
						<svg class="c-sponsor__icon c-sponsor__icon--question c-icon"><use xlink:href="#question"></svg>
					</div>
				</div>
			</div>
		</div>
		`,
	},

	/**
	 * Make display functions available for access globally
	 * @method init
	 */
	init() {
		window.gnca_native_ads_lib = {
			display: ( templateId, data, frame ) => {
				this.display( templateId, data, frame );
			},
		};

		/* eslint-disable camelcase */
		window.gn_ads = {};
		window.gn_ads.NativeAd = this;
		/* eslint-enable camelcase */
	},

	/**
	 * Set up ad load listener.
	 *
	 * @method setup
	 * @param {HTMLElement} $ad - DOM element for the ad div
	 */
	setup( $ad ) {
		$ad.addEventListener( customEvent.LOADED, evt => this.otfAdLoaded( true, evt ) );
	},

	/**
	 * Inject a native ad into designated slot
	 *
	 * @method display
	 * @param {string} templateId - ID of a template
	 * @param {object} data - key value pair of data used to populate template
	 * @param {element} frame - DOM element of an ad
	 */
	display( templateId, data, frame ) {
		if ( ! frame || ! frame.id ) {
			return;
		}

		// if ad already exists, bail
		const nativeId = `${frame.id}_native_ad`;
		if ( document.getElementById( nativeId ) ) {
			return;
		}

		const $adNode = this.generateAd( templateId, data, nativeId );

		// hide original frame
		frame.style.display = 'none'; /* eslint-disable-line no-param-reassign */

		// Ensure the targetted element is an ad unit component
		if ( $adNode
			&& frame.parentNode
			&& frame.parentNode.parentNode
			&& frame.parentNode.parentNode.classList.contains( this.adUnitCss ) ) {
			frame.parentNode.parentNode.appendChild( $adNode );
		}
	},

	/**
	 * Collapse an empty ad div or fallback to outbrian widget or branded content native ad
	 *
	 * @method collapseAd
	 */
	collapseAd( $targetAdUnit = false ) {
		let $adUnit = false;

		// Allow OB to manually insert AR_8 widget for testing
		if ( $targetAdUnit ) {
			$adUnit = $targetAdUnit;
		} else {
			let $currentScript = document.currentScript;

			// IE does not support currentScript
			// Look for the script tag that called "NativeAd.collapseAd"
			if ( ! $currentScript ) {
				const $adScripts = document.querySelectorAll( this.selectors.collapseAdScript );
				[].forEach.call( $adScripts, ( $adScript ) => {
					if ( $adScript.textContent.indexOf( 'NativeAd.collapseAd' ) > 0 ) {
						$currentScript = $adScript;
						$currentScript.dataset.detected = true;
					}
				});
			}

			$adUnit = $currentScript.parentNode.parentNode.parentNode;
		}

		const $placeholder = document.querySelector( $adUnit.dataset.adContent );

		// Bail if fallback already handled.
		if ( $adUnit.dataset.adFallbackHandled ) {
			return;
		}

		const adId = $adUnit.getAttribute( 'id' );
		const fallbackType = $adUnit.dataset.adFallbackType ? $adUnit.dataset.adFallbackType.toUpperCase() : '';

		if ( 'BC' === fallbackType ) {
			// Fallback to a branded content native ad
			const fallbackAdId = `${adId}-fallback`;
			const $adNode = document.createElement( 'div' );
			$adNode.setAttribute( 'id', fallbackAdId );
			$adNode.dataset.adContent = $adUnit.dataset.adContent;
			$adNode.classList.add( this.adUnitCss );
			$adUnit.parentNode.append( $adNode );

			$adNode.addEventListener( customEvent.LOADED, evt => this.otfAdLoaded( false, evt ) );

			/* global gn_monetize */
			/* eslint-disable camelcase */
			if ( 'undefined' !== typeof ( gn_monetize ) || 'undefined' !== typeof ( gn_monetize.Ads ) ) {
				gn_monetize.Ads.create({
					sizes: '[2,2]',
					biddable: false,
					id: fallbackAdId,
					lazy: true,
					targeting: {
						pos: `${$adUnit.dataset.adPos}0`,
					},
				});
			}
			/* eslint-enable camelcase */

			$adUnit.classList.add( this.hiddenCss );
		} else if ( 0 === fallbackType.indexOf( 'AR_' ) ) {
			// Fallback to an Outbrain widget AR_8
			const $ob = document.createElement( 'div' );
			$ob.dataset.src = document.location.href.replace( 'local.', '' );
			$ob.classList.add( 'OUTBRAIN' );
			$ob.dataset.widgetId = fallbackType;
			$ob.dataset.obTemplate = 'GlobalNews.ca';

			$placeholder.classList.remove( this.hiddenCss );
			$adUnit.classList.add( this.hiddenCss );
			$placeholder.appendChild( $ob );

			// Only embed outbrain.js script when it hasn't yet been embedded.
			if ( ! document.querySelector( this.selectors.outbrainScript ) ) {
				const $tag = document.createElement( 'script' );
				$tag.src = 'https://widgets.outbrain.com/outbrain.js';
				$tag.async = true;
				$placeholder.appendChild( $tag );
			}

			/* global gnca_settings */
			/* eslint-disable camelcase */
			if ( 'undefined' !== typeof ( gnca_settings ) && false === Boolean( gnca_settings.is_preprod ) ) {
				// Ensure the ad div is visible (it could have been made empty from and empty ad result).
				if ( $adUnit.parentNode && $adUnit.parentNode.classList.contains( this.hiddenCss ) ) {
					$adUnit.parentNode.classList.remove( this.hiddenCss );
				}

				/* global OBR */
				OBR.extern.researchWidget();
			}
			/* eslint-enable camelcase */
		} else {
			// Fallback to a content tile
			const $parent = document.querySelector( `#${$adUnit.dataset.adParent}` );
			const $fallback = $parent ? $parent.querySelectorAll( '[data-ad-fallback="false"]' ) : [];
			const $container = document.querySelector( `[data-ad-container-for=${adId}]` );
			if ( $container ) {
				$container.classList.add( this.hiddenCss );
			}

			if ( $fallback.length > 0 ) {
				$fallback[0].dataset.adFallback = 'true';
				$fallback[0].classList.remove( this.hiddenCss );
			}

			if ( $parent ) {
				const $posts = $parent.querySelector( this.selectors.posts );
				const numCollapsed = $posts.dataset.adNumCollapsed ? $posts.dataset.adNumCollapsed : 0;
				$posts.dataset.adNumCollapsed = numCollapsed + 1;
			}
		}

		$adUnit.dataset.adFallbackHandled = true;
	},

	/**
	 * Generate a native ad node with template markup
	 *
	 * @method generateAd
	 * @param {string} templateId - ID of a template
	 * @param {object} data - key value pair for populating an ad template
	 */
	generateAd( templateId, data, adId = '' ) {
		templateId = templateId.replace( /-/g, '_' ); /* eslint-disable-line no-param-reassign */
		let template = this.templates[templateId];
		if ( ! template ) {
			return null;
		}

		const keys = Object.keys( data );
		[].forEach.call( keys, ( key ) => {
			const search = `{{data.${key}}}`;
			const reg = new RegExp( search, 'g' );
			template = template.replace( reg, data[key]);
		});

		const $adNode = document.createElement( 'div' );
		$adNode.classList.add( this.adContainerCss );

		if ( adId ) {
			$adNode.id = adId;
		}

		$adNode.innerHTML = template;

		const $icon = $adNode.querySelector( this.selectors.icon );
		updateIconPath( $icon );

		return $adNode;
	},

	/**
	 * Populate native ad with data returned from ad server
	 * as gnca-sponsored-featured-content input field
	 *
	 * @method otfAdLoaded
	 */
	otfAdLoaded( initialLoad, evt ) {
		const $iframe        = evt.target.querySelector( 'iframe' );
		const $placeholder   = document.querySelector( evt.target.dataset.adContent );
		let $adInput = false;

		try {
			if ( $iframe ) {
				$adInput = $iframe.contentDocument.querySelector( '.gnca-sponsored-featured-content' );
			}
		} catch ( e ) {
			// accessing $iframe.contentDocument causes access deined errors on IE.
		}

		if ( $placeholder && $adInput ) {
			// Hide actual ad div
			evt.target.classList.add( this.hiddenCss );

			const imageUrl = $adInput.dataset.ad_post_image.replace( /w=\d+&h=\d+/, 'w=720&h=480' );
			const adData = {
				title: $adInput.dataset.ad_post_title,
				image: imageUrl,
				url: $adInput.dataset.ad_post_link,
				logo: $adInput.dataset.ad_auth_image,
				sponsor: $adInput.dataset.ad_auth_bold,
				sponsorLabel: $adInput.dataset.ad_auth_text,
				sponsorUrl: '',
				excerpt: $adInput.dataset.ad_post_content,
				pos: evt.target.dataset.adPos,
			};

			if ( $adInput.dataset.ad_auth_link ) {
				adData.sponsorUrl = $adInput.dataset.ad_auth_link;
			}

			const $adNode = this.generateAd( 'post-tile', adData );
			$placeholder.appendChild( $adNode );
			$placeholder.classList.remove( this.hiddenCss );

			/* global gn_main */
			/* global gn_analytics */
			/* eslint-disable camelcase */

			// Add permutive affiliated link tracking
			const $affiliatedLinks = $placeholder.querySelectorAll( 'a' );
			[].forEach.call( $affiliatedLinks, ( $link ) => {
				$link.addEventListener( 'click', () => {
					if ( 'undefined' !== typeof ( gn_analytics )
						&& 'undefined' !== typeof ( gn_analytics.Analytics ) ) {
						gn_analytics.Analytics.track(['permutive'], {
							eventType: 'click',
							data: {
								affiliate: adData.sponsor.trim(),
								href: adData.url,
							},
						});
					}
				});
			});

			// Add tool tip
			// This must be done after the node's been added in order to determine
			// whether or not the tip stretches beyond the browser width and
			// apply appropriate custom css to handle the scenario
			if ( 'undefined' !== typeof ( gn_main ) && gn_main.ToolTip ) {
				const $toolTip = $placeholder.querySelector( this.selectors.toolTip );
				gn_main.ToolTip.initItem( $toolTip, adData.pos, $placeholder );
			}
			/* eslint-enable camelcase */
		} else if ( initialLoad && evt.detail.isEmpty && evt.target.dataset.adFallbackType ) {
			this.collapseAd( evt.target );
		} else if ( $placeholder ) {
			$placeholder.classList.add( this.hiddenCss );
		}
	},
};

export default NativeAd;