if (typeof window.db == 'undefined') window.db = { libs: {} }; (function() { 'use strict'; /** * Component responsible for requesting ads from DFP, both * lazyloaded and normal. * @namespace */ db.libs.adsDefer = (function(){ /** * The name of the component. * @private * @memberof db.libs.adsDefer * @type {string} */ var name = "adsDefer"; /** * The threshold of how many pixels before an adunit comes into * view the request to DFP should be made. * @private * @memberof db.libs.adsDefer * @type {number} */ var threshold = 400; /** * The array for all the ads to be requested on load. * @private * @memberof db.libs.adsDefer * @type {array} */ var ads = []; /** * The array for all the ads to be lazyloaded. * @private * @memberof db.libs.adsDefer * @type {array} */ var lazyAds = []; /** * Boolean to check if listeners for ads have been added. * @private * @memberof db.libs.adsDefer * @type {boolean} */ var listenersAdded = false; /** * Is set to the sum of scrollTop + window.innerHeight + threshold. * @private * @memberof db.libs.adsDefer * @type {number} */ var offset = 0; /** * The amount of scroll in the browser's window. * @private * @memberof db.libs.adsDefer * @type {number} */ var scrollTop = 0; /** * Boolean to check if a scroll listener has been added to the window. * @private * @memberof db.libs.adsDefer * @type {boolean} */ var scrollIsBound = false; /** * Array of ad objects that have been rendered, meaning * they have received response from DFP. * @private * @memberof db.libs.adsDefer * @type {array} */ var renderedAds = []; /** * Adds a listener that's called each time an ad on the page has finished rendering. * @private * @memberof db.libs.adsDefer * @docs https://developers.google.com/doubleclick-gpt/reference#googletag.events.SlotRenderEndedEvent */ function addSlotRenderEndedListener() { googletag.pubads().addEventListener('slotRenderEnded', function(event) { var renderedAtTime = Date.now(); var id = event.slot.getSlotElementId(); var adunit = document.getElementById(id); var requestedOn = adunit.getAttribute('data-load-on'); var isEmpty = event.isEmpty; if(!isEmpty) { renderedAds.push({ id: id, renderedAtTime: renderedAtTime }); } if(typeof dataLayer !== 'undefined' && dataLayer) { dataLayer.push({ 'eventCategory': 'ads', 'eventAction': 'loadedAds', 'eventLabel': id, 'event': 'adInteraction', 'eventValue': 1, 'isEmpty': isEmpty, 'requestedOn': requestedOn }); } }); } /** * Adds a listener that's called each time an ad has been considered viewed * based on some criterias (see docs). * Here we also calculate how long it took for an ad to be viewable from the * time it was rendered. * @private * @memberof db.libs.adsDefer * @docs https://support.google.com/dfp_premium/answer/4574077?hl=en */ function addImpressionViewableListener() { googletag.pubads().addEventListener('impressionViewable', function(event) { var viewedAtTime = Date.now(); var id = event.slot.getSlotElementId(); var adunit = document.getElementById(id); var requestedOn = adunit.getAttribute('data-load-on'); var renderedAtTime = 0; for(var i = 0; i < renderedAds.length; i++) { if(renderedAds[i] !== null) { if(renderedAds[i].id === id) { renderedAtTime = renderedAds[i].renderedAtTime; renderedAds[i] = null; } } } var timeFromRender = 0; if(renderedAtTime > 0) { timeFromRender = (viewedAtTime - renderedAtTime) / 1000; } if(typeof dataLayer !== 'undefined' && dataLayer) { dataLayer.push({ 'eventCategory': 'ads', 'eventAction': 'viewableAds', 'eventLabel': id, 'event': 'adInteraction', 'eventValue': 1, 'isEmpty': false, 'requestedOn': requestedOn, 'timeFromRender': timeFromRender }); } }); } /** * Enables the googletag services if it hasn't been enabled already. * Also starts listeners for slot events. * @private * @memberof db.libs.adsDefer */ function enableAndListen() { if(!googletag.pubadsReady) { // Set Publisher ID for app if(typeof window.dbApp !== 'undefined' && window.dbApp && dbApp.getAdid() !== ""){ googletag.pubads().setPublisherProvidedId(dbApp.getAdid()); } else { if(typeof window.localStorage !== 'undefined' && window.localStorage.getItem("dbApp") !== null && window.localStorage.getItem("dbApp") !== "") { googletag.pubads().setPublisherProvidedId(window.localStorage.getItem("dbApp")); } } // Center ads googletag.pubads().setCentering(true); googletag.enableServices(); } if(!listenersAdded) { addSlotRenderEndedListener(); addImpressionViewableListener(); listenersAdded = true; } } /** * Checks if an adunit has the right attributes to be considered valid. * These attributes are required to make a correct request to DFP. * @param {HTMLElement} HTMLElement The HTMLElement to check * @return {object} An object representing the adunit * @private * @memberof db.libs.adsDefer */ function assertValidAdUnit(adunit) { var isValid = true; if(!adunit) { console.warn('DB ads-defer: Not an adunit'); return; } // Check the ID of the adunit var id = adunit.id; if(!id) { console.warn('DB ads-defer: An ID is not defined for an adunit'); return; } // Get the slot var slot = adunit.getAttribute('data-slot'); // Check the sizes var rawSizes = adunit.getAttribute('data-sizes'); var sizes = null; if(rawSizes) { try { JSON.parse(rawSizes); } catch (e) { console.warn('DB ads-defer: Ad request error: Not valid sizes for ' + id); return; } sizes = JSON.parse(rawSizes); } // Check the JSON var rawJSON = adunit.getAttribute('json'); var targeting = {}; if(rawJSON) { try { JSON.parse(rawJSON); } catch (e) { console.warn('DB ads-defer: Ad request error: Not valid JSON for ' + id); return; } var json = JSON.parse(rawJSON); targeting = json.targeting; } // Check if adunit is set to collapse var collapse = adunit.getAttribute('data-collapse'); var setToCollapse = false; if(typeof collapse !== 'undefined') { if(collapse === '1') { setToCollapse = true; } } var outOfPage = adunit.getAttribute('data-out-of-page'); var setOutOfPage = false; if(typeof outOfPage !== 'undefined') { if(outOfPage === '1') { setOutOfPage = true; } } if(!slot) { console.warn('DB ads-defer: Slot is not defined for adunit ' + id); isValid = false; } if(!sizes) { console.warn('DB ads-defer: Sizes is not defined for adunit ' + id); isValid = false; } if(!isValid) { return null; } else { return { id: id, slot: slot, sizes: sizes, targeting: targeting, setToCollapse: setToCollapse, setOutOfPage: setOutOfPage }; } } /** * Defines a single slot which is pushed to the googletag.cmd object. * @param {object} ad An object representing the adunit * @private * @memberof db.libs.adsDefer */ function defineSlot(adunit) { // Define a new slot var slot; if(adunit.setOutOfPage) { slot = googletag.defineOutOfPageSlot(adunit.slot, adunit.id); } else { slot = googletag.defineSlot(adunit.slot, adunit.sizes, adunit.id); } slot.addService(googletag.pubads()); // Set targeting for(var key in adunit.targeting) { slot.setTargeting(key, adunit.targeting[key]); } // Collapse empty divs if(adunit.setToCollapse) { slot.setCollapseEmptyDiv(true); } } /** * Creates slots for each adunit that has <code>data-load-on="load"</code> attribute. * The request for these slots are then made to DFP in a single request. * This enables roadblocking. * @private * @memberof db.libs.adsDefer */ function createOnLoadSlots() { googletag.cmd.push(function(){ var adsToDisplay = []; for(var i = 0; i < ads.length; i++) { var adunit = assertValidAdUnit(ads[i]); if(adunit) { // Define a new slot defineSlot(adunit); // Add the adunit's ID to the array adsToDisplay.push(adunit.id); } } // Make a single request to DFP for these ads googletag.pubads().enableSingleRequest(); // Enable the services and listen for slot events enableAndListen(); // We need to display each ad _after_ we've enabled the service for(var j = 0; j < adsToDisplay.length; j++) { googletag.display(adsToDisplay[j]); } }); } /** * Create a slot for an adunit that has the <code>data-load-on="view"</code> attribute. * @private * @memberof db.libs.adsDefer * @param {object} ad The DOM element representing the adunit. */ function createLazySlot(adunit) { var ad = assertValidAdUnit(adunit); if(ad) { googletag.cmd.push(function() { // Define a new slot defineSlot(ad); // Enable the services and listen for slot events enableAndListen(); // Display the ad googletag.display(ad.id); }); } } /** * Searches for adunits with <code>data-load-on="view"</code> attribute that are within the viewport threshold. * If found, a slot for this adunit is defined. * @private * @memberof db.libs.adsDefer */ function getAdunitsWithinViewThreshold() { scrollTop = getScrollTop(); offset = scrollTop + window.innerHeight + threshold; for(var i = 0; i < lazyAds.length; i++) { var adunit = lazyAds[i]; if(adunit !== null) { var adTopOffset; if(adunit.className.indexOf('sticky') > -1) { // For sticky, we need to calculate the top offset on each scroll adTopOffset = getTopOffset(adunit); } else { adTopOffset = parseInt(adunit.getAttribute('data-top-offset'), 10); } var offsetHeight = parseInt(adunit.getAttribute('data-offsetheight'), 10); var adBottomOffset = adTopOffset + offsetHeight; if(offset > adTopOffset) { // First, check if the adunit is located within the bottom threshold if((adBottomOffset + threshold) > scrollTop) { // Then, check if the scroll does not exceed the adunit's offset + height + treshold createLazySlot(adunit); lazyAds[i] = null; } } } } } /** * Bind the scroll event that handles when to do requests for lazyloaded ads. * @private * @memberof db.libs.adsDefer */ function bindScroll() { scrollIsBound = true; window.addEventListener('scroll', function(){ window.requestAnimationFrame(getAdunitsWithinViewThreshold); }); } /** * Get the scroll from the top. * @private * @memberof db.libs.adsDefer * @return {number} The scroll amount */ function getScrollTop() { var doc = document.documentElement; var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); return top; } /** * Get the top offset of an element. * @private * @memberof db.libs.adsDefer * @param {HTMLElement} element The element of which to get the offset * @return {number} The top offset */ function getTopOffset(element) { var topOffset = 0; while(element) { topOffset += (element.offsetTop + element.clientTop); element = element.offsetParent; } return topOffset; } /** * Get the offsetHeight of an element. * The <code>offsetHeight</code> is the height of an element including * its padding, borders and horizontal scrollbar (if present). * @private * @memberof db.libs.adsDefer * @param {HTMLElement} element The element of which to get the offsetHeight * @return {number} The element's offsetHeight */ function getOffsetHeight(element) { return element.offsetHeight; } /** * Initialize ads loading and bind scroll-listener. * @private * @memberof db.libs.adsDefer */ function initialize() { createOnLoadSlots(); if(lazyAds.length) { getAdunitsWithinViewThreshold(); if(scrollIsBound === false) { bindScroll(); } } } /** * Creates an array of <code>HTMLElements</code> from a <code>NodeList</code> that have * not already been configured. * It also sets the adunit's topoffset and offsetheight data attributes on the element. * @private * @memberof db.libs.adsDefer * @param {NodeList} nodeList The NodeList * @return {array} An array with HTMLElements */ function createArray(nodeList) { var arr = []; for(var i = 0; i < nodeList.length; i++) { var adunit = nodeList[i]; var isConfigured = adunit.hasAttribute('data-is-configured'); if(isConfigured === false) { adunit.setAttribute('data-top-offset', getTopOffset(adunit)); adunit.setAttribute('data-offsetheight', getOffsetHeight(adunit)); adunit.setAttribute('data-is-configured', 'true'); arr.push(adunit); } } return arr; } /** * Find adunits with either the <code>data-load-on="load"</code> or <code>data-load-on="view"</code> attribute. * Populate the two arrays <code>ads</code> and <code>lazyAds</code> with these adunits respectively. * @private * @memberof db.libs.adsDefer */ function findAdunits() { ads = createArray(document.querySelectorAll("[data-load-on=load]")); lazyAds = createArray(document.querySelectorAll("[data-load-on=view]")); } /** * Reflow the component. Is for example called when lazyloading the rest of the Dagbladet frontpage on mobile. * @private * @memberof db.libs.adsDefer */ function reflow() { // Find new adunits findAdunits(); // No need to initialize if there are no ads on the page if(ads.length === 0 && lazyAds.length === 0) { return; } // Initialize! initialize(); } function load() { } /** * Initialize the component. * @private * @memberof db.libs.adsDefer */ function init() { var initiated = document.querySelector('html').hasAttribute('data-ads-defer'); if(!initiated){ // Find all the adunits on the page findAdunits(); // No need to initialize if there are no ads on the page if(ads.length === 0 && lazyAds.length === 0) { document.querySelector('html').setAttribute('data-ads-defer', 'true'); return; } // Initialize before the document is completely ready if (document.readyState === 'interactive') { initialize(); } else { document.addEventListener('DOMContentLoaded', function(){ setTimeout(function(){ initialize(); }, 0); }); } document.querySelector('html').setAttribute('data-ads-defer', 'true'); } } return { init: init, reflow: reflow, load: load }; })(); })();