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
};
})();
})();