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;