import customEvent from '../../utils/customEvent';
import InView from '../../utils/classes/InView';
import NativeAd from './NativeAd';
import Cookies from '../../vendor/jscookie';
import '../../vendor/amazon-bidding-helper';
/* global googletag */
/* global GNCA_APS_Helper */
/* global gnAdSettings */
/* global apstag */
/**
* Module for handling pre-defined ad slots and dynamically generated ad slots.
*
* @module Ads
* @prop {string} gptAdPath - ad unit path
* @prop {string} states.loaded - CSS class for div with ad loaded
* @prop {string} states.empty - CSS Class for empty ad div
* @prop {string} states.hidden - CSS class for hiding an element
* @prop {Object} ads - all pre-defined ad slots and data
* @prop {Object} watcher - in view watcher for lazy loading of ads
*/
const Ads = {
states: {
loaded: 'c-ad__unit--loaded',
empty: 'c-ad__unit--empty',
adUnitFallback: 'c-ad__unit--fallback',
adFallback: 'c-ad--fallback',
hidden: 'is-hidden',
},
inViewSelector: '.c-ad__unit[data-in-view=true]',
gptAdPath: '',
ads: {},
watcher: null,
pageLoaded: false,
bidTimeout: 1200,
bidEnabled: true,
overNineteenCookie: 'over-nineteen',
/**
* Scrape global variable gnAdSettings defined on the page to define ad slots,
* set up page targeting, and set up listeners for ad loaded events
*
* @method init
*/
init() {
this.gptAdPath = 'undefined' !== typeof ( gnAdSettings ) && gnAdSettings.adPath ? gnAdSettings.adPath : '/6872/shaw.globalnews.ca';
// set up in view watcher for lazy load ads
this.watcher = new InView({
threshold: 0.1,
rootMargin: `${window.innerHeight * 0.4}px`,
}, 'adWatcher' );
this.watcher.init();
/* global gnca_settings */
/* eslint-disable camelcase */
if ( gnca_settings && gnca_settings.ad_settings ) {
if ( 'undefined' !== typeof ( gnca_settings.ad_settings.bidding_timeout ) ) {
this.bidTimeout = gnca_settings.ad_settings.bidding_timeout;
}
if ( 'undefined' !== typeof ( gnca_settings.ad_settings.bidding_enabled ) ) {
this.bidEnabled = gnca_settings.ad_settings.bidding_enabled;
}
}
/* eslint-enable camelcase */
// Configure google ads and enable googletag services
googletag.cmd.push( () => {
googletag.pubads().collapseEmptyDivs();
googletag.companionAds().setRefreshUnfilledSlots( true );
// Set page targeting
[].forEach.call( Object.keys( gnAdSettings.pageTargeting ), ( key ) => {
googletag.pubads().setTargeting( key, gnAdSettings.pageTargeting[key]);
});
if ( 'undefined' !== typeof ( window.ageGateRequired ) && Cookies.get( this.overNineteenCookie ) ) {
googletag.pubads().setTargeting( 'userOverNineteen', 'true' );
}
googletag.pubads().collapseEmptyDivs();
googletag.companionAds().setRefreshUnfilledSlots( true );
// Batch SRA implemented
googletag.pubads().enableSingleRequest();
// Disable initial load
googletag.pubads().disableInitialLoad();
googletag.enableServices();
});
// Slot render ended listener
googletag.cmd.push( () => {
googletag.pubads().addEventListener( 'slotRenderEnded', ( event ) => {
const divId = event.slot.getSlotElementId();
const adDiv = document.querySelector( `#${divId}` );
if ( adDiv ) {
adDiv.classList.add( this.states.loaded );
adDiv.dataset.loadTime = ( new Date() ).getTime();
let [width, height] = [0, 0];
if ( event.isEmpty ) {
let adUnitCss = this.states.empty;
let adCss = this.states.hidden;
// Only trigger NativeAd.collapse manually if the ad slot isn't a NativeAd.
if ( adDiv.dataset.adFallbackType && ! adDiv.dataset.isNativeAd ) {
adUnitCss = this.states.adUnitFallback;
adCss = this.states.adFallback;
NativeAd.collapseAd( adDiv );
}
adDiv.classList.add( adUnitCss );
if ( adDiv.parentNode ) {
adDiv.parentNode.classList.add( adCss );
}
} else if ( event.size ) {
[width, height] = event.size;
adDiv.dataset.width = width;
adDiv.dataset.height = height;
}
const parentComponent = document.querySelector( `[data-ad="${divId}"]` );
if ( parentComponent ) {
parentComponent.dataset.adEmpty = event.isEmpty.toString();
}
this.broadcastLoaded( adDiv, {
isEmpty: event.isEmpty,
size: [width, height],
});
}
});
});
// Create ads defined in gnAdSettings.ads, including lazy and non-lazy ads.
if ( 'undefined' !== typeof ( gnAdSettings )
&& 'object' === typeof ( gnAdSettings.ads ) ) {
[].forEach.call( gnAdSettings.ads, ( ad ) => {
this.create( ad );
});
}
// Request non-lazy load ads
googletag.cmd.push( () => {
this.requestOnPageLoad();
});
},
/**
* Request all non-lazy and in view ads on page load
*
* @method requestOnPageLoad
*/
requestOnPageLoad() {
this.pageLoaded = true;
// Rely on this.display() to make ad request via bidding
const ads = this.getAds( '', { lazy: false });
const $inViewAds = document.querySelectorAll( this.inViewSelector );
[].forEach.call( $inViewAds, ( $ad ) => {
const adId = $ad.getAttribute( 'id' );
ads.push( this.ads[adId]);
});
this.display( ads );
},
/**
* Register ad related data to be retrieved for ad bidding.
* Add inView event listener to trigger lazy load ads.
*
* @method registerAd
* @param {Object} ad - div id, slot defined via googletag, ad size mapping for responsive ads
*/
register( ad ) {
if ( ad && ad.id ) {
this.ads[ad.id] = ad;
if ( ad.lazy ) {
const $targetDiv = document.querySelector( `#${ad.id}` );
if ( $targetDiv ) {
$targetDiv.addEventListener( customEvent.IN_VIEW, ( evt ) => {
if ( evt && evt.detail && evt.detail.isInView ) {
evt.detail.caller.stopWatching( $targetDiv );
// If initial ad request hasn't been made yet,
// do not trigger display(), allow iniitial request
// to load all non-lazy and in-view ads.
if ( this.initialRequestMade() ) {
this.display( ad );
}
}
});
// This fires an IN_VIEW event right away if $targetDiv is already in view.
this.watcher.startWatching( $targetDiv );
}
}
}
},
/**
* Call this function when an ad unit has been loaded
*
* @method broadcastLoaded
* @param {Object} $el - an element reference to the ad div that is ready to load
*/
broadcastLoaded( $el, data ) {
customEvent.fire( $el, customEvent.LOADED, data );
},
/**
* Get all registered ad data and ad slot.
* @method getAds
* @param {String} attr - optional. When provided, retrieve the property of each ad
* @param {Object} filter - optional. Filters results by key/value specified in $filter
* @returns {Array} - All registered ads or an array of attributes of register ads
*/
getAds( attr, filter ) {
let result = this.ads ? Object.keys( this.ads ).map( key => this.ads[key]) : [];
if ( filter ) {
const filterKeys = Object.keys( filter );
result = result.filter( ( ad ) => {
let include = true;
[].forEach.call( filterKeys, ( k ) => {
include = include && ( filter[k] === ad[k]);
});
return include;
});
}
if ( attr ) {
let values = Object.keys( result ).map( key => result[key]);
values = values.map( ( ad ) => {
if ( Object.keys( ad ).indexOf( attr ) >= 0 ) {
return ad[attr];
}
return false;
});
result = values.filter( val => val );
}
return result;
},
/**
* Display ads via ad bidding or regular ad refresh
*
* @method display
* @param {Array} ads - An array of ad objects
*/
display( ads ) {
const thatGptAdPath = this.gptAdPath;
const adsToDisplay = ( 'undefined' === typeof ( ads.length ) ) ? [ads] : ads;
const gptSlots = [];
const biddingSlots = [];
for ( let i = 0; i < adsToDisplay.length; i += 1 ) {
const ad = adsToDisplay[i];
const $adDiv = document.querySelector( `#${ad.id}` );
// Ignore ad if it isn't physically visible,
// unless it's a out of page slot (wallpaper / catfish).
if ( ad.outOfPage
|| ( $adDiv.parentNode
&& 'none' !== getComputedStyle( $adDiv.parentNode ).getPropertyValue( 'display' ) ) ) {
$adDiv.dataset.adInitialized = true;
if ( ad.biddable ) {
/* eslint-disable camelcase */
const biddingSizes = ad.mappingObj && ( 'undefined' !== typeof ( GNCA_APS_Helper ) ) ? GNCA_APS_Helper.parseSizeMappings( ad.mappingObj ) : JSON.parse( ad.sizes );
/* eslint-enable camelcase */
biddingSlots.push({
slotID: ad.id,
slotName: `${thatGptAdPath}/${ad.id}`,
sizes: biddingSizes,
});
}
gptSlots.push( ad.slot );
}
}
if ( this.amazonBiddingSupported() && biddingSlots.length > 0 ) {
apstag.fetchBids({
slots: biddingSlots,
timeout: this.bidTimeout,
}, ( bids ) => { // eslint-disable-line no-unused-vars
// bids information returned from Amazon, might be useful in the future
googletag.cmd.push( () => {
apstag.setDisplayBids();
googletag.pubads().refresh( gptSlots );
});
});
} else {
googletag.cmd.push( () => {
googletag.pubads().refresh( gptSlots );
});
}
},
/**
* Refresh ads specified by an array of ad objects.
*
* Note:
* Currently, all we have to do is call display(), but in case anything changes in the future
* where refreshing of ads need to behave differently we can update this function.
*
* @method refresh
* @param {Array} ads - An array of ad objects
*/
refresh( ads ) {
this.display( ads );
},
/**
* Create dynamic ads that are not defined in the markup
*
* @method createAd
* @param {Object} ad - ad data used for creating an ad
*/
create( ad ) {
const adData = ad;
googletag.cmd.push( () => {
let slot = false;
const $adDiv = document.querySelector( `#${adData.id}` );
// Bail if ad div does not exist
if ( ! $adDiv ) {
return;
}
if ( $adDiv.dataset.isNativeAd ) {
NativeAd.setup( $adDiv );
}
if ( ! ad.outOfPage ) {
let sizes = JSON.parse( ad.sizes );
if ( ad.fluid ) {
if ( 'number' === typeof ( sizes[0]) ) {
sizes = [sizes];
}
sizes.push( 'fluid' );
}
slot = googletag.defineSlot( this.gptAdPath, sizes, ad.id )
.addService( googletag.pubads() );
} else {
slot = googletag.defineOutOfPageSlot( this.gptAdPath, ad.id )
.addService( googletag.pubads() );
}
if ( ad.companion ) {
slot.addService( googletag.companionAds() );
}
const { mapping } = ad;
if ( mapping ) {
const mappingObj = googletag.sizeMapping();
[].forEach.call( Object.keys( mapping ), ( bp ) => {
mappingObj.addSize( JSON.parse( bp ), JSON.parse( mapping[bp]) );
});
adData.mappingObj = mappingObj.build();
slot.defineSizeMapping( adData.mappingObj );
}
const { targeting } = ad;
if ( targeting ) {
[].forEach.call( Object.keys( targeting ), ( key ) => {
slot.setTargeting( key, targeting[key]);
});
}
adData.slot = slot;
// Ensure in view event listener in place before watching for in view.
this.register( adData );
});
},
/**
* Check if apstag amazon bidding is available
*
* @method amazonBiddingSupported
*/
amazonBiddingSupported() {
let result = false;
if ( this.bidEnabled && apstag && apstag.targetingKeys ) {
const keys = apstag.targetingKeys();
// keys return empty array if apstag script failed to load
result = keys && keys.length > 0;
}
return result;
},
/**
* Check if initial ad request made.
*
* @method initialRequestMade
*/
initialRequestMade() {
return this.pageLoaded;
},
};
export default Ads;