Source: components/adverts/_dfp.js

'use strict'

/* global UCASUtilities, jQuery */
var googletag = googletag || {}
googletag.cmd = googletag.cmd || []

var UCASDesignFramework = UCASDesignFramework || {};

/**
 * GDFP adverts.
 * @namespace googleadverts
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 */
(function (global, _u) {
  global.googleadverts = {}

  var slotDictionary = {}

  // Define advert sizes as per DFP
  var skyScraperSize = [160, 600]
  var leaderboardSize = [728, 90]
  var mobileBannerSize = [320, 50]
  var mpuSize = [300, 250]
  var verticalRectangleSize = [240, 400]
  var pixel1x1Size = [1, 1]

  /**
   * Initialise Google DFP adverts.
   * @function init
   * @memberof! UCASDesignFramework.googleadverts
   * @public
   * @param {object} [context=document] - DOM element that we are adding adverts to.
   * @example
   * UCASDesignFramework.googleadverts.init()
   */
  function init (context) {
    context = context || document

    if (!document.getElementById('google-dfp-script')) {
      var script = document.createElement('script')
      script.id = 'google-dfp-script'
      script.src = 'https://www.googletagservices.com/tag/js/gpt.js'
      script.type = 'text/javascript'
      script.async = true
      document.head.appendChild(script)

      googletag.cmd.push(function () {
        googletag.pubads().enableAsyncRendering()
        googletag.pubads().enableSingleRequest()
        googletag.pubads().disableInitialLoad()
        googletag.enableServices()
      })
    }

    // Insert the initial adverts currently defined in the context.
    insertAdverts(context, false)
  }

  /**
   * Refresh the adverts in a context. Performs a simple reload of each advert.
   * E.g. loads a new advert for the slot should it have multiple creatives defined in DFP.
   * @function refreshAdverts
   * @memberof! UCASDesignFramework.googleadverts
   * @public
   * @param {object} [context=document] - DOM element that contains the adverts we want to refresh.
   * @param {object} [ids] - list of slot placement ids to refresh adverts for.
   * Optional parameter, if null then obtain placement Ids from the passed context. If the context is
   * document then we don't require the Ids as we're refreshing all ads on the page. We could obviously do without
   * this parameter but if we've already obtain the list of Ids, it saves querying the DOM again.
   */
  function refreshAdverts (context, ids) {
    context = context || document

    var idsToRefresh = []

    // if no Ids passed then obtain list from passed context.
    if (!ids) {
      googletag.cmd.push(function () {
        var targetingSet = false
        var placements = context.querySelectorAll('[data-google-query-id]')
        _u.forEach(placements, function (i, el) {
          var parent = el.parentElement

          if (parent.classList.contains('google-placement-delayed')) {
            return true
          }

          idsToRefresh[i] = el.getAttribute('id')

          var keyValues = parseKVP(parent.getAttribute('data-google-target-values'))

          if (jQuery !== 'undefined') {
            keyValues = parseKVP(jQuery(parent).data('googleTargetValues'))
          }

          // Add a slot level targeting on the placement name if present in the keyValues object. This is a specific
          // request from Elliott Donnelly.
          if (keyValues['placement']) {
            slotDictionary[idsToRefresh[i]].setTargeting('placement', keyValues['placement'])
          }

          // If we don't yet have targeting set, set it from the values on the first valid banner we find.
          if (!targetingSet) {
            targetPageLevel(keyValues)
            targetingSet = true

            _u.log.log('Setting page level targeting on reload')
            _u.log.log(keyValues)
          }
        })
      })
    } else {
      idsToRefresh = ids
    }

    // Find the slots to refresh from the slotDictionary, then pass slot array to refresh function.
    var slotsToRefresh = []
    _u.forEach(idsToRefresh, function (i, el) {
      slotsToRefresh[i] = slotDictionary[el]
    })

    if (slotsToRefresh.length > 0) {
      googletag.cmd.push(function () {
        _u.log.log('Refreshing specific banners')
        _u.log.log(slotsToRefresh)
        googletag.pubads().refresh(slotsToRefresh)
      })
    }
  }

  /**
   * Parses Key Value Pairs and outputs as true JSON if valid.
   * @private
   * @param {string} keyValues - data attribute of key value pairs that require parsing.
   * @returns {object|string} keyValues - JSON encoded key value pairs, or orignal invalid JSON string.
   */
  function parseKVP (keyValues) {
    if (keyValues) {
      if (typeof keyValues === 'string' || keyValues instanceof String) {
        // Replace any single quotes with double quotes to make it valid JSON
        keyValues = keyValues.replace(new RegExp('\'', 'g'), '"')
        try {
          keyValues = JSON.parse(keyValues)
        } catch (e) {
          // Do nothing just pass back original string.
        }
      }
    }
    return keyValues
  }

  /**
   * Sets the page level targeting based on the KVPs passed in. Expected to have been called from within a cmd.push
   * @private
   * @param {object} keyValues - data attribute of key value pairs that require parsing
   */
  function targetPageLevel (keyValues) {
    // Add page level targetting. Each ad tag has a set of KVPs but it will only use the ones from the first tag it processess.
    // Placement KVP is sent as a slot level target so is ignored in this loop.
    // All ads in search will have the same set of KVPs and it seems that UCAS.com does too. Coded this way as originally planned
    // to have these items as slot level targetting but Elliott requested that I change to page level. Not sure if this will be problematic in future.
    // Obviously, if this function is called with a context containing a new slot that has different targetting values it will replace the existing.
    // Adverts refreshed after this point will use the new page level targetting values. This could surely be inproved in a future version.
    _u.forEach(Object.keys(keyValues), function (i, el) {
      if (el.toLowerCase() !== 'placement') {
        googletag.pubads().setTargeting(el, keyValues[el])
      }
    })
  }

  /**
   * Insert new advert slots based on the context passed in.
   * @function insertAdverts
   * @memberof! UCASDesignFramework.googleadverts
   * @public
   * @param {object} [context=document] - DOM element that contains the new advert slot we want to load.
   * @param {bool} [immediateLoad=true] - Bool to decide whether to immediately load an advert or wait until all are defined and then load.
   * @returns {bool} - success
   */
  function insertAdverts (context, immediateLoad) {
    context = context || document

    if (immediateLoad === undefined) {
      immediateLoad = true
    }

    var keyValues = []
    var placementIds = []

    // Only obtain advert divs that have not already been loaded.
    var placements = context.querySelectorAll('[data-google-slot-id]:not(.google-placement-loaded):not(.google-placement-delayed)')

    if (placements.length < 1) {
      return true
    }

    var targetingSet = false

    googletag.cmd.push(function () {
      for (var i = 0; i < placements.length; i++) {
        var element = placements[i]

        // Prevent double loading of slots. Selector above should hopefully avoid this.
        // This is a fail safe.
        if (element.classList.contains('google-placement-loaded')) {
          return true
        }

        // Make sure there is nothing else in this container.
        while (element.firstChild) {
          element.removeChild(element.firstChild)
        }

        placementIds[i] = _u.unique.genId()

        var slotId = element.getAttribute('data-google-slot-id')
        if (jQuery !== 'undefined') {
          slotId = jQuery(element).data('googleSlotId')
        }

        var sizeAttribute = element.getAttribute('data-google-size')

        // Parse any JSON provided by the attribute.
        var parsedSize = parseKVP(sizeAttribute)

        // Set defaults to always show.
        var customMinBreakpoint = 1 // Set to minimum possible.
        var customMaxBreakpoint = 9999 // Set to some arbitrary large width.
        var size
        var customSize

        if (parsedSize !== null && typeof parsedSize === 'object') {
          size = parsedSize.size
          if (typeof parsedSize.min !== 'undefined') {
            customMinBreakpoint = parsedSize.min
          }
          if (typeof parsedSize.max !== 'undefined') {
            customMaxBreakpoint = parsedSize.max
          }
          if (typeof parsedSize.width !== 'undefined' && typeof parsedSize.height !== 'undefined') {
            customSize = [parsedSize.width, parsedSize.height]
          }
        } else {
          size = element.getAttribute('data-google-size')
        }

        if (size) {
          var slot
          switch (size) {
            case 'skyscraper':
              // Skyscraper advert only to show in 1024px width or above (any height)
              var desktopSkyscraperMapping = googletag.sizeMapping()
                .addSize([0, 0], [])
                .addSize([UCASDesignFramework.size.breakpoints.medium, 1], skyScraperSize)
                .build()
              slot = googletag.defineSlot(slotId, skyScraperSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(desktopSkyscraperMapping)
              break

            case 'skyscraper-large':
              // Skyscraper advert (used in the search results page) only to show in 1366px width or above (any height)
              var largeDesktopSkyscraperMapping = googletag.sizeMapping()
                .addSize([0, 0], [])
                .addSize([1366, 1], skyScraperSize)
                .build()
              slot = googletag.defineSlot(slotId, skyScraperSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(largeDesktopSkyscraperMapping)
              break

            case 'leaderboard':
              // Leaderboard advert only to show in 1200px width or above (any height)
              var desktopLeaderboardMapping = googletag.sizeMapping()
                .addSize([0, 0], [])
                .addSize([UCASDesignFramework.size.breakpoints.large, 1], leaderboardSize)
                .build()
              slot = googletag.defineSlot(slotId, leaderboardSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(desktopLeaderboardMapping)
              break

            case 'mobile-banner':
              // Mobile banner advert only to show in px width less than 721 (any height)
              var mobileBannerMapping = googletag.sizeMapping()
                .addSize([0, 0], mobileBannerSize)
                .addSize([UCASDesignFramework.size.breakpoints.small, 1], [])
                .build()
              slot = googletag.defineSlot(slotId, mobileBannerSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(mobileBannerMapping)
              break

            case 'mpu':
              // MPU advert to show in specified browser width
              var mpuMapping = googletag.sizeMapping()
                .addSize([0, 0], [])
                .addSize([customMinBreakpoint, 0], mpuSize) // Show mpuSize from min upwards.
                .addSize([customMaxBreakpoint, 0], []) // Show nothing from max upwards.
                .build()

              slot = googletag.defineSlot(slotId, mpuSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(mpuMapping)
              break

            case 'vertical-rectangle':
              // Verticle Rectangle to show in anything above 540px width
              var verticalRectangleMapping = googletag.sizeMapping()
                .addSize([0, 0], [])
                .addSize([541, 1], verticalRectangleSize)
                .build()
              slot = googletag.defineSlot(slotId, mpuSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(verticalRectangleMapping)
              break

            case '1x1-pixel':
              // 1x1 pixel advert to show in any px width.
              var pixel1x1Mapping = googletag.sizeMapping()
                .addSize([0, 0], pixel1x1Size)
                .build()
              slot = googletag.defineSlot(slotId, pixel1x1Size, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(pixel1x1Mapping)
              break

            default:
              // Unknown size so return false
              return false

            case 'custom':
              // Custom advert to show in specified browser width
              var customMapping = googletag.sizeMapping()
                .addSize([0, 0], [])
                .addSize([customMinBreakpoint, 0], customSize) // Show mpuSize from min upwards.
                .addSize([customMaxBreakpoint, 0], []) // Show nothing from max upwards.
                .build()

              slot = googletag.defineSlot(slotId, customSize, placementIds[i]).addService(googletag.pubads())
              slot.defineSizeMapping(customMapping)
              break
          }

          keyValues = parseKVP(element.getAttribute('data-google-target-values'))
          if (jQuery !== 'undefined') {
            keyValues = parseKVP(jQuery(element).data('googleTargetValues'))
          }

          _u.log.log('Inserted banner')
          _u.log.log(slotId)

          // Add a slot level targeting on the placement name if present in the keyValues object. This is a specific
          // request from Elliott Donnelly.
          if (keyValues['placement']) {
            slot.setTargeting('placement', keyValues['placement'])
          }

          // If we don't yet have targeting set, set it from the values on the first valid banner we find.
          if (!targetingSet) {
            targetPageLevel(keyValues)
            targetingSet = true

            _u.log.log('Setting page level targetting on insert')
            _u.log.log(keyValues)
          }

          // Record the slot in the dictionary so that we can refresh on a per slot basis if requested.
          slotDictionary[placementIds[i]] = slot
        }

        // If successfully loaded, add the class to indicate that we shouldn't process it again.
        if (element.classList.length > 0) {
          element.className += (' google-placement-loaded')
        } else {
          element.className = ('google-placement-loaded')
        }

        // Insert the Ad Div into the context.
        var divId = placementIds[i]
        var advertContainer = document.createElement('div')
        advertContainer.id = divId
        placements[i].appendChild(advertContainer)
        googletag.display(divId)

        // Immediately load the slot?
        if (immediateLoad) {
          googletag.pubads().refresh([slot])
        }
      }
      if (!immediateLoad) {
        googletag.pubads().refresh()
      }
    })
  }

  // Refresh all ads on a page, e.g. this is triggered when a user resizes the page and
  // switches to a different Design Framework breakpoint. If in future we require a single, whole page advert refresh
  // we could extrapolate this into a function and expose it.
  document.addEventListener('dfpAdvertResize', function () {
    googletag.cmd.push(function () {
      googletag.pubads().refresh()
    })
  })

  global.googleadverts = {
    init: init,
    insertAdverts: insertAdverts,
    refreshAdverts: refreshAdverts
  }
})(UCASDesignFramework, UCASUtilities)