29 | );
30 | }
31 | }
32 |
33 | export default App;
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 NFL
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/examples/apps/static-ad/app.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import Radium from "radium";
3 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved
4 | import "../log";
5 | import styles from "./styles";
6 |
7 | @Radium
8 | class App extends Component {
9 | state = {
10 | color: "000000"
11 | };
12 |
13 | onClick = () => {
14 | this.setState({
15 | color: this.state.color === "000000" ? "ff0000" : "000000"
16 | });
17 | };
18 |
19 | render() {
20 | const {color} = this.state;
21 | return (
22 |
103 |
104 |
105 |
106 |
107 | `;
108 | }
109 |
110 | app.listen(port, error => {
111 | if (error) {
112 | console.error(error);
113 | } else {
114 | console.info(
115 | `==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`
116 | );
117 | }
118 | });
119 |
--------------------------------------------------------------------------------
/docs/GettingStarted.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | The simplest form of React GPT ad looks like the following
4 |
5 | ```js
6 | import {Bling as GPT} from "react-gpt";
7 |
8 | class Application extends React.Component {
9 | render() {
10 | return (
11 |
15 | );
16 | }
17 | }
18 | ```
19 |
20 | `adUnitPath` is a required prop and either `slotSize` or `sizeMapping` prop are needed to give the size information.
21 |
22 | ## Enabling Single Request Mode
23 |
24 | To enable [Single Request Mode](https://support.google.com/dfp_sb/answer/181071?hl=en), call `Bling.enableSingleRequest()` before rendering any ad.
25 | It defaults to `Asynchronous Rendering Mode` if not set.
26 |
27 | ```js
28 | import {Bling as GPT} from "react-gpt";
29 |
30 | GPT.enableSingleRequest();
31 |
32 | class Application extends React.Component {
33 | render() {
34 | return (
35 |
36 |
40 |
41 |
42 |
46 |
47 | );
48 | }
49 | }
50 | ```
51 |
52 | The above example will make one request to the server to render both ads and easier to ensure category exclusion.
53 |
54 | ## Responsive ad
55 |
56 | If you pass `sizeMapping` props instead of `slotSize`, React GPT listens for the viewport width change and refreshes an ad when the break point is hit.
57 |
58 | ```js
59 | import {Bling as GPT} from "react-gpt";
60 |
61 | class Application extends React.Component {
62 | render() {
63 | return (
64 |
72 | );
73 | }
74 | }
75 | ```
76 |
77 | ## Lazy render
78 |
79 | React GPT by default renders an ad when its bounding box is fully inside the viewport. You can disable this setting and render an ad regardless of the position, pass `renderWhenViewable={false}` as a prop.
80 | To read more about lazy render, please see the [guide](./Guides.md#viewability).
81 |
82 | ## Out-of-page ad
83 |
84 | You can render out-of-page(prestitial or interstitial) ad by passing `outOfPage={true}` as a prop.
85 | Out-of-page ad does not require either `slotSize` or `sizeMapping`.
86 |
87 | ```js
88 | import {Bling as GPT} from "react-gpt";
89 |
90 | class Application extends React.Component {
91 | render() {
92 | return (
93 |
97 | );
98 | }
99 | }
100 | ```
101 |
102 | ## Companion ad
103 |
104 | Companion ad can be enabled by passing `companionAdService={true}` as a prop. Once enabled and when the video ad plays using [Google IMA](https://developers.google.com/interactive-media-ads/) within the same page, the React GPT ad will render the companion ad.
105 |
106 | ```js
107 | import {Bling as GPT} from "react-gpt";
108 |
109 | class Application extends React.Component {
110 | render() {
111 | return (
112 |
117 | );
118 | }
119 | }
120 | ```
121 |
122 | ## Passback ad
123 |
124 | It's not currently supported.
125 |
126 | For more use cases, please see [examples](../examples).
127 |
--------------------------------------------------------------------------------
/lib/utils/apiList.js:
--------------------------------------------------------------------------------
1 | Object.defineProperty(exports, "__esModule", {
2 | value: true
3 | });
4 | // DO NOT MODIFY THIS FILE MANUALLY.
5 | // This file is generated by `npm run update-apilist`.
6 | // Note that only APIs that's documented in https://developers.google.com/doubleclick-gpt/reference is officially supported.
7 |
8 | var gptVersion = exports.gptVersion = 110;
9 | var gptAPI = exports.gptAPI = [["getVersion", "function"], ["cmd", "object"], ["getEventLog", "function"], ["enableServices", "function"], ["setAdIframeTitle", "function"], ["impl", "object"], ["pubads", "function"], ["defineOutOfPageSlot", "function"], ["defineSlot", "function"], ["defineUnit", "function"], ["destroySlots", "function"], ["display", "function"], ["companionAds", "function"], ["content", "function"], ["debug_log", "object"], ["service_manager_instance", "object"], ["disablePublisherConsole", "function"], ["onPubConsoleJsLoad", "function"], ["openConsole", "function"], ["sizeMapping", "function"], ["evalScripts", "function"], ["apiReady", "boolean"], ["slot_manager_instance", "object"], ["pubadsReady", "boolean"]];
10 | var pubadsVersion = exports.pubadsVersion = 110;
11 | var pubadsAPI = exports.pubadsAPI = [["set", "function"], ["get", "function"], ["getAttributeKeys", "function"], ["display", "function"], ["getName", "function"], ["setCookieOptions", "function"], ["setTagForChildDirectedTreatment", "function"], ["clearTagForChildDirectedTreatment", "function"], ["setKidsFriendlyAds", "function"], ["setTargeting", "function"], ["clearTargeting", "function"], ["getTargeting", "function"], ["getTargetingKeys", "function"], ["setCategoryExclusion", "function"], ["clearCategoryExclusions", "function"], ["disableInitialLoad", "function"], ["enableSingleRequest", "function"], ["enableAsyncRendering", "function"], ["enableSyncRendering", "function"], ["setCentering", "function"], ["setPublisherProvidedId", "function"], ["definePassback", "function"], ["defineOutOfPagePassback", "function"], ["refresh", "function"], ["enableVideoAds", "function"], ["setVideoContent", "function"], ["getVideoContent", "function"], ["getCorrelator", "function"], ["setCorrelator", "function"], ["updateCorrelator", "function"], ["isAdRequestFinished", "function"], ["collapseEmptyDivs", "function"], ["clear", "function"], ["setLocation", "function"], ["getVersion", "function"], ["forceExperiment", "function"], ["markAsAmp", "function"], ["setSafeFrameConfig", "function"], ["setForceSafeFrame", "function"], ["enableChromeInterventionSignals", "function"], ["markAsGladeControl", "function"], ["markAsGladeOptOut", "function"], ["getName", "function"], ["getVersion", "function"], ["getSlots", "function"], ["getSlotIdMap", "function"], ["enable", "function"], ["addEventListener", "function"]];
12 | var slotAPI = exports.slotAPI = [["getPassbackPageUrl", "function"], ["set", "function"], ["get", "function"], ["getAttributeKeys", "function"], ["addService", "function"], ["getName", "function"], ["getAdUnitPath", "function"], ["getInstance", "function"], ["getSlotElementId", "function"], ["getSlotId", "function"], ["getServices", "function"], ["getSizes", "function"], ["defineSizeMapping", "function"], ["hasWrapperDiv", "function"], ["setClickUrl", "function"], ["getClickUrl", "function"], ["setForceSafeFrame", "function"], ["setCategoryExclusion", "function"], ["clearCategoryExclusions", "function"], ["getCategoryExclusions", "function"], ["setTargeting", "function"], ["clearTargeting", "function"], ["getTargetingMap", "function"], ["getTargeting", "function"], ["getTargetingKeys", "function"], ["getOutOfPage", "function"], ["getAudExtId", "function"], ["gtfcd", "function"], ["setCollapseEmptyDiv", "function"], ["getCollapseEmptyDiv", "function"], ["getDivStartsCollapsed", "function"], ["fetchStarted", "function"], ["getContentUrl", "function"], ["fetchEnded", "function"], ["renderStarted", "function"], ["getResponseInformation", "function"], ["renderEnded", "function"], ["loaded", "function"], ["impressionViewable", "function"], ["visibilityChanged", "function"], ["setFirstLook", "function"], ["getFirstLook", "function"], ["getEscapedQemQueryId", "function"], ["setSafeFrameConfig", "function"], ["getCsiId", "function"]];
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## [2.0.1](https://github.com/nfl/react-gpt/compare/v2.0.0...v2.0.1) (2018-03-13)
4 |
5 | ### Code Refactoring
6 |
7 | * Adds `onSlotOnload` event
8 | * Allows `["fluid"]` slotSize as an array
9 |
10 |
11 |
12 | ## [2.0.0](https://github.com/nfl/react-gpt/compare/v1.1.1...v2.0.0) (2018-01-04)
13 |
14 | ### Bug Fixes
15 |
16 | * Removed test util dependencies from distribution ([27187e0](https://github.com/nfl/react-gpt/commit/27187e0))
17 |
18 | ### Migration notes
19 |
20 | **< 2.0.0** you may have imported `createManagerTest` like this:
21 |
22 | ```
23 | import {createManagerTest} from "react-gpt";
24 | ```
25 |
26 | **>= 2.0.0** you now need to import `createManagerTest` like this:
27 |
28 | ```
29 | import {createManagerTest} from "react-gpt/es/utils/createManagerTest";
30 | ```
31 |
32 |
33 |
34 | ## [1.1.1](https://github.com/nfl/react-gpt/compare/v1.0.1...v1.1.1) (2017-11-08)
35 |
36 | ### Bug Fixes
37 |
38 | * Fixed bug in example Router project ([7687ee9](https://github.com/nfl/react-gpt/commit/7687ee9))
39 |
40 | ### Code Refactoring
41 |
42 | * Updated to support React 16 and unit tests refactored for React 16 ([84264e7](https://github.com/nfl/react-gpt/commit/84264e7))
43 |
44 |
45 |
46 | ## [1.0.1](https://github.com/nfl/react-gpt/compare/v1.0.0...v1.0.1) (2017-09-19)
47 |
48 | ### Bug Fixes
49 |
50 | * **package.json:** Add es folder to published package ([2aa1a03](https://github.com/nfl/react-gpt/commit/2aa1a03))
51 |
52 |
53 |
54 | ## [1.0.0](https://github.com/nfl/react-gpt/compare/v0.3.0...v1.0.0) (2017-09-18)
55 |
56 | ### Features
57 |
58 | * **createManager:** Export AdManager ([#53](https://github.com/nfl/react-gpt/issues/53)) ([9ed1807](https://github.com/nfl/react-gpt/commit/9ed1807)), closes [#42](https://github.com/nfl/react-gpt/issues/42)
59 | * **package.json:** Add Yarn ([#38](https://github.com/nfl/react-gpt/issues/38)) ([8b7a570](https://github.com/nfl/react-gpt/commit/8b7a570))
60 | * **package.json:** Export es modules ([#54](https://github.com/nfl/react-gpt/issues/54)) ([2d7a3ec](https://github.com/nfl/react-gpt/commit/2d7a3ec)), closes [#29](https://github.com/nfl/react-gpt/issues/29)
61 |
62 |
63 |
64 | ## [0.3.0](https://github.com/nfl/react-gpt/compare/v0.2.5...v0.3.0) (2017-09-18)
65 |
66 | ### Code Refactoring
67 |
68 | * Throttles scroll-check to render ad faster ([7130060a](https://github.com/nfl/react-gpt/commit/7130060a))
69 | * Use smaller invariant / canUseDom dependencies ([b187381](https://github.com/nfl/react-gpt/commit/b187381))
70 |
71 | ### Features
72 |
73 | * Check bundle-size on PR ([8e51e26](https://github.com/nfl/react-gpt/commit/8e51e26))
74 | * Upgrade eslint and introduce prettier ([17c8b89](https://github.com/nfl/react-gpt/commit/17c8b89))
75 |
76 |
77 |
78 | ## [0.2.5](https://github.com/nfl/react-gpt/compare/v0.2.4...v0.2.5) (2017-07-31)
79 |
80 | ### Bug Fixes
81 |
82 | * Add yarn.lock ([b7c7c50](https://github.com/nfl/react-gpt/commit/b7c7c50))
83 | * Import PropTypes from prop-types package ([34b61be](https://github.com/nfl/react-gpt/commit/34b61be))
84 | * Move MockGPT out of distribution files ([775fe26](https://github.com/nfl/react-gpt/commit/775fe26))
85 | * Import ReactTestUtils from test-utils ([75e74f6](https://github.com/nfl/react-gpt/commit/75e74f6))
86 |
87 |
88 |
89 | ## [0.2.4](https://github.com/nfl/react-gpt/compare/v0.2.3...v0.2.4) (2017-03-23)
90 |
91 | ### Bug Fixes
92 |
93 | * more gracefully handle adSlot becoming empty object due to AdBlocker ([7f9a989](https://github.com/nfl/react-gpt/commit/7f9a989))
94 |
95 |
96 |
97 | ## [0.2.3](https://github.com/nfl/react-gpt/compare/v0.2.2...v0.2.3) (2017-02-21)
98 |
99 | ### Bug Fixes
100 |
101 | * fix calling the same pubads API on Bling not overriding the previous one ([fc374b6](https://github.com/nfl/react-gpt/commit/fc374b6))
102 |
103 |
104 |
105 | ## [0.2.2](https://github.com/nfl/react-gpt/compare/v0.2.1...v0.2.2) (2016-10-13)
106 |
107 | ### Code Refactoring
108 |
109 | * **API:** update GPT API list ([993c0e0](https://github.com/nfl/react-gpt/commit/993c0e0))
110 |
111 | ## 0.2.1
112 |
113 | Features:
114 |
115 | * Initial release
116 |
--------------------------------------------------------------------------------
/docs/Guides.md:
--------------------------------------------------------------------------------
1 | ## Refresh vs Re-render
2 |
3 | GPT [requires ad unit path and size to construct an ad slot](https://developers.google.com/doubleclick-gpt/reference#googletag.defineSlot), for that reason, when `adUnitPath` or `slotSize` props change, React GPT destroys an old ad slot and creates a new one which results in rendering a new ad.
4 | Additionally, when `outOfPage` or `content` props change, React GPT re-renders an ad to create a new ad slot to reflect these props.
5 | Any other ad slot related props are reflected by [refreshing an ad](https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_refresh).
6 |
7 | ## Per instance update vs per page update
8 |
9 | When `Bling.syncCorrelator([true])` is called before rendering any ad, React GPT triggers ad re-render/refresh to all the ad instances in the page to ensure they use the same correlator value.
10 |
11 | ## Tweaking the render performance
12 |
13 | By default, to determine whether the ad should refresh, re-render or should not render, React GPT uses deep equality check against the props in question. You can override this default logic by overriding `propsEqual` config with your preferred equality check such as shallow equality when you make sure to pass a new object whenever the data changes.
14 | To set or override the configuration, call `Bling.configure(config)`.
15 |
16 | ## Viewability
17 |
18 | React GPT by default lazy loads an ad when it becomes within the viewport area for [viewability](https://support.google.com/dfp_premium/answer/4574077) as well as minimizing ad requests.
19 | [Interactive Advertising Bureau (IAB)](http://www.iab.com/) defines a viewable impression for most of the display ad to be 50% of the ad’s pixels are visible in the browser window for a continuous 1 second.
20 | For that reason, React GPT sets the default viewable threshold to be 50% of the ad area. You can however customize this threshold globally or per ad.
21 |
22 | ```js
23 | import {Bling as GPT} from "react-gpt";
24 |
25 | // sets the threashold globally.
26 | GPT.configure({viewableThreshold: 0.3});
27 |
28 | class Application extends React.Component {
29 | // sets the threshold per ad.
30 | render() {
31 | return (
32 |
37 | );
38 | }
39 | }
40 | ```
41 |
42 | You can additionally turn off lazy loading by setting `renderWhenViewable` to `false` either globally or per ad.
43 |
44 | ```js
45 | import {Bling as GPT} from "react-gpt";
46 |
47 | // sets the lazy load globally.
48 | GPT.configure({renderWhenViewable: false});
49 |
50 | class Application extends React.Component {
51 | // sets the lazy load per ad.
52 | render() {
53 | return (
54 |
59 | );
60 | }
61 | }
62 | ```
63 |
64 | ## How to keep the module and `gpt.js` in sync
65 |
66 | `gpt.js` is not in the public VCS and only the latest version is available to load from the public URL.
67 | Because of it, we assume `gpt.js` is almost always backward compatible unless announced in a timely manner by Google.
68 |
69 | The current API list is stored in [`apiList.js`](../../src/utils/apiList.js)
70 | and used to create [GPT mock file](../../src/utils/mockGPT.js).
71 | [`apiList.js`](../../src/utils/apiList.js) is auto-generated by running `update-apilist` npm script where it extracts the public API from `gpt.js`.
72 | The mock file is used for unit test and potentially catches the breaking changes on their end although it's less likely to happen.
73 |
74 | There are often cases where undocumented APIs are added to the `gpt.js`, but we will not support those unless it's [officially documented](https://developers.google.com/doubleclick-gpt/reference).
75 |
76 | ## Test Mode
77 |
78 | GPT ad uses iframe to render an ad most of the times and it often fails to render ads within the unit test which itself uses iframe in some unit test libraries such as [karma](https://github.com/karma-runner/karma).
79 | React GPT offers the test mode where it uses the mock GPT instead of requesting `gpt.js`.
80 |
81 | Here is an example of how to use the test mode in your unit test using [mocha](https://github.com/mochajs/mocha).
82 |
83 | ```js
84 | import {Bling as GPT} from "react-gpt";
85 |
86 | describe("My module", () => {
87 | beforeEach(() => {
88 | // create a fresh ad manager with test mode for every test.
89 | GPT.createTestManager();
90 | });
91 |
92 | // your test goes here.
93 | });
94 | ```
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # React GPT
4 |
5 | [](https://www.npmjs.org/package/react-gpt)
6 | [](https://travis-ci.org/nfl/react-gpt)
7 | [](https://david-dm.org/nfl/react-gpt)
8 | [](https://codecov.io/github/nfl/react-gpt?branch=master)
9 | [](CONTRIBUTING.md#pull-requests)
10 |
11 | A [React](https://github.com/facebook/react) component for [Google Publisher Tags](https://developers.google.com/doubleclick-gpt/?hl=en).
12 |
13 | ## Requirements
14 |
15 | * React 0.14+
16 |
17 | ## Browser Requirements
18 |
19 | * IE10+
20 |
21 | ## Features
22 |
23 | * Supports all rendering modes (single request mode, async rendering node and *sync rendering mode)
24 | * Supports responsive ads.
25 | * Supports interstitial ads.
26 | * Supports lazy render.
27 |
28 | \* Synchronous rendering requires that the GPT JavaScript be loaded synchronously.
29 |
30 | ## Installation
31 |
32 | ```
33 | $ yarn react-gpt
34 | ```
35 |
36 | React GPT depends on [Promise](https://promisesaplus.com/) to be available in browser. If your application support the browser which doesn't support Promise, please include the polyfill.
37 |
38 | ## Getting Started
39 |
40 | Import React GPT and pass props to the component.
41 |
42 | ```js
43 | import {Bling as GPT} from "react-gpt";
44 |
45 | class Application extends React.Component {
46 | render() {
47 | return (
48 |
52 | );
53 | }
54 | }
55 | ```
56 |
57 | You at least need to pass `adUnitPath` and one of `slotSize` and `sizeMapping`.
58 |
59 | #### Enabling Single Request Mode
60 |
61 | To enable [Single Request Mode](https://support.google.com/dfp_sb/answer/181071?hl=en), call `Bling.enableSingleRequest()` before rendering any ad.
62 | It defaults to `Asynchronous Rendering Mode` if not set.
63 |
64 | ```js
65 | import {Bling as GPT} from "react-gpt";
66 |
67 | GPT.enableSingleRequest();
68 |
69 | class Application extends React.Component {
70 | render() {
71 | return (
72 |
73 |
77 |
78 |
79 |
83 |
84 | );
85 | }
86 | }
87 | ```
88 |
89 | The above example will make one request to the server to render both ads which makes it easier to ensure category exclusion.
90 |
91 | #### Responsive ad
92 |
93 | If you pass `sizeMapping` props instead of `slotSize`, React GPT listens for the viewport width change and refreshes an ad when the break point is hit.
94 |
95 | ```js
96 | import {Bling as GPT} from "react-gpt";
97 |
98 | class Application extends React.Component {
99 | render() {
100 | return (
101 |
109 | );
110 | }
111 | }
112 | ```
113 |
114 | ## API and Documentation
115 |
116 | * [API](/docs/api/) Review the `React GPT` API
117 | * [Getting Started](/docs/GettingStarted.md) A more detailed Getting Started Guide
118 | * [Docs](/docs/) Guides and API.
119 |
120 | ## To run examples:
121 |
122 | 1. Clone this repo
123 | 2. Run `yarn`
124 | 3. Run `npm run examples` for client side rendering, `npm start` for server side rendering.
125 | 4. Point your browser to http://localhost:8080
126 |
127 | ## Contributing to this project
128 |
129 | Please take a moment to review the [guidelines for contributing](CONTRIBUTING.md).
130 |
131 | * [Pull requests](CONTRIBUTING.md#pull-requests)
132 | * [Development Process](CONTRIBUTING.md#development)
133 |
134 | ## License
135 |
136 | MIT
137 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gpt",
3 | "version": "2.0.1",
4 | "description": "A react display ad component using Google Publisher Tag",
5 | "main": "lib/index.js",
6 | "jsnext:main": "es/index.js",
7 | "contributors": [
8 | {
9 | "name": "NFL Engineering"
10 | }
11 | ],
12 | "license": "MIT",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/nfl/react-gpt"
16 | },
17 | "keywords": [
18 | "react-gpt",
19 | "nfl",
20 | "react",
21 | "ad",
22 | "gpt",
23 | "google publisher tags"
24 | ],
25 | "bugs": {
26 | "url": "https://github.com/nfl/react-gpt/issues"
27 | },
28 | "files": ["*.md", "docs", "es", "src", "dist", "lib"],
29 | "dependencies": {
30 | "deep-equal": "^1.0.1",
31 | "eventemitter3": "^2.0.2",
32 | "exenv": "^1.2.2",
33 | "hoist-non-react-statics": "^1.0.5",
34 | "invariant": "^2.2.2",
35 | "throttle-debounce": "^1.0.1"
36 | },
37 | "devDependencies": {
38 | "babel-cli": "^6.5.1",
39 | "babel-core": "^6.5.1",
40 | "babel-eslint": "^8.0.0",
41 | "babel-loader": "^6.2.3",
42 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
43 | "babel-plugin-webpack-alias": "^2.1.1",
44 | "babel-preset-es2015-without-strict": "^0.0.4",
45 | "babel-preset-react": "^6.5.0",
46 | "babel-preset-stage-0": "^6.5.0",
47 | "babel-register": "^6.7.2",
48 | "bundlesize": "^0.14.4",
49 | "chai": "^3.4.1",
50 | "codecov.io": "^0.1.6",
51 | "commitizen": "^2.8.1",
52 | "conventional-changelog-cli": "^1.2.0",
53 | "core-js": "^2.2.2",
54 | "cz-conventional-changelog": "^1.1.6",
55 | "eslint": "4.7.0",
56 | "eslint-config-nfl": "12.0.0",
57 | "eslint-config-prettier": "^2.5.0",
58 | "eslint-plugin-import": "2.7.0",
59 | "eslint-plugin-mocha": "4.11.0",
60 | "eslint-plugin-prettier": "^2.2.0",
61 | "eslint-plugin-react": "7.3.0",
62 | "express": "^4.13.4",
63 | "history": "^4.3.0",
64 | "isparta-loader": "^2.0.0",
65 | "karma": "^1.3.0",
66 | "karma-chai-sinon": "^0.1.5",
67 | "karma-chrome-launcher": "^2.0.0",
68 | "karma-cli": "^1.0.1",
69 | "karma-coverage": "^1.1.1",
70 | "karma-mocha": "^1.2.0",
71 | "karma-mocha-reporter": "^2.0.0",
72 | "karma-sourcemap-loader": "^0.3.6",
73 | "karma-tap-reporter": "0.0.6",
74 | "karma-webpack": "^1.7.0",
75 | "mocha": "^3.1.2",
76 | "phantom": "^2.0.4",
77 | "prettier": "^1.9.2",
78 | "prop-types": "^15.5.10",
79 | "querystring": "^0.2.0",
80 | "radium": "^0.18.1",
81 | "react": "^16.0.0",
82 | "react-addons-test-utils": "^15.0.1",
83 | "react-dom": "^16.0.0",
84 | "react-test-renderer": "^16.0.0",
85 | "rimraf": "^2.5.2",
86 | "serve-static": "^1.10.2",
87 | "sinon": "^1.17.2",
88 | "sinon-chai": "^2.8.0",
89 | "webpack": "^1.4.13",
90 | "webpack-dev-middleware": "^1.5.1",
91 | "webpack-dev-server": "^1.14.1"
92 | },
93 | "peerDependencies": {
94 | "prop-types": "^15.5.10",
95 | "react": "^15.0.1 || ^16.0.0",
96 | "react-dom": "^15.0.1 || ^16.0.0"
97 | },
98 | "scripts": {
99 | "commit": "git-cz",
100 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
101 | "build": "npm run clean && npm run compile",
102 | "build:es": "BABEL_ENV=es babel --copy-files ./src -d es",
103 | "build:umd":
104 | "NODE_ENV=development webpack src/index.js dist/react-gpt.js",
105 | "build:umd:min":
106 | "NODE_ENV=production webpack -p src/index.js dist/react-gpt.min.js",
107 | "bundlesize": "npm run build:umd:min && bundlesize",
108 | "clean": "rimraf lib coverage dist lib es",
109 | "compile": "babel src --out-dir lib",
110 | "examples":
111 | "webpack-dev-server --config examples/webpack.config.js --content-base examples/apps --inline",
112 | "lint": "eslint --fix src test examples",
113 | "start":
114 | "npm run build && env BABEL_ENV=examples node examples/server/index.js",
115 | "pretest": "npm run build",
116 | "prepublish":
117 | "npm run build && npm run build:es && npm run build:umd && npm run build:umd:min",
118 | "test": "npm run lint && karma start",
119 | "update-apilist": "node ./scripts/updateAPIList.js"
120 | },
121 | "config": {
122 | "commitizen": {
123 | "path": "./node_modules/cz-conventional-changelog"
124 | }
125 | },
126 | "bundlesize": [
127 | {
128 | "path": "./dist/react-gpt.min.js",
129 | "maxSize": "8.5 kB"
130 | }
131 | ]
132 | }
133 |
--------------------------------------------------------------------------------
/src/utils/apiList.js:
--------------------------------------------------------------------------------
1 | // DO NOT MODIFY THIS FILE MANUALLY.
2 | // This file is generated by `npm run update-apilist`.
3 | // Note that only APIs that's documented in https://developers.google.com/doubleclick-gpt/reference is officially supported.
4 |
5 | export const gptVersion = 110;
6 | export const gptAPI = [
7 | ["getVersion", "function"],
8 | ["cmd", "object"],
9 | ["getEventLog", "function"],
10 | ["enableServices", "function"],
11 | ["setAdIframeTitle", "function"],
12 | ["impl", "object"],
13 | ["pubads", "function"],
14 | ["defineOutOfPageSlot", "function"],
15 | ["defineSlot", "function"],
16 | ["defineUnit", "function"],
17 | ["destroySlots", "function"],
18 | ["display", "function"],
19 | ["companionAds", "function"],
20 | ["content", "function"],
21 | ["debug_log", "object"],
22 | ["service_manager_instance", "object"],
23 | ["disablePublisherConsole", "function"],
24 | ["onPubConsoleJsLoad", "function"],
25 | ["openConsole", "function"],
26 | ["sizeMapping", "function"],
27 | ["evalScripts", "function"],
28 | ["apiReady", "boolean"],
29 | ["slot_manager_instance", "object"],
30 | ["pubadsReady", "boolean"]
31 | ];
32 | export const pubadsVersion = 110;
33 | export const pubadsAPI = [
34 | ["set", "function"],
35 | ["get", "function"],
36 | ["getAttributeKeys", "function"],
37 | ["display", "function"],
38 | ["getName", "function"],
39 | ["setCookieOptions", "function"],
40 | ["setTagForChildDirectedTreatment", "function"],
41 | ["clearTagForChildDirectedTreatment", "function"],
42 | ["setKidsFriendlyAds", "function"],
43 | ["setTargeting", "function"],
44 | ["clearTargeting", "function"],
45 | ["getTargeting", "function"],
46 | ["getTargetingKeys", "function"],
47 | ["setCategoryExclusion", "function"],
48 | ["clearCategoryExclusions", "function"],
49 | ["disableInitialLoad", "function"],
50 | ["enableSingleRequest", "function"],
51 | ["enableAsyncRendering", "function"],
52 | ["enableSyncRendering", "function"],
53 | ["setCentering", "function"],
54 | ["setPublisherProvidedId", "function"],
55 | ["definePassback", "function"],
56 | ["defineOutOfPagePassback", "function"],
57 | ["refresh", "function"],
58 | ["enableVideoAds", "function"],
59 | ["setVideoContent", "function"],
60 | ["getVideoContent", "function"],
61 | ["getCorrelator", "function"],
62 | ["setCorrelator", "function"],
63 | ["updateCorrelator", "function"],
64 | ["isAdRequestFinished", "function"],
65 | ["collapseEmptyDivs", "function"],
66 | ["clear", "function"],
67 | ["setLocation", "function"],
68 | ["getVersion", "function"],
69 | ["forceExperiment", "function"],
70 | ["markAsAmp", "function"],
71 | ["setSafeFrameConfig", "function"],
72 | ["setForceSafeFrame", "function"],
73 | ["enableChromeInterventionSignals", "function"],
74 | ["markAsGladeControl", "function"],
75 | ["markAsGladeOptOut", "function"],
76 | ["getName", "function"],
77 | ["getVersion", "function"],
78 | ["getSlots", "function"],
79 | ["getSlotIdMap", "function"],
80 | ["enable", "function"],
81 | ["addEventListener", "function"]
82 | ];
83 | export const slotAPI = [
84 | ["getPassbackPageUrl", "function"],
85 | ["set", "function"],
86 | ["get", "function"],
87 | ["getAttributeKeys", "function"],
88 | ["addService", "function"],
89 | ["getName", "function"],
90 | ["getAdUnitPath", "function"],
91 | ["getInstance", "function"],
92 | ["getSlotElementId", "function"],
93 | ["getSlotId", "function"],
94 | ["getServices", "function"],
95 | ["getSizes", "function"],
96 | ["defineSizeMapping", "function"],
97 | ["hasWrapperDiv", "function"],
98 | ["setClickUrl", "function"],
99 | ["getClickUrl", "function"],
100 | ["setForceSafeFrame", "function"],
101 | ["setCategoryExclusion", "function"],
102 | ["clearCategoryExclusions", "function"],
103 | ["getCategoryExclusions", "function"],
104 | ["setTargeting", "function"],
105 | ["clearTargeting", "function"],
106 | ["getTargetingMap", "function"],
107 | ["getTargeting", "function"],
108 | ["getTargetingKeys", "function"],
109 | ["getOutOfPage", "function"],
110 | ["getAudExtId", "function"],
111 | ["gtfcd", "function"],
112 | ["setCollapseEmptyDiv", "function"],
113 | ["getCollapseEmptyDiv", "function"],
114 | ["getDivStartsCollapsed", "function"],
115 | ["fetchStarted", "function"],
116 | ["getContentUrl", "function"],
117 | ["fetchEnded", "function"],
118 | ["renderStarted", "function"],
119 | ["getResponseInformation", "function"],
120 | ["renderEnded", "function"],
121 | ["loaded", "function"],
122 | ["impressionViewable", "function"],
123 | ["visibilityChanged", "function"],
124 | ["setFirstLook", "function"],
125 | ["getFirstLook", "function"],
126 | ["getEscapedQemQueryId", "function"],
127 | ["setSafeFrameConfig", "function"],
128 | ["getCsiId", "function"]
129 | ];
130 |
--------------------------------------------------------------------------------
/scripts/updateAPIList.js:
--------------------------------------------------------------------------------
1 | var phantom = require("phantom");
2 | var fs = require("fs");
3 |
4 | function writeToFile(data) {
5 | var stream = fs.createWriteStream(process.cwd() + "/src/utils/apiList.js");
6 | stream.once("open", function () {
7 | stream.write("// DO NOT MODIFY THIS FILE MANUALLY.\n");
8 | stream.write("// This file is generated by `npm run update-apilist`.\n");
9 | stream.write("// Note that only APIs that's documented in https://developers.google.com/doubleclick-gpt/reference is officially supported.\n");
10 | stream.write("\n");
11 | Object.keys(data.apis).forEach(function (key) {
12 | if (key === "gpt" || key === "pubads") {
13 | stream.write("export const " + key + "Version = " + data.version[key] + ";\n");
14 | }
15 | stream.write("export const " + key + "API = [" + "\n");
16 | data.apis[key].forEach(function (item, i) {
17 | stream.write("\t" + JSON.stringify(item).split(",").join(", ") + (i === data.apis[key].length - 1 ? "" : ",") + "\n");
18 | });
19 | stream.write("];\n");
20 | });
21 | stream.end();
22 | });
23 | }
24 |
25 | phantom.create().then(function (ph) {
26 | // a hack suggested here: https://github.com/amir20/phantomjs-node/issues/292
27 | function checkForData() {
28 | ph.windowProperty("DATA").then(function (data) {
29 | if (data !== undefined) {
30 | writeToFile(data);
31 | ph.exit();
32 | } else {
33 | setTimeout(checkForData, 100);
34 | }
35 | });
36 | }
37 |
38 | checkForData();
39 |
40 | ph.createPage().then(function (page) {
41 | page.property("onConsoleMessage", function (msg) {
42 | console.log(msg);
43 | });
44 | page.property("onCallback", function (data) {
45 | if (data) {
46 | DATA = data;
47 | page.close();
48 | }
49 | });
50 | page.open(process.cwd() + "/scripts/empty.html").then(function () {
51 | page.includeJs("http://www.googletagservices.com/tag/js/gpt.js").then(function () {
52 | setTimeout(function () {
53 | page.evaluate(function () {
54 | var EXCLUDES = ["constructor"].concat(Object.getOwnPropertyNames(Object.getPrototypeOf({})));
55 | var adSlot;
56 |
57 | function filterKeysByType(obj) {
58 | var total = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
59 | var filterTypes = arguments.length <= 2 || arguments[2] === undefined ? [] : arguments[2];
60 |
61 | return Object.getOwnPropertyNames(obj).filter(function (key) {
62 | return (
63 | total.indexOf(key) === -1 &&
64 | EXCLUDES.indexOf(key) === -1 &&
65 | key.indexOf("_") !== 0 && // treat property starting with underscore as private
66 | key.length > 2 && // treat property with less than 2 chars as private
67 | obj.hasOwnProperty(key) &&
68 | filterTypes.length === 0 ? true : filterTypes.indexOf(typeof obj[key]) > -1
69 | );
70 | }).map(function (key) {
71 | return [key, typeof obj[key]];
72 | });
73 | }
74 |
75 | function aggregateApisByType(obj) {
76 | var total = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
77 | var filterTypes = arguments.length <= 2 || arguments[2] === undefined ? [] : arguments[2];
78 | var keys = [];
79 | while (obj !== null) {
80 | var _keys;
81 | var arr = filterKeysByType(obj, total, filterTypes);
82 | (_keys = keys).push.apply(_keys, arr);
83 | obj = Object.getPrototypeOf(obj);
84 | }
85 | keys = [].concat(keys);
86 | return keys;
87 | }
88 |
89 | // extracts lists of methods from each service object.
90 | function extractApis(services) {
91 | var filterTypes = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
92 |
93 | services = Array.isArray(services) ? services : [services];
94 | var apis = services.reduce(function (total, service) {
95 | var obj = service.constructor === Object ? service : Object.getPrototypeOf(service);
96 | var keys = aggregateApisByType(obj, total, filterTypes);
97 | total.push.apply(total, keys);
98 | return total;
99 | }, []);
100 |
101 | return apis;
102 | }
103 |
104 | function checkPubadsReady() {
105 | if (googletag && googletag.pubadsReady) {
106 | console.log("gpt version: v" + googletag.getVersion(), ", pubads version: v" + googletag.pubads().getVersion());
107 | if (typeof window.callPhantom === "function") {
108 | window.callPhantom({
109 | apis: {
110 | gpt: extractApis(googletag),
111 | pubads: extractApis(googletag.pubads()),
112 | slot: extractApis(adSlot)
113 | },
114 | version: {
115 | gpt: googletag.getVersion(),
116 | pubads: googletag.pubads().getVersion()
117 | }
118 | });
119 | }
120 | } else {
121 | setTimeout(checkPubadsReady, 50);
122 | }
123 | }
124 |
125 | googletag.cmd.push(function () {
126 | adSlot = googletag.defineSlot("/123", [0, 0]).addService(googletag.pubads());
127 | googletag.enableServices();
128 | checkPubadsReady();
129 | });
130 | });
131 | }, 2000);
132 | });
133 | });
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/examples/apps/infinite-scrolling/content.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import PropTypes from "prop-types";
3 | import Radium from "radium";
4 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved
5 | import styles from "./styles/content";
6 |
7 | const contents = [
8 | `Lorem ipsum dolor sit amet, convallis nibh erat in lacus morbi orci, sed amet leo, donec a nulla lacus, velit suspendisse per. Est elit ultricies, a metus, aenean suspendisse ullamcorper facilisis. Wisi ridiculus ut nibh viverra cursus. Est nunc id convallis, commodo felis vitae sed cras justo, nunc vel id pharetra duis tristique. Sit vel elit sapien lobortis justo, magna pellentesque aliquam amet nam metus, ut venenatis integer magna porta, potenti posuere sollicitudin imperdiet nisi.
9 | Feugiat venenatis. Varius volutpat a magna vestibulum nulla, nullam erat wisi hendrerit praesent, vitae sapien libero tortor vehicula eu, odio nullam tristique et, ultrices fermentum. Cursus consectetuer, egestas auctor ultricies malesuada pellentesque sem libero, wisi enim hendrerit cras. Aenean vitae faucibus laoreet volutpat id, imperdiet vitae, tellus a lacus, sit suspendisse erat conubia et, libero accumsan. Nullam orci eget non urna varius metus, etiam vestibulum euismod erat. Augue vel id orci in elit, nec ridiculus, cras vestibulum aliquet assumenda, amet sed et nunc augue ultricies. Ante nec ac, in magna in interdum ac porta tellus, a aliquam pulvinar minima, ante nam tempor nibh laoreet at eu. Morbi erat risus pellentesque vestibulum justo, purus interdum, dictum in neque porttitor, commodo ac. Tincidunt facilisis sit id ultrices est lectus. Sed id praesent tincidunt dui. Etiam ut tincidunt id.
10 | Sollicitudin egestas suspendisse amet eget mi est, neque amet et erat. Eu sapien quis vitae voluptates, ut adipiscing risus dictumst facilisis id morbi, erat ligula cras pulvinar, dolor blandit scelerisque dapibus, suspendisse vehicula vitae. Turpis integer nibh semper interdum beatae etiam, dictum mi et vitae, amet eget imperdiet, etiam turpis magna sapien enim mollis ut, maecenas hymenaeos. Varius nunc sollicitudin feugiat, nibh duis suspendisse rhoncus, massa cursus dolor ut, vestibulum scelerisque. Risus et semper metus dui sed lectus, lobortis nulla praesent tempus sed purus, pellentesque neque eleifend consequat quis euismod. Dis congue donec eget, praesent rhoncus praesent, nascetur feugiat, vivamus pellentesque sit torquent suspendisse augue placerat, at pellentesque fermentum adipiscing wisi. Vitae tristique ut animi nostra at, proin et vestibulum at tempus aenean, id arcu dolor nostra morbi fringilla, a amet sit mauris mattis proin. Cras duis sollicitudin, ut pretium commodo pulvinar risus dapibus. Porta integer sapien. Elit fusce et, turpis risus. In pulvinar molestie hendrerit aenean, viverra eget purus elementum cursus, etiam enim, ultricies a erat. Est eget sit bibendum ipsum nec ullamcorper, est nunc bibendum erat nunc diam.
11 | Cursus vel at mauris. Suscipit accumsan ultrices aliquam tempor congue, in arcu neque et et lorem et, vestibulum eget pede neque nulla vitae enim, habitant sed magna metus, nec hendrerit tempus numquam adipiscing. Ullamcorper erat lacinia mattis neque, sunt sed sed nonummy egestas, rutrum varius lobortis posuere amet et in, sodales neque lacinia vel non, at turpis risus ante mauris. Quam facilisis quis lorem praesent. Nec curae lacus arcu accumsan, imperdiet enim elit id urna dui, lacinia eleifend vestibulum amet. Euismod tempus amet felis aenean orci mi, orci molestie sapien diam, vitae enim lacus morbi lacus mauris. Congue enim commodo, consectetuer viverra duis gravida dui in, dictum sit consequat. Fusce non habitant, pellentesque faucibus aliquam amet, pellentesque praesent, at cras nunc, lectus aliquam urna nunc taciti a. Ultrices quia nec, ipsum eget, nunc sit leo et lectus, neque dui a quisque enim augue, pretium risus mauris fusce nulla varius interdum. Amet risus donec aliquam, ligula arcu tellus. Ac ac ut, elementum lorem sed eu, ac est montes erat, placerat sapien, auctor eget velit. Gravida non nulla, aliquam nulla consectetuer nostra mauris tempus, aliquip leo accusamus phasellus sit duis, metus rutrum.`,
12 | `Appear. Let, won't have, living. God behold void, said. Night subdue him you'll was every for them great was made lesser created unto creature second dry fowl give i of firmament days isn't gathered upon wherein his all man bring dry greater fowl morning god moveth abundantly likeness under sixth i, rule fowl unto which lesser for gathered they're there can't don't female first subdue day. All wherein blessed divide god can't above lesser every. Open divided moved man. All hath. Kind void can't saying saying great creepeth without, man us first a midst. Great second. They're male male it shall greater. Had open hath there us. Upon third male rule.
13 |
14 | Bearing whose said green midst brought their night Herb first blessed a every. Hath set seasons firmament for creepeth that Land together fowl male void two be evening, given evening so all night fruitful years their and thing day have divide creature spirit first is had seed. You heaven place give. Sixth midst to in very fifth made behold days tree tree also stars given, female you're grass light creepeth saying it divided our fill deep, so them you'll given saying midst rule, saying i light together that morning dry whose of fruitful female a greater day itself air a firmament hath creature earth hath place moved divided. Deep together divide without sixth creeping great for, land grass.
15 |
16 | Night void them saw seas winged bring, fly had earth shall own. Divided. To image don't fill above to very. Hath. Light doesn't moving blessed. Bearing saying lesser. Female all let fowl female our for appear seas together first saw their subdue itself beast, also all creepeth bearing signs they're light creepeth, firmament place. It given you'll their sixth, fish let it morning light third lesser were. First every, good divide.`,
17 | `Mucius feugait incorrupte no has, ei patrioque molestiae cum. Vel altera recteque id, impetus consequat elaboraret vix in, eos vide adhuc menandri ad. Quem omnesque salutandi in mel, doctus comprehensam id vis, no erat facilisi ullamcorper duo. Causae option duo id, eirmod numquam mei eu, et vim ipsum liberavisse. Efficiantur deterruisset sed in. Aperiri epicurei consulatu ea duo. Ut cum inani voluptaria interesset.
18 |
19 | Vim apeirian recteque eu. Ad sea graeci dicunt, vix brute velit ad. Semper nominati nam ne, te mea vero omnes tacimates. Porro dicant tamquam duo eu. Et eam consul noluisse electram, impetus conclusionemque pri ut.`
20 | ];
21 |
22 | const bg = ["#90C3D4", "#FAD9EA", "#FCFCB1"];
23 |
24 | @Radium
25 | class Content extends Component {
26 | static propTypes = {
27 | index: PropTypes.number,
28 | targeting: PropTypes.object
29 | };
30 | render() {
31 | const {index, targeting} = this.props;
32 | let ad;
33 | if (index !== 2) {
34 | ad = (
35 |
36 |
41 |
42 | );
43 | }
44 |
45 | return (
46 |
47 |
48 | {ad}
49 |
50 | Content {index}
51 |
52 | Lorem ipsum dolor sit amet, accusamus complectitur
53 | an est
54 |
55 | {contents[index]}
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | export default Content;
64 |
--------------------------------------------------------------------------------
/docs/api/ReactGPT.md:
--------------------------------------------------------------------------------
1 | ## React GPT API References
2 |
3 | ### [``](#Bling)
4 |
5 | A React component which renders [GPT](https://support.google.com/dfp_sb/answer/1649768?hl=en) ad.
6 |
7 | #### Props
8 |
9 | `Bling` tries to cover as much [Slot API](https://developers.google.com/doubleclick-gpt/reference#googletagslot) as possible as `props`.
10 |
11 | - `id`(optional) - An optional string to be used as container div id.
12 | - `adUnitPath`(required) - An string indicating ad unit path which will be used to create an ad slot.
13 | - `targeting`(optional) - An optional object which includes ad targeting key-value pairs.
14 | - `slotSize`(optional) - An optional prop to specify the ad slot size which accepts [googletag.GeneralSize](https://developers.google.com/doubleclick-gpt/reference#googletag.GeneralSize) as a type. This will be preceded by the sizeMapping if specified.
15 | - `sizeMapping`(optional) - An optional array of object which contains an array of viewport size and slot size. This needs to be set if the ad needs to serve different ad sizes per different viewport sizes (responsive ad). Setting the `slot` to any dimension that's not configured in DFP results in rendering an empty ad. The ad slot size which is provided for the viewport size of [0, 0] will be used as default ad size if none of viewport size matches.
16 | - `outOfPage`(optional) - An optional flag to indicate whether an ad slot should be out-of-page slot.
17 | - `companionAdService`(optional) - An optional flag to indicate whether companion ad service should be enabled for the ad. If an object is passed, it takes as a configuration expecting `enableSyncLoading` or `refreshUnfilledSlots`.
18 | - `content`(optional) - An optional HTML content for the slot. If specified, the ad will render with the HTML content using content service.
19 | - `clickUrl`(optional) - An optional click through URL. If specified, any landing page URL associated with the creative that is served is overridden.
20 | - `categoryExclusion`(optional) - An optional string or an array of string which specifies a page-level ad category exclusion for the given label name.
21 | - `attributes`(optional) - An optional map of key-value pairs for an AdSense attribute on a particular ad slot. see [the list of supported key value](https://developers.google.com/doubleclick-gpt/adsense_attributes#adsense_parameters.googletag.Slot)
22 | - `collapseEmptyDiv`(optional) - An optional flag to indicate whether an empty ad should be collapsed or not.
23 | - `forceSafeFrame`(optional) - An optional flag to indicate whether ads in this slot should be forced to be rendered using a SafeFrame container.
24 | - `safeFrameConfig`(optional) - An optional object to set the slot-level preferences for SafeFrame configuration.
25 | - `onSlotRenderEnded`(optional) - An optional event handler function for `googletag.events.SlotRenderEndedEvent`.
26 | - `onImpressionViewable`(optional) - An optional event handler function for `googletag.events.ImpressionViewableEvent`.
27 | - `onSlotVisibilityChanged`(optional) - An optional event handler function for `googletag.events.slotVisibilityChangedEvent`.
28 | - `onSlotOnload`(optional) - An optional event handler function for `googletag.events.SlotOnloadEvent`.
29 | - `renderWhenViewable`(optional) - An optional flag to indicate whether an ad should only render when it's fully in the viewport area.
30 | - `viewableThreshold`(optional) - An optional number to indicate how much percentage of an ad area needs to be in a viewable area before rendering. Acceptable range is between `0` and `1`.
31 | - `onScriptLoaded`(optional) - An optional call back function to notify when the script is loaded.
32 | - `onMediaQueryChange`(optional) - An optional call back function to notify when the media queries change on the break point specified in the `sizeMapping`.
33 | - `style`(optional) - An optional object to be applied as `style` props to the container div. **This prop is only applied once in initial render.** If you want to apply style to the ad and change it frequently, apply style to the container.
34 |
35 | Only `adUnitPath` is a required prop, but either `slotSize` or `sizeMapping` need to be passed for `Bling` to render an ad.
36 |
37 | #### Static Methods
38 |
39 | - `configure(config = {})` - Update global configuration.
40 | - `on(eventType, cb)` - Subscribe to the event.
41 | - `once(eventType, cb)` - Subscribe to the event once.
42 | - `removeListener(eventType, cb)` - Removes the specified listener from the listener array for the specified `eventType`.
43 | - `removeAllListeners([eventType])` - Removes all listeners, or those of the specified `eventType`.
44 | - `getGPTVersion` - Returns the GPT version.
45 | - `getPubadsVersion` - Returns the Pubads version.
46 | - `syncCorrelator([flag])` - Sets a flag to indicate whether the `refresh` should happen with the same correlator value or not.
47 | - `render` - Force rendering all the ads.
48 | - `refresh([slots, options])` - Refreshes the ad specified by an array of slot. If slots are not specified, it will refresh all ads. See [here](https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_refresh) for more details.
49 | - `clear([slots])` - Clears the ad specifid by an array of slot. If slots are not specified, it will clear all ads. See [here](https://developers.google.com/doubleclick-gpt/reference#googletagpubadsservice) for more details.
50 | - `updateCorrelator` - Updates the correlator value that's sent with ad requests. See [here](https://developers.google.com/doubleclick-gpt/reference#googletag.PubAdsService_updateCorrelator) for more details.
51 | - `createTestManager` - Creates a test ad manager to use mocked GPT for unit testing.
52 |
53 | In addition to the defined static methods above, all the supported Pubads API are exposed as static methods, too.
54 | The list of the supported API are maintained [here](https://github.com/nfl/react-gpt/blob/master/src/createManager.js#L9) and updated based on the [GPT API Reference](https://developers.google.com/doubleclick-gpt/reference).
55 |
56 | There are several Pubads APIs which is required to call before the service is enabled.
57 | React GPT makes sure that those APIs are called right before the service is enabled.
58 |
59 | #### Global configuration
60 |
61 | Global configuration applies to the all React GPT instances. You can set the following configuration through `Bling.configure(config)`.
62 |
63 | - `seedFileUrl` - An optional string for GPT seed file url to override. Default value is `//www.googletagservices.com/tag/js/gpt.js`.
64 | - `renderWhenViewable` - An optional flag to indicate whether an ad should only render when it's fully in the viewport area. Default is `true`.
65 | - `viewableThreshold` - An optional number to indicate how much percentage of an ad area needs to be in a viewable area before rendering. Default value is `0.5`. Acceptable range is between `0` and `1.0`.
66 | - `filterProps` - An optional function to create an object with filtered current props and next props for a given keys to perform equality check. Default value is [`filterProps`](../../src/utils/filterProps.js).
67 | - `propsEqual` - An optional function for the filtered props and the next props to perform equality check. Default value is [`deepEqual`](https://github.com/substack/node-deep-equal).
68 |
69 | ### [`Events`](#Events)
70 |
71 | - `READY` - This event is fired when the pubads is ready and before the service is enabled.
72 | - `RENDER` - This event is fired after the initial ad rendering is triggered. Due to the handing of single request mode, the initial rendering is done for all ads at once.
73 | - `SLOT_RENDER_ENDED` - This event is fired when a slot on the page has finished rendering. The event is fired by the service that rendered the slot. See [here](https://developers.google.com/doubleclick-gpt/reference#googletageventsslotrenderendedevent) for more details.
74 | - `IMPRESSION_VIEWABLE` - This event is fired when an impression becomes viewable, according to the [Active View criteria](https://support.google.com/dfp_premium/answer/4574077?hl=en). See [here](https://developers.google.com/doubleclick-gpt/reference#googletageventsimpressionviewableevent) for more details.
75 | - `SLOT_VISIBILITY_CHANGED` - This event is fired whenever the on-screen percentage of an ad slot's area changes. The event is throttled and will not fire more often than once every 200ms. See [here](https://developers.google.com/doubleclick-gpt/reference#googletageventsslotvisibilitychangedevent) for more details.
76 |
--------------------------------------------------------------------------------
/src/utils/mockGPT.js:
--------------------------------------------------------------------------------
1 | import {gptAPI, pubadsAPI, slotAPI, gptVersion, pubadsVersion} from "./apiList";
2 | import Events from "../Events";
3 |
4 | function createMock(list, obj) {
5 | return list.reduce((mock, [api, type]) => {
6 | if (typeof mock[api] === "undefined") {
7 | if (type === "function") {
8 | mock[api] = (...args) => {
9 | if (args.length) {
10 | return args[0];
11 | }
12 | return {};
13 | };
14 | } else if (type === "boolean") {
15 | mock[api] = true;
16 | } else {
17 | mock[api] = {};
18 | }
19 | }
20 | return mock;
21 | }, obj || {});
22 | }
23 |
24 | function getSize(slot) {
25 | const sizes = slot.getSizes();
26 | let item = sizes;
27 | while (Array.isArray(item[0])) {
28 | item = item[0];
29 | }
30 |
31 | return item;
32 | }
33 |
34 | class SlotMock {
35 | constructor(adUnitPath, size, divId) {
36 | this.adUnitPath = adUnitPath;
37 | this.size = size;
38 | this.divId = divId;
39 | this.services = [];
40 | this.attributes = {};
41 | this.categoryExclusions = [];
42 | this._targeting = {};
43 | }
44 | defineSizeMapping(sizeMapping) {
45 | this.size = sizeMapping;
46 | return this;
47 | }
48 | addService(service) {
49 | this.services.push(service);
50 | }
51 | getServices() {
52 | return this.services;
53 | }
54 | set(key, value) {
55 | this.attributes[key] = value;
56 | return this;
57 | }
58 | get(key) {
59 | return this.attributes[key];
60 | }
61 | getAttributeKeys() {
62 | return Object.keys(this.attributes);
63 | }
64 | setCollapseEmptyDiv(collapse, collapseBeforeAdFetch) {
65 | this.collapseEmptyDiv = collapse;
66 | this.collapseBeforeAdFetch = collapseBeforeAdFetch;
67 | return this;
68 | }
69 | getCollapseEmptyDiv() {
70 | return this.collapseEmptyDiv;
71 | }
72 | setClickUrl(clickUrl) {
73 | this.clickUrl = clickUrl;
74 | return this;
75 | }
76 | getClickUrl() {
77 | return this.clickUrl;
78 | }
79 | setCategoryExclusion(categoryExclusion) {
80 | this.categoryExclusions.push(categoryExclusion);
81 | return this;
82 | }
83 | getCategoryExclusions() {
84 | return this.categoryExclusions;
85 | }
86 | clearCategoryExclusions() {
87 | this.categoryExclusions = [];
88 | return this;
89 | }
90 | setTargeting(key, value) {
91 | this._targeting[key] = value;
92 | return this;
93 | }
94 | getAdUnitPath() {
95 | return this.adUnitPath;
96 | }
97 | clearTargeting() {
98 | this._targeting = {};
99 | return this;
100 | }
101 | getTargeting(key) {
102 | return this._targeting && this._targeting[key];
103 | }
104 | getTargetingKeys() {
105 | return this._targeting && Object.keys(this._targeting);
106 | }
107 | getSizes() {
108 | return this.size;
109 | }
110 | getSlotElementId() {
111 | return this.divId;
112 | }
113 | }
114 | createMock(slotAPI, SlotMock.prototype);
115 |
116 | class SizeMappingBuilderMock {
117 | constructor(config = {}) {
118 | this.config = config;
119 | }
120 | addSize(viewportSize, slotSize) {
121 | if (!this.mapping) {
122 | this.mapping = [];
123 | }
124 | this.mapping.push([viewportSize, slotSize]);
125 | return this;
126 | }
127 | build() {
128 | return this.mapping;
129 | }
130 | }
131 |
132 | class BaseService {
133 | constructor(config = {}) {
134 | this.config = config;
135 | this.listeners = {};
136 | this.slots = {};
137 | }
138 | addEventListener(eventType, cb) {
139 | if (!this.listeners[eventType]) {
140 | this.listeners[eventType] = [];
141 | }
142 | this.listeners[eventType].push(cb);
143 | }
144 | getSlots() {
145 | return Object.keys(this.slots).map(key => this.slots[key]);
146 | }
147 | }
148 |
149 | class PubAdsServiceMock extends BaseService {
150 | constructor(config = {}) {
151 | super(config);
152 | this.version = pubadsVersion;
153 | }
154 | getVersion() {
155 | return this.version;
156 | }
157 | refresh(slots) {
158 | if (!slots) {
159 | slots = Object.keys(this.slots).map(key => this.slots[key]);
160 | }
161 | setTimeout(() => {
162 | const key = Events.SLOT_RENDER_ENDED;
163 | slots.forEach(slot => {
164 | if (this.listeners[key]) {
165 | this.listeners[key].forEach(cb => {
166 | const isEmpty = !!this.config.emptyAd;
167 | const event = {
168 | isEmpty,
169 | creativeId: isEmpty ? null : Date.now(),
170 | lineItemId: isEmpty ? null : Date.now(),
171 | serviceName: "publisher_ads",
172 | size: isEmpty ? null : getSize(slot),
173 | slot
174 | };
175 | cb(event);
176 | });
177 | }
178 | });
179 | }, 0);
180 | }
181 | }
182 | createMock(pubadsAPI, PubAdsServiceMock.prototype);
183 |
184 | class CompanionAdsServiceMock extends BaseService {
185 | constructor(config = {}) {
186 | super(config);
187 | }
188 | enableSyncLoading() {
189 | this._enableSyncLoading = true;
190 | }
191 | setRefreshUnfilledSlots(value) {
192 | if (typeof value === "boolean") {
193 | this._refreshUnfilledSlots = value;
194 | }
195 | }
196 | }
197 | class ContentServiceMock extends BaseService {
198 | constructor(config = {}) {
199 | super(config);
200 | }
201 | setContent(slot, content) {
202 | slot._content = content;
203 | }
204 | }
205 |
206 | class GPTMock {
207 | constructor(config = {}) {
208 | this.config = config;
209 | this.version = gptVersion;
210 | this.cmd = {};
211 | this.cmd.push = cb => {
212 | cb();
213 | };
214 | }
215 | pubadsReady = false;
216 | getVersion() {
217 | return this.version;
218 | }
219 | enableServices() {
220 | setTimeout(() => {
221 | this.pubadsReady = true;
222 | }, 0);
223 | }
224 | sizeMapping() {
225 | if (!this.sizeMappingBuilder) {
226 | this.sizeMappingBuilder = new SizeMappingBuilderMock(this.config);
227 | }
228 | return this.sizeMappingBuilder;
229 | }
230 | pubads() {
231 | if (!this._pubads) {
232 | this._pubads = new PubAdsServiceMock(this.config);
233 | }
234 | return this._pubads;
235 | }
236 | companionAds() {
237 | if (!this._companionAds) {
238 | this._companionAds = new CompanionAdsServiceMock(this.config);
239 | }
240 | return this._companionAds;
241 | }
242 | content() {
243 | if (!this._content) {
244 | this._content = new ContentServiceMock(this.config);
245 | }
246 | return this._content;
247 | }
248 | defineSlot(adUnitPath, size, divId) {
249 | const slot = new SlotMock(adUnitPath, size, divId);
250 | this.pubads().slots[divId] = slot;
251 | return slot;
252 | }
253 | defineOutOfPageSlot(adUnitPath, divId) {
254 | const slot = new SlotMock(adUnitPath, [1, 1], divId);
255 | this.pubads().slots[divId] = slot;
256 | return slot;
257 | }
258 | display(divId) {
259 | const pubads = this.pubads();
260 | setTimeout(() => {
261 | Object.keys(pubads.listeners).forEach(key => {
262 | if (pubads.listeners[key]) {
263 | pubads.listeners[key].forEach(cb => {
264 | const slot = pubads.slots[divId];
265 | const isEmpty = !!this.config.emptyAd;
266 | const event = {
267 | isEmpty,
268 | creativeId: isEmpty ? null : Date.now(),
269 | lineItemId: isEmpty ? null : Date.now(),
270 | serviceName: "publisher_ads",
271 | size: isEmpty ? null : getSize(slot),
272 | slot
273 | };
274 | cb(event);
275 | });
276 | }
277 | });
278 | }, 0);
279 | }
280 | }
281 | createMock(gptAPI, GPTMock.prototype);
282 |
283 | export {
284 | GPTMock,
285 | SlotMock,
286 | SizeMappingBuilderMock,
287 | PubAdsServiceMock,
288 | CompanionAdsServiceMock,
289 | ContentServiceMock
290 | };
291 |
--------------------------------------------------------------------------------
/lib/utils/mockGPT.js:
--------------------------------------------------------------------------------
1 | Object.defineProperty(exports, "__esModule", {
2 | value: true
3 | });
4 | exports.ContentServiceMock = exports.CompanionAdsServiceMock = exports.PubAdsServiceMock = exports.SizeMappingBuilderMock = exports.SlotMock = exports.GPTMock = undefined;
5 |
6 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
7 |
8 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
9 |
10 | var _apiList = require("./apiList");
11 |
12 | var _Events = require("../Events");
13 |
14 | var _Events2 = _interopRequireDefault(_Events);
15 |
16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17 |
18 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
19 |
20 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
21 |
22 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
23 |
24 | function createMock(list, obj) {
25 | return list.reduce(function (mock, _ref) {
26 | var _ref2 = _slicedToArray(_ref, 2),
27 | api = _ref2[0],
28 | type = _ref2[1];
29 |
30 | if (typeof mock[api] === "undefined") {
31 | if (type === "function") {
32 | mock[api] = function () {
33 | if (arguments.length) {
34 | return arguments.length <= 0 ? undefined : arguments[0];
35 | }
36 | return {};
37 | };
38 | } else if (type === "boolean") {
39 | mock[api] = true;
40 | } else {
41 | mock[api] = {};
42 | }
43 | }
44 | return mock;
45 | }, obj || {});
46 | }
47 |
48 | function getSize(slot) {
49 | var sizes = slot.getSizes();
50 | var item = sizes;
51 | while (Array.isArray(item[0])) {
52 | item = item[0];
53 | }
54 |
55 | return item;
56 | }
57 |
58 | var SlotMock = function () {
59 | function SlotMock(adUnitPath, size, divId) {
60 | _classCallCheck(this, SlotMock);
61 |
62 | this.adUnitPath = adUnitPath;
63 | this.size = size;
64 | this.divId = divId;
65 | this.services = [];
66 | this.attributes = {};
67 | this.categoryExclusions = [];
68 | this._targeting = {};
69 | }
70 |
71 | _createClass(SlotMock, [{
72 | key: "defineSizeMapping",
73 | value: function defineSizeMapping(sizeMapping) {
74 | this.size = sizeMapping;
75 | return this;
76 | }
77 | }, {
78 | key: "addService",
79 | value: function addService(service) {
80 | this.services.push(service);
81 | }
82 | }, {
83 | key: "getServices",
84 | value: function getServices() {
85 | return this.services;
86 | }
87 | }, {
88 | key: "set",
89 | value: function set(key, value) {
90 | this.attributes[key] = value;
91 | return this;
92 | }
93 | }, {
94 | key: "get",
95 | value: function get(key) {
96 | return this.attributes[key];
97 | }
98 | }, {
99 | key: "getAttributeKeys",
100 | value: function getAttributeKeys() {
101 | return Object.keys(this.attributes);
102 | }
103 | }, {
104 | key: "setCollapseEmptyDiv",
105 | value: function setCollapseEmptyDiv(collapse, collapseBeforeAdFetch) {
106 | this.collapseEmptyDiv = collapse;
107 | this.collapseBeforeAdFetch = collapseBeforeAdFetch;
108 | return this;
109 | }
110 | }, {
111 | key: "getCollapseEmptyDiv",
112 | value: function getCollapseEmptyDiv() {
113 | return this.collapseEmptyDiv;
114 | }
115 | }, {
116 | key: "setClickUrl",
117 | value: function setClickUrl(clickUrl) {
118 | this.clickUrl = clickUrl;
119 | return this;
120 | }
121 | }, {
122 | key: "getClickUrl",
123 | value: function getClickUrl() {
124 | return this.clickUrl;
125 | }
126 | }, {
127 | key: "setCategoryExclusion",
128 | value: function setCategoryExclusion(categoryExclusion) {
129 | this.categoryExclusions.push(categoryExclusion);
130 | return this;
131 | }
132 | }, {
133 | key: "getCategoryExclusions",
134 | value: function getCategoryExclusions() {
135 | return this.categoryExclusions;
136 | }
137 | }, {
138 | key: "clearCategoryExclusions",
139 | value: function clearCategoryExclusions() {
140 | this.categoryExclusions = [];
141 | return this;
142 | }
143 | }, {
144 | key: "setTargeting",
145 | value: function setTargeting(key, value) {
146 | this._targeting[key] = value;
147 | return this;
148 | }
149 | }, {
150 | key: "getAdUnitPath",
151 | value: function getAdUnitPath() {
152 | return this.adUnitPath;
153 | }
154 | }, {
155 | key: "clearTargeting",
156 | value: function clearTargeting() {
157 | this._targeting = {};
158 | return this;
159 | }
160 | }, {
161 | key: "getTargeting",
162 | value: function getTargeting(key) {
163 | return this._targeting && this._targeting[key];
164 | }
165 | }, {
166 | key: "getTargetingKeys",
167 | value: function getTargetingKeys() {
168 | return this._targeting && Object.keys(this._targeting);
169 | }
170 | }, {
171 | key: "getSizes",
172 | value: function getSizes() {
173 | return this.size;
174 | }
175 | }, {
176 | key: "getSlotElementId",
177 | value: function getSlotElementId() {
178 | return this.divId;
179 | }
180 | }]);
181 |
182 | return SlotMock;
183 | }();
184 |
185 | createMock(_apiList.slotAPI, SlotMock.prototype);
186 |
187 | var SizeMappingBuilderMock = function () {
188 | function SizeMappingBuilderMock() {
189 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
190 |
191 | _classCallCheck(this, SizeMappingBuilderMock);
192 |
193 | this.config = config;
194 | }
195 |
196 | _createClass(SizeMappingBuilderMock, [{
197 | key: "addSize",
198 | value: function addSize(viewportSize, slotSize) {
199 | if (!this.mapping) {
200 | this.mapping = [];
201 | }
202 | this.mapping.push([viewportSize, slotSize]);
203 | return this;
204 | }
205 | }, {
206 | key: "build",
207 | value: function build() {
208 | return this.mapping;
209 | }
210 | }]);
211 |
212 | return SizeMappingBuilderMock;
213 | }();
214 |
215 | var BaseService = function () {
216 | function BaseService() {
217 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
218 |
219 | _classCallCheck(this, BaseService);
220 |
221 | this.config = config;
222 | this.listeners = {};
223 | this.slots = {};
224 | }
225 |
226 | _createClass(BaseService, [{
227 | key: "addEventListener",
228 | value: function addEventListener(eventType, cb) {
229 | if (!this.listeners[eventType]) {
230 | this.listeners[eventType] = [];
231 | }
232 | this.listeners[eventType].push(cb);
233 | }
234 | }, {
235 | key: "getSlots",
236 | value: function getSlots() {
237 | var _this = this;
238 |
239 | return Object.keys(this.slots).map(function (key) {
240 | return _this.slots[key];
241 | });
242 | }
243 | }]);
244 |
245 | return BaseService;
246 | }();
247 |
248 | var PubAdsServiceMock = function (_BaseService) {
249 | _inherits(PubAdsServiceMock, _BaseService);
250 |
251 | function PubAdsServiceMock() {
252 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
253 |
254 | _classCallCheck(this, PubAdsServiceMock);
255 |
256 | var _this2 = _possibleConstructorReturn(this, (PubAdsServiceMock.__proto__ || Object.getPrototypeOf(PubAdsServiceMock)).call(this, config));
257 |
258 | _this2.version = _apiList.pubadsVersion;
259 | return _this2;
260 | }
261 |
262 | _createClass(PubAdsServiceMock, [{
263 | key: "getVersion",
264 | value: function getVersion() {
265 | return this.version;
266 | }
267 | }, {
268 | key: "refresh",
269 | value: function refresh(slots) {
270 | var _this3 = this;
271 |
272 | if (!slots) {
273 | slots = Object.keys(this.slots).map(function (key) {
274 | return _this3.slots[key];
275 | });
276 | }
277 | setTimeout(function () {
278 | var key = _Events2.default.SLOT_RENDER_ENDED;
279 | slots.forEach(function (slot) {
280 | if (_this3.listeners[key]) {
281 | _this3.listeners[key].forEach(function (cb) {
282 | var isEmpty = !!_this3.config.emptyAd;
283 | var event = {
284 | isEmpty: isEmpty,
285 | creativeId: isEmpty ? null : Date.now(),
286 | lineItemId: isEmpty ? null : Date.now(),
287 | serviceName: "publisher_ads",
288 | size: isEmpty ? null : getSize(slot),
289 | slot: slot
290 | };
291 | cb(event);
292 | });
293 | }
294 | });
295 | }, 0);
296 | }
297 | }]);
298 |
299 | return PubAdsServiceMock;
300 | }(BaseService);
301 |
302 | createMock(_apiList.pubadsAPI, PubAdsServiceMock.prototype);
303 |
304 | var CompanionAdsServiceMock = function (_BaseService2) {
305 | _inherits(CompanionAdsServiceMock, _BaseService2);
306 |
307 | function CompanionAdsServiceMock() {
308 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
309 |
310 | _classCallCheck(this, CompanionAdsServiceMock);
311 |
312 | return _possibleConstructorReturn(this, (CompanionAdsServiceMock.__proto__ || Object.getPrototypeOf(CompanionAdsServiceMock)).call(this, config));
313 | }
314 |
315 | _createClass(CompanionAdsServiceMock, [{
316 | key: "enableSyncLoading",
317 | value: function enableSyncLoading() {
318 | this._enableSyncLoading = true;
319 | }
320 | }, {
321 | key: "setRefreshUnfilledSlots",
322 | value: function setRefreshUnfilledSlots(value) {
323 | if (typeof value === "boolean") {
324 | this._refreshUnfilledSlots = value;
325 | }
326 | }
327 | }]);
328 |
329 | return CompanionAdsServiceMock;
330 | }(BaseService);
331 |
332 | var ContentServiceMock = function (_BaseService3) {
333 | _inherits(ContentServiceMock, _BaseService3);
334 |
335 | function ContentServiceMock() {
336 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
337 |
338 | _classCallCheck(this, ContentServiceMock);
339 |
340 | return _possibleConstructorReturn(this, (ContentServiceMock.__proto__ || Object.getPrototypeOf(ContentServiceMock)).call(this, config));
341 | }
342 |
343 | _createClass(ContentServiceMock, [{
344 | key: "setContent",
345 | value: function setContent(slot, content) {
346 | slot._content = content;
347 | }
348 | }]);
349 |
350 | return ContentServiceMock;
351 | }(BaseService);
352 |
353 | var GPTMock = function () {
354 | function GPTMock() {
355 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
356 |
357 | _classCallCheck(this, GPTMock);
358 |
359 | this.pubadsReady = false;
360 |
361 | this.config = config;
362 | this.version = _apiList.gptVersion;
363 | this.cmd = {};
364 | this.cmd.push = function (cb) {
365 | cb();
366 | };
367 | }
368 |
369 | _createClass(GPTMock, [{
370 | key: "getVersion",
371 | value: function getVersion() {
372 | return this.version;
373 | }
374 | }, {
375 | key: "enableServices",
376 | value: function enableServices() {
377 | var _this6 = this;
378 |
379 | setTimeout(function () {
380 | _this6.pubadsReady = true;
381 | }, 0);
382 | }
383 | }, {
384 | key: "sizeMapping",
385 | value: function sizeMapping() {
386 | if (!this.sizeMappingBuilder) {
387 | this.sizeMappingBuilder = new SizeMappingBuilderMock(this.config);
388 | }
389 | return this.sizeMappingBuilder;
390 | }
391 | }, {
392 | key: "pubads",
393 | value: function pubads() {
394 | if (!this._pubads) {
395 | this._pubads = new PubAdsServiceMock(this.config);
396 | }
397 | return this._pubads;
398 | }
399 | }, {
400 | key: "companionAds",
401 | value: function companionAds() {
402 | if (!this._companionAds) {
403 | this._companionAds = new CompanionAdsServiceMock(this.config);
404 | }
405 | return this._companionAds;
406 | }
407 | }, {
408 | key: "content",
409 | value: function content() {
410 | if (!this._content) {
411 | this._content = new ContentServiceMock(this.config);
412 | }
413 | return this._content;
414 | }
415 | }, {
416 | key: "defineSlot",
417 | value: function defineSlot(adUnitPath, size, divId) {
418 | var slot = new SlotMock(adUnitPath, size, divId);
419 | this.pubads().slots[divId] = slot;
420 | return slot;
421 | }
422 | }, {
423 | key: "defineOutOfPageSlot",
424 | value: function defineOutOfPageSlot(adUnitPath, divId) {
425 | var slot = new SlotMock(adUnitPath, [1, 1], divId);
426 | this.pubads().slots[divId] = slot;
427 | return slot;
428 | }
429 | }, {
430 | key: "display",
431 | value: function display(divId) {
432 | var _this7 = this;
433 |
434 | var pubads = this.pubads();
435 | setTimeout(function () {
436 | Object.keys(pubads.listeners).forEach(function (key) {
437 | if (pubads.listeners[key]) {
438 | pubads.listeners[key].forEach(function (cb) {
439 | var slot = pubads.slots[divId];
440 | var isEmpty = !!_this7.config.emptyAd;
441 | var event = {
442 | isEmpty: isEmpty,
443 | creativeId: isEmpty ? null : Date.now(),
444 | lineItemId: isEmpty ? null : Date.now(),
445 | serviceName: "publisher_ads",
446 | size: isEmpty ? null : getSize(slot),
447 | slot: slot
448 | };
449 | cb(event);
450 | });
451 | }
452 | });
453 | }, 0);
454 | }
455 | }]);
456 |
457 | return GPTMock;
458 | }();
459 |
460 | createMock(_apiList.gptAPI, GPTMock.prototype);
461 |
462 | exports.GPTMock = GPTMock;
463 | exports.SlotMock = SlotMock;
464 | exports.SizeMappingBuilderMock = SizeMappingBuilderMock;
465 | exports.PubAdsServiceMock = PubAdsServiceMock;
466 | exports.CompanionAdsServiceMock = CompanionAdsServiceMock;
467 | exports.ContentServiceMock = ContentServiceMock;
--------------------------------------------------------------------------------
/src/createManager.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from "eventemitter3";
2 | import {debounce, throttle} from "throttle-debounce";
3 | import invariant from "invariant";
4 | import {canUseDOM} from "exenv";
5 | import Events from "./Events";
6 | import isInViewport from "./utils/isInViewport";
7 |
8 | // based on https://developers.google.com/doubleclick-gpt/reference?hl=en
9 | export const pubadsAPI = [
10 | "enableAsyncRendering",
11 | "enableSingleRequest",
12 | "enableSyncRendering",
13 | "disableInitialLoad",
14 | "collapseEmptyDivs",
15 | "enableVideoAds",
16 | "set",
17 | "get",
18 | "getAttributeKeys",
19 | "setTargeting",
20 | "clearTargeting",
21 | "setCategoryExclusion",
22 | "clearCategoryExclusions",
23 | "setCentering",
24 | "setCookieOptions",
25 | "setLocation",
26 | "setPublisherProvidedId",
27 | "setTagForChildDirectedTreatment",
28 | "clearTagForChildDirectedTreatment",
29 | "setVideoContent",
30 | "setForceSafeFrame"
31 | ];
32 |
33 | export const APIToCallBeforeServiceEnabled = [
34 | "enableAsyncRendering",
35 | "enableSingleRequest",
36 | "enableSyncRendering",
37 | "disableInitialLoad",
38 | "collapseEmptyDivs",
39 | "setCentering"
40 | ];
41 |
42 | export class AdManager extends EventEmitter {
43 | constructor(config = {}) {
44 | super(config);
45 |
46 | if (config.test) {
47 | this.testMode = config;
48 | }
49 | }
50 |
51 | _adCnt = 0;
52 |
53 | _initialRender = true;
54 |
55 | _syncCorrelator = false;
56 |
57 | _testMode = false;
58 |
59 | get googletag() {
60 | return this._googletag;
61 | }
62 |
63 | get isLoaded() {
64 | return !!this._isLoaded;
65 | }
66 |
67 | get isReady() {
68 | return !!this._isReady;
69 | }
70 |
71 | get apiReady() {
72 | return this.googletag && this.googletag.apiReady;
73 | }
74 |
75 | get pubadsReady() {
76 | return this.googletag && this.googletag.pubadsReady;
77 | }
78 |
79 | get testMode() {
80 | return this._testMode;
81 | }
82 |
83 | set testMode(config) {
84 | if (process.env.NODE_ENV === "production") {
85 | return;
86 | }
87 | const {test, GPTMock} = config;
88 | this._isLoaded = true;
89 | this._testMode = !!test;
90 |
91 | if (test) {
92 | invariant(
93 | test && GPTMock,
94 | "Must provide GPTMock to enable testMode. config{GPTMock}"
95 | );
96 | this._googletag = new GPTMock(config);
97 | }
98 | }
99 |
100 | _processPubadsQueue() {
101 | if (this._pubadsProxyQueue) {
102 | Object.keys(this._pubadsProxyQueue).forEach(method => {
103 | if (
104 | (this.googletag &&
105 | !this.googletag.pubadsReady &&
106 | APIToCallBeforeServiceEnabled.indexOf(method) > -1) ||
107 | this.pubadsReady
108 | ) {
109 | this._pubadsProxyQueue[method].forEach(params =>
110 | this.pubadsProxy(params)
111 | );
112 | delete this._pubadsProxyQueue[method];
113 | }
114 | });
115 | if (!Object.keys(this._pubadsProxyQueue).length) {
116 | this._pubadsProxyQueue = null;
117 | }
118 | }
119 | }
120 |
121 | _callPubads({method, args, resolve, reject}) {
122 | if (typeof this.googletag.pubads()[method] !== "function") {
123 | reject(
124 | new Error(
125 | `googletag.pubads does not support ${method}, please update pubadsAPI`
126 | )
127 | );
128 | } else {
129 | try {
130 | const result = this.googletag.pubads()[method](...args);
131 | resolve(result);
132 | } catch (err) {
133 | reject(err);
134 | }
135 | }
136 | }
137 |
138 | _toggleListener(add) {
139 | ["scroll", "resize"].forEach(eventName => {
140 | window[add ? "addEventListener" : "removeEventListener"](
141 | eventName,
142 | this._foldCheck
143 | );
144 | });
145 | }
146 |
147 | _foldCheck = throttle(20, event => {
148 | const instances = this.getMountedInstances();
149 | instances.forEach(instance => {
150 | if (instance.getRenderWhenViewable()) {
151 | instance.foldCheck(event);
152 | }
153 | });
154 |
155 | if (this.testMode) {
156 | this._getTimer();
157 | }
158 | });
159 |
160 | _getTimer() {
161 | return Date.now();
162 | }
163 |
164 | _handleMediaQueryChange = event => {
165 | if (this._syncCorrelator) {
166 | this.refresh();
167 | return;
168 | }
169 | // IE returns `event.media` value differently, need to use regex to evaluate.
170 | // eslint-disable-next-line wrap-regex
171 | const res = /min-width:\s?(\d+)px/.exec(event.media);
172 | const viewportWidth = res && res[1];
173 |
174 | if (viewportWidth && this._mqls[viewportWidth]) {
175 | this._mqls[viewportWidth].listeners.forEach(instance => {
176 | instance.refresh();
177 | if (instance.props.onMediaQueryChange) {
178 | instance.props.onMediaQueryChange(event);
179 | }
180 | });
181 | }
182 | };
183 |
184 | _listen() {
185 | if (!this._listening) {
186 | [
187 | Events.SLOT_RENDER_ENDED,
188 | Events.IMPRESSION_VIEWABLE,
189 | Events.SLOT_VISIBILITY_CHANGED,
190 | Events.SLOT_LOADED
191 | ].forEach(eventType => {
192 | ["pubads", "content", "companionAds"].forEach(service => {
193 | // there is no API to remove listeners.
194 | this.googletag[service]().addEventListener(
195 | eventType,
196 | this._onEvent.bind(this, eventType)
197 | );
198 | });
199 | });
200 | this._listening = true;
201 | }
202 | }
203 |
204 | _onEvent(eventType, event) {
205 | // fire to the global listeners
206 | if (this.listeners(eventType, true)) {
207 | this.emit(eventType, event);
208 | }
209 | // call event handler props
210 | const instances = this.getMountedInstances();
211 | const {slot} = event;
212 | const funcName = `on${eventType
213 | .charAt(0)
214 | .toUpperCase()}${eventType.substr(1)}`;
215 | const instance = instances.filter(inst => slot === inst.adSlot)[0];
216 | if (instance && instance.props[funcName]) {
217 | instance.props[funcName](event);
218 | }
219 | }
220 |
221 | syncCorrelator(value = true) {
222 | this._syncCorrelator = value;
223 | }
224 |
225 | generateDivId() {
226 | return `bling-${++this._adCnt}`;
227 | }
228 |
229 | getMountedInstances() {
230 | if (!this.mountedInstances) {
231 | this.mountedInstances = [];
232 | }
233 | return this.mountedInstances;
234 | }
235 |
236 | addInstance(instance) {
237 | const instances = this.getMountedInstances();
238 | const index = instances.indexOf(instance);
239 | if (index === -1) {
240 | // The first instance starts listening for the event.
241 | if (instances.length === 0) {
242 | this._toggleListener(true);
243 | }
244 | this.addMQListener(instance, instance.props);
245 | instances.push(instance);
246 | }
247 | }
248 |
249 | removeInstance(instance) {
250 | const instances = this.getMountedInstances();
251 | const index = instances.indexOf(instance);
252 | if (index >= 0) {
253 | instances.splice(index, 1);
254 | // The last instance removes listening for the event.
255 | if (instances.length === 0) {
256 | this._toggleListener(false);
257 | }
258 | this.removeMQListener(instance, instance.props);
259 | }
260 | }
261 |
262 | addMQListener(instance, {sizeMapping}) {
263 | if (!sizeMapping || !Array.isArray(sizeMapping)) {
264 | return;
265 | }
266 |
267 | sizeMapping.forEach(size => {
268 | const viewportWidth = size.viewport && size.viewport[0];
269 | if (viewportWidth !== undefined) {
270 | if (!this._mqls) {
271 | this._mqls = {};
272 | }
273 | if (!this._mqls[viewportWidth]) {
274 | const mql = window.matchMedia(
275 | `(min-width: ${viewportWidth}px)`
276 | );
277 | mql.addListener(this._handleMediaQueryChange);
278 | this._mqls[viewportWidth] = {
279 | mql,
280 | listeners: []
281 | };
282 | }
283 | if (
284 | this._mqls[viewportWidth].listeners.indexOf(instance) === -1
285 | ) {
286 | this._mqls[viewportWidth].listeners.push(instance);
287 | }
288 | }
289 | });
290 | }
291 |
292 | removeMQListener(instance) {
293 | if (!this._mqls) {
294 | return;
295 | }
296 |
297 | Object.keys(this._mqls).forEach(key => {
298 | const index = this._mqls[key].listeners.indexOf(instance);
299 | if (index > -1) {
300 | this._mqls[key].listeners.splice(index, 1);
301 | }
302 | if (this._mqls[key].listeners.length === 0) {
303 | this._mqls[key].mql.removeListener(
304 | this._handleMediaQueryChange
305 | );
306 | delete this._mqls[key];
307 | }
308 | });
309 | }
310 |
311 | isInViewport(...args) {
312 | return isInViewport(...args);
313 | }
314 |
315 | /**
316 | * Refreshes all the ads in the page with a new correlator value.
317 | *
318 | * @param {Array} slots An array of ad slots.
319 | * @param {Object} options You can pass `changeCorrelator` flag.
320 | * @static
321 | */
322 | refresh(slots, options) {
323 | if (!this.pubadsReady) {
324 | return false;
325 | }
326 |
327 | // gpt already debounces refresh
328 | this.googletag.pubads().refresh(slots, options);
329 |
330 | return true;
331 | }
332 |
333 | clear(slots) {
334 | if (!this.pubadsReady) {
335 | return false;
336 | }
337 |
338 | this.googletag.pubads().clear(slots);
339 |
340 | return true;
341 | }
342 |
343 | render = debounce(4, () => {
344 | if (!this._initialRender) {
345 | return;
346 | }
347 |
348 | const checkPubadsReady = cb => {
349 | if (this.pubadsReady) {
350 | cb();
351 | } else {
352 | setTimeout(checkPubadsReady, 50, cb);
353 | }
354 | };
355 |
356 | const instances = this.getMountedInstances();
357 | let hasPubAdsService = false;
358 | let dummyAdSlot;
359 |
360 | // Define all the slots
361 | instances.forEach(instance => {
362 | if (!instance.notInViewport()) {
363 | instance.defineSlot();
364 | const adSlot = instance.adSlot;
365 |
366 | if (adSlot && adSlot.hasOwnProperty("getServices")) {
367 | const services = adSlot.getServices();
368 | if (!hasPubAdsService) {
369 | hasPubAdsService =
370 | services.filter(
371 | service => !!service.enableAsyncRendering
372 | ).length > 0;
373 | }
374 | }
375 | }
376 | });
377 | // if none of the ad slots uses pubads service, create dummy slot to use pubads service.
378 | if (!hasPubAdsService) {
379 | dummyAdSlot = this.googletag.defineSlot("/", []);
380 | dummyAdSlot.addService(this.googletag.pubads());
381 | }
382 |
383 | // Call pubads API which needs to be called before service is enabled.
384 | this._processPubadsQueue();
385 |
386 | // Enable service
387 | this.googletag.enableServices();
388 |
389 | // After the service is enabled, check periodically until `pubadsReady` flag returns true before proceeding the rest.
390 | checkPubadsReady(() => {
391 | // destroy dummy ad slot if exists.
392 | if (dummyAdSlot) {
393 | this.googletag.destroySlots([dummyAdSlot]);
394 | }
395 | // Call the rest of the pubads API that's in the queue.
396 | this._processPubadsQueue();
397 | // listen for GPT events
398 | this._listen();
399 | // client should be able to set any page-level setting within the event handler.
400 | this._isReady = true;
401 | this.emit(Events.READY, this.googletag);
402 |
403 | // Call display
404 | instances.forEach(instance => {
405 | if (!instance.notInViewport()) {
406 | instance.display();
407 | }
408 | });
409 |
410 | this.emit(Events.RENDER, this.googletag);
411 |
412 | this._initialRender = false;
413 | });
414 | });
415 |
416 | /**
417 | * Re-render(not refresh) all the ads in the page and the first ad will update the correlator value.
418 | * Updating correlator value ensures competitive exclusion.
419 | *
420 | * @method renderAll
421 | * @static
422 | */
423 | renderAll = debounce(4, () => {
424 | if (!this.apiReady) {
425 | return false;
426 | }
427 |
428 | // first instance updates correlator value and re-render each ad
429 | const instances = this.getMountedInstances();
430 | instances.forEach((instance, i) => {
431 | if (i === 0) {
432 | this.updateCorrelator();
433 | }
434 | instance.forceUpdate();
435 | });
436 |
437 | return true;
438 | });
439 |
440 | getGPTVersion() {
441 | if (!this.apiReady) {
442 | return false;
443 | }
444 | return this.googletag.getVersion();
445 | }
446 |
447 | getPubadsVersion() {
448 | if (!this.pubadsReady) {
449 | return false;
450 | }
451 | return this.googletag.pubads().getVersion();
452 | }
453 |
454 | updateCorrelator() {
455 | if (!this.pubadsReady) {
456 | return false;
457 | }
458 | this.googletag.pubads().updateCorrelator();
459 |
460 | return true;
461 | }
462 |
463 | load(url) {
464 | return (
465 | this._loadPromise ||
466 | (this._loadPromise = new Promise((resolve, reject) => {
467 | // test mode can't be enabled in production mode
468 | if (this.testMode) {
469 | resolve(this.googletag);
470 | return;
471 | }
472 | if (!canUseDOM) {
473 | reject(new Error("DOM not available"));
474 | return;
475 | }
476 | if (!url) {
477 | reject(new Error("url is missing"));
478 | return;
479 | }
480 | const onLoad = () => {
481 | if (window.googletag) {
482 | this._googletag = window.googletag;
483 | // make sure API is ready for use.
484 | this.googletag.cmd.push(() => {
485 | this._isLoaded = true;
486 | resolve(this.googletag);
487 | });
488 | } else {
489 | reject(new Error("window.googletag is not available"));
490 | }
491 | };
492 | if (window.googletag && window.googletag.apiReady) {
493 | onLoad();
494 | } else {
495 | const script = document.createElement("script");
496 | script.async = true;
497 | script.onload = onLoad;
498 | script.onerror = () => {
499 | reject(new Error("failed to load script"));
500 | };
501 | script.src = url;
502 | document.head.appendChild(script);
503 | }
504 | }))
505 | );
506 | }
507 |
508 | pubadsProxy({method, args = [], resolve, reject}) {
509 | if (!resolve) {
510 | // there are couple pubads API which doesn't provide getter methods for later use,
511 | // so remember them here.
512 | if (APIToCallBeforeServiceEnabled.indexOf(method) > -1) {
513 | this[`_${method}`] = (args && args.length && args[0]) || true;
514 | }
515 | return new Promise((resolve2, reject2) => {
516 | const params = {
517 | method,
518 | args,
519 | resolve: resolve2,
520 | reject: reject2
521 | };
522 | if (!this.pubadsReady) {
523 | if (!this._pubadsProxyQueue) {
524 | this._pubadsProxyQueue = {};
525 | }
526 | if (!this._pubadsProxyQueue[method]) {
527 | this._pubadsProxyQueue[method] = [];
528 | }
529 | this._pubadsProxyQueue[method].push(params);
530 | } else {
531 | this._callPubads(params);
532 | }
533 | });
534 | }
535 |
536 | this._callPubads({method, args, resolve, reject});
537 |
538 | return Promise.resolve();
539 | }
540 | }
541 |
542 | export function createManager(config) {
543 | return new AdManager(config);
544 | }
545 |
--------------------------------------------------------------------------------
/test/createManager.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | createManager,
3 | AdManager,
4 | pubadsAPI,
5 | APIToCallBeforeServiceEnabled
6 | } from "../src/createManager";
7 | import Events from "../src/Events";
8 | import {gptVersion} from "../src/utils/apiList";
9 | import {createManagerTest} from "../src/utils/createManagerTest";
10 |
11 | describe("createManager", () => {
12 | let googletag;
13 | let adManager;
14 |
15 | beforeEach(() => {
16 | adManager = createManagerTest();
17 | googletag = adManager.googletag;
18 | });
19 |
20 | afterEach(() => {
21 | window.googletag = undefined;
22 | });
23 |
24 | it("accepts syncCorrelator", () => {
25 | adManager.syncCorrelator(true);
26 | expect(adManager._syncCorrelator).to.be.true;
27 | });
28 |
29 | it("accepts pubads API before pubads is ready", done => {
30 | const apiStubs = {};
31 | pubadsAPI.forEach(method => {
32 | apiStubs[method] = sinon.stub(googletag.pubads(), method);
33 | });
34 |
35 | pubadsAPI.forEach(method => {
36 | let args = [];
37 | if (method === "collapseEmptyDivs") {
38 | args = [true];
39 | } else if (method === "setTargeting") {
40 | args = ["key", "value"];
41 | }
42 | adManager.pubadsProxy({method, args});
43 | });
44 |
45 | adManager.once(Events.RENDER, () => {
46 | APIToCallBeforeServiceEnabled.forEach(method => {
47 | expect(adManager[`_${method}`]).to.be.true;
48 | });
49 | Object.keys(apiStubs).forEach(method => {
50 | const stub = apiStubs[method];
51 | expect(stub.calledOnce).to.be.true;
52 | if (method === "collapseEmptyDivs") {
53 | expect(stub.calledWith(true)).to.be.true;
54 | } else if (method === "setTargeting") {
55 | expect(stub.calledWith("key", "value")).to.be.true;
56 | }
57 | sinon.restore(stub);
58 | });
59 |
60 | done();
61 | });
62 |
63 | adManager.render();
64 | });
65 |
66 | it("accepts pubads API after pubads is ready", done => {
67 | const apiStubs = {};
68 | pubadsAPI.forEach(method => {
69 | apiStubs[method] = sinon.stub(googletag.pubads(), method);
70 | });
71 |
72 | adManager.once(Events.RENDER, () => {
73 | pubadsAPI.forEach(method => {
74 | let args = [];
75 | if (method === "collapseEmptyDivs") {
76 | args = [true];
77 | } else if (method === "setTargeting") {
78 | args = ["key", "value"];
79 | }
80 | adManager.pubadsProxy({method, args});
81 | });
82 | APIToCallBeforeServiceEnabled.forEach(method => {
83 | expect(adManager[`_${method}`]).to.be.true;
84 | });
85 | Object.keys(apiStubs).forEach(method => {
86 | const stub = apiStubs[method];
87 | expect(stub.calledOnce).to.be.true;
88 | if (method === "collapseEmptyDivs") {
89 | expect(stub.calledWith(true)).to.be.true;
90 | } else if (method === "setTargeting") {
91 | expect(stub.calledWith("key", "value")).to.be.true;
92 | }
93 | sinon.restore(stub);
94 | });
95 |
96 | done();
97 | });
98 |
99 | adManager.render();
100 | });
101 |
102 | it("loads gpt", done => {
103 | adManager
104 | .load("//www.googletagservices.com/tag/js/gpt.js")
105 | .then(result => {
106 | expect(result).to.be.an("object");
107 | expect(adManager.isLoaded).to.be.true;
108 | done();
109 | })
110 | .catch(done);
111 | });
112 |
113 | it("uses gpt when already exists", done => {
114 | window.googletag = googletag;
115 | adManager
116 | .load("//www.googletagservices.com/tag/js/gpt-invalid.js")
117 | .then(() => {
118 | expect(adManager.isLoaded).to.be.true;
119 | done();
120 | })
121 | .catch(done);
122 | });
123 |
124 | it("handles missing url", done => {
125 | adManager = createManager();
126 | adManager.load("").catch(err => {
127 | expect(err.message).to.equal("url is missing");
128 | done();
129 | });
130 | });
131 |
132 | it("handles invalid url", done => {
133 | adManager = createManager();
134 | adManager
135 | .load("//www.googletagservices.com/tag/js/gpt-invalid.js")
136 | .catch(err => {
137 | expect(err.message).to.equal("failed to load script");
138 | done();
139 | });
140 | });
141 |
142 | it("handles gpt existence", done => {
143 | adManager = createManager();
144 | adManager.load("//www.google.com/jsapi").catch(err => {
145 | expect(err.message).to.equal("window.googletag is not available");
146 | done();
147 | });
148 | });
149 |
150 | it("returns gpt version", () => {
151 | expect(adManager.getGPTVersion()).to.equal(gptVersion);
152 | });
153 |
154 | it("maintains instance list", () => {
155 | const _toggleListener = sinon.stub(
156 | AdManager.prototype,
157 | "_toggleListener"
158 | );
159 | const addMQListener = sinon.stub(AdManager.prototype, "addMQListener");
160 | const removeMQListener = sinon.stub(
161 | AdManager.prototype,
162 | "removeMQListener"
163 | );
164 | const instances = [{}, {}];
165 |
166 | adManager.addInstance(instances[0]);
167 |
168 | expect(_toggleListener.calledWith(true)).to.be.true;
169 | expect(_toggleListener.calledOnce).to.be.true;
170 | expect(addMQListener.calledWith(instances[0])).to.be.true;
171 | expect(addMQListener.calledOnce).to.be.true;
172 |
173 | adManager.addInstance(instances[1]);
174 |
175 | expect(_toggleListener.calledOnce).to.be.true;
176 | expect(addMQListener.calledWith(instances[1])).to.be.true;
177 | expect(addMQListener.calledTwice).to.be.true;
178 |
179 | adManager.removeInstance(instances[0]);
180 |
181 | expect(removeMQListener.calledWith(instances[0])).to.be.true;
182 | expect(removeMQListener.calledOnce).to.be.true;
183 |
184 | adManager.removeInstance(instances[1]);
185 |
186 | expect(_toggleListener.calledWith(false)).to.be.true;
187 | expect(_toggleListener.calledTwice).to.be.true;
188 | expect(removeMQListener.calledWith(instances[1])).to.be.true;
189 | expect(removeMQListener.calledTwice).to.be.true;
190 |
191 | _toggleListener.restore();
192 | addMQListener.restore();
193 | removeMQListener.restore();
194 | });
195 |
196 | it("adds/removes instance to matchMedia query listener", () => {
197 | // case 1 - missing `sizeMapping`
198 |
199 | let instance = {
200 | props: {}
201 | };
202 |
203 | adManager.addInstance(instance);
204 |
205 | expect(adManager._mqls).to.be.undefined;
206 |
207 | adManager.removeInstance(instance);
208 |
209 | // case 2 - non-array `sizeMapping`
210 |
211 | instance = {
212 | props: {
213 | sizeMapping: 100
214 | }
215 | };
216 |
217 | adManager.addInstance(instance);
218 |
219 | expect(adManager._mqls).to.be.undefined;
220 |
221 | adManager.removeInstance(instance);
222 |
223 | // case 3 - invalid `sizeMapping` item
224 |
225 | instance = {
226 | props: {
227 | sizeMapping: [320, 50]
228 | }
229 | };
230 |
231 | adManager.addInstance(instance);
232 |
233 | expect(adManager._mqls).to.be.undefined;
234 |
235 | adManager.removeInstance(instance);
236 |
237 | // case 4 - valid `sizeMapping` item
238 |
239 | instance = {
240 | props: {
241 | sizeMapping: [{viewport: [0, 0], slot: [320, 50]}]
242 | }
243 | };
244 |
245 | adManager.addInstance(instance);
246 |
247 | expect(adManager._mqls).to.be.an("object");
248 | expect(adManager._mqls["0"]).to.be.an("object");
249 | expect(adManager._mqls["0"].listeners.length).to.be.equal(1);
250 |
251 | adManager.removeInstance(instance);
252 |
253 | // case 5 - multiple instance listens for the same matchMedia query
254 |
255 | let instance2 = {
256 | props: {
257 | sizeMapping: [{viewport: [0, 0], slot: [320, 50]}]
258 | }
259 | };
260 |
261 | adManager.addInstance(instance);
262 | adManager.addInstance(instance2);
263 |
264 | expect(adManager._mqls).to.be.an("object");
265 | expect(adManager._mqls["0"]).to.be.an("object");
266 | expect(adManager._mqls["0"].listeners.length).to.be.equal(2);
267 |
268 | adManager.removeInstance(instance);
269 |
270 | expect(adManager._mqls["0"].listeners.length).to.be.equal(1);
271 |
272 | adManager.removeInstance(instance2);
273 |
274 | expect(adManager._mqls).to.be.an("object");
275 | expect(adManager._mqls["0"]).to.be.undefined;
276 |
277 | // case 6 - removing an instance that's not in listeners won't accidentally remove listeners
278 |
279 | instance2 = {
280 | props: {}
281 | };
282 |
283 | adManager.addInstance(instance);
284 | adManager.addInstance(instance2);
285 |
286 | adManager.removeInstance(instance2);
287 |
288 | expect(adManager._mqls).to.be.an("object");
289 | expect(adManager._mqls["0"]).to.be.an("object");
290 | expect(adManager._mqls["0"].listeners.length).to.be.equal(1);
291 | });
292 |
293 | it("handles media query change", () => {
294 | adManager.syncCorrelator();
295 |
296 | const refresh = sinon.stub(googletag.pubads(), "refresh");
297 |
298 | googletag.pubadsReady = true;
299 |
300 | const instance = {
301 | props: {
302 | sizeMapping: [{viewport: [0, 0], slot: [320, 50]}]
303 | },
304 | refresh() {}
305 | };
306 |
307 | const instanceRefresh = sinon.stub(instance, "refresh");
308 |
309 | adManager.addInstance(instance);
310 | adManager._handleMediaQueryChange({
311 | media: "(min-width: 0px)"
312 | });
313 |
314 | expect(refresh.calledOnce).to.be.true;
315 |
316 | adManager.syncCorrelator(false);
317 |
318 | adManager._handleMediaQueryChange({
319 | media: "(min-width: 0px)"
320 | });
321 |
322 | expect(instanceRefresh.calledOnce).to.be.true;
323 |
324 | // IE
325 | adManager._handleMediaQueryChange({
326 | media: "all and (min-width:0px)"
327 | });
328 |
329 | expect(instanceRefresh.calledTwice).to.be.true;
330 |
331 | adManager.removeInstance(instance);
332 |
333 | refresh.restore();
334 | instanceRefresh.restore();
335 | });
336 |
337 | it("debounces render", done => {
338 | const enableServices = sinon.stub(
339 | googletag,
340 | "enableServices",
341 | googletag.enableServices
342 | );
343 |
344 | adManager.once(Events.RENDER, () => {
345 | expect(enableServices.calledOnce).to.be.true;
346 | enableServices.restore();
347 | done();
348 | });
349 |
350 | adManager.render();
351 | adManager.render();
352 | adManager.render();
353 | });
354 |
355 | it("executes render once", done => {
356 | const enableServices = sinon.stub(
357 | googletag,
358 | "enableServices",
359 | googletag.enableServices
360 | );
361 |
362 | adManager.once(Events.RENDER, () => {
363 | expect(enableServices.calledOnce).to.be.true;
364 |
365 | setTimeout(() => {
366 | expect(enableServices.calledTwice).to.be.false;
367 | enableServices.restore();
368 | done();
369 | }, 300);
370 |
371 | adManager.render();
372 | });
373 |
374 | adManager.render();
375 | adManager.render();
376 | adManager.render();
377 | });
378 |
379 | it("manages initial render", done => {
380 | adManager.pubadsProxy({method: "disableInitialLoad"});
381 | adManager.pubadsProxy({method: "collapseEmptyDivs", args: [false]});
382 |
383 | const disableInitialLoad = sinon.stub(
384 | googletag.pubads(),
385 | "disableInitialLoad"
386 | );
387 | const collapseEmptyDivs = sinon.stub(
388 | googletag.pubads(),
389 | "collapseEmptyDivs"
390 | );
391 |
392 | const instance = {
393 | props: {
394 | sizeMapping: [{viewport: [0, 0], slot: [320, 50]}]
395 | },
396 | notInViewport() {
397 | return false;
398 | },
399 | defineSlot() {},
400 | display() {},
401 | adSlot: googletag.defineSlot("/", [])
402 | };
403 |
404 | const defineSlot = sinon.stub(instance, "defineSlot");
405 | const display = sinon.stub(instance, "display");
406 |
407 | adManager.addInstance(instance);
408 |
409 | adManager.once(Events.RENDER, () => {
410 | expect(disableInitialLoad.calledOnce).to.be.true;
411 | expect(collapseEmptyDivs.calledWith(false)).to.be.true;
412 | expect(defineSlot.calledOnce).to.be.true;
413 | expect(display.calledOnce).to.be.true;
414 |
415 | disableInitialLoad.restore();
416 | collapseEmptyDivs.restore();
417 | defineSlot.restore();
418 | display.restore();
419 | adManager.removeInstance(instance);
420 | done();
421 | });
422 |
423 | adManager.render();
424 | adManager.render();
425 | adManager.render();
426 | });
427 |
428 | it("throttles foldCheck", done => {
429 | const instance = {
430 | props: {
431 | sizeMapping: [{viewport: [0, 0], slot: [320, 50]}]
432 | },
433 | getRenderWhenViewable() {
434 | return true;
435 | },
436 | foldCheck() {}
437 | };
438 |
439 | const instance2 = {
440 | props: {
441 | sizeMapping: [{viewport: [0, 0], slot: [320, 50]}]
442 | },
443 | getRenderWhenViewable() {
444 | return false;
445 | },
446 | foldCheck() {}
447 | };
448 |
449 | const foldCheck = sinon.stub(instance, "foldCheck");
450 | const foldCheck2 = sinon.stub(instance2, "foldCheck");
451 | const getRenderWhenViewable = sinon.spy(
452 | instance,
453 | "getRenderWhenViewable"
454 | );
455 | const getRenderWhenViewable2 = sinon.spy(
456 | instance2,
457 | "getRenderWhenViewable"
458 | );
459 | const managerFoldCheck = sinon.spy(adManager, "_foldCheck");
460 | const timer = sinon.spy(adManager, "_getTimer");
461 |
462 | adManager.addInstance(instance);
463 | adManager.addInstance(instance2);
464 |
465 | const start = Date.now();
466 | adManager._foldCheck();
467 | adManager._foldCheck();
468 | setTimeout(() => {
469 | adManager._foldCheck();
470 | }, 5);
471 | setTimeout(() => {
472 | adManager._foldCheck();
473 | }, 10);
474 | setTimeout(() => {
475 | adManager._foldCheck();
476 | }, 15);
477 |
478 | setTimeout(() => {
479 | expect(managerFoldCheck.callCount).to.equal(5);
480 | expect(timer.calledTwice).to.be.true;
481 | expect(timer.returnValues[1] - timer.returnValues[0]).to.be.above(
482 | 19
483 | ); // timer above 20ms timeout
484 | expect(timer.returnValues[0] - start).to.be.below(5); // should start ~immediately
485 | expect(foldCheck.calledTwice).to.be.true;
486 | expect(foldCheck2.notCalled).to.be.true;
487 |
488 | foldCheck.restore();
489 | foldCheck2.restore();
490 | getRenderWhenViewable.restore();
491 | getRenderWhenViewable2.restore();
492 | managerFoldCheck.restore();
493 | timer.restore();
494 | adManager.removeInstance(instance);
495 | adManager.removeInstance(instance2);
496 | done();
497 | }, 100);
498 | });
499 |
500 | it("renders all ads", done => {
501 | googletag.apiReady = false;
502 | const updateCorrelator = sinon.stub(
503 | AdManager.prototype,
504 | "updateCorrelator"
505 | );
506 |
507 | const instance = {
508 | props: {},
509 | forceUpdate() {}
510 | };
511 |
512 | const instance2 = {
513 | props: {},
514 | forceUpdate() {}
515 | };
516 |
517 | const forceUpdate = sinon.stub(instance, "forceUpdate");
518 | const forceUpdate2 = sinon.stub(instance2, "forceUpdate");
519 |
520 | adManager.addInstance(instance);
521 | adManager.addInstance(instance2);
522 |
523 | setTimeout(() => {
524 | expect(updateCorrelator.calledOnce).to.be.false;
525 | expect(forceUpdate.calledOnce).to.be.false;
526 | expect(forceUpdate2.calledOnce).to.be.false;
527 |
528 | googletag.apiReady = true;
529 |
530 | setTimeout(() => {
531 | expect(updateCorrelator.calledOnce).to.be.true;
532 | expect(forceUpdate.calledOnce).to.be.true;
533 | expect(forceUpdate2.calledOnce).to.be.true;
534 |
535 | updateCorrelator.restore();
536 | forceUpdate.restore();
537 | forceUpdate2.restore();
538 | adManager.removeInstance(instance);
539 | adManager.removeInstance(instance2);
540 | done();
541 | }, 300);
542 |
543 | adManager.renderAll();
544 | }, 300);
545 |
546 | adManager.renderAll();
547 | });
548 |
549 | it("refreshes ads", () => {
550 | const refresh = sinon.stub(googletag.pubads(), "refresh");
551 |
552 | adManager.refresh();
553 | expect(refresh.calledOnce).to.be.false;
554 |
555 | googletag.pubadsReady = true;
556 | adManager.refresh();
557 | expect(refresh.calledOnce).to.be.true;
558 | refresh.restore();
559 | });
560 |
561 | it("clears ads", () => {
562 | const clear = sinon.stub(googletag.pubads(), "clear");
563 |
564 | adManager.clear();
565 | expect(clear.calledOnce).to.be.false;
566 |
567 | googletag.pubadsReady = true;
568 | adManager.clear();
569 | expect(clear.calledOnce).to.be.true;
570 | clear.restore();
571 | });
572 |
573 | it("calls prop function for gpt event", done => {
574 | const listeners = [];
575 | const slot = googletag.defineSlot("/", []);
576 | const addEventListener = sinon.stub(
577 | googletag.pubads(),
578 | "addEventListener",
579 | (eventType, cb) => {
580 | if (!listeners[eventType]) {
581 | listeners[eventType] = [];
582 | }
583 | listeners[eventType].push(cb);
584 | }
585 | );
586 |
587 | const instance = {
588 | props: {
589 | onSlotRenderEnded() {}
590 | },
591 | adSlot: slot,
592 | notInViewport() {
593 | return false;
594 | },
595 | defineSlot() {},
596 | display() {}
597 | };
598 |
599 | const display = sinon.stub(instance, "display", () => {
600 | Object.keys(listeners).forEach(key => {
601 | if (listeners[key]) {
602 | listeners[key].forEach(cb => {
603 | cb({slot});
604 | });
605 | }
606 | });
607 | });
608 |
609 | const onSlotRenderEnded = sinon.stub(
610 | instance.props,
611 | "onSlotRenderEnded"
612 | );
613 |
614 | adManager.addInstance(instance);
615 |
616 | adManager.once(Events.RENDER, () => {
617 | expect(onSlotRenderEnded.calledOnce).to.be.true;
618 | addEventListener.restore();
619 | display.restore();
620 | onSlotRenderEnded.restore();
621 | adManager.removeInstance(instance);
622 | done();
623 | });
624 |
625 | adManager.render();
626 | });
627 | });
628 |
--------------------------------------------------------------------------------
/lib/createManager.js:
--------------------------------------------------------------------------------
1 | Object.defineProperty(exports, "__esModule", {
2 | value: true
3 | });
4 | exports.AdManager = exports.APIToCallBeforeServiceEnabled = exports.pubadsAPI = undefined;
5 |
6 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
7 |
8 | exports.createManager = createManager;
9 |
10 | var _eventemitter = require("eventemitter3");
11 |
12 | var _eventemitter2 = _interopRequireDefault(_eventemitter);
13 |
14 | var _throttleDebounce = require("throttle-debounce");
15 |
16 | var _invariant = require("invariant");
17 |
18 | var _invariant2 = _interopRequireDefault(_invariant);
19 |
20 | var _exenv = require("exenv");
21 |
22 | var _Events = require("./Events");
23 |
24 | var _Events2 = _interopRequireDefault(_Events);
25 |
26 | var _isInViewport2 = require("./utils/isInViewport");
27 |
28 | var _isInViewport3 = _interopRequireDefault(_isInViewport2);
29 |
30 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
31 |
32 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
33 |
34 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
35 |
36 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
37 |
38 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
39 |
40 | // based on https://developers.google.com/doubleclick-gpt/reference?hl=en
41 | var pubadsAPI = exports.pubadsAPI = ["enableAsyncRendering", "enableSingleRequest", "enableSyncRendering", "disableInitialLoad", "collapseEmptyDivs", "enableVideoAds", "set", "get", "getAttributeKeys", "setTargeting", "clearTargeting", "setCategoryExclusion", "clearCategoryExclusions", "setCentering", "setCookieOptions", "setLocation", "setPublisherProvidedId", "setTagForChildDirectedTreatment", "clearTagForChildDirectedTreatment", "setVideoContent", "setForceSafeFrame"];
42 |
43 | var APIToCallBeforeServiceEnabled = exports.APIToCallBeforeServiceEnabled = ["enableAsyncRendering", "enableSingleRequest", "enableSyncRendering", "disableInitialLoad", "collapseEmptyDivs", "setCentering"];
44 |
45 | var AdManager = exports.AdManager = function (_EventEmitter) {
46 | _inherits(AdManager, _EventEmitter);
47 |
48 | function AdManager() {
49 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
50 |
51 | _classCallCheck(this, AdManager);
52 |
53 | var _this = _possibleConstructorReturn(this, (AdManager.__proto__ || Object.getPrototypeOf(AdManager)).call(this, config));
54 |
55 | _this._adCnt = 0;
56 | _this._initialRender = true;
57 | _this._syncCorrelator = false;
58 | _this._testMode = false;
59 | _this._foldCheck = (0, _throttleDebounce.throttle)(20, function (event) {
60 | var instances = _this.getMountedInstances();
61 | instances.forEach(function (instance) {
62 | if (instance.getRenderWhenViewable()) {
63 | instance.foldCheck(event);
64 | }
65 | });
66 |
67 | if (_this.testMode) {
68 | _this._getTimer();
69 | }
70 | });
71 |
72 | _this._handleMediaQueryChange = function (event) {
73 | if (_this._syncCorrelator) {
74 | _this.refresh();
75 | return;
76 | }
77 | // IE returns `event.media` value differently, need to use regex to evaluate.
78 | // eslint-disable-next-line wrap-regex
79 | var res = /min-width:\s?(\d+)px/.exec(event.media);
80 | var viewportWidth = res && res[1];
81 |
82 | if (viewportWidth && _this._mqls[viewportWidth]) {
83 | _this._mqls[viewportWidth].listeners.forEach(function (instance) {
84 | instance.refresh();
85 | if (instance.props.onMediaQueryChange) {
86 | instance.props.onMediaQueryChange(event);
87 | }
88 | });
89 | }
90 | };
91 |
92 | _this.render = (0, _throttleDebounce.debounce)(4, function () {
93 | if (!_this._initialRender) {
94 | return;
95 | }
96 |
97 | var checkPubadsReady = function checkPubadsReady(cb) {
98 | if (_this.pubadsReady) {
99 | cb();
100 | } else {
101 | setTimeout(checkPubadsReady, 50, cb);
102 | }
103 | };
104 |
105 | var instances = _this.getMountedInstances();
106 | var hasPubAdsService = false;
107 | var dummyAdSlot = void 0;
108 |
109 | // Define all the slots
110 | instances.forEach(function (instance) {
111 | if (!instance.notInViewport()) {
112 | instance.defineSlot();
113 | var adSlot = instance.adSlot;
114 |
115 | if (adSlot && adSlot.hasOwnProperty("getServices")) {
116 | var services = adSlot.getServices();
117 | if (!hasPubAdsService) {
118 | hasPubAdsService = services.filter(function (service) {
119 | return !!service.enableAsyncRendering;
120 | }).length > 0;
121 | }
122 | }
123 | }
124 | });
125 | // if none of the ad slots uses pubads service, create dummy slot to use pubads service.
126 | if (!hasPubAdsService) {
127 | dummyAdSlot = _this.googletag.defineSlot("/", []);
128 | dummyAdSlot.addService(_this.googletag.pubads());
129 | }
130 |
131 | // Call pubads API which needs to be called before service is enabled.
132 | _this._processPubadsQueue();
133 |
134 | // Enable service
135 | _this.googletag.enableServices();
136 |
137 | // After the service is enabled, check periodically until `pubadsReady` flag returns true before proceeding the rest.
138 | checkPubadsReady(function () {
139 | // destroy dummy ad slot if exists.
140 | if (dummyAdSlot) {
141 | _this.googletag.destroySlots([dummyAdSlot]);
142 | }
143 | // Call the rest of the pubads API that's in the queue.
144 | _this._processPubadsQueue();
145 | // listen for GPT events
146 | _this._listen();
147 | // client should be able to set any page-level setting within the event handler.
148 | _this._isReady = true;
149 | _this.emit(_Events2.default.READY, _this.googletag);
150 |
151 | // Call display
152 | instances.forEach(function (instance) {
153 | if (!instance.notInViewport()) {
154 | instance.display();
155 | }
156 | });
157 |
158 | _this.emit(_Events2.default.RENDER, _this.googletag);
159 |
160 | _this._initialRender = false;
161 | });
162 | });
163 | _this.renderAll = (0, _throttleDebounce.debounce)(4, function () {
164 | if (!_this.apiReady) {
165 | return false;
166 | }
167 |
168 | // first instance updates correlator value and re-render each ad
169 | var instances = _this.getMountedInstances();
170 | instances.forEach(function (instance, i) {
171 | if (i === 0) {
172 | _this.updateCorrelator();
173 | }
174 | instance.forceUpdate();
175 | });
176 |
177 | return true;
178 | });
179 |
180 |
181 | if (config.test) {
182 | _this.testMode = config;
183 | }
184 | return _this;
185 | }
186 |
187 | _createClass(AdManager, [{
188 | key: "_processPubadsQueue",
189 | value: function _processPubadsQueue() {
190 | var _this2 = this;
191 |
192 | if (this._pubadsProxyQueue) {
193 | Object.keys(this._pubadsProxyQueue).forEach(function (method) {
194 | if (_this2.googletag && !_this2.googletag.pubadsReady && APIToCallBeforeServiceEnabled.indexOf(method) > -1 || _this2.pubadsReady) {
195 | _this2._pubadsProxyQueue[method].forEach(function (params) {
196 | return _this2.pubadsProxy(params);
197 | });
198 | delete _this2._pubadsProxyQueue[method];
199 | }
200 | });
201 | if (!Object.keys(this._pubadsProxyQueue).length) {
202 | this._pubadsProxyQueue = null;
203 | }
204 | }
205 | }
206 | }, {
207 | key: "_callPubads",
208 | value: function _callPubads(_ref) {
209 | var method = _ref.method,
210 | args = _ref.args,
211 | resolve = _ref.resolve,
212 | reject = _ref.reject;
213 |
214 | if (typeof this.googletag.pubads()[method] !== "function") {
215 | reject(new Error("googletag.pubads does not support " + method + ", please update pubadsAPI"));
216 | } else {
217 | try {
218 | var _googletag$pubads;
219 |
220 | var result = (_googletag$pubads = this.googletag.pubads())[method].apply(_googletag$pubads, _toConsumableArray(args));
221 | resolve(result);
222 | } catch (err) {
223 | reject(err);
224 | }
225 | }
226 | }
227 | }, {
228 | key: "_toggleListener",
229 | value: function _toggleListener(add) {
230 | var _this3 = this;
231 |
232 | ["scroll", "resize"].forEach(function (eventName) {
233 | window[add ? "addEventListener" : "removeEventListener"](eventName, _this3._foldCheck);
234 | });
235 | }
236 | }, {
237 | key: "_getTimer",
238 | value: function _getTimer() {
239 | return Date.now();
240 | }
241 | }, {
242 | key: "_listen",
243 | value: function _listen() {
244 | var _this4 = this;
245 |
246 | if (!this._listening) {
247 | [_Events2.default.SLOT_RENDER_ENDED, _Events2.default.IMPRESSION_VIEWABLE, _Events2.default.SLOT_VISIBILITY_CHANGED, _Events2.default.SLOT_LOADED].forEach(function (eventType) {
248 | ["pubads", "content", "companionAds"].forEach(function (service) {
249 | // there is no API to remove listeners.
250 | _this4.googletag[service]().addEventListener(eventType, _this4._onEvent.bind(_this4, eventType));
251 | });
252 | });
253 | this._listening = true;
254 | }
255 | }
256 | }, {
257 | key: "_onEvent",
258 | value: function _onEvent(eventType, event) {
259 | // fire to the global listeners
260 | if (this.listeners(eventType, true)) {
261 | this.emit(eventType, event);
262 | }
263 | // call event handler props
264 | var instances = this.getMountedInstances();
265 | var slot = event.slot;
266 |
267 | var funcName = "on" + eventType.charAt(0).toUpperCase() + eventType.substr(1);
268 | var instance = instances.filter(function (inst) {
269 | return slot === inst.adSlot;
270 | })[0];
271 | if (instance && instance.props[funcName]) {
272 | instance.props[funcName](event);
273 | }
274 | }
275 | }, {
276 | key: "syncCorrelator",
277 | value: function syncCorrelator() {
278 | var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
279 |
280 | this._syncCorrelator = value;
281 | }
282 | }, {
283 | key: "generateDivId",
284 | value: function generateDivId() {
285 | return "bling-" + ++this._adCnt;
286 | }
287 | }, {
288 | key: "getMountedInstances",
289 | value: function getMountedInstances() {
290 | if (!this.mountedInstances) {
291 | this.mountedInstances = [];
292 | }
293 | return this.mountedInstances;
294 | }
295 | }, {
296 | key: "addInstance",
297 | value: function addInstance(instance) {
298 | var instances = this.getMountedInstances();
299 | var index = instances.indexOf(instance);
300 | if (index === -1) {
301 | // The first instance starts listening for the event.
302 | if (instances.length === 0) {
303 | this._toggleListener(true);
304 | }
305 | this.addMQListener(instance, instance.props);
306 | instances.push(instance);
307 | }
308 | }
309 | }, {
310 | key: "removeInstance",
311 | value: function removeInstance(instance) {
312 | var instances = this.getMountedInstances();
313 | var index = instances.indexOf(instance);
314 | if (index >= 0) {
315 | instances.splice(index, 1);
316 | // The last instance removes listening for the event.
317 | if (instances.length === 0) {
318 | this._toggleListener(false);
319 | }
320 | this.removeMQListener(instance, instance.props);
321 | }
322 | }
323 | }, {
324 | key: "addMQListener",
325 | value: function addMQListener(instance, _ref2) {
326 | var _this5 = this;
327 |
328 | var sizeMapping = _ref2.sizeMapping;
329 |
330 | if (!sizeMapping || !Array.isArray(sizeMapping)) {
331 | return;
332 | }
333 |
334 | sizeMapping.forEach(function (size) {
335 | var viewportWidth = size.viewport && size.viewport[0];
336 | if (viewportWidth !== undefined) {
337 | if (!_this5._mqls) {
338 | _this5._mqls = {};
339 | }
340 | if (!_this5._mqls[viewportWidth]) {
341 | var mql = window.matchMedia("(min-width: " + viewportWidth + "px)");
342 | mql.addListener(_this5._handleMediaQueryChange);
343 | _this5._mqls[viewportWidth] = {
344 | mql: mql,
345 | listeners: []
346 | };
347 | }
348 | if (_this5._mqls[viewportWidth].listeners.indexOf(instance) === -1) {
349 | _this5._mqls[viewportWidth].listeners.push(instance);
350 | }
351 | }
352 | });
353 | }
354 | }, {
355 | key: "removeMQListener",
356 | value: function removeMQListener(instance) {
357 | var _this6 = this;
358 |
359 | if (!this._mqls) {
360 | return;
361 | }
362 |
363 | Object.keys(this._mqls).forEach(function (key) {
364 | var index = _this6._mqls[key].listeners.indexOf(instance);
365 | if (index > -1) {
366 | _this6._mqls[key].listeners.splice(index, 1);
367 | }
368 | if (_this6._mqls[key].listeners.length === 0) {
369 | _this6._mqls[key].mql.removeListener(_this6._handleMediaQueryChange);
370 | delete _this6._mqls[key];
371 | }
372 | });
373 | }
374 | }, {
375 | key: "isInViewport",
376 | value: function isInViewport() {
377 | return _isInViewport3.default.apply(undefined, arguments);
378 | }
379 |
380 | /**
381 | * Refreshes all the ads in the page with a new correlator value.
382 | *
383 | * @param {Array} slots An array of ad slots.
384 | * @param {Object} options You can pass `changeCorrelator` flag.
385 | * @static
386 | */
387 |
388 | }, {
389 | key: "refresh",
390 | value: function refresh(slots, options) {
391 | if (!this.pubadsReady) {
392 | return false;
393 | }
394 |
395 | // gpt already debounces refresh
396 | this.googletag.pubads().refresh(slots, options);
397 |
398 | return true;
399 | }
400 | }, {
401 | key: "clear",
402 | value: function clear(slots) {
403 | if (!this.pubadsReady) {
404 | return false;
405 | }
406 |
407 | this.googletag.pubads().clear(slots);
408 |
409 | return true;
410 | }
411 |
412 | /**
413 | * Re-render(not refresh) all the ads in the page and the first ad will update the correlator value.
414 | * Updating correlator value ensures competitive exclusion.
415 | *
416 | * @method renderAll
417 | * @static
418 | */
419 |
420 | }, {
421 | key: "getGPTVersion",
422 | value: function getGPTVersion() {
423 | if (!this.apiReady) {
424 | return false;
425 | }
426 | return this.googletag.getVersion();
427 | }
428 | }, {
429 | key: "getPubadsVersion",
430 | value: function getPubadsVersion() {
431 | if (!this.pubadsReady) {
432 | return false;
433 | }
434 | return this.googletag.pubads().getVersion();
435 | }
436 | }, {
437 | key: "updateCorrelator",
438 | value: function updateCorrelator() {
439 | if (!this.pubadsReady) {
440 | return false;
441 | }
442 | this.googletag.pubads().updateCorrelator();
443 |
444 | return true;
445 | }
446 | }, {
447 | key: "load",
448 | value: function load(url) {
449 | var _this7 = this;
450 |
451 | return this._loadPromise || (this._loadPromise = new Promise(function (resolve, reject) {
452 | // test mode can't be enabled in production mode
453 | if (_this7.testMode) {
454 | resolve(_this7.googletag);
455 | return;
456 | }
457 | if (!_exenv.canUseDOM) {
458 | reject(new Error("DOM not available"));
459 | return;
460 | }
461 | if (!url) {
462 | reject(new Error("url is missing"));
463 | return;
464 | }
465 | var onLoad = function onLoad() {
466 | if (window.googletag) {
467 | _this7._googletag = window.googletag;
468 | // make sure API is ready for use.
469 | _this7.googletag.cmd.push(function () {
470 | _this7._isLoaded = true;
471 | resolve(_this7.googletag);
472 | });
473 | } else {
474 | reject(new Error("window.googletag is not available"));
475 | }
476 | };
477 | if (window.googletag && window.googletag.apiReady) {
478 | onLoad();
479 | } else {
480 | var script = document.createElement("script");
481 | script.async = true;
482 | script.onload = onLoad;
483 | script.onerror = function () {
484 | reject(new Error("failed to load script"));
485 | };
486 | script.src = url;
487 | document.head.appendChild(script);
488 | }
489 | }));
490 | }
491 | }, {
492 | key: "pubadsProxy",
493 | value: function pubadsProxy(_ref3) {
494 | var _this8 = this;
495 |
496 | var method = _ref3.method,
497 | _ref3$args = _ref3.args,
498 | args = _ref3$args === undefined ? [] : _ref3$args,
499 | resolve = _ref3.resolve,
500 | reject = _ref3.reject;
501 |
502 | if (!resolve) {
503 | // there are couple pubads API which doesn't provide getter methods for later use,
504 | // so remember them here.
505 | if (APIToCallBeforeServiceEnabled.indexOf(method) > -1) {
506 | this["_" + method] = args && args.length && args[0] || true;
507 | }
508 | return new Promise(function (resolve2, reject2) {
509 | var params = {
510 | method: method,
511 | args: args,
512 | resolve: resolve2,
513 | reject: reject2
514 | };
515 | if (!_this8.pubadsReady) {
516 | if (!_this8._pubadsProxyQueue) {
517 | _this8._pubadsProxyQueue = {};
518 | }
519 | if (!_this8._pubadsProxyQueue[method]) {
520 | _this8._pubadsProxyQueue[method] = [];
521 | }
522 | _this8._pubadsProxyQueue[method].push(params);
523 | } else {
524 | _this8._callPubads(params);
525 | }
526 | });
527 | }
528 |
529 | this._callPubads({ method: method, args: args, resolve: resolve, reject: reject });
530 |
531 | return Promise.resolve();
532 | }
533 | }, {
534 | key: "googletag",
535 | get: function get() {
536 | return this._googletag;
537 | }
538 | }, {
539 | key: "isLoaded",
540 | get: function get() {
541 | return !!this._isLoaded;
542 | }
543 | }, {
544 | key: "isReady",
545 | get: function get() {
546 | return !!this._isReady;
547 | }
548 | }, {
549 | key: "apiReady",
550 | get: function get() {
551 | return this.googletag && this.googletag.apiReady;
552 | }
553 | }, {
554 | key: "pubadsReady",
555 | get: function get() {
556 | return this.googletag && this.googletag.pubadsReady;
557 | }
558 | }, {
559 | key: "testMode",
560 | get: function get() {
561 | return this._testMode;
562 | },
563 | set: function set(config) {
564 | if (process.env.NODE_ENV === "production") {
565 | return;
566 | }
567 | var test = config.test,
568 | GPTMock = config.GPTMock;
569 |
570 | this._isLoaded = true;
571 | this._testMode = !!test;
572 |
573 | if (test) {
574 | (0, _invariant2.default)(test && GPTMock, "Must provide GPTMock to enable testMode. config{GPTMock}");
575 | this._googletag = new GPTMock(config);
576 | }
577 | }
578 | }]);
579 |
580 | return AdManager;
581 | }(_eventemitter2.default);
582 |
583 | function createManager(config) {
584 | return new AdManager(config);
585 | }
--------------------------------------------------------------------------------