angular-dfp
Version:
Semantic DoubleClick integration with AngularJS
1,544 lines (1,352 loc) • 102 kB
JavaScript
/**
* @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.