├── .eslintignore ├── test ├── polyfill.js ├── isInViewport.spec.js ├── mockGPT.spec.js └── createManager.spec.js ├── examples ├── server │ ├── index.js │ ├── routes.js │ └── server.js ├── apps │ ├── routing │ │ ├── styles │ │ │ ├── page.js │ │ │ └── index.js │ │ ├── main.js │ │ ├── home.js │ │ ├── index.html │ │ ├── page.js │ │ └── app.js │ ├── interstitial │ │ ├── main.js │ │ ├── styles │ │ │ └── index.js │ │ ├── index.html │ │ └── app.js │ ├── lazy-render │ │ ├── main.js │ │ ├── index.html │ │ ├── styles │ │ │ └── index.js │ │ └── app.js │ ├── responsive │ │ ├── main.js │ │ ├── styles │ │ │ └── index.js │ │ ├── index.html │ │ ├── button.js │ │ └── app.js │ ├── static-ad │ │ ├── main.js │ │ ├── styles │ │ │ └── index.js │ │ ├── index.html │ │ └── app.js │ ├── single-request │ │ ├── main.js │ │ ├── index.html │ │ ├── button.js │ │ ├── styles │ │ │ └── index.js │ │ └── app.js │ ├── infinite-scrolling │ │ ├── main.js │ │ ├── index.html │ │ ├── styles │ │ │ ├── content.js │ │ │ └── index.js │ │ ├── app.js │ │ └── content.js │ ├── global.css │ ├── index.html │ └── log.js ├── webpack.config.js └── webpack.config.server.js ├── scripts ├── empty.html └── updateAPIList.js ├── src ├── index.js ├── utils │ ├── createManagerTest.js │ ├── filterProps.js │ ├── isInViewport.js │ ├── apiList.js │ └── mockGPT.js ├── Events.js └── createManager.js ├── .gitignore ├── docs ├── api │ ├── README.md │ └── ReactGPT.md ├── README.md ├── GettingStarted.md └── Guides.md ├── .editorconfig ├── .babelrc ├── lib ├── Events.js ├── utils │ ├── filterProps.js │ ├── createManagerTest.js │ ├── isInViewport.js │ ├── apiList.js │ └── mockGPT.js ├── index.js └── createManager.js ├── karma.conf.ci.js ├── .travis.yml ├── .eslintrc ├── webpack.config.js ├── LICENSE ├── CONTRIBUTING.md ├── karma.conf.js ├── CHANGELOG.md ├── README.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | /scripts 2 | -------------------------------------------------------------------------------- /test/polyfill.js: -------------------------------------------------------------------------------- 1 | import "core-js/fn/promise"; 2 | -------------------------------------------------------------------------------- /examples/server/index.js: -------------------------------------------------------------------------------- 1 | require("babel-register"); 2 | require("./server"); 3 | -------------------------------------------------------------------------------- /scripts/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default as Bling} from "./Bling"; 2 | export {default as Events} from "./Events"; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | es/ 2 | node_modules/ 3 | dist/ 4 | coverage/ 5 | npm-debug.log 6 | yarn-error.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /examples/apps/routing/styles/page.js: -------------------------------------------------------------------------------- 1 | export default { 2 | container: { 3 | display: "flex" 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ### React GPT Exports 4 | 5 | * [Bling](/docs/api/ReactGPT.md#Bling) 6 | * [Events](/docs/api/ReactGPT.md#Events) 7 | 8 | -------------------------------------------------------------------------------- /examples/apps/routing/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/interstitial/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/lazy-render/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/responsive/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/static-ad/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/interstitial/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /examples/apps/single-request/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/infinite-scrolling/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app"; 4 | 5 | ReactDOM.render(, document.getElementById("example")); 6 | -------------------------------------------------------------------------------- /examples/apps/routing/home.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | 3 | class Home extends Component { 4 | render() { 5 | return

Home

; 6 | } 7 | } 8 | 9 | export default Home; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Getting Started](/docs/GettingStarted.md) 4 | * [API](/docs/api) 5 | * [Guides](/docs/Guides.md) 6 | 7 | **In this documentation, `GPT` refers to Google Publisher Tags and `React GPT` refers to the npm package this repository offers.** 8 | -------------------------------------------------------------------------------- /src/utils/createManagerTest.js: -------------------------------------------------------------------------------- 1 | import {createManager} from "../createManager.js"; 2 | import {GPTMock} from "./mockGPT"; 3 | 4 | export function createManagerTest(config) { 5 | return createManager({ 6 | ...config, 7 | test: true, 8 | GPTMock 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/Events.js: -------------------------------------------------------------------------------- 1 | const Events = { 2 | READY: "ready", 3 | RENDER: "render", 4 | SLOT_RENDER_ENDED: "slotRenderEnded", 5 | IMPRESSION_VIEWABLE: "impressionViewable", 6 | SLOT_VISIBILITY_CHANGED: "slotVisibilityChanged", 7 | SLOT_LOADED: "slotOnload" 8 | }; 9 | 10 | export default Events; 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-without-strict", "stage-0", "react"], 3 | "plugins": [ 4 | "transform-decorators-legacy" 5 | ], 6 | "env": { 7 | "examples": { 8 | "plugins": [ 9 | ["babel-plugin-webpack-alias", {"config": "./examples/webpack.config.server.js"}] 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/apps/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | h1, h2, h3 { 7 | font-weight: 100; 8 | } 9 | 10 | a { 11 | color: hsl(150, 50%, 50%); 12 | } 13 | 14 | a.active { 15 | color: hsl(40, 50%, 50%); 16 | } 17 | 18 | .breadcrumbs a { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /examples/apps/responsive/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | }, 7 | adBorder: { 8 | padding: 10, 9 | border: "1px dashed #666" 10 | }, 11 | lb: { 12 | position: "relative" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /examples/apps/routing/index.html: -------------------------------------------------------------------------------- 1 | 2 | Router Example 3 | 4 | 5 |

React GPT Examples / Router Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/apps/static-ad/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | }, 7 | adBorder: { 8 | padding: 10, 9 | border: "1px dashed #666" 10 | }, 11 | lb: { 12 | position: "relative" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /examples/apps/static-ad/index.html: -------------------------------------------------------------------------------- 1 | 2 | Static Ad Example 3 | 4 | 5 |

React GPT Examples / Static Ad Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/apps/responsive/index.html: -------------------------------------------------------------------------------- 1 | 2 | Responsive Example 3 | 4 | 5 |

React GPT Examples / Responsive Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/apps/lazy-render/index.html: -------------------------------------------------------------------------------- 1 | 2 | Lazy Render Example 3 | 4 | 5 |

React GPT Examples / Lazy Render Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/apps/interstitial/index.html: -------------------------------------------------------------------------------- 1 | 2 | Interstitial Example 3 | 4 | 5 |

React GPT Examples / Interstitial Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/apps/single-request/index.html: -------------------------------------------------------------------------------- 1 | 2 | Single Request Example 3 | 4 | 5 |

React GPT Examples / Single Request Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/Events.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | var Events = { 5 | READY: "ready", 6 | RENDER: "render", 7 | SLOT_RENDER_ENDED: "slotRenderEnded", 8 | IMPRESSION_VIEWABLE: "impressionViewable", 9 | SLOT_VISIBILITY_CHANGED: "slotVisibilityChanged", 10 | SLOT_LOADED: "slotOnload" 11 | }; 12 | 13 | exports.default = Events; -------------------------------------------------------------------------------- /examples/apps/infinite-scrolling/index.html: -------------------------------------------------------------------------------- 1 | 2 | Infinite Scrolling Example 3 | 4 | 5 |

React GPT Examples / Infinite Scrolling Example

6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/filterProps.js: -------------------------------------------------------------------------------- 1 | export default function filterProps(propKeys, props, nextProps) { 2 | return propKeys.reduce( 3 | (filtered, key) => { 4 | filtered.props[key] = props[key]; 5 | filtered.nextProps[key] = nextProps[key]; 6 | return filtered; 7 | }, 8 | { 9 | props: {}, 10 | nextProps: {} 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/apps/routing/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | }, 7 | dummyHeight: { 8 | width: 1, 9 | height: 2000 10 | }, 11 | container: { 12 | marginTop: 20, 13 | position: "relative" 14 | }, 15 | topAd: { 16 | margin: "0 auto" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/utils/filterProps.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | exports.default = filterProps; 5 | function filterProps(propKeys, props, nextProps) { 6 | return propKeys.reduce(function (filtered, key) { 7 | filtered.props[key] = props[key]; 8 | filtered.nextProps[key] = nextProps[key]; 9 | return filtered; 10 | }, { 11 | props: {}, 12 | nextProps: {} 13 | }); 14 | } -------------------------------------------------------------------------------- /karma.conf.ci.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | module.exports = function (config) { 3 | config.set({ 4 | singleRun: true 5 | }); 6 | 7 | console.log("running default test on Chrome"); 8 | config.set({ 9 | customLaunchers: { 10 | Chrome_CI: { 11 | base: "Chrome", 12 | flags: ["--no-sandbox"] 13 | } 14 | }, 15 | browsers: ["Chrome_CI"] 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "node" 5 | cache: 6 | directories: 7 | - node_modules 8 | matrix: 9 | fast_finish: true 10 | include: 11 | - node_js: stable 12 | before_install: 13 | - export CHROME_BIN=chromium-browser 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | script: 17 | - npm test 18 | - npm run bundlesize 19 | after_success: 20 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 21 | -------------------------------------------------------------------------------- /examples/apps/infinite-scrolling/styles/content.js: -------------------------------------------------------------------------------- 1 | export default { 2 | main: { 3 | flex: "1 1 auto", 4 | padding: 5 5 | }, 6 | mr: { 7 | float: "right", 8 | margin: 15 9 | }, 10 | title: { 11 | display: "block", 12 | marginBottom: 10, 13 | fontSize: 24, 14 | fontWeight: "bold" 15 | }, 16 | description: { 17 | display: "block", 18 | marginBottom: 10, 19 | fontSize: 20 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /examples/apps/infinite-scrolling/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | }, 7 | container: { 8 | marginTop: 20, 9 | position: "relative" 10 | }, 11 | main: { 12 | display: "flex", 13 | flexDirection: "column" 14 | }, 15 | adBorder: { 16 | padding: 10, 17 | border: "1px dashed #666" 18 | }, 19 | lb: { 20 | position: "relative" 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/apps/index.html: -------------------------------------------------------------------------------- 1 | 2 | React GPT Examples 3 | 4 | 5 |

React GPT Examples

6 | 15 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | 5 | var _Bling = require("./Bling"); 6 | 7 | Object.defineProperty(exports, "Bling", { 8 | enumerable: true, 9 | get: function get() { 10 | return _interopRequireDefault(_Bling).default; 11 | } 12 | }); 13 | 14 | var _Events = require("./Events"); 15 | 16 | Object.defineProperty(exports, "Events", { 17 | enumerable: true, 18 | get: function get() { 19 | return _interopRequireDefault(_Events).default; 20 | } 21 | }); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /test/isInViewport.spec.js: -------------------------------------------------------------------------------- 1 | import isInViewport from "../src/utils/isInViewport"; 2 | 3 | describe("isInViewport", () => { 4 | it("returns false when an element is invalid", () => { 5 | let result = isInViewport(); 6 | expect(result).to.be.false; 7 | const textNode = document.createTextNode("text"); 8 | result = isInViewport(textNode); 9 | expect(result).to.be.false; 10 | }); 11 | 12 | it("checks intersection with viewport", () => { 13 | const el = document.createElement("div"); 14 | document.body.appendChild(el); 15 | const result = isInViewport(el, [0, 0], 0); 16 | expect(result).to.be.true; 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/utils/createManagerTest.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | 5 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 6 | 7 | exports.createManagerTest = createManagerTest; 8 | 9 | var _createManager = require("../createManager.js"); 10 | 11 | var _mockGPT = require("./mockGPT"); 12 | 13 | function createManagerTest(config) { 14 | return (0, _createManager.createManager)(_extends({}, config, { 15 | test: true, 16 | GPTMock: _mockGPT.GPTMock 17 | })); 18 | } -------------------------------------------------------------------------------- /examples/apps/responsive/button.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import PropTypes from "prop-types"; 3 | import styles from "./styles"; 4 | 5 | export default class Button extends Component { 6 | static propTypes = { 7 | children: PropTypes.node, 8 | onClick: PropTypes.func.isRequired, 9 | params: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) 10 | .isRequired 11 | }; 12 | onClick = () => { 13 | this.props.onClick(this.props.params); 14 | }; 15 | render() { 16 | return ( 17 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/apps/single-request/button.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import PropTypes from "prop-types"; 3 | import styles from "./styles"; 4 | 5 | export default class Button extends Component { 6 | static propTypes = { 7 | children: PropTypes.node, 8 | onClick: PropTypes.func.isRequired, 9 | params: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) 10 | .isRequired 11 | }; 12 | onClick = () => { 13 | this.props.onClick(this.props.params); 14 | }; 15 | render() { 16 | return ( 17 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/apps/single-request/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | }, 7 | container: { 8 | marginTop: 20, 9 | position: "relative" 10 | }, 11 | main: { 12 | display: "flex", 13 | flexDirection: "row", 14 | "@media (max-width: 768px)": { 15 | flexDirection: "column" 16 | } 17 | }, 18 | adBorder: { 19 | padding: 10, 20 | border: "1px dashed #666" 21 | }, 22 | lb: { 23 | position: "relative" 24 | }, 25 | mr: { 26 | position: "relative", 27 | order: 1, 28 | flex: "1 1 auto" 29 | }, 30 | ws: { 31 | position: "relative", 32 | order: 2, 33 | flex: "0 1 auto" 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-nfl/base", 4 | "eslint-config-nfl/rules/strict", 5 | "eslint-config-nfl/rules/react" 6 | ], 7 | "plugins": [ 8 | "import", 9 | "prettier", 10 | "react" 11 | ], 12 | "rules": { 13 | "prettier/prettier": ["error", { 14 | "printWidth": 80, 15 | "tabWidth": 4, 16 | "singleQuote": false, 17 | "trailingComma": "none", 18 | "bracketSpacing": false, 19 | "semi": true, 20 | "useTabs": false, 21 | "parser": "babylon", 22 | "jsxBracketSameLine": false 23 | }] 24 | }, 25 | "env": { 26 | "browser": true, 27 | "mocha": true 28 | }, 29 | "globals": { 30 | "expect": true, 31 | "sinon": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/isInViewport.js: -------------------------------------------------------------------------------- 1 | export default function isInViewport(el, [width, height] = [0, 0], offset = 0) { 2 | if (!el || el.nodeType !== 1) { 3 | return false; 4 | } 5 | const clientRect = el.getBoundingClientRect(); 6 | const rect = { 7 | top: clientRect.top, 8 | left: clientRect.left, 9 | bottom: clientRect.bottom, 10 | right: clientRect.right 11 | }; 12 | const viewport = { 13 | top: 0, 14 | left: 0, 15 | bottom: window.innerHeight, 16 | right: window.innerWidth 17 | }; 18 | const inViewport = 19 | rect.bottom >= viewport.top + height * offset && 20 | rect.right >= viewport.left + width * offset && 21 | rect.top <= viewport.bottom - height * offset && 22 | rect.left <= viewport.right - width * offset; 23 | return inViewport; 24 | } 25 | -------------------------------------------------------------------------------- /examples/apps/lazy-render/styles/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: { 3 | marginTop: 20, 4 | padding: "2px 6px 3px", 5 | border: "2px outset buttonface" 6 | }, 7 | container: { 8 | marginTop: 20, 9 | position: "relative" 10 | }, 11 | content: { 12 | display: "inline-block", 13 | width: 1500, 14 | height: 1500, 15 | backgroundColor: "#ffffcc" 16 | }, 17 | hWrap: { 18 | whiteSpace: "nowrap" 19 | }, 20 | adBorder: { 21 | padding: 10, 22 | border: "1px dashed #666", 23 | width: "100%", 24 | height: "100%" 25 | }, 26 | lb: { 27 | position: "relative", 28 | display: "inline-block", 29 | width: 728, 30 | height: 90, 31 | verticalAlign: "top" 32 | }, 33 | mr: { 34 | position: "relative", 35 | width: 300, 36 | height: 250 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /examples/apps/routing/page.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | import React, {Component} from "react"; 3 | import PropTypes from "prop-types"; 4 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved 5 | import styles from "./styles/page"; 6 | 7 | class Page extends Component { 8 | static propTypes = { 9 | adUnitPath: PropTypes.string, 10 | params: PropTypes.object 11 | }; 12 | 13 | render() { 14 | const {adUnitPath, params} = this.props; 15 | return ( 16 |
17 |

Page: {params.id}

18 |
19 | 24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | export default Page; 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var env = process.env.NODE_ENV; 3 | 4 | var reactExternal = { 5 | root: "React", 6 | commonjs2: "react", 7 | commonjs: "react", 8 | amd: "react" 9 | }; 10 | 11 | var reactDomExternal = { 12 | root: "ReactDOM", 13 | commonjs2: "react-dom", 14 | commonjs: "react-dom", 15 | amd: "react-dom" 16 | }; 17 | 18 | var config = { 19 | externals: { 20 | "react": reactExternal, 21 | "react-dom": reactDomExternal 22 | }, 23 | module: { 24 | loaders: [ 25 | {test: /\.js$/, loaders: ["babel-loader"], exclude: /node_modules/} 26 | ] 27 | }, 28 | output: { 29 | library: "ReactGPT", 30 | libraryTarget: "umd" 31 | }, 32 | plugins: [ 33 | new webpack.optimize.OccurenceOrderPlugin(), 34 | new webpack.DefinePlugin({ 35 | "process.env.NODE_ENV": JSON.stringify(env) 36 | }) 37 | ] 38 | }; 39 | 40 | module.exports = config; 41 | -------------------------------------------------------------------------------- /examples/server/routes.js: -------------------------------------------------------------------------------- 1 | import InfiniteScrollApp from "../apps/infinite-scrolling/app"; 2 | import LazyRenderApp from "../apps/lazy-render/app"; 3 | import ResponsiveApp from "../apps/responsive/app"; 4 | import RoutingApp from "../apps/routing/app"; 5 | import SingleRequestApp from "../apps/single-request/app"; 6 | import StaticAdApp from "../apps/static-ad/app"; 7 | import InterstitialApp from "../apps/interstitial/app"; 8 | 9 | export default { 10 | "infinite-scrolling": { 11 | title: "Infinite Scrolling Example", 12 | app: InfiniteScrollApp 13 | }, 14 | "lazy-render": {title: "Lazy Render Example", app: LazyRenderApp}, 15 | responsive: {title: "Responsive Example", app: ResponsiveApp}, 16 | routing: {title: "Routing Example", app: RoutingApp}, 17 | "single-request": {title: "Single Request Example", app: SingleRequestApp}, 18 | "static-ad": {title: "Static Ad Example", app: StaticAdApp}, 19 | interstitial: {title: "Interstitial Example", app: InterstitialApp} 20 | }; 21 | -------------------------------------------------------------------------------- /examples/apps/interstitial/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved 3 | import "../log"; 4 | import styles from "./styles"; 5 | 6 | class App extends Component { 7 | state = { 8 | adUnitPath: "/4595/nfl.test.open/page/A" 9 | }; 10 | 11 | onClick = () => { 12 | this.setState({ 13 | adUnitPath: 14 | this.state.adUnitPath.indexOf("B") > -1 15 | ? "/4595/nfl.test.open/page/A" 16 | : "/4595/nfl.test.open/page/B" 17 | }); 18 | }; 19 | 20 | render() { 21 | const {adUnitPath} = this.state; 22 | return ( 23 |
24 | 27 | 28 |
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 |
23 | 26 |
27 | `} 30 | slotSize={[728, 90]} 31 | style={styles.adBorder} 32 | /> 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | var webpack = require("webpack"); 5 | 6 | var appDir = __dirname + "/apps"; 7 | 8 | module.exports = { 9 | devtool: "inline-source-map", 10 | 11 | entry: fs.readdirSync(appDir).reduce(function (entries, dir) { 12 | if (fs.statSync(path.join(appDir, dir)).isDirectory()) { 13 | entries[dir] = ["core-js/fn/promise", path.join(appDir, dir, "main.js")]; 14 | } 15 | 16 | return entries; 17 | }, {}), 18 | 19 | output: { 20 | path: __dirname + "/__build__", 21 | filename: "[name].js", 22 | chunkFilename: "[id].chunk.js", 23 | publicPath: "/__build__/" 24 | }, 25 | 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.jsx?$/, 30 | exclude: /node_modules/, 31 | loader: "babel", 32 | query: { 33 | presets: ["es2015-without-strict", "stage-0", "react"], 34 | plugins: ["transform-decorators-legacy"] 35 | } 36 | } 37 | ] 38 | }, 39 | 40 | resolve: { 41 | alias: { 42 | "react-gpt": process.cwd() + "/src" 43 | } 44 | }, 45 | 46 | cache: false, 47 | 48 | plugins: [ 49 | new webpack.optimize.CommonsChunkPlugin("shared.js"), 50 | new webpack.DefinePlugin({ 51 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development") 52 | }) 53 | ] 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /examples/apps/lazy-render/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 | Gpt.configure({viewableThreshold: 0}); 8 | 9 | @Radium 10 | class App extends Component { 11 | onClick = () => { 12 | Gpt.render(); 13 | }; 14 | render() { 15 | return ( 16 |
17 | 20 |
21 |
22 |
23 | 29 |
30 |
31 |
32 | 39 |
40 |
41 | ); 42 | } 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /examples/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | var webpack = require("webpack"); 5 | 6 | var appDir = __dirname + "/apps"; 7 | 8 | module.exports = { 9 | devtool: "inline-source-map", 10 | 11 | entry: fs.readdirSync(appDir).reduce(function (entries, dir) { 12 | if (fs.statSync(path.join(appDir, dir)).isDirectory()) { 13 | entries[dir] = ["core-js/fn/promise", path.join(appDir, dir, "main.js")]; 14 | } 15 | 16 | return entries; 17 | }, {}), 18 | 19 | output: { 20 | path: __dirname + "/__build__", 21 | filename: "[name].js", 22 | chunkFilename: "[id].chunk.js", 23 | publicPath: "/__build__/" 24 | }, 25 | 26 | plugins: [ 27 | new webpack.optimize.CommonsChunkPlugin("shared.js"), 28 | new webpack.optimize.OccurenceOrderPlugin(), 29 | new webpack.DefinePlugin({ 30 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production") 31 | }) 32 | ], 33 | 34 | module: { 35 | loaders: [ 36 | { 37 | test: /\.js$/, 38 | loader: "babel", 39 | exclude: /node_modules/, 40 | include: __dirname, 41 | query: { 42 | presets: ["es2015-without-strict", "stage-0", "react"], 43 | plugins: ["transform-decorators-legacy"] 44 | } 45 | } 46 | ] 47 | }, 48 | 49 | resolve: { 50 | alias: { 51 | "react-gpt": process.cwd() + "/lib" 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /lib/utils/isInViewport.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | 5 | 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"); } }; }(); 6 | 7 | exports.default = isInViewport; 8 | function isInViewport(el) { 9 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 0], 10 | _ref2 = _slicedToArray(_ref, 2), 11 | width = _ref2[0], 12 | height = _ref2[1]; 13 | 14 | var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; 15 | 16 | if (!el || el.nodeType !== 1) { 17 | return false; 18 | } 19 | var clientRect = el.getBoundingClientRect(); 20 | var rect = { 21 | top: clientRect.top, 22 | left: clientRect.left, 23 | bottom: clientRect.bottom, 24 | right: clientRect.right 25 | }; 26 | var viewport = { 27 | top: 0, 28 | left: 0, 29 | bottom: window.innerHeight, 30 | right: window.innerWidth 31 | }; 32 | var inViewport = rect.bottom >= viewport.top + height * offset && rect.right >= viewport.left + width * offset && rect.top <= viewport.bottom - height * offset && rect.left <= viewport.right - width * offset; 33 | return inViewport; 34 | } -------------------------------------------------------------------------------- /examples/apps/log.js: -------------------------------------------------------------------------------- 1 | import {Bling as GPT, Events} from "react-gpt"; // eslint-disable-line import/no-unresolved 2 | 3 | GPT.on(Events.SLOT_RENDER_ENDED, event => { 4 | const slot = event.slot; 5 | const divId = slot.getSlotElementId(); 6 | const targetingKeys = slot.getTargetingKeys(); 7 | const targeting = targetingKeys.reduce((t, key) => { 8 | const val = slot.getTargeting(key); 9 | t[key] = val.length === 1 ? val[0] : val; 10 | return t; 11 | }, {}); 12 | 13 | if (!event.isEmpty && event.size) { 14 | console.log( 15 | `ad creative '${ 16 | event.creativeId 17 | }' is rendered to slot '${divId}' of size '${event.size[0]}x${ 18 | event.size[1] 19 | }'`, 20 | event, 21 | targeting 22 | ); 23 | } else { 24 | console.log( 25 | `ad rendered but empty, div id is ${divId}`, 26 | event, 27 | targeting 28 | ); 29 | } 30 | }); 31 | 32 | // Turn on these logs when checking these events. 33 | /* GPT.on(Events.IMPRESSION_VIEWABLE, event => { 34 | const slot = event.slot; 35 | const divId = slot.getSlotElementId(); 36 | const sizes = slot.getSizes(); 37 | console.log(`IMPRESSION_VIEWABLE for ${divId}(${JSON.stringify(sizes)})`, event); 38 | }); 39 | 40 | GPT.on(Events.SLOT_VISIBILITY_CHANGED, event => { 41 | const slot = event.slot; 42 | const divId = slot.getSlotElementId(); 43 | const sizes = slot.getSizes(); 44 | console.log(`SLOT_VISIBILITY_CHANGED for ${divId}(${JSON.stringify(sizes)}) to ${event.inViewPercentage}`, event); 45 | }); 46 | 47 | GPT.on(Events.SLOT_LOADED, event => { 48 | const slot = event.slot; 49 | const divId = slot.getSlotElementId(); 50 | const sizes = slot.getSizes(); 51 | console.log(`SLOT_LOADED for ${divId}(${JSON.stringify(sizes)})`, event); 52 | });*/ 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | _**Please Note:** These guidelines are adapted from [@necolas](https://github.com/necolas)'s 7 | [issue-guidelines](https://github.com/necolas/issue-guidelines) and serve as 8 | an excellent starting point for contributing to any open source project._ 9 | 10 | 11 | ## Pull requests 12 | 13 | Good pull requests - patches, improvements, new features - are a fantastic 14 | help. They should remain focused in scope and avoid containing unrelated 15 | commits. 16 | 17 | **Please ask first** before embarking on any significant pull request (e.g. 18 | implementing features, refactoring code, porting to a different language), 19 | otherwise you risk spending a lot of time working on something that the 20 | project's developers might not want to merge into the project. 21 | 22 | 23 | ## Development Process 24 | Here are some guidelines to making changes and preparing your PR: 25 | 26 | 1. Make your proposed changes to the repository, along with updating/adding test cases. 27 | 2. (Optional) If you prefer to also test your changes in a real application, you can do the following: 28 | 1. Run `npm link` in `react-gpt` repository. 29 | 2. `cd` to your favorite React application, run `npm link react-gpt` to point to your local repository. 30 | 3. Run your application to verify your changes. 31 | 3. Run `npm test` to verify all test cases pass. 32 | 4. Run `npm run lint` to verify there are no linting errors. 33 | 34 | 35 | ## Travis CI Build 36 | Travis CI build will test your PR before it is merged. Browser testing may not run on Travis for PR, so please test your PR with supported browsers locally before submitting PR. 37 | 38 | 39 | ## Contributor License Agreement (CLA) 40 | 41 | In order for your pull requests to be accepted, you must accept the [NFL Indivudal Contributor License Agreement](https://cla.nfl.com/agreements/nfl/react-gpt). 42 | 43 | Corporate contributors can email engineers@nfl.com and request the **Corporate CLA** which can be signed digitally. 44 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | module.exports = function (config) { 3 | config.set({ 4 | basePath: "", 5 | 6 | browserNoActivityTimeout: 60000, 7 | 8 | client: { 9 | mocha: { 10 | reporter: "html" 11 | } 12 | }, 13 | 14 | frameworks: [ 15 | "chai-sinon", 16 | "mocha" 17 | ], 18 | 19 | files: [ 20 | "test/polyfill.js", 21 | "test/**/*spec.js" 22 | ], 23 | 24 | preprocessors: { 25 | "test/polyfill.js": ["webpack"], 26 | "test/**/*spec.js": ["webpack", "sourcemap"] 27 | }, 28 | 29 | coverageReporter: { 30 | reporters: [{ 31 | type: "html", 32 | subdir: "html" 33 | }, { 34 | type: "text" 35 | }, { 36 | type: "lcovonly", 37 | subdir: "." 38 | }] 39 | }, 40 | 41 | webpack: { 42 | devtool: "inline-source-map", 43 | module: { 44 | loaders: [{ 45 | test: /\.js$/, 46 | exclude: /node_modules/, 47 | loader: "babel" 48 | }, { 49 | test: /\.js$/, 50 | // exclude this dirs from coverage 51 | exclude: /(test|node_modules)\//, 52 | loader: "isparta" 53 | }] 54 | }, 55 | resolve: { 56 | extensions: ["", ".js"] 57 | }, 58 | plugins: [ 59 | new webpack.DefinePlugin({ 60 | "process.env.NODE_ENV": JSON.stringify("test") 61 | }) 62 | ], 63 | watch: true 64 | }, 65 | 66 | webpackServer: { 67 | noInfo: true 68 | }, 69 | 70 | reporters: [ 71 | "mocha", 72 | "coverage" 73 | ], 74 | 75 | port: 9876, 76 | 77 | colors: true, 78 | 79 | logLevel: config.LOG_INFO, 80 | 81 | autoWatch: true, 82 | 83 | browsers: ["Chrome"], 84 | 85 | captureTimeout: 60000, 86 | 87 | singleRun: false 88 | }); 89 | 90 | if (process.env.CI) { 91 | require("./karma.conf.ci.js")(config); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /test/mockGPT.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | GPTMock, 3 | SlotMock, 4 | SizeMappingBuilderMock, 5 | PubAdsServiceMock, 6 | CompanionAdsServiceMock, 7 | ContentServiceMock 8 | } from "../src/utils/mockGPT"; 9 | import {gptVersion} from "../src/utils/apiList"; 10 | 11 | describe("mockGPT", () => { 12 | let gptMock; 13 | 14 | beforeEach(() => { 15 | gptMock = new GPTMock(); 16 | }); 17 | 18 | it("returns version from getVersion()", () => { 19 | expect(gptMock.getVersion()).to.equal(gptVersion); 20 | }); 21 | 22 | it("returns sizeMappingBuilder from sizeMapping()", () => { 23 | const sizeMappingBuilder = gptMock.sizeMapping(); 24 | expect(sizeMappingBuilder).to.be.an.instanceof(SizeMappingBuilderMock); 25 | const sizeMappingBuilder2 = sizeMappingBuilder 26 | .addSize([1024, 768], [970, 250]) 27 | .addSize([980, 690], [728, 90]) 28 | .addSize([640, 480], "fluid"); 29 | expect(sizeMappingBuilder).to.equal(sizeMappingBuilder2); 30 | const mapping = sizeMappingBuilder2.build(); 31 | expect(mapping).to.eql([ 32 | [[1024, 768], [970, 250]], 33 | [[980, 690], [728, 90]], 34 | [[640, 480], "fluid"] 35 | ]); 36 | }); 37 | 38 | it("returns pubAdsService from pubads()", () => { 39 | const pubAdsService = gptMock.pubads(); 40 | expect(pubAdsService).to.be.an.instanceof(PubAdsServiceMock); 41 | }); 42 | 43 | it("returns companionAdsService from companionAds()", () => { 44 | const companionAdsService = gptMock.companionAds(); 45 | expect(companionAdsService).to.be.an.instanceof( 46 | CompanionAdsServiceMock 47 | ); 48 | }); 49 | 50 | it("returns contentService from content()", () => { 51 | const contentService = gptMock.content(); 52 | expect(contentService).to.be.an.instanceof(ContentServiceMock); 53 | }); 54 | 55 | it("returns slot from defineSlot()", () => { 56 | const adUnitPath = "/1234/abc"; 57 | const size = [728, 90]; 58 | const divId = "div-1"; 59 | const slot = gptMock.defineSlot(adUnitPath, size, divId); 60 | expect(slot).to.be.an.instanceof(SlotMock); 61 | expect(slot.getSlotElementId()).to.equal(divId); 62 | expect(slot.getSizes()).to.equal(size); 63 | expect(slot.getAdUnitPath()).to.equal(adUnitPath); 64 | 65 | const adUnitPath2 = "/1234/def"; 66 | const size2 = [300, 250]; 67 | const divId2 = "div-2"; 68 | const slot2 = gptMock.defineSlot(adUnitPath2, size2, divId2); 69 | 70 | const pubAdsService = gptMock.pubads(); 71 | expect(pubAdsService.getSlots()).to.eql([slot, slot2]); 72 | }); 73 | 74 | it("executes callback from cmd()", () => { 75 | const spy = sinon.spy(); 76 | gptMock.cmd.push(spy); 77 | expect(spy.called).to.be.true; 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /examples/apps/responsive/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | import React, {Component} from "react"; 3 | import Radium from "radium"; 4 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved 5 | import "../log"; 6 | import Button from "./button"; 7 | import styles from "./styles"; 8 | 9 | @Radium 10 | class App extends Component { 11 | state = { 12 | adUnitPath: "/4595/nfl.test.open", 13 | targeting: { 14 | test: "responsive" 15 | }, 16 | sizeMapping: [ 17 | {viewport: [0, 0], slot: [1, 1]}, 18 | {viewport: [340, 0], slot: [320, 50]}, 19 | {viewport: [750, 200], slot: [728, 90]}, 20 | {viewport: [1050, 200], slot: [1024, 120]} 21 | ], 22 | style: styles.adBorder 23 | }; 24 | 25 | onClick = params => { 26 | if (params === "refresh") { 27 | Gpt.refresh(); 28 | return; 29 | } 30 | let newState; 31 | if (params === "adUnitPath") { 32 | newState = { 33 | adUnitPath: 34 | this.state.adUnitPath === "/4595/nfl.test.open" 35 | ? "/4595/nfl.test.open/new" 36 | : "/4595/nfl.test.open" 37 | }; 38 | } else if (params === "targeting") { 39 | newState = { 40 | targeting: { 41 | test: "responsive", 42 | changed: Date.now() 43 | } 44 | }; 45 | } else if (params === "size") { 46 | newState = { 47 | sizeMapping: 48 | this.state.sizeMapping[1].slot[1] === 50 49 | ? [ 50 | {viewport: [0, 0], slot: [1, 1]}, 51 | {viewport: [340, 0], slot: [300, 250]}, 52 | {viewport: [750, 200], slot: [728, 90]}, 53 | {viewport: [1050, 200], slot: [1024, 120]} 54 | ] 55 | : [ 56 | {viewport: [0, 0], slot: [1, 1]}, 57 | {viewport: [340, 0], slot: [320, 50]}, 58 | {viewport: [750, 200], slot: [728, 90]}, 59 | {viewport: [1050, 200], slot: [1024, 120]} 60 | ] 61 | }; 62 | } 63 | this.setState(newState); 64 | }; 65 | 66 | render() { 67 | return ( 68 |
69 | 72 | 75 | 78 | 81 |
82 | 83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /examples/apps/single-request/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {StyleRoot} from "radium"; 3 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved 4 | import {canUseDOM} from "fbjs/lib/ExecutionEnvironment"; 5 | import querystring from "querystring"; 6 | import "../log"; 7 | import Button from "./button"; 8 | import styles from "./styles"; 9 | 10 | const qs = canUseDOM 11 | ? querystring.decode(window.location.search.substr(1)) 12 | : {}; 13 | 14 | Gpt.enableSingleRequest().then(value => { 15 | console.log("value", value); 16 | }); 17 | if (qs.mode === "disableInitialLoad") { 18 | Gpt.disableInitialLoad(); 19 | } 20 | 21 | class App extends Component { 22 | state = { 23 | adUnitPath: "/4595/nfl.test.open", 24 | targeting: { 25 | test: "responsive" 26 | } 27 | }; 28 | onClick = params => { 29 | if (params === "refresh") { 30 | Gpt.refresh(); 31 | } else if (params === "disableInitialLoad") { 32 | window.location.href = `${window.location.pathname}?mode=${params}`; 33 | } else if (params === "adUnitPath") { 34 | this.setState({ 35 | adUnitPath: "/4595/nfl.test.open/new" 36 | }); 37 | } else if (params === "targeting") { 38 | this.setState({ 39 | targeting: { 40 | test: "responsive", 41 | changed: Date.now() 42 | } 43 | }); 44 | } 45 | }; 46 | render() { 47 | const {adUnitPath, targeting} = this.state; 48 | return ( 49 | 50 | 53 | 56 | 59 | 62 |
63 | 69 |
70 |
71 |
72 | 77 |
78 |
79 | 84 |
85 |
86 |
87 | ); 88 | } 89 | } 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /examples/apps/infinite-scrolling/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/sort-comp */ 2 | import React, {Component} from "react"; 3 | import Radium from "radium"; 4 | import debounce from "throttle-debounce/debounce"; 5 | import {Bling as Gpt, Events} from "react-gpt"; // eslint-disable-line import/no-unresolved 6 | import "../log"; 7 | import Content from "./content"; 8 | import styles from "./styles"; 9 | 10 | Gpt.syncCorrelator(); 11 | Gpt.enableSingleRequest(); 12 | Gpt.disableInitialLoad(); 13 | 14 | @Radium 15 | class App extends Component { 16 | state = { 17 | page: 1, 18 | size: [728, 90] 19 | }; 20 | time = 0; 21 | componentDidMount() { 22 | window.addEventListener("scroll", this.onScroll); 23 | window.addEventListener("resize", this.onScroll); 24 | this.onScroll(); 25 | this.startTimer(); 26 | Gpt.on(Events.RENDER, () => { 27 | let changeCorrelator = false; 28 | if (this.time >= 30) { 29 | changeCorrelator = true; 30 | this.startTimer(); 31 | } 32 | Gpt.refresh(null, {changeCorrelator}); 33 | }); 34 | } 35 | componentDidUpdate() { 36 | Gpt.refresh(); 37 | } 38 | componentWillUnmount() { 39 | window.removeEventListener("scroll", this.onScroll); 40 | window.removeEventListener("resize", this.onScroll); 41 | this.stopTimer(); 42 | } 43 | onScroll = debounce(66, () => { 44 | const scrollTop = window.scrollY || document.documentElement.scrollTop; 45 | if (scrollTop + window.innerHeight >= document.body.clientHeight) { 46 | this.setState({ 47 | page: ++this.state.page 48 | }); 49 | } 50 | }); 51 | startTimer() { 52 | this.stopTimer(); 53 | this.timer = setInterval(() => { 54 | this.time++; 55 | }, 1000); 56 | } 57 | stopTimer() { 58 | if (this.timer) { 59 | clearInterval(this.timer); 60 | this.time = 0; 61 | this.timer = null; 62 | } 63 | } 64 | render() { 65 | const {page} = this.state; 66 | let contentCnt = 0; 67 | const contents = []; 68 | const targeting = { 69 | test: "infinitescroll" 70 | }; 71 | while (contentCnt < page * 3) { 72 | contents.push( 73 | 81 | ); 82 | contentCnt++; 83 | } 84 | return ( 85 |
86 |
87 | 94 |
95 |
{contents}
96 |
97 | ); 98 | } 99 | } 100 | 101 | export default App; 102 | -------------------------------------------------------------------------------- /examples/apps/routing/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | import React, {Component} from "react"; 3 | import PropTypes from "prop-types"; 4 | import createHistory from "history/createHashHistory"; 5 | import {Bling as Gpt} from "react-gpt"; // eslint-disable-line import/no-unresolved 6 | import "../log"; 7 | import Home from "./home"; 8 | import Page from "./page"; 9 | import styles from "./styles"; 10 | 11 | Gpt.syncCorrelator(); 12 | Gpt.enableSingleRequest(); 13 | 14 | class App extends Component { 15 | static propTypes = { 16 | location: PropTypes.object, 17 | history: PropTypes.object, 18 | children: PropTypes.node 19 | }; 20 | 21 | createHref(path) { 22 | return `${window.location.origin}${window.location.pathname}#${path}`; 23 | } 24 | 25 | render() { 26 | const {location, children} = this.props; 27 | const adUnitPath = `/4595/nfl.test.open${location.pathname}`; 28 | const props = { 29 | ...this.props, 30 | adUnitPath 31 | }; 32 | 33 | return ( 34 |
35 | 50 |
51 | 56 |
57 | {children && React.cloneElement(children, props)} 58 |
59 | ); 60 | } 61 | } 62 | 63 | class AppContainer extends Component { 64 | // eslint-disable-next-line react/sort-comp 65 | routes = { 66 | "/Travel/Europe": {component: Home}, 67 | "/Travel/Europe/France": {component: Page, params: {id: "France"}}, 68 | "/Travel/Europe/Spain": {component: Page, params: {id: "Spain"}} 69 | }; 70 | 71 | state = { 72 | routeComponent: this.routes["/Travel/Europe"].component 73 | }; 74 | 75 | componentWillMount() { 76 | this.unlisten = this.history.listen(location => { 77 | const route = 78 | this.routes[location.pathname] || this.routes["/Travel/Europe"]; 79 | const {component: routeComponent, params} = route; 80 | this.setState({routeComponent, location, params}); 81 | }); 82 | this.history.replace("/Travel/Europe"); 83 | } 84 | 85 | componentWillUnmount() { 86 | this.unlisten(); 87 | } 88 | 89 | history = createHistory(); 90 | 91 | render() { 92 | return ( 93 | 98 | {React.createElement(this.state.routeComponent)} 99 | 100 | ); 101 | } 102 | } 103 | 104 | export default AppContainer; 105 | -------------------------------------------------------------------------------- /examples/server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-use-before-define, import/default */ 2 | import fs from "fs"; 3 | import path from "path"; 4 | import Express from "express"; 5 | 6 | import webpack from "webpack"; 7 | import webpackDevMiddleware from "webpack-dev-middleware"; 8 | import webpackConfig from "../webpack.config.server"; 9 | 10 | import React from "react"; 11 | import {renderToString} from "react-dom/server"; 12 | 13 | import routes from "./routes"; 14 | 15 | const globalStyle = fs.readFileSync( 16 | path.resolve(__dirname, "../apps/global.css"), 17 | "utf8" 18 | ); 19 | 20 | const app = new Express(); 21 | const port = 8080; 22 | 23 | app.use( 24 | webpackDevMiddleware(webpack(webpackConfig), { 25 | noInfo: true, 26 | publicPath: webpackConfig.output.publicPath 27 | }) 28 | ); 29 | app.use(handleRoutes); 30 | 31 | function handleRoutes(req, res) { 32 | const routeName = req.url.substr(1).split("/")[0]; 33 | 34 | if (routeName === "") { 35 | res.send(renderIndex()); 36 | return; 37 | } 38 | 39 | if (!routes[routeName]) { 40 | res.send(render404()); 41 | return; 42 | } 43 | 44 | const App = routes[routeName].app; 45 | const title = routes[routeName].title; 46 | const html = renderToString( 47 | 48 | ); 49 | 50 | res.send(renderPage(routeName, html, title)); 51 | } 52 | 53 | function renderIndex() { 54 | return ` 55 | 56 | 57 | 58 | React GPT Examples 59 | 60 | 61 | 62 |

React GPT Examples

63 | 71 | 72 | 73 | `; 74 | } 75 | 76 | function render404() { 77 | return ` 78 | 79 | 80 | 81 | Page Not Found 82 | 83 | 84 | 85 |

Page Not Found

86 | React GPT Examples 87 | 88 | 89 | `; 90 | } 91 | 92 | function renderPage(name, html, title) { 93 | return ` 94 | 95 | 96 | 97 | ${title} 98 | 99 | 100 | 101 |

React GPT Examples / ${title}

102 |
${html}
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 | [![npm Version](https://img.shields.io/npm/v/react-gpt.svg?style=flat-square)](https://www.npmjs.org/package/react-gpt) 6 | [![Build Status](https://img.shields.io/travis/nfl/react-gpt/master.svg?style=flat-square)](https://travis-ci.org/nfl/react-gpt) 7 | [![Dependency Status](https://img.shields.io/david/nfl/react-gpt.svg?style=flat-square)](https://david-dm.org/nfl/react-gpt) 8 | [![codecov.io](https://img.shields.io/codecov/c/github/nfl/react-gpt/master.svg?style=flat-square)](https://codecov.io/github/nfl/react-gpt?branch=master) 9 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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 | } --------------------------------------------------------------------------------