UNPKG

angular-dfp

Version:

Semantic DoubleClick integration with AngularJS

3,137 lines 102 kB
/**
 * @license Apache
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// eslint-disable-next-line no-use-before-define, no-var
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];

// eslint-disable-next-line no-undef, no-unused-vars
let angularDfp = angular.module('angularDfp', []);

/**
* @module http-error
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:http-error */ function(module) {
  'use strict';

  /**
 * The factory for the `httpError` service.
 *
 * @private
 * @param  {Function} $log    The Angular `$log` service.
 * @return {Function} The `httpError` service.
 */
  function httpErrorFactory($log) {
    /**
   * The `httpError` service.
   * @param  {!Object} response An XHR response object.
   * @param  {!string} message The error message to show.
   */
    function httpError(response, message) {
      $log.error(`Error (${response.status})`);
    }

    /**
    * Tests if a given HTTP response status is an error code.
    * @param  {number|!string}  code The response status code.
    * @return {!boolean} True if the code is an error code, else false.
    */
    httpError.isErrorCode = function(code) {
      if (typeof code === 'number') {
        return !(code >= 200 && code < 300);
      }

      console.assert(typeof code === 'string');

      return code[0] !== '2';
    };

    return httpError;
  }

  module.factory('httpError', ['$log', httpErrorFactory]);

  // eslint-disable-next-line
})(angularDfp);

/**
* @module parse-duration
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:parse-duration */ function(module) {
  'use strict';

  /**
  * An error thrown by the `parseDuration` service.
  * @private
  */
  class DFPDurationError extends Error {
    constructor(interval) {
      super(`Invalid interval: '${interval}'ls`);
    }
  }

  /**
  * A factory for the `parseDuration` service.
  *
  * This service allows parsing of strings specifying
  * durations, such as '2s' or '5min'.
  *
  * @private
  * @return {Function} The `parseDuration` service.
  */
  function parseDurationFactory() {
    /**
    * Converts a given time in a given unit to milliseconds.
    * @param  {!number} time A time number in a certain unit.
    * @param  {!string} unit A string describing the unit (ms|s|min|h).
    * @return {!number} The time, in milliseconds.
    */
    function convertToMilliseconds(time, unit) {
      console.assert(/^(m?s|min|h)$/g.test(unit));

      if (unit === 'ms') return time;
      if (unit === 's') return time * 1000;
      if (unit === 'min') return time * 60 * 1000;

      // hours
      return time * 60 * 60 * 1000;
    }

    /**
    * Converts a regular expression match into a duration.
    * @param  {!Array} match A regular expression match object.
    * @return {!number} The converted milliseconds.
    */
    function convert(match) {
      const time = parseFloat(match[1]);

       // No unit means milliseconds
       // Note: match[0] is the entire matched string
      if (match.length === 2) return time;

      return convertToMilliseconds(time, match[2]);
    }

    /**
    * Given an interval string, returns the corresponding milliseconds.
    * @param  {number|string} interval The string to parse.
    * @return {number} The corresponding number of milliseconds.
    */
    function parseDuration(interval) {
      // The interval may well be zero so don't just write !interval
      if (interval === undefined || interval === null) {
        throw new DFPDurationError(interval);
      }

      if (typeof interval === 'number') {
        return interval;
      }

      if (typeof interval !== 'string') {
        throw new TypeError(`'${interval}' must be of number or string type`);
      }

      // Convert any allowed time format into milliseconds
      const match = interval.match(/((?:\d+)?.?\d+)(m?s|min|h)?/);

      if (!match) {
        throw new DFPDurationError(interval);
      }

      return convert(match);
    }

    return parseDuration;
  }

  module.factory('parseDuration', parseDurationFactory);

// eslint-disable-next-line
})(angularDfp);

/**
* @module script-injector
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:script-injector */ function(module) {
  'use strict';

  /**
  * The factory for the `scriptInjector` service.
  *
  * @private
  * @param {!angular.$q} $q The Angular `$q` service.
  * @param {Function} httpError The `httpError` service.
  * @return {Function} The `scriptInjector` service.
  */
  function scriptInjectorFactory($q, httpError) {
    /**
    * Creates an HTML script tag.
    * @param  {!string} url The string of the script to inject.
    * @return {Element} An `Element` ready for injection.
    */
    function createScript(url) {
      const script = document.createElement('script');
      const ssl = document.location.protocol === 'https:';

      script.async = 'async';
      script.type = 'text/javascript';
      script.src = (ssl ? 'https:' : 'http:') + url;

      return script;
    }

    /**
    * Creates a promise, to be resolved after the script is loaded.
    * @param  {Element} script The script tag.
    * @param {!string} url The url of the request.
    * @return {angular.$q.Promise<null>} The promise for the asynchronous script injection.
    */
    function promiseScript(script, url) {
      const deferred = $q.defer();

      /**
      * Resolves the promise.
      */
      function resolve() {
        deferred.resolve();
      }

      /**
      * Rejects the promise for a given faulty response.
      * @param  {?Object} response The response object.
      */
      function reject(response) {
        response = response || {status: 400};
        httpError(response, 'loading script "{0}".', url);

        // Reject the promise and pass the reponse
        // object to the error callback (if any)
        deferred.reject(response);
      }

      // IE
      script.onreadystatechange = function() {
        if (this.readyState === 4) {
          if (httpError.isErrorCode(this.status)) {
            reject(this);
          } else {
            resolve();
          }
        }
      };

      // Other viewports
      script.onload = resolve;
      script.onerror = reject;

      return deferred.promise;
    }

    /**
    * Injects a script tag into the DOM (at the end of <head>).
    * @param  {Element} script The Element script.
    */
    function injectScript(script) {
      const head = document.head || document.querySelector('head');
      head.appendChild(script);
    }

    /**
    * The `scriptInjector` service.
    * @param  {!string} url The string to inject.
    * @return {angular.$q.Promise<null>} A promise, resolved after
    *                   loading the script or reject on error.
    */
    function scriptInjector(url) {
      const script = createScript(url);
      injectScript(script);
      return promiseScript(script, url);
    }

    return scriptInjector;
  }

  module.factory('scriptInjector', ['$q', 'httpError', scriptInjectorFactory]);

// eslint-disable-next-line
})(angularDfp);

/**
* @file The primary directive for specifying an ad slot using the library.
*
* This directive is repsponsible for collecting all nested configuration options
* and ultimately making the ad call. All other tags in the library, except
* `dfp-video` and `dfp-audience-pixel` can and must be nested under this tag.
*
* @example <caption>Example usage of the `dfp-ad` directive.</caption>
* <dfp-ad force-safe-frame
*         collapse-if-empty
*         refresh='3s'
*         ad-unit="/path/to/my/ad-unit">
*   <dfp-size width="728" height="90"></dfp-size>
*   <dfp-targeting key="sport" value="football"></dfp-targeting>
*   <dfp-targeting key="food">
*     <dfp-value>chicken</dfp-value>
*     <dfp-value>meatballs</dfp-value>
*   </dfp-targeting>
*   <dfp-responsive viewport-width="320" viewport-height="0">
*     <dfp-size width=320 height=50></dfp-size>
*   </dfp-responsive>
* </dfp-ad>
*
* @module dfp-ad
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line no-use-before-define, no-var
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-ad */ function(module) {
  'use strict';

  /**
  * The controller for the `dfp-ad` directive.
  * @param {Function} DFPIncompleteError The `DFPIncompleteError` service.
  * @private
  */
  function dfpAdController(DFPIncompleteError) {
    /**
    * The fixed (non-responsive) sizes for the ad slot.
    * @type {Array}
    */
    const sizes = [];

    /**
    * Any `{viewportSize, adSizes}` objects to create responsive mappings.
    * @type {Array<{viewportSize: Array<number>, adSizes: Array<number>}>}
    */
    const responsiveMapping = [];

    /**
    * Any key/value targeting objects.
    * @type {Array}
    */
    const targetings = [];

    /**
    * Any category exclusion labels.
    * @type {Array}
    */
    const exclusions = [];

    /**
    * Any additional scripts to execute for the slot.
    * @type {Array}
    */
    const scripts = [];

    /**
     * Returns the boolean property defined on the controller.
     *
     * Boolean properties will either be undefined, or the empty string if
     * they were defined on the directive (e.g. force-safe-frame). This function
     * just gets a real boolean for their value.
     * @param  {!string} name The name of the property to lookup.
     * @return {boolean} True if the property was set, else false.
     */
    this.booleanProperty = function(name) {
      return this[name] !== undefined;
    };

    /**
    * Tests if the state of the directive is valid and complete.
    * @throws {DFPIncompleteError} If the ad slot definition is not complete.
    */
    this.checkValid = function() {
      if (sizes.length === 0) {
        throw new DFPIncompleteError('dfp-ad', 'dfp-size');
      }
      // eslint-disable-next-line dot-notation
      if (!this['adUnit']) {
        throw new DFPIncompleteError('dfp-ad', 'ad-unit', true);
      }
    };

    /* eslint-disable dot-notation */
    /**
    * Returns the public state of the controller for use by the directive.
    * @return {Object} An object of all properties the directive will
    *                  need to create an ad slot.
    */
    this.getState = function() {
      this.checkValid();
      return Object.freeze({
        sizes,
        responsiveMapping,
        targetings,
        exclusions,
        adUnit: this['adUnit'],
        forceSafeFrame: this.booleanProperty('forceSafeFrame'),
        safeFrameConfig: this['safeFrameConfig'],
        clickUrl: this['clickUrl'],
        refresh: this['refresh'],
        scripts,
        collapseIfEmpty: this.booleanProperty('collapseIfEmpty')
      });
    };
    /* eslint-enable dot-notation */

    /**
    * Registers a (fixed) size for the ad slot.
    *
    * @param {Array} size A [width, height] array.
    * @see [Google DFP Support]{@link https://support.google.com/dfp_premium/answer/1697712?hl=en}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.defineSlot}
    */
    this.addSize = function(size) {
      sizes.push(size);
    };

    /**
    * Registers a responsive mapping for the ad slot.
    * @param {Object} mapping A `{viewportSize, adSizes}` mapping.
    * @see [Google DFP Support]{@link https://support.google.com/dfp_premium/answer/3423562?hl=en}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.SizeMappingBuilder}
    */
    this.addResponsiveMapping = function(mapping) {
      responsiveMapping.push(mapping);
    };

    /**
    * Registers a targeting object for the ad slot.
    * @param {Object} targeting A {viewportSize, adSizes} object.
    * @see [Google DFP Support]{@link https://support.google.com/dfp_premium/answer/177383?hl=en}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PassbackSlot_setTargeting}
    */
    this.addTargeting = function(targeting) {
      targetings.push(targeting);
    };

    /**
    * Registers a category exclusion for the slot.
    * @param {string} exclusion The category exclusion label.
    * @see [Google Developer Support]{@link https://support.google.com/dfp_premium/answer/3238504?hl=en&visit_id=1-636115253122574896-2326272409&rd=1}
    * @see [GPT Reference] {@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_setCategoryExclusion}
    */
    this.addExclusion = function(exclusion) {
      exclusions.push(exclusion);
    };

    /**
    * Registers a script for the slot.
    *
    * Scripts can be run during ad slot definition and before the actual ad
    * call, to perform any auxiliary configuration taks not handled by our
    * interface.
    *
    * @param {string} script The script string to be evaluated.
    */
    this.addScript = function(script) {
      scripts.push(script);
    };
  }

  /**
  * The directive for the `dfp-ad` tag.
  *
  * This is the primary directive used for defining ad slots. All other
  * directives, except `dfp-video`, are nested under this slot. It is
  * standalone except for the necessity of (at least) one nested `dfp-size`
  * directive.
  *
  * @private
  * @param {Object} scope          The Angular element scope.
  * @param {Object} element        The jQuery/jQlite element of the directive.
  * @param {Object} attributes     The attributes defined on the element.
  * @param {Object} controller     The `dfpAdController` object.
  * @param  {Function} $injector {@link http://docs.angularjs.org/api/ng.$injector}
  */
  function dfpAdDirective(scope, element, attributes, controller, $injector) {
    const dfp = $injector.get('dfp');
    const dfpIDGenerator = $injector.get('dfpIDGenerator');
    const dfpRefresh = $injector.get('dfpRefresh');
    const dfpResponsiveResize = $injector.get('dfpResponsiveResize');

    const ad = controller.getState();

    const jQueryElement = element;
    element = element[0];

      // Generate an ID or check for uniqueness of an existing one
    dfpIDGenerator(element);

    /**
    * Handles the responsive mapping (`sizeMapping`) building.
    * @param {googletag.Slot} slot The ad slot.
    */
    function addResponsiveMapping(slot) {
      if (ad.responsiveMapping.length === 0) return;

      const sizeMapping = googletag.sizeMapping();

      ad.responsiveMapping.forEach(function(mapping) {
        sizeMapping.addSize(mapping.viewportSize, mapping.adSizes);
      });

      slot.defineSizeMapping(sizeMapping.build());
    }

    /**
     * Extracts the viewport dimensions from the responsive mapping.
     *
     * This is necessar7 to pass to the responsiveResize service.
     *
     * @param  {!Array<!ResponsiveMapping>} responsiveMappings The responsive mappings.
     * @return {!Array<!ViewportDimensions>} An array containing objects with the viewport dimensions.
     */
    function extractViewportDimensions(responsiveMappings) {
      return responsiveMappings.map(mapping => ({
        width: mapping.viewportSize[0],
        height: mapping.viewportSize[1]
      }));
    }

    /**
    * Defines the ad slot, aggregating all nested directives.
    *
    * This function combines all the properties added by nested directives.
    * Recall, for this, that angular executes controllers on the way down the
    * DOM and directives on the way up. As such, this directive is executed
    * after all nested directives were been invoked (adding properties such as
    * sizes, responsive mappings or key/value pairs to the controller). The
    * full ad slot definition can then be sent into the `googletag` command
    * queue to fetch an ad from the DoubleClick ad network.
    */
    function defineSlot() {
      const slot = googletag.defineSlot(ad.adUnit, ad.sizes, element.id);

      if (ad.forceSafeFrame !== undefined) {
        slot.setForceSafeFrame(true);
      }

      if (ad.clickUrl) {
        slot.setClickUrl(ad.clickUrl);
      }

      if (ad.collapseIfEmpty) {
        slot.setCollapseEmptyDiv(true, true);
      }

      if (ad.safeFrameConfig) {
        slot.setSafeFrameConfig(
          /** @type {googletag.SafeFrameConfig} */
          (JSON.parse(ad.safeFrameConfig))
        );
      }

      addResponsiveMapping(slot);

      ad.targetings.forEach(targeting => {
        slot.setTargeting(targeting.key, targeting.values);
      });

      ad.exclusions.forEach(exclusion => {
        slot.setCategoryExclusion(exclusion);
      });

      ad.scripts.forEach(script => { script(slot); });

      slot.addService(googletag.pubads());

      // When initialLoad is disabled, display()
      // will only register the slot as ready, but not actually
      // fetch an ad for it yet. This is done via refresh().
      googletag.display(element.id);

      // Send to the refresh proxy
      dfpRefresh(slot, ad.refresh).then(() => {
        if (ad.responsiveMapping.length > 0) {
          const dimensions = extractViewportDimensions(ad.responsiveMapping);
          dfpResponsiveResize(jQueryElement, slot, dimensions);
        }
      });

      scope.$on('$destroy', () => {
        // Release resources allocated for the slot and assert
        // that it really did destroy the slot
        console.assert(googletag.destroySlots([slot]));
      });
    }

    // Push the ad slot definition into the command queue.
    dfp.then(defineSlot);
  }

  module.directive('dfpAd', ['$injector', function($injector) {
    return {
      restrict: 'AE',
      controller: ['DFPIncompleteError', dfpAdController],
      controllerAs: 'controller',
      bindToController: true,
      link: function(...args) {
        dfpAdDirective.apply(null, args.slice(0, 4).concat($injector));
      },
      /* eslint-disable quote-props */
      scope: {
        'adUnit': '@',
        'clickUrl': '@',
        'forceSafeFrame': '@',
        'safeFrameConfig': '@',
        'refresh': '@',
        'collapseIfEmpty': '@'
      }
      /* eslint-enable quote-props */
    };
  }
  ]);

// eslint-disable-next-line
})(angularDfp);

/**
* @module dfp-audience-pixel
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-audience-pixel */ function(module) {
  'use strict';

  /**
  *
  * The `dfp-audience-pixel` tag.
  *
  * Audience pixels are useful for getting audience impressions on parts of a
  * page that do not show ads. Usually, audience impressions are generated when
  * a user sees an ad (unit) and is then eventually added to that audience
  * segment. However, when you have no ads but still want to record an
  * impression for an audience segment, you can add a transparent 1x1 pixel to
  * do so.
  *
  * @private
  * @see [Google DFP Support]{@link https://support.google.com/dfp_premium/answer/2508388?hl=en}
  *
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  */
  function dfpAudiencePixelDirective(scope, element, attributes) {
    const axel = String(Math.random());
    const random = axel * 10000000000000;

    /* eslint-disable dot-notation */

    let adUnit = '';
    if (scope.adUnit) {
      adUnit = `dc_iu=${scope['adUnit']}`;
    }

    let ppid = '';
    if (scope.ppid) {
      ppid = `ppid=${scope['ppid']}`;
    }

    const pixel = document.createElement('img');

    pixel.src = 'https://pubads.g.doubleclick.net/activity;ord=';
    pixel.src += `${random};dc_seg=${scope['segmentId']};${adUnit}${ppid}`;

    /* eslint-enable dot-notation */

    pixel.width = 1;
    pixel.height = 1;
    pixel.border = 0;
    pixel.style.visibility = 'hidden';

    element.append(pixel);
  }

  module.directive('dfpAudiencePixel', [() => {
    return {
      restrict: 'E',
      link: dfpAudiencePixelDirective,
      // eslint-disable-next-line quote-props
      scope: {'adUnit': '@', 'segmentId': '@', 'ppid': '@'}
    };
  }]);

// eslint-disable-next-line
})(angularDfp);

/**
* @module dfp-incomplete-error
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-incomplete-error */ function(module) {
  'use strict';

  /**
  * Factory for the DFPIncompleteError service.
  * @return {Function} The DFPIncompleteError service.
  */
  function dfpIncompleteErrorFactory() {
    class DFPIncompleteError extends Error {
      constructor(directiveName, missingName, isAttribute) {
        super(
          `Incomplete definition of '${directiveName}': ` +
          `Missing ${isAttribute ? 'attribute' : 'child directive'} ` +
          `'${missingName}'.`
        );
      }
    }

    return DFPIncompleteError;
  }

  /**
  * Factory for the DFPTypeError service.
  * @return {Function} The DFPTypeError service.
  */
  function dfpTypeErrorFactory() {
    class DFPTypeError extends Error {
      constructor(directiveName, attributeName, wrongValue, expectedType) {
        super(
          `Wrong type for attribute '${attributeName}' on ` +
          `directive '${directiveName}': Expected ${expectedType}` +
          `, got ${typeof wrongValue}`
        );
      }
    }

    return DFPTypeError;
  }
  /**
  * Factory for the DFPMissingParentError service.
  * @return {Function} The DFPMissingParentError service.
  */
  function dfpMissingParentErrorFactory() {
    class DFPMissingParentError extends Error {
      constructor(directiveName, ...parents) {
        console.assert(parents && parents.length > 0);
        if (Array.isArray(parents[0])) {
          parents = parents[0];
        }

        let parentMessage;
        if (parents.length > 1) {
          parents = parents.map(p => `'${p}'`);
          parentMessage = ', which must be ';
          parentMessage += parents.slice(0, -1).join(', ');
          parentMessage += ` or ${parents[parents.length - 1]}`;
        } else {
          parentMessage = ` '${parents[0]}'`;
        }

        super(
          `Invalid use of '${directiveName}' directive. ` +
          `Missing parent directive${parentMessage}.`
        );
      }
    }

    return DFPMissingParentError;
  }

  module.factory('DFPIncompleteError', dfpIncompleteErrorFactory);
  module.factory('DFPTypeError', dfpTypeErrorFactory);
  module.factory('DFPMissingParentError', dfpMissingParentErrorFactory);

// eslint-disable-next-line
})(angularDfp);

/**
* @file Defines a value for a category exclusion
*
* This directive allows specifying a category exclusion label, such that ads
* from that category exclusion will not show in this slot. This ensures, for
* example, that airline ads don't show next to articles of an airplane
* accident.
*
* The value itself is taken from the inner contents of the `dfp-exclusion` tag.
*
* @example <caption>Example usage of the `dfp-exclusion` directive.</caption>
* <dfp-ad ad-unit="/path/to/my/ad-unit">
*   <dfp-exclusion>airlines</dfp-exclusion>
* </dfp-ad>
*
* @module dfp-exclusion
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-targeting */ function(module) {
  'use strict';

  /**
  * The `dfp-exclusion` directive.
  *
  * @private
  * @see [Google DFP Support]{@link https://support.google.com/dfp_premium/answer/2627086?hl=en}
  *
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} ad The parent `dfp-ad` controller.
  */
  function dfpExclusionDirective(scope, element, attributes, ad) {
    ad.addExclusion(element.html());
  }

  module.directive('dfpExclusion', [function() {
    return {
      restrict: 'E',
      require: '^^dfpAd',
      link: dfpExclusionDirective
    };
  }]);

// eslint-disable-next-line
})(angularDfp);

/**
 * @module dfp-id-generator
 * @license Apache
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-id-generator */ function(module) {
  'use strict';

  /**
  * Returns the `dfpIDGenerator` service.
  *
  * @private
  * @return {Function} The dfpIDGenerator service.
  */
  function dfpIDGeneratorFactory() {
    /**
    * The hash of IDs generated so far.
    * @type {Object}
    */
    const generatedIDs = {};

    /**
    * Generates random IDs until unique one is found.
    * @return {string} The unique ID.
    */
    function generateID() {
      let id = null;

      do {
        const number = Math.random().toString().slice(2);
        id = 'gpt-ad-' + number;
      } while (id in generatedIDs);

      generatedIDs[id] = true;

      return id;
    }

    /**
    * The ID generator service.
    *
    * If the element passed has an ID already defined, it's uniqueness will be
    * checked. If it is not unique or not set at all, a new unique, random ID
    * is generated for the element.
    *
    * @param {Object} element The element whose ID to check or assign.
    * @return {string} The unique ID of the element, or a new generated one.
    */
    function dfpIDGenerator(element) {
      if (element && element.id && !(element.id in generatedIDs)) {
        return element.id;
      }

      const id = generateID();
      if (element) element.id = id;

      return id;
    }

    /**
    * Tests if an ID is taken.
    * @param  {number} id The ID to test.
    * @return {boolean} True if the ID is not unique, else false.
    * @see dfpIDGenerator.isUnique()
    */
    dfpIDGenerator.isTaken = function(id) {
      return id in generatedIDs;
    };

    /**
    * Tests if an ID is unique (not taken).
    * @param  {number} id The ID to test.
    * @return {boolean} True if the ID is unique, else false.
    * @see dfpIDGenerator.isTaken()
    */
    dfpIDGenerator.isUnique = function(id) {
      return !dfpIDGenerator.isTaken(id);
    };

    return dfpIDGenerator;
  }

  module.factory('dfpIDGenerator', [dfpIDGeneratorFactory]);

// eslint-disable-next-line
})(angularDfp);

/**
* @module dfp-refresh
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line no-use-before-define, no-var
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-refresh */ function(module) {
  'use strict';

  /**
  * An error thrown by the `dfpRefresh` service.
  * @private
  */
  class DFPRefreshError extends Error {}

  /**
 * The core unit handling refresh calls to DFP.
 *
 * This provider exposes the `dfpRefresh` function, which, at the simplest, is
 * simply a proxy for `googletag.pubads().refresh()` and allows for dynamic ad
 * calls. However, do note that is has more complex refreshing functionality
 * built in, such as being able to buffer refresh calls and flush at certain
 * intervals, or have refresh call "barriers" (a fixed number of calls to wait
 * for) and global refresh intervals.
 * @private
 */
  function dfpRefreshProvider() {
      // Store reference
    const self = this;

    /**
    * The milliseconds to wait after receiving a refresh request
    * to see if more requests come that we can buffer.
    * @type {?number}
    */
    self.bufferInterval = null;

    /**
   * The current limit of requests to buffer before sending a request.
   * If a proxy timeout is set and times out but the amount has not
   * yet been reached, the timeout will*not* be respected. That is,
   * setting a barrier temporarily (disables) the timeout.
   * @type {?number}
   */
    self.bufferBarrier = null;

    /**
   * If true, disables any barrier set once it was reached and re-enables
   * any timeout. If false, the barrier must be manually
   * disables via clearBarrier().
   * @type {boolean}
   */
    self.oneShotBarrier = true;

    /**
   * The interval after which *all* ads on the page are refreshed.
   * @type {?number}
   */
    self.refreshInterval = null;

    /* eslint-disable quote-props */
    /**
    * Dynamic weighting to prioritize certain
    * refresh mechanisms over others.
    * @type {Object}
    */
    self.priority = {
      'refresh': 1,
      'interval': 1,
      'barrier': 1
    };
    /* eslint-enable quote-props */

    self.$get = [
      '$rootScope',
      '$interval',
      '$q',
      '$log',
      'parseDuration',
      function($rootScope, $interval, $q, $log, parseDuration) {
        /**
        * The possible buffering/refreshing options (as an "enum")
        * @type {!Object}
        */
        const Options = Object.freeze({
          REFRESH: 'refresh',
          INTERVAL: 'interval',
          BARRIER: 'barrier'
        });

        /**
        * This external enum has string keys so that the closure compiler
        * does not rename them, while we can still use dot-notation internally.
        * @type {!Object}
        */
        /* eslint-disable quote-props */
        dfpRefresh.Options = Object.freeze({
          'REFRESH': Options.REFRESH,
          'INTERVAL': Options.INTERVAL,
          'BARRIER': Options.BARRIER
        });
        /* eslint-enable quote-props */

        /**
        * The buffered ads waiting to be refreshed.
        * @type {Array}
        */
        let buffer = [];

        /**
        * Need to store all intervals because any interval created
        * using $interval must explicitly be destroyed, and to enable
        * stopping a refresh.
        * @type {Object}
        */
        const intervals = {refresh: null, buffer: null};

        /**
        * Stores the activity status of the buffering/refreshing options.
        * @type {Object}
        */
        /* eslint-disable quote-props */
        const isEnabled = Object.seal({
          refresh: self.refreshInterval !== null,
          interval: self.bufferInterval !== null,
          barrier: self.bufferBarrier !== null
        });
        /* eslint-enable quote-props */

        /**
        * The main interfacing function to the `dfpRefresh` proxy.
        *
        * Depending on the buffering configuration currently in place, the slot
        * passed may be buffered until either a barrier is reached or the
        * buffering interval elapses. If no buffering is set, the slot is
        * refreshed immediately.
        *
        * @param  {googletag.Slot} slot  The adslot to refresh.
        * @param  {string|number=} interval The interval at which to refresh.
        * @param  {!boolean=} defer If an interval is passed and defer is false, a regular refresh call will be made immediately.
        * @return {Promise} A promise, resolved after the refresh call.
        */
        function dfpRefresh(slot, interval, defer) {
          const deferred = $q.defer();
          const task = {slot, deferred};

          if (interval) {
            addSlotInterval(task, interval);
          }

          if (!interval || !defer) {
            scheduleRefresh(task);
          }

          return deferred.promise;
        }

        /**
        * Cancels an interval set for a certain ad slot.
        * @param  {googletag.Slot} slot The ad slot to cancel the interval for.
        * @throws DFPRefreshError When the given slot has not interval associated.
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.cancelInterval = function(slot) {
          if (!dfpRefresh.hasSlotInterval(slot)) {
            throw new DFPRefreshError("No interval for given slot");
          }

          $interval.cancel(intervals[slot]);
          delete intervals[slot];

          return dfpRefresh;
        };

        /**
        * Tests if the given slot has an interval set.
        * @param  {googletag.Slot}  slot The slot to check.
        * @return {!boolean} True if an interval is set for the slot, else false.
        */
        dfpRefresh.hasSlotInterval = function(slot) {
          return slot in intervals;
        };

        /**
        * Sets a new value for the buffer interval.
        *
        * The buffer interval is the interval at which
        * the proxy buffer is flushed.
        *
        * @param {!string|!number} interval An interval string or number
        *                                 (asis valid for `parseDuration`).
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.setBufferInterval = function(interval) {
          self.bufferInterval = parseDuration(interval);
          prioritize();

          return dfpRefresh;
        };

        /**
        * Clears any interval set for the buffering mechanism.
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.clearBufferInterval = function() {
          if (!dfpRefresh.hasBufferInterval()) {
            console.warn("clearBufferInterval had no " +
                         "effect because no interval was set.");
            return dfpRefresh;
          }

          disableBufferInterval();
          self.bufferInterval = null;

          prioritize();

          return dfpRefresh;
        };

        /**
        * Tests if currently any buffering interval is set.
        *
        * Note that even if a buffering interval is set, it may not currently
        * be active when also a barrier or global refresh interval with a
        * higher priority is active. This method will return true if
        * `setBufferInterval()` was ever called or a value was assigned to the
        * buffer interval property during configuration.
        *
        * @return {boolean} True if a buffer interval exists.
        ** @see dfpRefresh.bufferIntervalIsEnabled
        */
        dfpRefresh.hasBufferInterval = function() {
          return self.bufferInterval !== null;
        };

        /**
        * Tests if the buffer interval is currently*enabled*.
        *
        * Even if the service has a buffer interval configured, it may not be
        * currently enabled due to a lower priority setting relative to other
        * buffering/refreshing mechanisms.
        *
        * @return {boolean} True if the buffering interval is enabled, else false.
        * @see dfpRefresh.hasBufferInterval
        */
        dfpRefresh.bufferIntervalIsEnabled = function() {
          return isEnabled.interval;
        };

        /**
        * Returns the buffer interval setting (may be null).
        * @return {?number} The current buffer interval (in ms), if any.
        */
        dfpRefresh.getBufferInterval = function() {
          return self.bufferInterval;
        };

        /**
        * Sets a buffer barrier.
        *
        * A barrier is a number of refresh calls to wait before actually
        * performing a single refresh. I.e. it is the minimum buffer capacity
        * at which a refresh call is made. This is useful if you know that a
        * certain number of independent (that is, uncoordindated) refresh calls
        * will be made in a certain unit of time and you wish to wait for all
        * of them to arrive before calling new ads for all of them. For
        * example, you may have infinite scroll enabled and know that with
        * every new content fetch 3 ads come. Then you can pass the number 3 to
        * this method and the service will wait for 3 refresh calls before
        * actually refreshing them.
        *
        * @param {number} numberOfAds The number of ads to wait for.
        * @param {boolean=} oneShot     Whether to uninstall the barrier after the first flush.
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.setBufferBarrier = function(numberOfAds, oneShot) {
          self.bufferBarrier = numberOfAds;
          self.oneShotBarrier = (oneShot === undefined) ? true : oneShot;
          prioritize();

          return dfpRefresh;
        };

        /**
        * Clears any buffer barrier set.
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.clearBufferBarrier = function() {
          if (!dfpRefresh.hasBufferBarrier()) {
            console.warn("clearBufferBarrier had not effect because " +
                         "no barrier was set.");
            return dfpRefresh;
          }

          self.bufferBarrier = null;
          prioritize();

          return dfpRefresh;
        };

        /**
        * Returns the any buffer barrier set.
        * @return {number?} The current barrier
        *                  (number of ads to buffer before flushing).
        */
        dfpRefresh.getBufferBarrier = function() {
          return self.bufferBarrier;
        };

        /**
        * Tests if any buffer barrier is set.
        *
        * Note that even if a buffer barrier is set, it may not currently
        * be active when also an interval or global refresh interval with a
        * higher priority is active. This method will return true if
        * `setBufferBarrier()` was ever called or a value was assigned to the
        * buffer barrier property during configuration.
        *
        * @return {boolean} True if a buffer barrier is set, else false.
        */
        dfpRefresh.hasBufferBarrier = function() {
          return self.bufferBarrier !== null;
        };

        /**
        * Tests if a buffer barrier is currently active.
        * @return {boolean} True if a buffer barrier is enabled, else false.
        */
        dfpRefresh.bufferBarrierIsEnabled = function() {
          return isEnabled.barrier;
        };

        /**
        * Tests if the current buffer barrier has "one-shot" behavior enabled.
        *
        * If a barrier is "one-shot", this means it is disabled after the
        * barrier count is reached for the first time.
        *
        * @return {boolean} True if "one-shot" behavior is active, else false.
        */
        dfpRefresh.bufferBarrierIsOneShot = function() {
          return self.oneShotBarrier;
        };

        /**
        * Sets the global refresh interval.
        *
        * This is the interval at which all ads are refreshed.
        *
        * @param {!number|!string} interval The new interval
        *                        (as valid for the `parseDuration` service.)
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.setRefreshInterval = function(interval) {
          // Maybe warn for too low an interval
          self.refreshInterval = parseDuration(interval);
          validateInterval(self.refreshInterval, interval);
          enableRefreshInterval();
          prioritize();

          return dfpRefresh;
        };

        /**
        * Tests if any refresh interval is set.
        *
        * Note that even if a refresh interval is set, it may not currently
        * be active when also a buffer barrier or interval with a
        * higher priority is active. This method will return true if
        * `setRefreshInterval()` was ever called or a value was assigned to
        * the refreshInterval property during configuration.
        *
        * @return {boolean} True if an interval is set, else false.
        */
        dfpRefresh.hasRefreshInterval = function() {
          return self.refreshInterval !== null;
        };

        /**
        * Tests if the refresh interval is currently active.
        * @return {boolean} True if a refresh interval
        *                   is currently active, else false.
        */
        dfpRefresh.refreshIntervalIsEnabled = function() {
          return isEnabled.refresh;
        };

        /**
        * Clears any refresh interval set.
        * @return {Function} The current `dfpRefresh` instance.
        */
        dfpRefresh.clearRefreshInterval = function() {
          if (!dfpRefresh.hasRefreshInterval()) {
            console.warn("clearRefreshInterval had no effect because " +
                         "no refresh interval was set.");
          }

          disableRefreshInterval();
          prioritize();

          return dfpRefresh;
        };

        /**
        * Returns the current refresh interval, if any (may be `null`).
        * @return {?number} The current refresh interval.
        */
        dfpRefresh.getRefreshInterval = function() {
          return self.refreshInterval;
        };

        /**
        * Checks if either of the buffering mechanisms are enabled.
        * @return {!boolean} True if either the buffer barrier or
        *                   interval are enabled, else false
        */
        dfpRefresh.isBuffering = function() {
          return isEnabled.barrier || isEnabled.interval;
        };

        /**
        * Tests if the given refreshing/buffering mechanism is installed.
        *
        * Installed does not mean active, as this is
        * determined by the prioritization algorithm.
        *
        * @param  {string}  option What to test activation for.
        * @return {!boolean} True if the given option was ever
        *                   installed, else false.
        */
        dfpRefresh.has = function(option) {
          switch (option) {
            case Options.REFRESH: return dfpRefresh.hasRefreshInterval();
            case Options.INTERVAL: return dfpRefresh.hasBufferInterval();
            case Options.BARRIER: return dfpRefresh.hasBufferBarrier();
            default: throw new DFPRefreshError(`Invalid option '${option}'`);
          }
        };

        /**
        * Sets the priority for the given option.
        *
        * The prioritzation algorithm allows mutual exclusion of any of the
        * three buffering/refreshing options. More precisely, only the
        * mechanisms whose priority is the maximum of all three will be
        * enabled, if installed. This means that when all have equal priority,
        * all three will be enabled (because their priority is equal to the
        * maximum), but when one has higher priority only that will run.
        *
        * @param {!string} option What to set the priority for.
        * @param {number} priority The priority to set.
        * @return {Function} The current dfpRefresh instance.
        * @see dfpRefresh.Options
        * @throws DFPRefreshError if the option is not one of
        *         the DFPRefresh.Options members.
        */
        dfpRefresh.setPriority = function(option, priority) {
          ensureValidOption(option);
          ensureValidPriority(priority);
          self.priority[option] = priority;

          return dfpRefresh;
        };

        /**
        * Gets the priority setting for a given option.
        * @param  {string} option The option to check.
        * @return {number} The priority of the option.
        */
        dfpRefresh.getPriority = function(option) {
          ensureValidOption(option);
          return self.priority[option];
        };

        /**
        * Sets the priority of the global refreshing mechanism.
        * @param {number} priority The priority to give.
        */
        dfpRefresh.setRefreshPriority = function(priority) {
          ensureValidPriority(priority);
          dfpRefresh.setPriority('refresh', priority);
        };

        /**
        * @return {number} The priority of the global refreshing mechanism.
        */
        dfpRefresh.getRefreshPriority = function() {
          return dfpRefresh.getPriority('refresh');
        };

        /**
        * Sets the priority of the buffer barrier.
        * @param {number} priority The priority to give.
        */
        dfpRefresh.setBarrierPriority = function(priority) {
          ensureValidPriority(priority);
          dfpRefresh.setPriority('barrier', priority);
        };

        /**
        * @return {number} The priority of the buffer barrier.
        */
        dfpRefresh.getBarrierPriority = function() {
          return dfpRefresh.getPriority('barrier');
        };

        /**
        * Sets the priority of the buffer interval.
        * @param {number} priority The priority to give.
        */
        dfpRefresh.setIntervalPriority = function(priority) {
          ensureValidPriority(priority);
          dfpRefresh.setPriority('interval', priority);
        };

        /**
        * @return {number} The priority of the buffer interval.
        */
        dfpRefresh.getIntervalPriority = function() {
          return dfpRefresh.getPriority('interval');
        };

        /**
        * Utility function to check if an option is valid and throw if not.
        * @param  {string} option The option to check.
        * @throws DFPRefreshError if the option is not valid.
        */
        function ensureValidOption(option) {
          if (!(option in Options)) {
            throw new DFPRefreshError(`Invalid option '${option}'`);
          }
        }

        /**
        * Utility function to check if a priority is valid and throw if not.
        * @param  {*} priority The priority to check.
        * @throws DFPRefreshError if the priority is not valid.
        */
        function ensureValidPriority(priority) {
          if (typeof priority !== `number`) {
            throw new DFPRefreshError(`Priority '${priority}' is not a number`);
          }
        }

        /**
        * Enables or disables an option.
        * @param  {string} option The option to check.
        * @param  {boolean=} yes Whether to enable or not.
        */
        function enable(option, yes) {
          if (yes === false) {
            disable(option);
            return;
          }

          switch (option) {
            case Options.REFRESH: enableRefreshInterval(); break;
            case Options.INTERVAL: enableBufferInterval(); break;
            case Options.BARRIER: enableBufferBarrier(); break;
            default: console.assert(false);
          }
        }

        /**
        * Disables the given option.
        * @param  {string} option The option to disable.
        */
        function disable(option) {
          switch (option) {
            case Options.REFRESH: disableRefreshInterval(); break;
            case Options.INTERVAL: disableBufferInterval(); break;
            case Options.BARRIER: disableBufferBarrier(); break;
            default: console.assert(false);
          }
          /* eslint-enable max-statements-per-line*/
        }

        /**
        * The prioritization algorithm.
        *
        * Given the set of all available options O, where availability is
        * determined by the semantics of dfpRefresh.has, where each element o
        * in O has a given priority p_o, the algorithm will proceed to find the
        * maximum ofver all p_o. Given the maximum, all those options o in O
        * whose priority is equal to the maximum are enabled and all others
        * disabled.
        */
        function prioritize() {
          /**
          * The options theoretically possible.
          * Closure does not yet recognize Object.values.
          * @type {Array}
          */
          let options = Object.keys(Options).map(key => Options[key]);

          /**
          * The options available (installed).
          * @type {Array}
          */
          let available = options.filter(dfpRefresh.has);

          /**
          * The priorities of the available options.
          * @type {Array}
          */
          let priorities = available.map(option => self.priority[option]);

          /**
          * The maximum priority.
          * @type {?number}
          */
          let maximum = null;
          if (priorities.length > 0) {
            maximum = priorities.reduce((a, b) => Math.max(a, b));
          }

          for (let index = 0; index < available.length; ++index) {
            if (priorities[index] === maximum) {
              enable(available[index]);
            } else {
              disable(available[index]);
            }
          }
        }

        /**
        * The main refreshing function.
        *
        * This function will either refresh all slots if called with no
        * arguments, or else all the slots passed in the array argument.
        *
        * @param  {?Array=} tasks An array of `(slot, promise)` pairs.
        */
        function refresh(tasks) {
          console.assert(tasks === undefined || tasks !== null);

          // If 'tasks' was not passed at all, we refresh all ads
          if (tasks === undefined) {
            googletag.cmd.push(() => {
              googletag.pubads().refresh();
            });
            return;
          }

          // Do nothing for a null or empty buffer
          if (tasks.length === 0) return;

          // Refresh any non-null slots. Slots can be null when the buffer is
          // not empty when the refresh interval triggers. The buffer can then
          // not be cleared, because that might mess with barriers. We also
          // can't reduce the barrier, because it may not be one-shot (i.e.
          // persistent).
          tasks = tasks.filter(pair => pair !== null);

          googletag.cmd.push(() => {
            googletag.pubads().refresh(tasks.map(task => task.slot));
            tasks.forEach(task => task.deferred.resolve());
          });
        }

        /**
        * Sends the buffer off for refreshing and clears it.
        */
        function flushBuffer() {
          refresh(buffer);
          buffer = [];
        }

        /**
        * Enables the global refresh interval.
        */
        function enableRefreshInterval() {
          console.assert(dfpRefresh.hasRefreshInterval());

          const task = function() {
            // Set the elments currently in the buffer to null,
            // since all ads will be refreshed, but the length
            // must remain unchanged in case the barrier is not yet fulfilled
            clearBufferRespectingBarrier();

            // Calling refresh() without any arguments
            // will refresh all registered ads on the site
            refresh();
          };

          const promise = $interval(task, self.refreshInterval);
          intervals.refresh = promise;
          isEnabled.refresh = true;
        }

        /**
        * Disables the refresh interval.
        *
        * The function is idempotent. That is, it is only effective if the
        * refresh interval is actually set, else it does nothing.
        */
        function disableRefreshInterval() {
          if (isEnabled.refresh) {
            $interval.cancel(intervals.refresh);
            intervals.refresh = null;
            isEnabled.refresh = false;
          }
        }

        /**
        * Enables the buffer interval.
        */
        function enableBufferInterval() {
          console.assert(dfpRefresh.hasBufferInterval());

          // Because the buffer interval may interleave with a barrier we don't
          // want the interval to take away elements that would prevent the
          // barrier from being reached just because the interval happened
          // before. As such, we still refresh, but then still take up the same
          // amount of space as before.
          const task = function() {
            refresh(buffer);
            clearBufferRespectingBarrier();
          };

          const promise = $interval(task, self.bufferInterval);
          intervals.buffer = promise;
          isEnabled.interval = true;
        }

        /**
        * Disables the buffer interval.
        *
        * The function is idempotent. That is, it is only effective if the
        * buffer interval is actually set, else it does nothing.
        */
        function disableBufferInterval() {
          if (isEnabled.interval) {
            $interval.cancel(intervals.buffer);
            intervals.buffer = null;
            isEnabled.interval = false;
          }
        }

        /**
        * Enables the buffer barrier.
        */
        function enableBufferBarrier() {
          console.assert(dfpRefresh.hasBufferBarrier());
          isEnabled.barrier = true;
        }

        /**
        * Disables the buffer barrier.
        *
        * The function is idempotent. That is, it is only effective if the
        * buffer barrier is actually set, else it does nothing.
        */
        function disableBufferBarrier() {
          isEnabled.barrier = false;
        }

        /**
         * Fills the buffer with `null` if a barrier is set, else clears it.
         */
        function clearBufferRespectingBarrier() {
          if (isEnabled.barrier) {
            for (let i = 0; i < buffer.length; ++i) {
              buffer[i] = null;
            }
          } else {
            buffer = [];
          }
        }

        /**
        * Adds an interval for a given slot.
        * @param {!Object} task The `(slot, promise)` object.
        * @param {string|number} interval The interval duration to set.
        */
        function addSlotInterval(task, interval) {
          const parsedInterval = parseDuration(interval);
          validateInterval(parsedInterval, interval);

          const promise = $interval(
            () => { scheduleRefresh(task); },
            parsedInterval
          );

          intervals[task.slot] = promise;
        }

        /**
        * Schedules a refresh for a slot.
        *
        * This function is basically a proxy to refresh(), as it may either
        * buffer the refresh call or do it immediately.
        *
        * @param  {!Object} task The `(slot, promise)` object.
        * @see bufferRefresh()
        */
        function scheduleRefresh(task) {
          if (dfpRefresh.isBuffering()) {
            bufferRefresh(task);
          } else {
            refresh([task]);
          }
        }

        /**
        * Buffers a refresh call for a slot.
        * @param  {!Object} task The `(slot, promise)` object.
        */
        function bufferRefresh(task) {
          buffer.push(task);

          if (!isEnabled[Options.BARRIER]) return;
          if (buffer.length === self.bufferBarrier) {
            flushBuffer();
            if (self.oneShotBarrier) {
              dfpRefresh.clearBufferBarrier();
            }
          }
        }

        /**
         * Validates a refresh interval passed.
         *
         * Just gives out a warning if the interval may be too low.
         *
         * @param  {?number} milliseconds  The interval, in milliseconds.
         * @param  {!number|!string} beforeParsing The interval, before parsing.
         */
        function validateInterval(milliseconds, beforeParsing) {
          console.assert(milliseconds);
          if (milliseconds < 500) {
            $log.warn('Careful: ${beforeParsing} is quite a low interval!');
          }
        }

        // Unregister all listeners when the root scope dies
        $rootScope.$on('$destroy', function() {
          // eslint-disable-next-line no-undef
          intervals.forEach(promise => {
            $interval.cancel(promise);
          });
        });

        if (self.refreshInterval) {
          self.refreshInterval = parseDuration(self.refreshInterval);
        }

        if (self.bufferInterval) {
          self.bufferInterval = parseDuration(self.bufferInterval);
        }

        prioritize();

        return dfpRefresh;
      }];
  }

  module.provider('dfpRefresh', [dfpRefreshProvider]);
// eslint-disable-next-line
})(angularDfp);

/**
* @module responsive-resize
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:responsive-resize */ function(module) {
  'use strict';

  /**
  * The factory of the `responsiveResize` service.
  *
  * The `responsiveResize` service ensures that ads with responsive mappings
  * defined always have iframes that fit their ad's dimensions and not their
  * containers' width. This ensures that centering ads, which is essential to
  * responsive ads, works well.
  *
  * @private
  */
  function dfpResponsiveResizeProvider() {
    /**
    * Reference to tthe responsiveResizeProvider.
    * @type {Function}
    */
    const self = this;

    /**
    * The delay after which to make an ad call once a resize was registered.
    *
    * Once a resize is registered, the `refreshing` CSS class is added to the
    * ad's container and only after this delay the actual refresh call is made.
    * This allows certain animations to take place. For example, you could
    * have an opacity of 0 in the `refreshing` class and fade out the ad.
    *
    * The value of this variable may be anything understood by parseDuration.
    * @type {!number|!string}
    */
    self.refreshDelay = 200;

    this.$get = [
      '$interval',
      '$timeout',
      '$window',
      'dfpRefresh',
      'parseDuration',
      function($interval, $timeout, $window, dfpRefresh, parseDuration) {
        // Turn into jQLite element
        // eslint-disable-next-line
        $window = angular.element($window);

        // If a duration expression was passed (e.g. '2s'), then parse it
        self.refreshDelay = parseDuration(self.refreshDelay);

        /**
        * The `responsiveResize` service.
        * @param  {!angular.JQLite} element The element to make responsive.
        * @param {!googletag.Slot} slot The ad slot to refresh responsively.
        * @param  {!Array<!ViewportDimensions>} dimensions The viewport dimensions at which to refresh.
        */
        function responsiveResize(element, slot, dimensions) {
          // Sort the dimensions first by width, then (on equality) by height
          dimensions.sort((first, second) => {
            if (first.width < second.width) return -1;
            if (first.width > second.width) return +1;

            // Else width is equal, so compare by height
            if (first.height < second.height) return -1;
            if (first.height > second.height) return +1;

            // Width and height are equal
            return 0;
          });

          /**
          * The interval for polling changes in the iframe's dimensions.
          * @type {number}
          * @constant
          */
          const POLL_INTERVAL = 100; // 100ms

          /**
          * How long we poll (at the rate of POLL_INTERVAL).
          * @type {number}
          * @constant
          */
          const POLL_DURATION = 2500; // 2.5s

          /**
          * Retrieves the iframe of the ad of the element.
          * @return {angular.JQLite} An iframe HTML element.
          */
          function queryIFrame() {
            return element.find('div iframe');
          }

          /**
          * Normalized the iframe dimensions.
          *
          * This operation here is the main goal of this service. To ensure that the
          * responsive ad can always be centered, it must have width and height
          * matching its content. However, upon new ad loads, it may happen that
          * even though the iframe width and height*attributes* change, the CSS
          * dimensions remain unchanged. This distorts the element. As such, we
          * simply normalize these two dimensionss here.
          *
          * @param  {angular.JQLite=} iframe Optionally, the iframe to normalize
          *                           (else it is queried).
          */
          function normalizeIFrame(iframe) {
            iframe = iframe || queryIFrame();
            iframe.css('width', iframe.attr('width') + 'px');
            iframe.css('height', iframe.attr('height') + 'px');
          }

          /**
          * Polls for a change in the dimensions of the
          * iframe and normalizes if a change was detected.
          * @param  {!Object} initial The initial dimensions against
          *                          which to compare.
          */
          function pollForChange(initial) {
            // The iframe element may change between calls
            const iframe = queryIFrame();

            const change = ['width', 'height'].some(dimension => {
              return iframe.attr(dimension) !== initial[dimension];
            });

            if (change) {
              normalizeIFrame(iframe);
              element.parent().removeClass('refreshing');
            }
          }

          /**
          * Starts polling for changes in the ad's dimensions.
          * @param  {!Object} initial The initial dimensions against
          *                          which to compare.
          */
          function startPolling(initial) {
            // Poll for a change every `POLL_INTERVAL` milliseconds
            const poll = $interval(() => pollForChange(initial), POLL_INTERVAL);

            // Stop polling after `POLL_DURATION`
            $timeout(() => $interval.cancel(poll), POLL_DURATION);
          }

          /**
          * @return {!{width: ?string, height: ?string}} The initial width of the iframe.
          */
          function getIframeDimensions() {
            const iframe = queryIFrame();
            const dimensions = [iframe.css('width'), iframe.css('height')];

            // Slice away the 'px' at the end, if set
            let plain = dimensions.map(dimension => {
              return dimension ? dimension.slice(0, -2) : null;
            });

            return {width: plain[0], height: plain[1]};
          }

          /**
          * Sets up the watching mechanisms for the responsive resizing.
          */
          function watchResize() {
            // The goal is to have the iFrame's width and height style (CSS)
            // properties match the width and height attributes, which are set by
            // DFP are thus the correct dimensions. However, the behavior here is
            // quite undeterministic and difficult to predict in terms of what
            // events trigger a change in size and when this happens. As such, the
            // best strategy I found was to periodically poll for any changes in the
            // width and height attributes for the first second, every 100ms. After
            // that any changes to the dimensions of the iframe should have been
            // captured and digested. Since changes to these dimensions will only
            // happen after a request, it is also not necessary to setup a resize
            // watch.
            startPolling(getIframeDimensions());

            // An additional resize listener helps for tricky cases
            // eslint-disable-next-line no-undef
            $window.on('resize', () => { normalizeIFrame(); });
          }

          /**
          * Returns a function suitable for responsive resize-event watching.
          * @return {Function} A function to pass as an event
          *                    listener for (window) resize events.
          */
          function makeResponsive() {
            /**
            * Determines in which of the boundaries the element is.
            *
            * Linear searches because there will never be many entries. Else do an
            * equal-range lower bound search to find the floor (largest value that
            * is less * than or equal) of the viewport width, then an upper bound
            * binary search for * the equal range, then a floor search for the
            * height. O(3 * lg N) but overkill here.
            * @return {number} The current index.
            */
            function determineIndex() {
              const width = window.innerWidth;
              const height = window.innerHeight;
              const numberOfDimensions = dimensions.length;

              let index = 1;
              for (; index < numberOfDimensions; ++index) {
                if (width < dimensions[index].width) break;
                if (height < dimensions[index].height) break;
              }

              // Returns the index that was still valid
              return index - 1;
            }

            /**
            * The index corresponding to the current boundaries of the element.
            * @type {number}
            */
            let index = determineIndex();

            /**
            * Tests if the element could grow in size.
            *
            * An element can grow if it's not yet maximally sized and
            * its width is at least as big as that of the next boundary.
            * @return {boolean} True if the index should be
            *                   incremented by one, else false.
            */
            function couldGrow() {
              if (index + 1 >= dimensions.length) return false;
              if (window.innerWidth < dimensions[index + 1].width) {
                return false;
              }
              if (window.innerHeight < dimensions[index + 1].height) {
                return false;
              }

              return true;
            }

            /**
            * Tests if the element could shrink in size.
            *
            * An element can grow if it's not yet minimally sized and
            * its width is less than the current boundary.
            * @return {boolean} True if the index should be
            *                   decremented by one, else false.
            */
            function couldShrink() {
              if (index === 0) return false;
              if (window.innerWidth < dimensions[index].width) return true;
              if (window.innerHeight < dimensions[index].height) return true;
              return false;
            }

            /**
            * Refreshes the ad slot.
            */
            function refresh() {
              dfpRefresh(slot).then(() => {
                watchResize();
              });
            }

            /**
            * Performs a size transition.
            * @param  {number} delta The delta by which to change the index.
            */
            function transition(delta) {
              console.assert(index >= 0 && index < dimensions.length);
              console.assert(delta === -1 || delta === +1);

              index += delta;

              // Add this class so the user can add styling while
              // the ad is refreshing and the iframe resizes
              element.parent().addClass('refreshing');

              // Refresh the ad slot now
              $timeout(refresh, self.refreshDelay);

              console.assert(index >= 0 && index < dimensions.length);
            }

            // Resize initially
            watchResize();

            return function watchListener() {
              if (couldGrow()) {
                transition(+1);
              } else if (couldShrink()) {
                transition(-1);
              }
            };
          }

          $window.on('resize', makeResponsive());
        }

        return responsiveResize;
      }];
  }

  module.provider('dfpResponsiveResize', dfpResponsiveResizeProvider);

    // eslint-disable-next-line
  })(angularDfp);

/**
* @file A directive for giving an ad responsive sizes.
*
* The `dfp-responsive` mapping is the `angular-dfp` pendant to GPT's
* `sizeMapping` and allows mapping viewport dimensions to ad sizes. More
* precisely, the `dfp-responsive` directive allows nesting one or more
* [`dfp-size`](@link module:dfp-size) tags, each specifying ad dimensions
* allowed for the given viewport width and height (and higher ones).
*
* Using such mappings you can, for example, render `320x50` mobile leaderboards
* on smartphones and `728x90` leaderboards on desktops, all in the same ad slot.
*
* The viewport height is optional and defaults to zero. It may not be required if
* you only care about viewport widths (phone, tablet, desktop screen sizes).
*
* @example <caption>Example usage of the `dfp-size` directive.</caption>
* <dfp-ad ad-unit="path/to/my/ad-unit">
*   <dfp-responsive viewport-width=320>
*     <dfp-size width=300 height=50></dfp-size>
*     <dfp-size width=320 height=50></dfp-size>
*   </dfp-responsive>
*   <dfp-responsive viewport-width=1024 viewport-height=800>
*     <dfp-size width=970 height=90></dfp-size>
*   </dfp-responsive>
* </dfp-ad>
*
* @see [`dfp-size`](@link module:dfp-size)
*
* @module dfp-responsive
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-responsive */ function(module) {
  'use strict';

  /**
  * The controller for the `dfp-responsive` directive.
  * @param {Function} DFPIncompleteError The `DFPIncompleteError` service.
  * @param {Function} DFPTypeError The `DFPTypeError` service.
  * @private
  */
  function DFPResponsiveController(DFPIncompleteError, DFPTypeError) {
    /* eslint-disable dot-notation */
    /**
    * The size of the viewport.
    *
    * A `dfp-responsive` tag always has one fixed viewport width and height, and
    * then many possible ad sizes viable for ad calls for those viewport
    * dimensions.
    *
    * @type {!googletag.SingleSizeArray}
    */
    const viewportSize = Object.seal([
      this['viewportWidth'],
      this['viewportHeight'] || 0
    ]);
    /* eslint-enable dot-notation */

    /**
    * The ad sizes for the viewport dimensions.
    * @type {!googletag.MultiSize}
    */
    const adSizes = [];

    /**
    * Asserts if the state of the controller is valid.
    * @throws {DFPTypeError|DFPIncompleteError} If the directive is not complete.
    */
    this.checkValid = function() {
      ['viewportWidth', 'viewportHeight'].forEach(dimension => {
        const value = this[dimension];
        if (typeof value !== 'number') {
          dimension = dimension.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
          throw new DFPTypeError('dfp-responsive', dimension, value, 'number');
        }
      });

      if (adSizes.length === 0) {
        throw new DFPIncompleteError('dfp-responsive', 'dfp-size', false);
      }
    };

    /**
    * Adds an ad size to the responsive mapping.
    * @param {!googletag.SingleSize} size The ad size to add for the viewport size.
    */
    this.addSize = function(size) {
      adSizes.push(size);
    };

    /**
    * Retrieves the state of the controller.
    * @return {ResponsiveMapping} The state of the controller, for use by the directive.
    */
    this.getState = function() {
      this.checkValid();
      return Object.freeze({
        viewportSize,
        adSizes
      });
    };
  }

  DFPResponsiveController.$inject = ['$scope'];

  /**
  * The directive for the responsive mapping.
  *
  * @private
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} ad The parent `dfp-ad` controller.
  */
  function dfpResponsiveDirective(scope, element, attributes, ad) {
    const mapping = scope.controller.getState();
    ad.addResponsiveMapping(mapping);
  }

  module.directive('dfpResponsive', [function() {
    return {
      restrict: 'E',
      require: '^^dfpAd',
      controller: [
        'DFPIncompleteError',
        'DFPTypeError',
        DFPResponsiveController
      ],
      controllerAs: 'controller',
      bindToController: true,
      link: dfpResponsiveDirective,
      // Need to quote props to avoid closure compiler renaming
      // eslint-disable-next-line quote-props
      scope: {'viewportWidth': '=', 'viewportHeight': '='}
    };
  }]);

// eslint-disable-next-line
})(angularDfp);

/**
* @file A directive for supplying additional JavaScript to configure an ad slot.
*
* Since our library does not cover all parts of the GPT library, it may be
* necessary to add your own, additional JavaScript to further configure an
* ad slot. For this, the `dfp-script` directive allows writing inline or
* external JS and manipulate the ad slot. More precisely, the library will
* inject the slot object (which can be renamed) into the scope of the
* JavaScript. Moreover, you may inject your own `scope` object that will
* be available inside the script.
*
* The `dfp-script` tag must be nested directly under the main `dfp-ad` tag.
*
* @example <caption>Example usage of the `dfp-script` directive.</caption>
* <dfp-ad ad-unit="path/to/my/ad-unit">
*   <dfp-script slot-as="mySlot" scope="{ path: 'spaghetti' }">
*     if (mySlot.getAdUnitPath() === scope.path) {
*       mySlot.clearTargeting();
*     }
*   </dfp-script>
* </dfp-ad>
*
* @module dfp-script
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-script */ function(module) {
  'use strict';

  /**
   * An error thrown when the `dfp-script` could not fetch a JS source.
   */
  class DFPScriptError extends Error {
    constructor(response) {
      super(
        `Error (${response.status}): could not fetch ` +
        `'dfp-script' source from '${response.config.url}'.`
      );
    }
  }

  /**
  * Defines the `dfp-script` directive.
  *
  * The purpose of this directive is to allow additional operations on the ad
  * slot, in case any functionality was not covered by this library. More
  * precisely, the script is given access to the slot object (which may be
  * renamed, optionally) as well as a custom (optional) scope, to then perform
  * any further customizations.
  *
  * @private
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} ad The parent `dfp-ad` controller.
  * @param {!angular.$injector} $injector The Angular `$injector` service.
  */
  function dfpScriptDirective(scope, element, attributes, ad, $injector) {
    const $http = $injector.get('$http');
    const $log = $injector.get('$log');

    /* eslint-disable dot-notation */

    /**
     * Adds a script with the given contents to the parent `dfp-ad` controller.
     * @param {!string} contents The contents of the script to add.
     */
    function addScript(contents) {
      let script = `(function(scope, ${scope['slotAs']}){${contents}})`;

      // Now we `eval` the script and bind the scope attribute (if any)
      // eslint-disable-next-line no-eval
      ad.addScript(eval(script).bind(null, scope['scope']));
    }

    if (scope['src']) {
      if (element.html()) {
        $log.warn(
          'Ignoring inner HTML of dfp-script ' +
          'in favor of src contents.'
        );
      }

      $http.get(scope['src']).then(
        response => { addScript(response.data); },
        response => { throw new DFPScriptError(response); }
      );
    } else {
      addScript(element.html().trim());
    }

    /* eslint-enable dot-notation */
  }

  module.directive('dfpScript', ['$injector', function($injector) {
    return {
      restrict: 'E',
      require: '^^dfpAd',
      // eslint-disable-next-line quote-props
      scope: {'slotAs': '@', 'scope': '=', 'src': '@'},
      link: function(...args) {
        dfpScriptDirective.apply(null, args.slice(0, 4).concat($injector));
      }
    };
  }]);

// eslint-disable-next-line
})(angularDfp);

/**
* @file A directive for specifying an ad size.
*
* A `dfp-size` directive specifies a `(width, height)` pair of ad dimensions.
* This directive can not be defined on its own, but is only valid:
*
* 1. when nested directly under a `dfp-ad` directive, or
* 2. when nested directly under a `dfp-responsive` directive.
*
* In the former case, the `dfp-size` directive specifies a fixed ad-slot size as
* would be passed as the second parameter to the
* `googletag.pubads().defineSlot()`` method. In the latter case, the directive
* specifies a size mapping member for certain viewport dimensions.
*
* @example <caption>Example usage of the `dfp-size` directive.</caption>
* <dfp-ad ad-unit="path/to/my/ad-unit">
*   <dfp-size width=728 height=90></dfp-size>
*   <dfp-responsive viewport-width=1024 viewport-height=800>
*     <dfp-size width=320 height=50></dfp-size>
*   </dfp-responsive>
* </dfp-ad>
*
* @see [dfp-ad]{@link module:dfp-ad}
* @see [dfp-responsive]{@link module:dfp-responsive}
*
* @module dfp-size
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-size */ function(module) {
  'use strict';

  /**
  * The `dfp-size` directive.
  *
  * This directive, when nested under either the `dfp-ad` or `dfp-responsive`
  * tag, adds a size value to the parent. This size can either be given as
  * width and height dimension via attributes, or as any valid string size
  * (e.g. 'fluid') between the tags.
  *
  * @private
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} parent     The parent controller.
  * @param {Function} DFPMissingParentError The `DFPMissingParentError` service.
  */
  function DFPSizeDirective(scope,
                            element,
                            attributes,
                            parent,
                            DFPMissingParentError) {
    // Only one of the two possible parents will be `null`
    // Pick the most nested parent (`dfp-responsive`)
    parent = parent[1] || parent[0];

    if (!parent) {
      throw new DFPMissingParentError('dfp-size', 'dfp-ad', 'dfp-responsive');
    }

    if (scope.width && scope.height) {
      parent.addSize([scope.width, scope.height]);
    } else {
      parent.addSize(element[0].innerHTML);
    }
  }

  module.directive('dfpSize', [
    'DFPMissingParentError',
    function(DFPMissingParentError) {
      return {
        restrict: 'E',
        require: ['?^^dfpAd', '?^^dfpResponsive'],
        scope: {width: '=', height: '='},
        link: function(...args) {
          args = args.slice(0, 4).concat(DFPMissingParentError);
          DFPSizeDirective.apply(null, args);
        }
      };
    }]);

// eslint-disable-next-line
})(angularDfp);

/**
* @file A directive for specifying DFP key/value targeting.
*
* DFP has a concept of "key/values" to allow targeting of ads against slots that
* have certain values for certain keys. This directive enables adding one or
* more such key/value pairs for an ad slot.
*
* The `dfp-targeting` directive must nested in a `dfp-ad` tag. It may then be
* used either by directly passing a `key` and a `value` via attributes, or
* alternatively by specifying only a `key` attribute and adding values via
* nested `dfp-value` tags.
*
* @example
* <dfp-ad ad-unit="path/to/my/ad-unit">
*   <dfp-targeting key="sport" value="quidditch"></dfp-targeting>
*   <dfp-targeting key="food">
*     <dfp-value>chicken</dfp-value>
*     <dfp-value>meatballs</dfp-value>
*   </dfp-targeting>
* </dfp-ad>
*
* @module dfp-targeting
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-targeting */ function(module) {
  'use strict';

  /**
  * The controller for DFP targeting (key-value) directives.
  *
  * This controller makes an `addValue` function available that allows the
  * `dfp-value` directive to add values for a single key attribute defined in
  * the directive.
  * @param {Function} DFPIncompleteError The `DFPIncompleteError` service.
  * @private
  */
  function dfpTargetingController(DFPIncompleteError) {
    /**
    * The values of the targeting.
    * @type {Array}
    */
    const values = this.value ? [this.value] : [];

    /**
    * Verifies that the controller has a complete (valid) state.
    * @throws {DFPIncompleteError} If the directive is not complete.
    */
    this.checkValid = function() {
      if (this.key === undefined) {
        throw new DFPIncompleteError('dfp-targeting', 'key', true);
      }

      if (values.length === 0) {
        throw new DFPIncompleteError('dfp-targeting', 'value', true);
      }
    };

    /**
    * Retrieves the public state of the controller for use by the directive.
    * @return {Object} The key and an array of values for the targeting.
    */
    this.getState = function() {
      this.checkValid();
      return Object.freeze({
        key: this.key,
        values
      });
    };

    /**
    * Adds a value for the key of the targeting.
    * @param {string} value The value to add for the key.
    */
    this.addValue = function(value) {
      values.push(value);
    };
  }

  /**
  * The directive for `dfp-targeting` tags.
  *
  * This directive requires to be nested in a `dfp-ad` tag. It may then be
  * used either by directly passing a `key` and a `value` via attributes, or
  * alternatively by specifying only a key and adding values via nested
  * `dfp-value` tags.
  *
  * @private
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} ad The parent `dfp-ad` controller.
  */
  function dfpTargetingDirective(scope, element, attributes, ad) {
    console.assert(ad !== undefined);

    // Retrieve the state from the controller and add it to the parent
    const targeting = scope.controller.getState();
    ad.addTargeting(targeting);
  }

  module.directive('dfpTargeting', [function() {
    return {
      restrict: 'E',
      require: '^^dfpAd', // require dfp-ad as parent
      controller: ['DFPIncompleteError', dfpTargetingController],
      controllerAs: 'controller',
      bindToController: true,
      scope: {key: '@', value: '@'},
      link: dfpTargetingDirective
    };
  }]);

// eslint-disable-next-line
})(angularDfp);

/**
* @file Defines a value for a targeting key.
*
* This directive can be used in conjunction with the `dfp-targeting` directive
* when you want to supply more than one value for a key (if you only want one
* key, just use the `key` attribute on `dfp-targeting`).
*
* The value itself is taken from the inner contents of the `dfp-value` tag.
*
* @example <caption>Example usage of the `dfp-value` directive.</caption>
* <dfp-ad ad-unit="/path/to/my/ad-unit">
*   <dfp-targeting key="food">
*     <dfp-value>spam</dfp-value>
*     <dfp-value>ham</dfp-value>
*   </dfp-targeting>
* </dfp-ad>
*
* @module dfp-value
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-value */ function(module) {
  'use strict';

  /**
  * The `dfp-value` directive.
  *
  * The `dfp-value` directive allows specifying multiple values for a single
  * key when nested in a `dfp-targeting` directive.
  *
  * @private
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} parent     The parent (`dfp-targeting`) controller.
  */
  function dfpValueDirective(scope, element, attributes, parent) {
    parent.addValue(element.html());
  }

  module.directive('dfpValue', [function() {
    return {
      restrict: 'E',
      require: '^^dfpTargeting',
      link: dfpValueDirective
    };
  }]);
// eslint-disable-next-line
})(angularDfp);

/**
* @file Enables video ads on a video element.
*
* This directive uses the [videojs]{@link http://videojs.com/} library to serve
* video ads on an HTML5 video tag. The ad tag must be supplied as an attribute.
*
* TODO: example
*
* @module dfp-video>
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line no-undef, no-unused-vars
let angularDfpVideo = angular.module('angularDfp');

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp-video */ function(module) {
  'use strict';

  /**
   * An Error class for the dfp-video directive.
   */
  class DFPVideoError extends Error { }

  /**
  * The `dfp-video` directive.
  *
  * This directive enables video ads to be shown over videos,
  * using `videojs` and the IMA SDK.
  *
  * @private
  * @param {Object} scope The angular scope.
  * @param {Object} element The HTML element on which the directive is defined.
  * @param {Object} attributes The attributes of the element.
  * @param {Object} $injector The Angular '$injector' service.
  */
  function dfpVideoDirective(scope, element, attributes, $injector) {
    const dfpIDGenerator = $injector.get('dfpIDGenerator');

     // Unpack jQuery object
    element = element[0];

    if (element.tagName !== 'VIDEO') {
      throw new DFPVideoError(
        "'dfp-video' directive must be attached to a <video> tag."
      );
    }

     // Generate an ID or check for uniqueness of an existing one
    dfpIDGenerator(element);

    // eslint-disable-next-line no-undef
    const player = videojs(element.id);

     // Register the video slot with the IMA SDK
     // eslint-disable-next-line dot-notation
    player.ima({id: element.id, adTagUrl: scope['adTag']});
    player.ima.requestAds();
    player.ima.initializeAdDisplayContainer();
  }

  module.directive('dfpVideo', ['$injector', function($injector) {
    return {
      restrict: 'A',
      // eslint-disable-next-line quote-props
      scope: {'adTag': '@'},
      link: function(...args) {
        dfpVideoDirective.apply(null, args.slice(0, 3).concat($injector));
      }
    };
  }]);

  return module;

// eslint-disable-next-line
})(angularDfpVideo);

/**
* @module dfp
* @license Apache
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line no-use-before-define, no-var
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];

// eslint-disable-next-line valid-jsdoc
(/** @lends module:dfp */ function(module) {
  'use strict';

  class DFPConfigurationError extends Error {}

  /**
  * The URL to the GPT library we want to load asynchronously.
  */
  module.constant(
    'GPT_LIBRARY_URL',
    '//www.googletagservices.com/tag/js/gpt.js'
  );

  /**
  * The provider for the doubleClick service.
  *
  * The doubleClick service is responsible for main initial configuration
  * tasks, injecting the GPT library asynchronously and providing the `then`
  * proxy to `googletag.cmd.push`.
  *
  * @param {string} GPT_LIBRARY_URL The URL of the GPT library to inject.
  */
  function dfpProvider(GPT_LIBRARY_URL) {
    /**
    * The doubleClickProvider function.
    * @type {Function}
    * @constant
    */
    const self = this;

    /**
    * Whether to enable video ads.
    * @type {!boolean}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_enableVideoAds}
    */
    self.enableVideoAds = true;

    /**
    * Whether to collapse empty divs for which ad calls fail.
    * @type {boolean}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_collapseEmptyDivs}
    */
    self.collapseIfEmpty = true;

    /**
    * Enables ad centering instead of left-alignment.
    * @type {boolean}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_setCentering}
    */
    self.centering = false;

    /**
    * The location information to pass to DFP.
    *
    * This should either be an array of `[longitude, latitude [, radius]]`
    * numbers or a freefrom address string. You must enable usage of this
    * information in DFP.
    *
    * @type {?Array|string}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_setLocation}
    */
    self.location = null;

    /**
    * Your Publisher-Provided Identifier, if you have any.
    * @type {?string}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_setPublisherProvidedId}
    */
    self.ppid = null;

    /**
    * An optional object of global targeting key/values.
    *
    * These will apply to all ad slots.
    * The format should be `(key, value|[values])`.
    * @type {?Object}
    * @see [GPT Reference]{@link https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_setTargeting}
    */
    self.globalTargeting = null;

    /**
    * Whether all ad slots should force safe frame by default.
    * @type {boolean}
    */
    self.forceSafeFrame = false;

    /**
    * Optionally, configuration information for safe frames.
    *
    * @example
    * var config = {
    *   allowOverlayExpansion: true,
    *   allowPushExpansion: true,
    *   sandbox: true
    *};
    * @type {?googletag.SafeFrameConfig}
    */
    self.safeFrameConfig = null;

    /**
    * Whether to download the GPT library.
    * @type {!boolean}
    */
    self.loadGPT = true;

    /**
    * Whether or not we have loaded the GPT library yet.
    * @type {!boolean}
    */
    let loaded = false;

    /**
    * Handles the safe-frame configuration.
    * @param {googletag.PubAdsService} pubads The googletag pubads service.
    */
    function addSafeFrameConfig(pubads) {
      if (!self.safeFrameConfig) return;
      if (typeof self.globalTargeting !== 'object') {
        throw new DFPConfigurationError('Targeting must be an object');
      }
      pubads.setSafeFrameConfig(self.safeFrameConfig);
    }

    /**
    * Handles the targeting configuration.
    * @param {googletag.PubAdsService} pubads The googletag pubads service.
    */
    function addTargeting(pubads) {
      if (!self.globalTargeting) return;
      if (typeof self.globalTargeting !== 'object') {
        throw new DFPConfigurationError('Targeting must be an object');
      }

      for (let key in self.globalTargeting) {
        if (self.globalTargeting.hasOwnProperty(key)) {
          pubads.setTargeting(key, self.globalTargeting[key]);
        }
      }
    }

    /**
    * Handles the location configuration.
    * @param {googletag.PubAdsService} pubads The googletag pubads service.
    */
    function addLocation(pubads) {
      if (!self.location) return;

      if (typeof self.location === 'string') {
        pubads.setLocation(self.location);
        return;
      }

      if (!Array.isArray(self.location)) {
        throw new DFPConfigurationError('Location must be an ' +
        'array or string');
      }

      pubads.setLocation.apply(pubads, self.location);
    }

    /**
    * Handles the ppid configuration.
    * @param {googletag.PubAdsService} pubads The googletag pubads service.
    */
    function addPPID(pubads) {
      if (!self.ppid) return;
      if (typeof self.ppid !== 'string') {
        throw new DFPConfigurationError('PPID must be a string');
      }

      pubads.setPublisherProvidedId(self.ppid);
    }

    // Fear not this syntax, my son!
    this.$get = ['scriptInjector', scriptInjector => {
      /**
      * Sets up the GPT and DFP services.
      */
      function setup() {
        const pubads = googletag.pubads();

        if (self.enableVideoAds) {
          pubads.enableVideoAds();
        }

        if (self.collapseIfEmpty) {
          pubads.collapseEmptyDivs();
        }

        // We always refresh ourselves
        pubads.disableInitialLoad();
        pubads.setForceSafeFrame(self.forceSafeFrame);
        pubads.setCentering(self.centering);

        addLocation(pubads);
        addPPID(pubads);
        addTargeting(pubads);
        addSafeFrameConfig(pubads);

        googletag.enableServices();
      }

      /**
     * The configuration function called to initialize the doubleClick service.
     */
      function dfp() {
        googletag.cmd.push(setup);

        if (self.loadGPT) {
          scriptInjector(GPT_LIBRARY_URL).then(() => {
            loaded = true;
          });
        }
      }

      /**
     * Tests if the GPT library has been injected yet.
     * @return {boolean} [description]
     */
      dfp.hasLoaded = function() {
        return loaded;
      };

      /**
     * Pushes a taks into GPT's asynchronous task queue.
     * @param  {Function} task The task function to execute in the queue.
     */
      dfp.then = function(task) {
        googletag.cmd.push(task);
      };

      return dfp;
    }];
  }

  /**
   * In order to avoid local variable creation while using bundlers such as browserify
   * or webpack googletag variable is set as global explicitly.
   */
  window.googletag = googletag;

  module.provider('dfp', ['GPT_LIBRARY_URL', dfpProvider]);

// eslint-disable-next-line
})(angularDfp);