UNPKG

angular-dfp

Version:

Semantic DoubleClick integration with AngularJS

1,544 lines (1,352 loc) 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.