├── .circleci └── config.yml ├── .github └── dependabot.yml ├── .gitignore ├── .prettierignore ├── .readthedocs.yml ├── CHANGELOG.rst ├── LICENSE ├── README.rst ├── dist ├── ethicalads.js └── ethicalads.min.js ├── docs ├── Makefile ├── _static │ ├── docs.css │ ├── fixedfooter.png │ ├── fixedheader.png │ └── stickybox.webm ├── changelog.rst ├── conf.py ├── img │ └── example.png ├── index.rst ├── make.bat ├── releasing.rst └── requirements.txt ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── styles.scss ├── tests ├── auto-placement.test.html ├── common.inc.js ├── keyword-detection.test.html ├── missing-placement.test.html └── placement-rotation.test.html ├── web-test-runner.config.mjs └── webpack.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | browser-tools: circleci/browser-tools@1.4.8 5 | 6 | commands: 7 | run-checks: 8 | description: "Run basic checks" 9 | steps: 10 | - checkout 11 | - run: npm ci 12 | - run: npm run lint 13 | run-build: 14 | description: "Ensure compiled assets are up to date" 15 | steps: 16 | - checkout 17 | - run: npm ci 18 | - run: npm run build 19 | - run: 20 | name: Ensure built assets are up to date 21 | command: | 22 | if [[ `git status dist/ --porcelain` ]] 23 | then 24 | echo "ERROR: assets are out of date. Make sure to run 'npm run build' on your branch." 25 | git status dist/ --porcelain 26 | exit 1 27 | fi 28 | run-test: 29 | description: "Run test suite" 30 | steps: 31 | - browser-tools/install-chrome 32 | - browser-tools/install-chromedriver 33 | - checkout 34 | - run: npm ci 35 | - run: npm test 36 | 37 | jobs: 38 | checks: 39 | docker: 40 | - image: "cimg/node:16.19" 41 | steps: 42 | - run-checks: {} 43 | build: 44 | docker: 45 | - image: "cimg/node:16.19" 46 | steps: 47 | - run-build: {} 48 | test: 49 | docker: 50 | - image: "cimg/node:16.19" 51 | steps: 52 | - run-test: {} 53 | 54 | workflows: 55 | version: 2 56 | build: 57 | jobs: 58 | - checks 59 | - build 60 | - test 61 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | version: 2 3 | updates: 4 | - package-ecosystem: "npm" 5 | directory: "/**" 6 | schedule: 7 | interval: "weekly" 8 | allow: 9 | - dependency-type: "production" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /docs/build/ 3 | /docs/_build/ 4 | .ropeproject/ 5 | *.pyc 6 | dist/ 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /docs/build/ 3 | /docs/_build/ 4 | /docs/_static/ethicalads.js 5 | index.html 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: docs/requirements.txt 30 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | .. The text for the changelog is generated with ``npm run changelog`` 5 | .. Then it is formatted and copied into this file. 6 | .. This is included by docs/changelog.rst 7 | 8 | 9 | Version v1.22.0 10 | --------------- 11 | 12 | This release added a new ad style (fixed header) for putting an ad fixed to the top of the page. 13 | The ad can be text-only or use an image in addition to the ad text. 14 | 15 | :date: June 3, 2025 16 | 17 | * @davidfischer: Add a fixedheader ad style (#218) 18 | 19 | 20 | Version v1.21.1 21 | --------------- 22 | 23 | This is a very minor release that catches some warnings (logged to the console at error previously) 24 | when loading ads manually. This means that using `data-verbosity="quiet"` will now catch them. 25 | 26 | :date: October 17, 2024 27 | 28 | * @davidfischer: Catch warnings when loading manually (#221) 29 | 30 | 31 | Version v1.21.0 32 | --------------- 33 | 34 | This release again makes some tweaks to our ad rotation algorithm 35 | by making it more configurable and disabling at least temporarily 36 | visibilitychange ad rotations. 37 | 38 | :date: October 17, 2024 39 | 40 | * @davidfischer: Make visibilitychange rotation an option (#214) 41 | 42 | 43 | Version v1.20.0 44 | --------------- 45 | 46 | This release again makes some tweaks to our ad rotation algorithm 47 | specifically around the timeframe and delay. 48 | 49 | :date: October 15, 2024 50 | 51 | * @davidfischer: Change ad rotation minimum view time (30s -> 45) (#212) 52 | * @davidfischer: Add additional blockchain keywords (#211) 53 | 54 | 55 | Version v1.19.0 56 | --------------- 57 | 58 | Release v1.18.0 was tested but never widely adopted as we saw a performance (CTR) degradation. 59 | We tested a delay before this release and think it will solve most of the performance 60 | problems that we saw in the previous release. 61 | 62 | :date: September 24, 2024 63 | 64 | * @davidfischer: Add a delay before a visibilitychange rotation (#209) 65 | 66 | 67 | Version v1.18.0 68 | --------------- 69 | 70 | Rotate ads if they've been visible long enough when a tab is refocused 71 | after being backgrounded. 72 | 73 | :date: August 15, 2024 74 | 75 | * @davidfischer: Rotate ads on a visibilitychange event (#207) 76 | 77 | 78 | Version v1.17.0 79 | --------------- 80 | 81 | Minor changes in this release to add some specific keywords 82 | 83 | :date: July 31, 2024 84 | 85 | * @davidfischer: Add billing/payments keywords (#205) 86 | 87 | 88 | Version v1.16.0 89 | --------------- 90 | 91 | This release contains styling changes to style the destination domain 92 | in the ad which we are adding to have more transparency. 93 | 94 | :date: July 17, 2024 95 | 96 | * @davidfischer: Style showing the domain in the ad client (#203) 97 | * @ericholscher: Rename README (#202) 98 | 99 | 100 | Version v1.15.0 101 | --------------- 102 | 103 | This release has a number of minor bugfixes and improves the ability 104 | to control the client's logging. 105 | 106 | :date: June 14, 2024 107 | 108 | * @davidfischer: Vertically center the close button (#200) 109 | * @davidfischer: Fix typo in styles.scss (#199) 110 | * @davidfischer: Remove unicode char from client (#198) 111 | * @davidfischer: Increase stickybox z-index (#196) 112 | * @davidfischer: Update client logging (#195) 113 | 114 | 115 | Version v1.14.0 116 | --------------- 117 | 118 | The changes in this release were minor but allow adjusting 119 | the position of the stickybox placement with the `data-ea-placement-bottom` 120 | attribute. 121 | 122 | :date: May 16, 2024 123 | 124 | * @davidfischer: Fix the unit tests (#191) 125 | * @humitos: Use `data-ea-placement-bottom` to set CSS `bottom` property (#190) 126 | * @ericholscher: Add UA for crawler (#189) 127 | * @humitos: Ad placement for Read the Docs addons (#188) 128 | 129 | 130 | Version v1.13.0 131 | --------------- 132 | 133 | This change lowers the ad rotation threshold 134 | and adds the notes on rotation to the documentation. 135 | 136 | :date: February 13, 2024 137 | 138 | * @davidfischer: Lower ad rotation threshold to 30 seconds (#186) 139 | 140 | 141 | Version v1.12.0 142 | --------------- 143 | 144 | We made a few tweaks around double-loading the module. 145 | We settled on just raising a warning. 146 | 147 | :date: February 6, 2024 148 | 149 | * @davidfischer: Ad client reloading check tweaks (#184) 150 | 151 | 152 | Version v1.11.0 153 | --------------- 154 | 155 | The client will not automatically rotate ads very conservatively. 156 | This is primarily for SPAs. 157 | Added a check to prevent double-loading the module. 158 | Also, added a more significant test suite. 159 | 160 | :date: December 5, 2023 161 | 162 | * @davidfischer: Prevent double loading the module (#181) 163 | * @davidfischer: Rotate ads (#180) 164 | * @agjohnson: Proof of concept for web-test-runner (#179) 165 | * @davidfischer: WIP: Add a test suite (#178) 166 | 167 | 168 | Version v1.10.0 169 | --------------- 170 | 171 | Fixed a bug with ``ethicalads.wait``. 172 | Updated the keywords and added documentation for falling back from 173 | EthicalAds to other networks. 174 | 175 | :date: October 24, 2023 176 | 177 | * @davidfischer: Fallback to other ad networks (#176) 178 | * @davidfischer: Fix promise bug for ethicalads.wait (#175) 179 | * @davidfischer: Use the same keywords as the server (#174) 180 | 181 | 182 | Version v1.9.0 183 | -------------- 184 | 185 | The client will send a placement index for multiple placements 186 | on the same screen. 187 | 188 | :date: October 11, 2023 189 | 190 | * @davidfischer: Index -> placement_index (#171) 191 | * @davidfischer: Include placement number with ad request (#170) 192 | * @davidfischer: Release update step (#169) 193 | 194 | 195 | Version v1.8.0 196 | -------------- 197 | 198 | The main difference in this release is a change to the z-index 199 | in the fixed footer ad. The z-index was a bit low and a higher 200 | one was needed especially on Read the Docs. 201 | 202 | :date: August 29, 2023 203 | 204 | * @humitos: FixedFooter: use a bigger `z-index` (#167) 205 | * @davidfischer: Note that page specific keywords are mostly unnecessary (#163) 206 | 207 | 208 | Version v1.7.0 209 | -------------- 210 | 211 | Improved single page app (SPA) support. See the docs for more details. 212 | 213 | :date: June 8, 2023 214 | 215 | * @davidfischer: Improved SPA support in the ad client (#161) 216 | * @davidfischer: Read the Docs docs config (#158) 217 | * @davidfischer: Use a fancy webm for the stickybox video (#153) 218 | * @agjohnson: Add basic test suite (#150) 219 | * @agjohnson: Fork basic circleci configuration here (#149) 220 | 221 | 222 | Version v1.6.2 223 | -------------- 224 | 225 | Fix a styling issue that caused the stickybox ad to float on smaller 226 | screen sizes. 227 | 228 | :date: September 6, 2022 229 | 230 | * @davidfischer: The stickybox shouldn't float except on ultrawide (#137) 231 | 232 | 233 | Version v1.6.1 234 | -------------- 235 | 236 | This release fixed a viewport detection issue that pertained 237 | to styled ads (fixedfooter and stickybox) that cause issues 238 | with views being counted for them. 239 | This release also contained a minor docs fix. 240 | 241 | :date: August 29, 2022 242 | 243 | * @davidfischer: Position the outer div for styled ads (#134) 244 | * @davidfischer: Fix the broken placeholder (#132) 245 | * @dependabot[bot]: Bump moment from 2.29.1 to 2.29.2 (#108) 246 | 247 | 248 | Version v1.6.0 249 | -------------- 250 | 251 | This version added a fixedfooter placement. 252 | 253 | :date: July 6, 2022 254 | 255 | * @fshabashev: Fix duplicated keys in the KEYWORDS dictionary (#123) 256 | * @davidfischer: Add a fixedfooter placement style (#121) 257 | 258 | 259 | Version v1.5.0 260 | -------------- 261 | 262 | Publisher house ads (fallback ads) were not enabled by default in the client. 263 | Starting in this release, they are. 264 | 265 | :date: June 20, 2022 266 | 267 | * @davidfischer: Make publisher-house ads enabled by default (#119) 268 | 269 | 270 | Version v1.4.4 271 | -------------- 272 | 273 | During the rollout of v1.4.3, we noticed that warnings were treated as errors 274 | in some situations due to a poorly documented, browser specific ``window.debug``. 275 | We are just not going to rely on that. 276 | 277 | :date: June 9, 2022 278 | 279 | * @davidfischer: Always treat warnings as warnings (#117) 280 | 281 | 282 | Version v1.4.3 283 | -------------- 284 | 285 | Fixes a release issue with 1.4.2. 286 | 287 | :date: June 9, 2022 288 | 289 | 290 | Version v1.4.2 291 | --------------- 292 | 293 | This release just demoted an error raised when there were no ads to show to a warning. 294 | 295 | :date: June 9, 2022 296 | 297 | * @davidfischer: Silence the no ads to show warning (#111) 298 | * @ericholscher: Highlight fallback ads (#109) 299 | * @dependabot[bot]: Bump url-parse from 1.5.3 to 1.5.7 (#104) 300 | * @dependabot[bot]: Bump follow-redirects from 1.12.1 to 1.14.7 (#96) 301 | * @davidfischer: "Placement is configured with invalid parameters" when there's just no ad to show (#26) 302 | 303 | 304 | Version v1.4.1 305 | --------------- 306 | 307 | This was a very minor change to a ``z-index`` that could 308 | obscure some content when using the stickybox placement. 309 | 310 | :date: January 25, 2022 311 | 312 | * @davidfischer: Decrease the z-index below most modals (#98) 313 | * @davidfischer: Tweak around releasing versions (#97) 314 | 315 | 316 | Version v1.4.0 317 | --------------- 318 | 319 | The big change here is to add custom placements with the ``data-ea-style`` 320 | option. 321 | 322 | :date: December 3, 2021 323 | 324 | * @davidfischer: Add stickybox floating placement to ad client (#94) 325 | * @davidfischer: Add MIT License file (#93) 326 | * @sureshjoshi: Static site support using CSS in lieu of JS (#92) 327 | * @voxpelli: Add `LICENSE` file to make license more discoverable by eg. GitHub (#89) 328 | 329 | 330 | Version v1.3.0 331 | --------------- 332 | 333 | In this change we removed our polyfills to support IE11. 334 | This shrinks the client by about 40%. 335 | We also move to support multiple placements on a page. 336 | This isn't something we're recommending to publishers (and in fact, you won't make more doing this) 337 | but a publisher who is beta testing our sponsorship model is using this feature. 338 | 339 | **Note:** Drops support for IE11. 340 | 341 | :date: September 2, 2021 342 | 343 | * @davidfischer: Remove polyfills and drop IE11 support (#88) 344 | * @davidfischer: Support multiple placements on a page (#87) 345 | * @davidfischer: Use ponyfills instead of polyfills to not change state on others' sites (#62) 346 | * @karthikdivi: Failing to display Ad in React environments, also crashing the websites (#59) 347 | 348 | 349 | Version v1.2.0 350 | --------------- 351 | 352 | Move the view time endpoint to a separate endpoint 353 | sent from the server. 354 | 355 | :date: August 13, 2021 356 | 357 | * @davidfischer: Use a separate view time endpoint (#85) 358 | * @dependabot[bot]: Bump url-parse from 1.5.1 to 1.5.3 (#84) 359 | * @davidfischer: Document the versioning process of the client (#83) 360 | * @dependabot[bot]: Bump path-parse from 1.0.6 to 1.0.7 (#82) 361 | 362 | 363 | Version v1.1.1 364 | --------------- 365 | 366 | There was a minor fix to new code that sends the amount of time an ad was viewed. 367 | 368 | :date: August 5, 2021 369 | 370 | * @davidfischer: Remove the view time listener after sending (#80) 371 | 372 | 373 | Version v1.1.0 374 | --------------- 375 | 376 | The major changes in this release were to send the client version with the ad request. 377 | In the future, we will begin warning users if their ad client is very out of date. 378 | The other major change was to send the amount of time an ad was viewed 379 | when the browser/page/tab loses focus or is closed. 380 | This is an important advertiser metric and we believe that we may be able to charge 381 | advertisers additional rates for high view time placements. 382 | 383 | :date: August 5, 2021 384 | 385 | * @davidfischer: Allowing forcing a specific ad campaign (#77) 386 | * @davidfischer: Send the ad view time to the server (#76) 387 | * @h-enk: Links to cross-origin destinations are unsafe (#75) 388 | * @davidfischer: Add some additional targeting keywords (#74) 389 | * @davidfischer: Pins needed after installing and verifying dependency updates (#73) 390 | * @davidfischer: Include client version in ad decision (#71) 391 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Read the Docs, Inc. dba. EthicalAds 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | EthicalAd Client 2 | ================ 3 | 4 | A JavaScript client for EthicalAd publishers. 5 | 6 | .. image:: https://ethical-ad-client.readthedocs.io/en/latest/_images/example.png 7 | 8 | Configuration and Usage 9 | ----------------------- 10 | 11 | You can find information on using this library on your site, and how to 12 | configure the ad placements, in `our documentation`_ 13 | 14 | .. _`our documentation`: https://ethical-ad-client.readthedocs.io/en/latest/#usage 15 | 16 | Development 17 | ----------- 18 | 19 | To make changes to the library or it's attached stylesheets, run the local 20 | development server to continually rebuild the assets and serve the test page: 21 | 22 | .. code:: prompt 23 | 24 | % npm run dev 25 | 26 | You can view the test styleguide page at: http://localhost:8080/ 27 | 28 | When you are satisfied with changes, make sure to run linting and apply 29 | automatic formatting to the files. 30 | 31 | To check which files don't meet linting guidelines run: 32 | 33 | .. code:: prompt 34 | 35 | % npm run lint 36 | 37 | To automatically apply formatting to the files: 38 | 39 | .. code:: prompt 40 | 41 | % npm run format 42 | 43 | Finally, create release distribution files -- this will generate the client 44 | libraries in ``dist/``: 45 | 46 | .. code:: prompt 47 | 48 | % npm run build 49 | 50 | You are now ready to create a pull request with the change. You will need to run 51 | the format and build steps over again on any changes to the library or 52 | stylesheet. 53 | -------------------------------------------------------------------------------- /dist/ethicalads.min.js: -------------------------------------------------------------------------------- 1 | var ethicalads=function(e){var a={};function t(o){if(a[o])return a[o].exports;var r=a[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}return t.m=e,t.c=a,t.d=function(e,a,o){t.o(e,a)||Object.defineProperty(e,a,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,a){if(1&a&&(e=t(e)),8&a)return e;if(4&a&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&a&&"string"!=typeof e)for(var r in e)t.d(o,r,function(a){return e[a]}.bind(null,r));return o},t.n=function(e){var a=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(a,"a",a),a},t.o=function(e,a){return Object.prototype.hasOwnProperty.call(e,a)},t.p="",t(t.s=1)}([function(e,a,t){var o,r;o=this,r=function(){var e={},a="undefined"!=typeof window&&window,t="undefined"!=typeof document&&document,o=t&&t.documentElement,r=a.matchMedia||a.msMatchMedia,d=r?function(e){return!!r.call(a,e).matches}:function(){return!1},n=e.viewportW=function(){var e=o.clientWidth,t=a.innerWidth;return e=0&&t.left<=n()},e.inY=function(e,a){var t=c(e,a);return!!t&&t.bottom>=0&&t.top<=i()},e.inViewport=function(e,a){var t=c(e,a);return!!t&&t.bottom>=0&&t.right>=0&&t.top<=i()&&t.left<=n()},e},e.exports?e.exports=r():o.verge=r()},function(e,a,t){"use strict";t.r(a),t.d(a,"Placement",(function(){return g})),t.d(a,"check_dependencies",(function(){return x})),t.d(a,"load_placements",(function(){return k})),t.d(a,"unload_placements",(function(){return w})),t.d(a,"set_verbosity",(function(){return _})),t.d(a,"wait",(function(){return j})),t.d(a,"load",(function(){return S})),t.d(a,"reload",(function(){return A})),t.d(a,"uplifted",(function(){return E})),t.d(a,"detectedKeywords",(function(){return C}));var o=t(0),r=t.n(o);t(2);function d(e){return(d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function n(e,a,t){return a=u(a),function(e,a){if(a&&("object"==d(a)||"function"==typeof a))return a;if(void 0!==a)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(e,c()?Reflect.construct(a,t||[],u(e).constructor):a.apply(e,t))}function i(e){var a="function"==typeof Map?new Map:void 0;return(i=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf("[native code]")}catch(a){return"function"==typeof e}}(e))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==a){if(a.has(e))return a.get(e);a.set(e,t)}function t(){return l(e,arguments,u(this).constructor)}return t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),s(t,e)})(e)}function l(e,a,t){if(c())return Reflect.construct.apply(null,arguments);var o=[null];o.push.apply(o,a);var r=new(e.bind.apply(e,o));return t&&s(r,t.prototype),r}function c(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(e){}return(c=function(){return!!e})()}function s(e,a){return(s=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,a){return e.__proto__=a,e})(e,a)}function u(e){return(u=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function p(e,a){if(!(e instanceof a))throw new TypeError("Cannot call a class as a function")}function f(e,a){for(var t=0;t=m.verbose){for(var a,t=arguments.length,o=new Array(t>1?t-1:0),r=1;r=m.verbose){for(var a,t=arguments.length,o=new Array(t>1?t-1:0),r=1;r=m.normal){for(var a,t=arguments.length,o=new Array(t>1?t-1:0),r=1;r=m.quiet){for(var a,t=arguments.length,o=new Array(t>1?t-1:0),r=1;r=300&&clearInterval(a.view_time_counter))}),1e3,a.target),a.hashchange_listener=function(){a.canRotate()&&(a.sendViewTime(),a.rotate())},window.addEventListener("hashchange",a.hashchange_listener),a.visibilitychange_listener=function(){"hidden"!==document.visibilityState&&"unloaded"!==document.visibilityState||(a.tab_hidden=!0,a.sendViewTime()),!0===a.tab_hidden&&"visible"===document.visibilityState&&(a.tab_hidden=!1,a.canRotate()&&(a.sendViewTime(),setTimeout((function(){a.rotate()}),3e3)))},e}))}},{key:"clearListeners",value:function(){this.view_time_counter&&clearInterval(this.view_time_counter),this.hashchange_listener&&window.removeEventListener("hashchange",this.hashchange_listener),this.visibilitychange_listener}},{key:"canRotate",value:function(){return!(!this.inViewport(this.target)||this.view_time<45||this.rotations>=3)}},{key:"rotate",value:function(){if(this.canRotate())return this.clearListeners(),this.view_time=0,this.view_time_sent=!1,this.response=null,this.tab_hidden=!1,this.rotations+=1,this.load()}},{key:"inViewport",value:function(e){return!!(this.response&&this.response.view_url&&r.a.inViewport(e,-3)&&"visible"===document.visibilityState)}},{key:"fetch",value:function(){var e=this,a="ad_"+Date.now()+"_"+Math.floor(1e6*Math.random()),t=a;this.target.id&&(t=this.target.id);var o={publisher:this.publisher,ad_types:this.ad_type,div_ids:t,callback:a,keywords:this.keywords.join("|"),campaign_types:this.campaign_types.join("|"),format:"jsonp",client_version:"1.22.0",placement_index:this.index,url:(window.location.origin+window.location.pathname).slice(0,256)};this.force_ad&&(o.force_ad=this.force_ad),this.force_campaign&&(o.force_campaign=this.force_campaign),this.rotations>1&&(o.rotations=this.rotations);var r=new URLSearchParams(o),d=new URL("https://server.ethicalads.io/api/v1/decision/?"+r.toString());return new Promise((function(t,o){window[a]=function(a){if(a&&a.html&&a.view_url){e.response=a;var o=document.createElement("div");return o.innerHTML=a.html,t(o.firstChild)}return t(null)};var r=document.createElement("script");r.src=d,r.type="text/javascript",r.async=!0,r.addEventListener("error",(function(e){return t()})),document.getElementsByTagName("head")[0].appendChild(r)}))}},{key:"sendViewTime",value:function(){if(!(this.view_time<=0||this.view_time_sent)&&this.response&&this.response.view_time_url){var e=document.createElement("img");e.src=this.response.view_time_url+"?view_time="+this.view_time,e.className="ea-pixel",this.target.appendChild(e),this.view_time_sent=!0}}},{key:"detectABP",value:function(e,a){var t=!1,o=2,r=!1,d=!1;if("function"==typeof a){e+="?ch=*&rn=*";var n=11*Math.random(),i=new Image;i.onload=c,i.onerror=function(){r=!0,c()},i.src=e.replace(/\*/,1).replace(/\*/,n);var l=new Image;l.onload=c,l.onerror=function(){d=!0,c()},l.src=e.replace(/\*/,2).replace(/\*/,n),function e(a,r){0==o||r>1e3?a(0==o&&t):setTimeout((function(){e(a,2*r)}),2*r)}(a,250)}function c(){--o||(t=!r&&d)}}},{key:"detectKeywords",value:function(){if(C)return C;for(var e={},a=(document.querySelector("[role='main']")||document.querySelector("main")||document.querySelector("body")).textContent.split(/\s+/),t=/^[\('"]?(.*?)[,\.\?\!:;\)'"]?$/g,o=0;o=2})).sort((function(e,a){return e[1]>a[1]?-1:e[1]1})),d=(a.getAttribute("data-ea-campaign-types")||"").split("|").filter((function(e){return e.length>1})),n="true"===a.getAttribute("data-ea-manual"),i=a.getAttribute("data-ea-style"),l=a.getAttribute("data-ea-force-ad"),c=a.getAttribute("data-ea-force-campaign");if("image"!==o&&"text"!==o||(o+="-v1"),(a.className||"").split(" ").indexOf("loaded")>=0)return v.warn("EthicalAd already loaded."),null;var s=a.getAttribute("data-ea-placement-bottom");return s&&a.style.setProperty("bottom",s),new e(t,o,a,{keywords:r,style:i,campaign_types:d,load_manually:n,force_ad:l,force_campaign:c})}}])}();function x(){return!!(Object.entries&&window.URL&&window.URLSearchParams&&window.Promise)||(v.error("Browser does not meet ethical ad client dependencies. Not showing ads"),!1)}function k(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],a=document.querySelectorAll("[data-ea-publisher]"),t=Array.prototype.slice.call(a);return 0===t.length&&v.warn("No ad placements found."),Promise.all(t.map((function(a,t){var o=g.from_element(a);return o?(o.index=t,0===t&&o&&!e&&o.detectABP("https://media.ethicalads.io/abp/px.gif",(function(e){E=e,e&&v.debug("Acceptable Ads enabled. Thanks for allowing our non-tracking ads :)")})),!o||!e&&o.load_manually?null:o.load()):null})))}function w(){var e=document.querySelectorAll("[data-ea-publisher]");Array.prototype.slice.call(e).forEach((function(e){e.innerHTML="",e.classList.remove("loaded")}))}function _(){var e=document.querySelector("[data-ea-publisher]");if(e){var a=e.getAttribute("data-ea-verbosity");m.hasOwnProperty(a)&&(v.verbosity=m[a])}}var j,S,A,O=function(e){function a(){return p(this,a),n(this,a,arguments)}return function(e,a){if("function"!=typeof a&&null!==a)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(a&&a.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),a&&s(e,a)}(a,e),h(a)}(i(Error)),E=!1,C=null;if(window.ethicalads&&console.warn("Double-loading the EthicalAds client. Use reload() instead. https://ethical-ad-client.readthedocs.io/en/latest/#single-page-apps"),x()){_();var z=new Promise((function(e){if("interactive"===document.readyState||"complete"===document.readyState)return e();document.addEventListener("DOMContentLoaded",(function(){e()}),{capture:!0,once:!0,passive:!0})}));j=new Promise((function(e){z.then((function(){k().then((function(a){e(a)})).catch((function(a){e([]),a instanceof O?v.warn(a.message):v.error(a.message)}))}))})),S=function(){v.debug("Loading placements manually"),k(!0).catch((function(e){e instanceof O?v.warn(e.message):v.error(e.message)}))},A=function(){v.debug("Reloading ad placement"),C=null,w(),k().catch((function(e){e instanceof O?v.warn(e.message):v.error(e.message)}))}}},function(e,a,t){var o=t(3),r=t(4);"string"==typeof(r=r.__esModule?r.default:r)&&(r=[[e.i,r,""]]);var d={insert:"head",singleton:!1};o(r,d);e.exports=r.locals||{}},function(e,a,t){"use strict";var o,r=function(){return void 0===o&&(o=Boolean(window&&document&&document.all&&!window.atob)),o},d=function(){var e={};return function(a){if(void 0===e[a]){var t=document.querySelector(a);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}e[a]=t}return e[a]}}(),n=[];function i(e){for(var a=-1,t=0;ta>img,[data-ea-publisher]:not([data-ea-type]).loaded .ea-content>a>img,.ea-type-image .ea-content>a>img{width:120px;height:90px;display:inline-block}[data-ea-type=image].loaded .ea-content>.ea-text,[data-ea-publisher]:not([data-ea-type]).loaded .ea-content>.ea-text,.ea-type-image .ea-content>.ea-text{margin-top:1em;font-size:1em;text-align:center}[data-ea-type=image].loaded .ea-callout,[data-ea-publisher]:not([data-ea-type]).loaded .ea-callout,.ea-type-image .ea-callout{max-width:180px;margin:0em 1em 1em 1em;padding-left:1em;padding-right:1em;font-style:italic;text-align:right}[data-ea-type=image].loaded.horizontal .ea-content,[data-ea-publisher]:not([data-ea-type]).loaded.horizontal .ea-content,.ea-type-image.horizontal .ea-content{max-width:320px}[data-ea-type=image].loaded.horizontal .ea-content>a>img,[data-ea-publisher]:not([data-ea-type]).loaded.horizontal .ea-content>a>img,.ea-type-image.horizontal .ea-content>a>img{float:left;margin-right:1em}[data-ea-type=image].loaded.horizontal .ea-content .ea-text,[data-ea-publisher]:not([data-ea-type]).loaded.horizontal .ea-content .ea-text,.ea-type-image.horizontal .ea-content .ea-text{margin-top:0em;text-align:left;overflow:auto}[data-ea-type=image].loaded.horizontal .ea-callout,[data-ea-publisher]:not([data-ea-type]).loaded.horizontal .ea-callout,.ea-type-image.horizontal .ea-callout{max-width:320px;text-align:right}[data-ea-type=text].loaded,.ea-type-text{font-size:14px}[data-ea-type=text].loaded .ea-content,.ea-type-text .ea-content{text-align:left}[data-ea-type=text].loaded .ea-callout,.ea-type-text .ea-callout{margin:.5em 1em 1em 1em;padding-left:1em;padding-right:1em;text-align:right;font-style:italic}[data-ea-style=stickybox].loaded{position:fixed;bottom:20px;right:20px;z-index:100}[data-ea-style=stickybox].loaded .ea-type-image .ea-stickybox-hide{cursor:pointer;position:absolute;top:.75em;right:.75em;background-color:#fefefe;border:1px solid #088cdb;border-radius:50%;color:#088cdb;font-size:1em;text-align:center;height:1.5em;width:1.5em;line-height:1.4}@media(max-width: 1300px){[data-ea-style=stickybox].loaded{position:static;bottom:0;right:0;margin:auto;text-align:center}[data-ea-style=stickybox].loaded .ea-stickybox-hide{display:none}}@media(min-width: 1301px){[data-ea-style=stickybox].loaded .ea-type-image .ea-content{background:#dcdcdc}[data-ea-style=stickybox].loaded.dark .ea-type-image .ea-content{background:#505050}}@media(min-width: 1301px)and (prefers-color-scheme: dark){[data-ea-style=stickybox].loaded.adaptive .ea-type-image .ea-content{background:#505050}}[data-ea-style=fixedfooter].loaded{position:fixed;bottom:0;left:0;z-index:200;width:100%;max-width:100%}[data-ea-style=fixedfooter].loaded .ea-type-text{width:100%;max-width:100%;display:flex;z-index:200;background:#dcdcdc}[data-ea-style=fixedfooter].loaded .ea-type-text .ea-content{border:0px;border-radius:3px;box-shadow:none}[data-ea-style=fixedfooter].loaded .ea-type-text .ea-content{background-color:inherit;max-width:100%;margin:0;padding:1em;flex:auto}[data-ea-style=fixedfooter].loaded .ea-type-text .ea-callout{max-width:100%;margin:0;padding:1em;flex:initial}@media(max-width: 576px){[data-ea-style=fixedfooter].loaded .ea-type-text .ea-callout{display:none}}[data-ea-style=fixedfooter].loaded .ea-type-text .ea-fixedfooter-hide{cursor:pointer;color:#505050;padding:1em;flex:initial;margin:auto 0}[data-ea-style=fixedfooter].loaded .ea-type-text .ea-fixedfooter-hide span{padding:.25em;font-size:.8em;font-weight:bold;border:.15em solid #505050;border-radius:.5em;white-space:nowrap}[data-ea-style=fixedfooter].loaded .ea-type-image{display:none !important}[data-ea-style=fixedfooter].loaded.dark .ea-type-text{background:#505050}[data-ea-style=fixedfooter].loaded.dark .ea-type-text .ea-fixedfooter-hide span{color:#dcdcdc;border-color:#dcdcdc}@media(prefers-color-scheme: dark){[data-ea-style=fixedfooter].loaded.adaptive .ea-type-text{background:#505050}[data-ea-style=fixedfooter].loaded.adaptive .ea-type-text .ea-fixedfooter-hide span{color:#dcdcdc;border-color:#dcdcdc}}[data-ea-style=fixedheader]{height:var(--ea-fixedheader-height);width:100%;max-width:100%;background:var(--ea-stylefixed-bgcolor);border-bottom:1px solid var(--ea-background-color)}@media(max-width: 768px){[data-ea-style=fixedheader]{display:none !important}}[data-ea-style=fixedheader].loaded .ea-type-image,[data-ea-style=fixedheader].loaded .ea-type-text{width:var(--ea-container-xl);margin:0 auto;display:flex}@media(max-width: 992px){[data-ea-style=fixedheader].loaded .ea-type-image,[data-ea-style=fixedheader].loaded .ea-type-text{width:var(--ea-container-md)}}@media(max-width: 1200px){[data-ea-style=fixedheader].loaded .ea-type-image,[data-ea-style=fixedheader].loaded .ea-type-text{width:var(--ea-container-lg)}}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content{border:0px;border-radius:3px;box-shadow:none}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content{background-color:inherit;max-width:100%;margin:0;padding:0;flex:auto;display:flex;color:var(--ea-link-color)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content a:link,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content a:link{color:var(--ea-link-color)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content a:visited,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content a:visited{color:var(--ea-link-color)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content a:hover,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content a:hover{color:var(--ea-link-color-active)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content a:active,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content a:active{color:var(--ea-link-color-active)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content a strong,[data-ea-style=fixedheader].loaded .ea-type-image .ea-content a b,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content a strong,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content a b{color:var(--ea-link-color-bold)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-content .ea-text,[data-ea-style=fixedheader].loaded .ea-type-text .ea-content .ea-text{margin-top:0;padding:1em;flex:auto;text-align:left}[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout{max-width:100%;margin:0;padding:1em;flex:initial;color:var(--ea-link-color-callout)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout a:link,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout a:link{color:var(--ea-link-color-callout)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout a:visited,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout a:visited{color:var(--ea-link-color-callout)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout a:hover,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout a:hover{color:var(--ea-link-color-callout-active)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout a:active,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout a:active{color:var(--ea-link-color-callout-active)}[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout a strong,[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout a b,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout a strong,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout a b{color:var(--ea-link-color-callout)}@media(max-width: 576px){[data-ea-style=fixedheader].loaded .ea-type-image .ea-callout,[data-ea-style=fixedheader].loaded .ea-type-text .ea-callout{display:none}}[data-ea-style=fixedheader].loaded .ea-type-image img{width:var(--ea-image-width-xs) !important;height:auto !important;margin:.6em}[data-ea-style=fixedheader].loaded .ea-type-image .ea-domain{display:none}[data-ea-style=fixedheader].loaded.dark{background-color:var(--ea-stylefixed-bgcolor-dark)}[data-ea-style=fixedheader].loaded.dark .ea-content{color:var(--ea-link-color-dark)}[data-ea-style=fixedheader].loaded.dark .ea-content a:link{color:var(--ea-link-color-dark)}[data-ea-style=fixedheader].loaded.dark .ea-content a:visited{color:var(--ea-link-color-dark)}[data-ea-style=fixedheader].loaded.dark .ea-content a:hover{color:var(--ea-link-color-dark-active)}[data-ea-style=fixedheader].loaded.dark .ea-content a:active{color:var(--ea-link-color-dark-active)}[data-ea-style=fixedheader].loaded.dark .ea-content a strong,[data-ea-style=fixedheader].loaded.dark .ea-content a b{color:var(--ea-link-color-bold-dark)}[data-ea-style=fixedheader].loaded.dark .ea-callout{color:var(--ea-link-color-callout-dark)}[data-ea-style=fixedheader].loaded.dark .ea-callout a:link{color:var(--ea-link-color-callout-dark)}[data-ea-style=fixedheader].loaded.dark .ea-callout a:visited{color:var(--ea-link-color-callout-dark)}[data-ea-style=fixedheader].loaded.dark .ea-callout a:hover{color:var(--ea-link-color-callout-dark-active)}[data-ea-style=fixedheader].loaded.dark .ea-callout a:active{color:var(--ea-link-color-callout-dark-active)}[data-ea-style=fixedheader].loaded.dark .ea-callout a strong,[data-ea-style=fixedheader].loaded.dark .ea-callout a b{color:var(--ea-link-color-callout-dark)}@media(prefers-color-scheme: dark){[data-ea-style=fixedheader].loaded.adaptive{background-color:var(--ea-stylefixed-bgcolor-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-content{color:var(--ea-link-color-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-content a:link{color:var(--ea-link-color-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-content a:visited{color:var(--ea-link-color-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-content a:hover{color:var(--ea-link-color-dark-active)}[data-ea-style=fixedheader].loaded.adaptive .ea-content a:active{color:var(--ea-link-color-dark-active)}[data-ea-style=fixedheader].loaded.adaptive .ea-content a strong,[data-ea-style=fixedheader].loaded.adaptive .ea-content a b{color:var(--ea-link-color-bold-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-callout{color:var(--ea-link-color-callout-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-callout a:link{color:var(--ea-link-color-callout-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-callout a:visited{color:var(--ea-link-color-callout-dark)}[data-ea-style=fixedheader].loaded.adaptive .ea-callout a:hover{color:var(--ea-link-color-callout-dark-active)}[data-ea-style=fixedheader].loaded.adaptive .ea-callout a:active{color:var(--ea-link-color-callout-dark-active)}[data-ea-style=fixedheader].loaded.adaptive .ea-callout a strong,[data-ea-style=fixedheader].loaded.adaptive .ea-callout a b{color:var(--ea-link-color-callout-dark)}}",""]),e.exports=a},function(e,a,t){"use strict";e.exports=function(e){var a=[];return a.toString=function(){return this.map((function(a){var t=function(e,a){var t=e[1]||"",o=e[3];if(!o)return t;if(a&&"function"==typeof btoa){var r=(n=o,i=btoa(unescape(encodeURIComponent(JSON.stringify(n)))),l="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(i),"/*# ".concat(l," */")),d=o.sources.map((function(e){return"/*# sourceURL=".concat(o.sourceRoot||"").concat(e," */")}));return[t].concat(d).concat([r]).join("\n")}var n,i,l;return[t].join("\n")}(a,e);return a[2]?"@media ".concat(a[2]," {").concat(t,"}"):t})).join("")},a.i=function(e,t,o){"string"==typeof e&&(e=[[null,e,""]]);var r={};if(o)for(var d=0;d 75 | 87 |
88 | Ad by EthicalAds 89 |
90 | 91 | """, 92 | 'text': """ 93 | 106 | """, 107 | } 108 | 109 | required_arguments = 0 110 | optional_arguments = 0 111 | final_argument_whitespace = True 112 | option_spec = { 113 | 'classes': str, 114 | 'ad_type': str, 115 | } 116 | has_content = True 117 | 118 | def run(self): 119 | ad_type = self.options.get('ad_type', 'image') 120 | attributes = { 121 | "classes": self.options.get('classes', 'raised'), 122 | "ad_type": ad_type, 123 | } 124 | text = self.templates.get(ad_type, "").format(**attributes) 125 | node = nodes.raw('', text, format='html') 126 | (node.source, node.line) = self.state_machine.get_source_and_line(self.lineno) 127 | return [node] 128 | 129 | 130 | def setup(app): 131 | app.add_directive('example', ExampleDirective) 132 | -------------------------------------------------------------------------------- /docs/img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readthedocs/ethical-ad-client/9e8339bda2f10878237a156ae334b65f6235ad91/docs/img/example.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. All the top-level TOC items are at the H1 level to make the sidebar show them all.. 2 | .. I tried with `collapse_navigation` set to False, but it didn't solve anything 3 | 4 | EthicalAds Client 5 | ----------------- 6 | 7 | This is the client library used to add an ad placement from EthicalAds_ to your 8 | site. To get started, you will need to first :ref:`become a publisher `, 9 | and then you can :ref:`configure your site `. 10 | 11 | .. image:: img/example.png 12 | :align: center 13 | 14 | Usage 15 | ----- 16 | 17 | There are two pieces required to add an ad placement to your site. You will need 18 | to create an empty ``
`` element where you would like to place a new ad 19 | placement, and you will need to include the client library. 20 | 21 | To start, add the following in your site's ````: 22 | 23 | .. code:: html 24 | 25 | 26 | 27 | To add the placement on your site, you will need to add an empty ``
`` with 28 | some added data attributes to configure the ad placement: 29 | 30 | .. code:: html 31 | 32 |
33 | 34 | Ad client playground 35 | ~~~~~~~~~~~~~~~~~~~~ 36 | 37 | You can play around with an example placement in our `ethical ad client playground `_ on JSBin. 38 | 39 | 40 | Configuration 41 | ------------- 42 | 43 | The following data attributes are supported on the ad placement element: 44 | 45 | ``data-ea-publisher`` 46 | **(Required)** The EthicalAds publisher id for your account. 47 | 48 | ``data-ea-type`` 49 | The ad placement type. This value can be either ``image`` or ``text`` -- the 50 | default is ``image``. 51 | 52 | ``id`` (optional) 53 | A placement identifier. If you define an ``id`` and :ref:`enable placements reporting `, 54 | this will allow you to see reports for each ``id``. 55 | 56 | ``data-ea-style`` (optional) 57 | Use a custom :ref:`placement style `. 58 | 59 | ``data-ea-keywords`` (optional) 60 | A pipe (``|``) separated array of keywords for this ad placement. 61 | This is page-specific (not publisher-specific) keywords related to where the ad is shown. 62 | 63 | ``data-ea-campaign-types`` (optional) 64 | A pipe (``|``) separated array of campaign types ("paid", "publisher-house", "community", "house"). 65 | This can only further reduce campaign types, not allow ones prohibited for the publisher. 66 | This is useful when you want certain users to not get certain types of ads. 67 | 68 | ``data-ea-manual`` (optional) 69 | Set to ``true`` if you want to :ref:`manually load ads ` at a specific future time for your app. 70 | This is useful if you want to conditionally load advertising for some users but not others 71 | or only load advertising when specific actions are performed. 72 | 73 | ``data-ea-verbosity`` (optional) 74 | This can be set to "quiet", "normal" (default), or "verbose". 75 | The client will log more or less depending on this value. 76 | The value of "verbose" is useful when setting up the client initially 77 | and "normal" is a good value for most publishers. 78 | 79 | ``data-ea-force-ad`` (optional) 80 | This parameter can be used to test the ad client on a specific ad. 81 | When used, any impressions will not be counted for billing purposes. 82 | 83 | ``data-ea-force-campaign`` (optional) 84 | This parameter can be used to test the ad client on a specific campaign (group of ads). 85 | When used, any impressions will not be counted for billing purposes. 86 | 87 | ``data-ea-placement-bottom`` (optional) 88 | Set to a valid value for the CSS bottom property (eg. '40px') to have a custom position. 89 | This must be used with ``data-ea-style`` to have any effect. 90 | 91 | 92 | Themes 93 | ------ 94 | 95 | The following themes are available on all ad placement types: 96 | 97 | .. container:: row 98 | 99 | .. container:: left 100 | 101 | **Raised theme** 102 | 103 | This is the default theme used if you do not specify a theme. 104 | 105 | .. code:: html 106 | 107 |
108 | 109 | Or you can also explicitly use the theme name: 110 | 111 | .. code:: html 112 | 113 |
114 | 115 | 116 | .. container:: right 117 | 118 | .. example:: 119 | :ad_type: image 120 | :classes: raised 121 | 122 | .. container:: row 123 | 124 | .. container:: left 125 | 126 | **Flat theme** 127 | 128 | .. code:: html 129 | 130 |
131 | 132 | .. container:: right 133 | 134 | .. example:: 135 | :ad_type: image 136 | :classes: flat 137 | 138 | .. container:: row 139 | 140 | .. container:: left 141 | 142 | **Bordered theme** 143 | 144 | .. code:: html 145 | 146 |
147 | 148 | .. container:: right 149 | 150 | .. example:: 151 | :ad_type: image 152 | :classes: bordered 153 | 154 | Dark mode 155 | ~~~~~~~~~ 156 | 157 | There are also dark variants for all of the themes. The dark variants can be 158 | used with the ``dark`` class: 159 | 160 | .. code:: html 161 | 162 |
163 | 164 | .. container:: row dark 165 | 166 | .. container:: column 167 | 168 | .. example:: 169 | :ad_type: image 170 | :classes: dark raised 171 | 172 | .. container:: column 173 | 174 | .. example:: 175 | :ad_type: image 176 | :classes: dark flat 177 | 178 | .. container:: column 179 | 180 | .. example:: 181 | :ad_type: image 182 | :classes: dark bordered 183 | 184 | If your site varies based on the user's color scheme (using ``prefers-color-scheme``), 185 | set the ``adaptive`` class: 186 | 187 | .. code:: html 188 | 189 |
190 | 191 | .. container:: row adaptive 192 | 193 | .. container:: column 194 | 195 | .. example:: 196 | :ad_type: image 197 | :classes: adaptive raised 198 | 199 | .. container:: column 200 | 201 | .. example:: 202 | :ad_type: image 203 | :classes: adaptive bordered 204 | 205 | 206 | Ad Types 207 | -------- 208 | 209 | Image placement 210 | ~~~~~~~~~~~~~~~ 211 | 212 | The image ad placement type has two variants: horizontal and veritcal. Vertical 213 | image placements are the default ad type. To use the horizontal variant, use 214 | 215 | Vertical image 216 | `````````````` 217 | 218 | .. code:: html 219 | 220 |
221 | 222 | 223 | .. container:: row 224 | 225 | .. container:: column 226 | 227 | .. example:: 228 | :ad_type: image 229 | :classes: raised 230 | 231 | .. container:: dark column 232 | 233 | .. example:: 234 | :ad_type: image 235 | :classes: dark raised 236 | 237 | 238 | Horizontal image 239 | ```````````````` 240 | 241 | This variant can be used with the ``horizontal`` theme variant class: 242 | 243 | .. code:: html 244 | 245 |
246 | 247 | .. container:: row 248 | 249 | .. container:: column 250 | 251 | .. example:: 252 | :ad_type: image 253 | :classes: horizontal raised 254 | 255 | .. container:: dark column 256 | 257 | .. example:: 258 | :ad_type: image 259 | :classes: dark horizontal raised 260 | 261 | Text placement 262 | ~~~~~~~~~~~~~~ 263 | 264 | Text placements can be defined using ``data-ea-type="text"``: 265 | 266 | .. code:: html 267 | 268 |
269 | 270 | .. example:: 271 | :ad_type: text 272 | :classes: raised 273 | 274 | .. container:: row dark 275 | 276 | .. example:: 277 | :ad_type: text 278 | :classes: dark raised 279 | 280 | 281 | .. _placement-styles: 282 | 283 | Placement style 284 | --------------- 285 | 286 | Placement styles are helpers to help integrate our ads into your site. 287 | They are completely optional but they can help you get started with a common pattern 288 | without writing custom JavaScript or CSS. 289 | 290 | 291 | StickyBox 292 | ~~~~~~~~~ 293 | 294 | .. versionadded:: 1.4 295 | 296 | The "StickyBox" style is a floating placement in the lower right corner on very wide screens 297 | (>1300px wide) and a static placement on smaller screens. 298 | By floating, it ensures that the ad is always seen 299 | (and therefore results in billed views that make the publisher money). 300 | On mobile or smaller screens, the ad will just be a static placement wherever the 301 | ad ``
`` is in the DOM. 302 | 303 | Using our StickyBox style: 304 | 305 | .. code:: html 306 | 307 |
308 | 309 | 310 | .. raw:: html 311 | 312 | 319 | 320 | This Stickybox placement as it transitions from ultrawide width where the placement floats 321 | to smaller widths where it is inline. 322 | 323 | 324 | FixedFooter 325 | ~~~~~~~~~~~ 326 | 327 | .. versionadded:: 1.6 328 | 329 | The "FixedFooter" style is a floating, text-only placement 330 | attached to the bottom of the screen. 331 | By floating, it ensures that the ad is always seen 332 | (resulting in the highest view rate, generating the most revenue). 333 | 334 | Using our FixedFooter style: 335 | 336 | .. code:: html 337 | 338 | 339 |
340 | 341 | 342 | .. figure:: _static/fixedfooter.png 343 | :align: center 344 | :width: 100% 345 | 346 | This FixedFooter placement on our homepage 347 | 348 | 349 | FixedHeader 350 | ~~~~~~~~~~~ 351 | 352 | .. versionadded:: 1.22 353 | 354 | The "FixedHeader" style is a static, text-only or text+image placement 355 | at the top of the screen. 356 | Since it's at the top of the screen, it's almost always seen 357 | (resulting in the highest view rate, generating the most revenue). 358 | 359 | There's a few notes and strong recommendations when using this ad style: 360 | 361 | * This ad is hidden on mobile. It isn't well suited for sites with a lot of mobile users. 362 | * There are a few breakpoint widths on this ad style and corresponding container sizes. 363 | If needed, these are customizable by setting CSS variables (eg. ``--ea-container-lg``) on the placement div. 364 | * In your CSS styles, you want a style like `[data-ea-style="fixedheader"] { height: 50px; }`. 365 | This ensures the space is pre-allocated for the ad slot so the content isn't pushed down when the ad loads. 366 | We set this on the ad client but there can be a flash where the space isn't allocated before the client loads. 367 | * We recommend you set up :ref:`fallback ads `. 368 | This ensures something always fills the slot at the top of the screen. 369 | You don't want to have 50px of blank real estate at the top of your page. 370 | 371 | Using our FixedHeader style: 372 | 373 | .. code:: html 374 | 375 | 376 | 377 | 378 |
379 | 380 | 381 | .. figure:: _static/fixedheader.png 382 | :align: center 383 | :width: 100% 384 | 385 | This FixedHeader placement on our homepage 386 | 387 | 388 | 389 | .. _placements: 390 | 391 | Ad placement reporting 392 | ---------------------- 393 | 394 | EthicalAds allows you to track all the different ad placements that you have on your site. 395 | This means that if you have an ad on your homepage template, 396 | blog listing template, 397 | and blog post template you can track them all seperately. 398 | 399 | This is enabled by adding an ``id`` to the EthicalAds ``div`` on your site: 400 | 401 | .. code:: html 402 | 403 |
404 | 405 | This feature is disabled by default, 406 | you can go to :guilabel:`Settings > Record placements` to enable this feature. 407 | 408 | .. tip:: We recommend that you provide an ``id`` for each of your different ad placements. 409 | This will enable you to track the performance of each placement, 410 | and make adjustments that increase your :abbr:`CTR (click-through rate)`. 411 | 412 | Page-specific keywords 413 | ---------------------- 414 | 415 | .. tip:: 416 | EthicalAds uses a crawler (similar to a search engine) 417 | to crawl our publishers' sites and figure out the appropriate keywords and topics for each 418 | page where ads appear. Most publishers won't need to do anything for EthicalAds 419 | to appropriately target the right advertisers to the right pages on publisher sites. 420 | This API is mostly for SPAs or other non-traditional sites 421 | where our crawler won't work. 422 | 423 | The user agent for our crawler is: ``EthicalAds Analyzer/$version `` 424 | 425 | 426 | EthicalAds allows our advertisers to target ads based on the content of pages. 427 | This provides value for everyone, giving users more relevent ads while still respecting their privacy. 428 | 429 | Publishers can set page-specific keywords dynamically on each page of their site based on the content of the pages. 430 | For example, if you have a blog post about Kubernetes, you could set tags of `devops` and `kubernetes`. 431 | 432 | This is enabled by adding an ``data-ea-keywords`` to the EthicalAds ``div`` on your site. 433 | They are ``|``-seperated, so you can include multiple for a single page. 434 | 435 | .. code:: html 436 | 437 |
438 | 439 | 440 | Single-page apps 441 | ---------------- 442 | 443 | Single-page applications (SPAs) rewrite rather than reload the current page 444 | to load new content. The goal is to seem more responsive to the site visitor. 445 | While ads should not change too frequently, for long lasting pages 446 | that transition based on user interaction, it may make sense to rotate the ad. 447 | 448 | .. code:: javascript 449 | 450 | ethicalads.reload(); 451 | 452 | 453 | Be careful that the ad placement (``
``) 454 | is not also removed by your screen transition or it will need to be recreated. 455 | 456 | 457 | Automatic ad rotation 458 | --------------------- 459 | 460 | .. note:: This feature is under active development and the conditions may change in future versions. 461 | 462 | The ad client will automatically rotate an ad and show a new ad when appropriate. 463 | Currently, the conditions for ad rotation are: 464 | 465 | * The ad must be visible for 30 seconds or more. 466 | * There must be a URL change (anchor link or using the `browser history API`_) 467 | **OR** the tab must come back into focus after being backgrounded or minimized 468 | (a visibilitychange_ event) 469 | 470 | .. _browser history API: https://developer.mozilla.org/en-US/docs/Web/API/History_API 471 | .. _visibilitychange: https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event 472 | 473 | 474 | Customization 475 | ------------- 476 | 477 | It's possible to extend the ad client, even if you are loading the client in 478 | your browser through a request. After loading the script, there will be an 479 | ``ethicalads`` global/window instance that can be used to extend the ad client 480 | interface. 481 | 482 | The easiest place to extend is the ``ethicalads.wait`` promise instance. This 483 | resolves to an array of placements that were successfully configured -- if no 484 | placements were loaded successfully, this will be an empty array. 485 | 486 | The ``ethicalads`` object needs to be instantiated first. If you aren't loading 487 | the ad client library asynchronously, you can delay execution by loading your 488 | additional script after loading the ad client. 489 | 490 | If you are loading the ad client library asynchronously, you should wait for a 491 | document ready event. For example, using jQuery: 492 | 493 | .. code:: javascript 494 | 495 | $(document).ready(() => { 496 | ethicalads.wait.then((placements) => { 497 | console.log('Ads are loaded'); 498 | }); 499 | }); 500 | 501 | 502 | Splitting traffic with other ad networks 503 | ---------------------------------------- 504 | 505 | While our `publisher policy `_ states 506 | that our ad should be the only ad visible when your page is loaded, 507 | you are free to split your traffic with other ad networks or fallback from 508 | EthicalAds to another network or vice versa. 509 | 510 | You can fallback to Carbon Ads with a snippet like this: 511 | 512 | .. code:: html 513 | 514 | 515 | 528 | 529 | 530 | .. _fallback-ads: 531 | 532 | Showing content when there isn't an ad 533 | -------------------------------------- 534 | 535 | The easiest way to show alternative content when we do not have a paid ad is to use fallback ads. 536 | Fallback ads are ads you as a publisher can create to show only on your own site. 537 | You can create and manage fallback ads in your publisher dashboard. 538 | 539 | However, if you want to show something custom to users who do not get an ad, 540 | you can show backup content with a code snippet like this: 541 | 542 | .. code:: html 543 | 544 | 545 | 557 | 558 | .. warning:: You need to have ``Allow house campaigns`` disabled in your publisher settings, otherwise we will always return a house ad. Go to :guilabel:`Settings > Control advertiser campaign types` to disable it. Alternatively, you may request *only* a paid ad or your own fallback ads by setting ``data-ea-campaign-types="paid|publisher-house"``. 559 | 560 | 561 | .. _load manually: 562 | 563 | Manually loading ads 564 | -------------------- 565 | 566 | You can precisely determine when an ad will be loaded by setting the ``data-ea-manual`` attribute to ``true``. 567 | This is useful if you want to conditionally show advertising or only show advertising when specific actions occur. 568 | 569 | .. code:: html 570 | 571 |
572 | 577 | 578 | 579 | .. _signup: 580 | 581 | Becoming a Publisher 582 | -------------------- 583 | 584 | Visit `EthicalAds`_ to apply to be a publisher. 585 | 586 | .. _`EthicalAds`: https://ethicalads.io 587 | 588 | 589 | Developing 590 | ---------- 591 | 592 | This section is for developers of the client itself. 593 | Development occurs on `GitHub `_. 594 | 595 | * `Issues `_ 596 | * `Pull requests `_ 597 | * :doc:`Releasing ` 598 | * :doc:`Changelog ` 599 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/releasing.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Releasing 4 | ========= 5 | 6 | This is the release process for the client itself. 7 | 8 | * First update the version in ``package.json`` **and** ``index.js``. 9 | The versions use `semantic versioning `_. 10 | * Run ``npm install && npm run build``. 11 | This ensures you have the latest dependencies and you've built 12 | the latest version of the documentation. 13 | * Run ``npm run changelog`` and update ``CHANGELOG.rst`` 14 | (included by :doc:`/changelog`) 15 | with the release date and details. 16 | * Commit these changes, create a pull request, and merge it. 17 | * Tag the release: 18 | 19 | .. code-block:: bash 20 | 21 | export VERSION=vX.Y.Z 22 | git checkout main 23 | git pull origin main 24 | git tag $VERSION 25 | git push --tags origin main 26 | 27 | * Create the `release on GitHub `_: 28 | 29 | .. code-block:: bash 30 | 31 | # Push this to GitHub and the CDN 32 | mkdir dist/$VERSION 33 | cp dist/ethicalads.js dist/$VERSION/ 34 | cp dist/ethicalads.min.js dist/$VERSION/ 35 | cp dist/ethicalads.min.js dist/$VERSION/ethicalads-$VERSION.min.js 36 | 37 | * Release the `beta client`_ and purge the client from the CDN. 38 | A few publishers (notably Read the Docs) use the beta client 39 | and we can roll it out to verify no breaking changes before pushing this to third-party publishers. 40 | * Release the `release client`_ and purge the CDN. 41 | After the release, the beta client and release client should be exactly the same. 42 | 43 | .. note:: In the future we plan to release the client to NPM directly for users, but there is still a lot of churn and we don't want users pinning to old versions quite yet. 44 | 45 | .. _beta client: https://media.ethicalads.io/media/client/beta/ethicalads.min.js 46 | .. _release client: https://media.ethicalads.io/media/client/ethicalads.min.js 47 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx >= 6.2.1, <7 2 | sphinx-rtd-theme==1.2.0 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 23 | 24 | 25 |
26 |

EthicalAds

27 | 28 | 29 | 30 | 42 | 43 |

Style tests

44 | 45 | 46 | Switch ad display type: 47 | 48 | 49 | 54 | 55 | 69 | 70 |

Vertical image ad

71 | 72 | 112 |
113 | 114 |
115 |

Horizontal image ad

116 | 117 | 157 |
158 | 159 |
160 |

Text ad

161 | 162 | 196 |
197 | 198 |
199 |

200 |
201 | 202 | 203 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* Ethical ad publisher JavaScript client 2 | * 3 | * Loads placement from Ethical Ad decision API. Searches for elements with 4 | * `ethical-ad` data binding attributes and uses these attributes to query the 5 | * decision API. 6 | * 7 | * This is native JavaScript, no JQuery. It uses the API JSONP interface to get 8 | * around CORS and related issues. A script is added with a callback on 9 | * `window`. The promise is rejected if there are errors with the request or the 10 | * response doesn't look correct. 11 | * 12 | * Currently, only two parameters are supported with the ad placement: publisher 13 | * id and the place type. All of this is determined by the server and this 14 | * client so far only renders the API return HTML. 15 | * 16 | * This can be loaded async. CSS styles are preloaded via webpack `style-loader`. 17 | * There is some potential for problems if CSP rules disallow inline 18 | * stylesheets, but webpack does allow for a hardcoded nonce. 19 | * 20 | * Usage: 21 | * 22 | * 23 | *
24 | */ 25 | 26 | import verge from "verge"; 27 | 28 | import "./styles.scss"; 29 | 30 | const AD_CLIENT_VERSION = "1.22.0"; // Sent with the ad request 31 | 32 | // For local testing, set this 33 | // const AD_DECISION_URL = "http://ethicaladserver:5000/api/v1/decision/"; 34 | const AD_DECISION_URL = "https://server.ethicalads.io/api/v1/decision/"; 35 | const AD_TYPES_VERSION = 1; // Used with the ad type slugs 36 | const ATTR_PREFIX = "data-ea-"; 37 | const ABP_DETECTION_PX = "https://media.ethicalads.io/abp/px.gif"; 38 | 39 | // Verbosity and logging 40 | // 41 | // Set with: 42 | // 43 | //
44 | const VERBOSITY = { 45 | quiet: 0, // Errors only 46 | normal: 1, // Warnings only (default) 47 | verbose: 2, // Debug messages 48 | }; 49 | const logger = { 50 | verbosity: VERBOSITY["normal"], // Default 51 | 52 | debug(message, ...params) { 53 | if (this.verbosity >= VERBOSITY["verbose"]) { 54 | console.debug(message, ...params); 55 | } 56 | }, 57 | info(message, ...params) { 58 | if (this.verbosity >= VERBOSITY["verbose"]) { 59 | console.info(message, ...params); 60 | } 61 | }, 62 | warn(message, ...params) { 63 | if (this.verbosity >= VERBOSITY["normal"]) { 64 | console.warn(message, ...params); 65 | } 66 | }, 67 | error(message, ...params) { 68 | if (this.verbosity >= VERBOSITY["quiet"]) { 69 | console.error(message, ...params); 70 | } 71 | }, 72 | }; 73 | 74 | // Keywords and topics 75 | // 76 | // This allows us to categorize pages simply and have better content targeting. 77 | // Additional categorization can be done on the server side for pages 78 | // that request ads commonly but this quick and easy categorization 79 | // works decently well most of the time. 80 | const KEYWORDS = new Set([ 81 | "2fa", 82 | "ai", 83 | "airflow", 84 | "algolia", 85 | "android", 86 | "angular", 87 | "angularjs", 88 | "ansible", 89 | "api", 90 | "appengine", 91 | "app-engine", 92 | "arangodb", 93 | "artificial-intelligence", 94 | "asp-net", 95 | "auth0", 96 | "authentication", 97 | "authorization", 98 | "aws", 99 | "azure", 100 | "babel", 101 | "backend", 102 | "backend-web", 103 | "bayes", 104 | "bayesian", 105 | "billing", 106 | "bitcoin", 107 | "blender", 108 | "blockchain", 109 | "celery", 110 | "chartjs", 111 | "chatbot", 112 | "chatbots", 113 | "chatgpt", 114 | "chatgpt3", 115 | "chatgpt4", 116 | "ci", 117 | "cicd", 118 | "ci-cd", 119 | "classifier", 120 | "cloud", 121 | "cloudformation", 122 | "cloud-formation", 123 | "cloudfront", 124 | "clustering", 125 | "cockroachdb", 126 | "commonjs", 127 | "computer-vision", 128 | "container", 129 | "containers", 130 | "continuousdeployment", 131 | "continuous-deployment", 132 | "continuousintegration", 133 | "continuous-integration", 134 | "cordova", 135 | "cplusplus", 136 | "cryptocurrency", 137 | "cryptography", 138 | "csharp", 139 | "c-sharp", 140 | "css", 141 | "cssinjs", 142 | "cuda", 143 | "cve", 144 | "cyber-attack", 145 | "cybersecurity", 146 | "cyber-security", 147 | "d3js", 148 | "dalle", 149 | "dall-e", 150 | "dao", 151 | "dapp", 152 | "dataanalytics", 153 | "data-analytics", 154 | "database", 155 | "datadog", 156 | "datalake", 157 | "data-lake", 158 | "datamesh", 159 | "data-mesh", 160 | "datascience", 161 | "data-science", 162 | "datascientist", 163 | "data-scientist", 164 | "data-visualization", 165 | "data-warehouse", 166 | "decryption", 167 | "deeplearning", 168 | "deep-learning", 169 | "deepreinforcement", 170 | "deep-reinforcement", 171 | "defi", 172 | "devops", 173 | "django", 174 | "djangorestframework", 175 | "django-rest-framework", 176 | "dnssec", 177 | "docker", 178 | "dockerhub", 179 | "docker-hub", 180 | "dockerizing", 181 | "dogecoin", 182 | "dotnet", 183 | "duckdb", 184 | "elasticsearch", 185 | "elastic-search", 186 | "emberjs", 187 | "erlang", 188 | "es6", 189 | "eslint", 190 | "ethereum", 191 | "express", 192 | "facedetection", 193 | "face-detection", 194 | "fiddler", 195 | "firebase", 196 | "firewall", 197 | "flask", 198 | "frontend", 199 | "frontend-web", 200 | "fsharp", 201 | "full-stack", 202 | "game", 203 | "gamedev", 204 | "gatsbyjs", 205 | "gcp", 206 | "gitguardian", 207 | "godot", 208 | "golang", 209 | "google-cloud", 210 | "gpt", 211 | "grafana", 212 | "grails", 213 | "graphql", 214 | "hacking", 215 | "haskell", 216 | "heroku", 217 | "hyperledger", 218 | "indiegame", 219 | "indie-game", 220 | "influxdb", 221 | "infosec", 222 | "invoice", 223 | "ionic", 224 | "ios", 225 | "ipfs", 226 | "iphone", 227 | "java", 228 | "javascript", 229 | "jenkins", 230 | "jfrog", 231 | "jinja", 232 | "jquery", 233 | "julia", 234 | "jupyter", 235 | "jvm", 236 | "kafka", 237 | "k-means-clustering", 238 | "kotlin", 239 | "kubernetes", 240 | "laravel", 241 | "lint", 242 | "linux", 243 | "llm", 244 | "llms", 245 | "log4j", 246 | "lucene", 247 | "machinelearning", 248 | "machine-learning", 249 | "mariadb", 250 | "matlab", 251 | "matplotlib", 252 | "maven", 253 | "metabase", 254 | "mfa", 255 | "midjourney", 256 | "minecraft", 257 | "mkdocs", 258 | "ml", 259 | "mobile", 260 | "model-training", 261 | "mongodb", 262 | "monitoring", 263 | "montecarlo", 264 | "monte-carlo", 265 | "mysql", 266 | "naivebayes", 267 | "naive-bayes", 268 | "neo4j", 269 | "neuralnet", 270 | "neural-net", 271 | "neural-nets", 272 | "neuralnetworks", 273 | "neural-networks", 274 | "newrelic", 275 | "new-relic", 276 | "nft", 277 | "nginx", 278 | "nlp", 279 | "node", 280 | "nodejs", 281 | "nosql", 282 | "numpy", 283 | "nuxt", 284 | "nuxtjs", 285 | "oauth", 286 | "obj-c", 287 | "objectdetection", 288 | "object-detection", 289 | "openai", 290 | "opencv-python-library", 291 | "openid", 292 | "openid-connect", 293 | "openjdk", 294 | "openshift", 295 | "openssl", 296 | "otp", 297 | "overfitting", 298 | "owasp", 299 | "pandas", 300 | "payment", 301 | "payments", 302 | "paypal", 303 | "penetration-test", 304 | "pentest", 305 | "perl", 306 | "phishing", 307 | "phonegap", 308 | "php", 309 | "pip", 310 | "postcss", 311 | "postgres", 312 | "postgresql", 313 | "privacy", 314 | "psf", 315 | "pwa", 316 | "pydata", 317 | "pygame", 318 | "pylint", 319 | "pypi", 320 | "pytest", 321 | "python", 322 | "pytorch", 323 | "pytorch3d", 324 | "rabbitmq", 325 | "rails", 326 | "rdbms", 327 | "rds", 328 | "react", 329 | "reactjs", 330 | "react-native", 331 | "redis", 332 | "redux", 333 | "regression", 334 | "regressionmodel", 335 | "regression-model", 336 | "reinforcement-learning", 337 | "rollbar", 338 | "ruby", 339 | "rust", 340 | "saltstack", 341 | "scala", 342 | "scikitlearn", 343 | "scikit-learn", 344 | "scipy", 345 | "scss", 346 | "security", 347 | "securityvulnerabilities", 348 | "security-vulnerabilities", 349 | "selenium", 350 | "selinux", 351 | "sencha", 352 | "sentiment-analysis", 353 | "sentry", 354 | "serverless", 355 | "single-page-application", 356 | "sklearn", 357 | "smartphone", 358 | "sms", 359 | "snowflake", 360 | "snyk", 361 | "solana", 362 | "solidity", 363 | "solr", 364 | "spa", 365 | "spacy", 366 | "sphinx", 367 | "sphinx-doc", 368 | "spring", 369 | "sql", 370 | "sqlite", 371 | "sqlserver", 372 | "sql-server", 373 | "stripe", 374 | "struts", 375 | "subscriptions", 376 | "svelte", 377 | "sveltejs", 378 | "swift", 379 | "symfony", 380 | "tableau", 381 | "tailwind", 382 | "tailwindcss", 383 | "tailwind-css", 384 | "tdd", 385 | "technical-writing", 386 | "tensor", 387 | "tensorflow", 388 | "tensorflowjs", 389 | "terraform", 390 | "test-driven-development", 391 | "testing", 392 | "tests", 393 | "textacy", 394 | "timescale", 395 | "timeseries", 396 | "training-data", 397 | "transformers", 398 | "travisci", 399 | "twilio", 400 | "two-factor-auth", 401 | "two-factor-authentication", 402 | "typescript", 403 | "ubuntu", 404 | "unittest", 405 | "unity", 406 | "vision-api", 407 | "visualization", 408 | "vue", 409 | "vuejs", 410 | "vuetify", 411 | "vuex", 412 | "vulnerability", 413 | "waf", 414 | "web3", 415 | "webapp-firewall", 416 | "webapplicationfirewall", 417 | "web-application-firewall", 418 | "webcomponents", 419 | "web-components", 420 | "webpack", 421 | "websecurity", 422 | "web-security", 423 | "werkzeug", 424 | "wireshark", 425 | "wsgi", 426 | "yarn", 427 | "zapier", 428 | ]); 429 | 430 | // Maximum number of words of a document to analyze looking for keywords 431 | // This is simply a check against taking too much time on very long documents 432 | const MAX_WORDS_ANALYZED = 9999; 433 | 434 | // Max number of detected keywords to send 435 | // Lowering this number means that only major topics of the page get sent on long pages 436 | const MAX_KEYWORDS = 3; 437 | 438 | // Minimum number of occurrences of a keyword to consider it 439 | const MIN_KEYWORD_OCCURRENCES = 2; 440 | 441 | // Time between checking whether the ad is in the viewport to count the time viewed 442 | // Time viewed is an important advertiser metric 443 | const VIEW_TIME_INTERVAL = 1; // seconds 444 | const VIEW_TIME_MAX = 5 * 60; // seconds 445 | 446 | // In-viewport fudge factor 447 | // A fudge factor of ~3 is needed for the case where the ad 448 | // is hidden off the side of the screen by a sliding sidebar 449 | // For example, if the right side of the ad is at x=0 450 | // or the left side of the ad is at the right side of the viewport 451 | const VIEWPORT_FUDGE_FACTOR = -3; // px 452 | 453 | // An ad may be rotated if it has been visible for sufficient time 454 | // And there is user interaction such as a hashchange or visibilitychange. 455 | // We rotate no more than the maximum number of rotations. 456 | // Loading the ad the first time counts as the first rotation. 457 | const MIN_VIEW_TIME_ROTATION_DURATION = 45; // seconds 458 | const MAX_ROTATIONS = 3; 459 | 460 | // Enable ad rotation on hash change (intra-site nav) 461 | const HASHCHANGE_ROTATION_ENABLE = true; 462 | 463 | // Seconds after a tab comes back into focus to rotate an ad. 464 | const VISIBILITYCHANGE_ROTATION_ENABLE = false; 465 | const VISIBILITYCHANGE_ROTATION_DELAY = 3; // seconds 466 | 467 | /* Placement object to query decision API and return an Element node 468 | * 469 | * @param {string} publisher - Publisher ID 470 | * @param {string} ad_type - Placement ad type id 471 | * @param {Element} target - Target element 472 | * @param {Object} options - Various options for configuring the placement such as: 473 | keywords, styles, campaign_types, load_manually, force_ad, force_campaign 474 | */ 475 | export class Placement { 476 | constructor(publisher, ad_type, target, options) { 477 | this.publisher = publisher; 478 | this.ad_type = ad_type; 479 | this.target = target; 480 | 481 | // Options 482 | this.options = options; 483 | this.style = options.style; 484 | this.keywords = options.keywords || []; 485 | this.load_manually = options.load_manually; 486 | this.force_ad = options.force_ad; 487 | this.force_campaign = options.force_campaign; 488 | this.campaign_types = options.campaign_types || []; 489 | if (!this.campaign_types.length) { 490 | this.campaign_types = ["paid", "publisher-house", "community", "house"]; 491 | } 492 | 493 | // Initialized and will be used in the future 494 | this.view_time = 0; 495 | this.view_time_sent = false; // true once the view time is sent to the server 496 | this.response = null; 497 | this.tab_hidden = false; 498 | 499 | this.rotations = 1; 500 | this.index = null; 501 | } 502 | 503 | /* Create a placement from an element 504 | * 505 | * Returns null if the placement is already loaded. 506 | * 507 | * @static 508 | * @param {Element} element - Load placement and append to this Element 509 | * @returns {Placement} 510 | */ 511 | static from_element(element) { 512 | // Get attributes from DOM node 513 | const publisher = element.getAttribute(ATTR_PREFIX + "publisher"); 514 | let ad_type = element.getAttribute(ATTR_PREFIX + "type"); 515 | if (!ad_type) { 516 | ad_type = "image"; 517 | element.setAttribute(ATTR_PREFIX + "type", "image"); 518 | } 519 | 520 | const keywords = (element.getAttribute(ATTR_PREFIX + "keywords") || "") 521 | .split("|") 522 | .filter((word) => word.length > 1); 523 | const campaign_types = ( 524 | element.getAttribute(ATTR_PREFIX + "campaign-types") || "" 525 | ) 526 | .split("|") 527 | .filter((word) => word.length > 1); 528 | 529 | const load_manually = 530 | element.getAttribute(ATTR_PREFIX + "manual") === "true"; 531 | const style = element.getAttribute(ATTR_PREFIX + "style"); 532 | const force_ad = element.getAttribute(ATTR_PREFIX + "force-ad"); 533 | const force_campaign = element.getAttribute(ATTR_PREFIX + "force-campaign"); 534 | 535 | // Add version to ad type to verison the HTML return 536 | if (ad_type === "image" || ad_type === "text") { 537 | ad_type += "-v" + AD_TYPES_VERSION; 538 | } 539 | 540 | let classes = (element.className || "").split(" "); 541 | if (classes.indexOf("loaded") >= 0) { 542 | logger.warn("EthicalAd already loaded."); 543 | return null; 544 | } 545 | 546 | // Note: this attribute value *must* contain a unit (eg. '200px') 547 | const placementBottom = element.getAttribute( 548 | ATTR_PREFIX + "placement-bottom" 549 | ); 550 | if (placementBottom) { 551 | element.style.setProperty("bottom", placementBottom); 552 | } 553 | 554 | return new Placement(publisher, ad_type, element, { 555 | keywords: keywords, 556 | style: style, 557 | campaign_types: campaign_types, 558 | load_manually, 559 | force_ad, 560 | force_campaign, 561 | }); 562 | } 563 | 564 | /* Transforms target element into a placement 565 | * 566 | * This method organizes all of the operations to transform the placement 567 | * configuration wrapper `div` into an ad placement -- including starting the 568 | * API transaction, displaying the ad element, 569 | * and handling the viewport detection. 570 | * 571 | * @returns {Promise} 572 | */ 573 | load() { 574 | // Detect the keywords 575 | this.keywords = this.keywords.concat(this.detectKeywords()); 576 | 577 | return this.fetch() 578 | .then((element) => { 579 | if (element === undefined) { 580 | throw new EthicalAdsWarning( 581 | "Ad decision request blocked or invalid." 582 | ); 583 | } 584 | if (!element) { 585 | throw new EthicalAdsWarning("No ads to show."); 586 | } 587 | 588 | // Add `loaded` class, signifying that the CSS styles should finally be 589 | // applied to the target element. 590 | let classes = this.target.className || ""; 591 | classes += " loaded"; 592 | this.target.className = classes.trim(); 593 | 594 | // Make this element the only child element of the target element 595 | while (this.target.firstChild) { 596 | this.target.removeChild(this.target.firstChild); 597 | } 598 | 599 | // Apply any styles based on the specified styling 600 | this.applyStyles(element); 601 | 602 | this.target.appendChild(element); 603 | 604 | return this; 605 | }) 606 | .then((placement) => { 607 | // Detect when the ad is in the viewport 608 | // Add the view pixel to the DOM to count the view 609 | // Also count the time the ad is in view 610 | // this will be sent before the page/tab is closed or navigated away 611 | 612 | let viewport_detection = setInterval( 613 | (element) => { 614 | if (placement.inViewport(element)) { 615 | // This ad was seen! 616 | let pixel = document.createElement("img"); 617 | pixel.src = placement.response.view_url; 618 | if (uplifted) { 619 | pixel.src += "?uplift=true"; 620 | } 621 | pixel.className = "ea-pixel"; 622 | element.appendChild(pixel); 623 | 624 | clearInterval(viewport_detection); 625 | } 626 | }, 627 | 100, 628 | placement.target 629 | ); 630 | 631 | placement.view_time_counter = setInterval( 632 | (element) => { 633 | if ( 634 | placement.tab_hidden === false && 635 | placement.inViewport(element) 636 | ) { 637 | // Increment the ad's time in view counter 638 | placement.view_time += VIEW_TIME_INTERVAL; 639 | 640 | if (placement.view_time >= VIEW_TIME_MAX) { 641 | clearInterval(placement.view_time_counter); 642 | } 643 | } 644 | }, 645 | VIEW_TIME_INTERVAL * 1000, 646 | placement.target 647 | ); 648 | 649 | placement.hashchange_listener = () => { 650 | if (placement.canRotate()) { 651 | placement.sendViewTime(); 652 | placement.rotate(); 653 | } 654 | }; 655 | if (HASHCHANGE_ROTATION_ENABLE) { 656 | window.addEventListener("hashchange", placement.hashchange_listener); 657 | } 658 | 659 | // Listens to the window visibility 660 | // Rotates the ad when the window comes back into focus if 661 | // other conditions (minimum view time, under max rotations) 662 | // are met. 663 | // When the tab loses focus, send the view time to the server. 664 | placement.visibilitychange_listener = () => { 665 | if ( 666 | document.visibilityState === "hidden" || 667 | document.visibilityState === "unloaded" 668 | ) { 669 | // Check if the tab loses focus/is closed or the browser/app is minimized/closed 670 | // In that case, no longer count further time that the ad is in view 671 | // Send the time the ad was viewed to the server 672 | placement.tab_hidden = true; 673 | placement.sendViewTime(); 674 | } 675 | 676 | // This tab was invisible and has come back into focus 677 | // Trigger an ad rotation 678 | if ( 679 | placement.tab_hidden === true && 680 | document.visibilityState === "visible" 681 | ) { 682 | placement.tab_hidden = false; 683 | 684 | if (placement.canRotate()) { 685 | placement.sendViewTime(); // Should already be sent, but just in case 686 | setTimeout(function () { 687 | placement.rotate(); 688 | }, VISIBILITYCHANGE_ROTATION_DELAY * 1000); 689 | } 690 | } 691 | }; 692 | if (VISIBILITYCHANGE_ROTATION_ENABLE) { 693 | document.addEventListener( 694 | "visibilitychange", 695 | placement.visibilitychange_listener 696 | ); 697 | } 698 | 699 | return this; 700 | }); 701 | } 702 | 703 | /* Clears all the placement's event listeners */ 704 | clearListeners() { 705 | if (this.view_time_counter) { 706 | clearInterval(this.view_time_counter); 707 | } 708 | 709 | if (this.hashchange_listener && HASHCHANGE_ROTATION_ENABLE) { 710 | window.removeEventListener("hashchange", this.hashchange_listener); 711 | } 712 | 713 | if (this.visibilitychange_listener && VISIBILITYCHANGE_ROTATION_ENABLE) { 714 | document.removeEventListener( 715 | "visibilitychange", 716 | this.visibilitychange_listener 717 | ); 718 | } 719 | } 720 | 721 | /* Returns whether the conditions to rotate are met 722 | * 723 | * @returns {boolean} True if the placement can rotate 724 | */ 725 | canRotate() { 726 | if ( 727 | !this.inViewport(this.target) || 728 | this.view_time < MIN_VIEW_TIME_ROTATION_DURATION || 729 | this.rotations >= MAX_ROTATIONS 730 | ) { 731 | return false; 732 | } 733 | 734 | return true; 735 | } 736 | 737 | /* Reloads the placement with a new ad (if applicable) 738 | * 739 | * @returns {Promise} 740 | */ 741 | rotate() { 742 | if (!this.canRotate()) { 743 | return; 744 | } 745 | this.clearListeners(); 746 | 747 | this.view_time = 0; 748 | this.view_time_sent = false; 749 | this.response = null; 750 | this.tab_hidden = false; 751 | 752 | this.rotations += 1; 753 | 754 | return this.load(); 755 | } 756 | 757 | /* Returns whether the ad is visible in the viewport 758 | * 759 | * @param {Element} element - The ad element 760 | * @returns {boolean} True if the ad is loaded and visible in the viewport 761 | * (including the tab being focused and not minimized) and returns false otherwise. 762 | */ 763 | inViewport(element) { 764 | if ( 765 | this.response && 766 | this.response.view_url && 767 | verge.inViewport(element, VIEWPORT_FUDGE_FACTOR) && 768 | document.visibilityState === "visible" 769 | ) { 770 | return true; 771 | } 772 | 773 | return false; 774 | } 775 | 776 | /* Get placement data from decision API 777 | * 778 | * @returns {Promise} Resolves with an Element converted from an HTML 779 | * string from API response. Can also be null, indicating a noop action. 780 | */ 781 | fetch() { 782 | // Make sure callbacks don't collide even with multiple placements 783 | const callback = 784 | "ad_" + Date.now() + "_" + Math.floor(Math.random() * 1000000); 785 | var div_id = callback; 786 | if (this.target.id) { 787 | div_id = this.target.id; 788 | } 789 | 790 | // There's no hard maximum on URL lengths (all of these get added to the query params) 791 | // but ideally we want to keep our URLs below ~2k which should work basically everywhere 792 | let params = { 793 | publisher: this.publisher, 794 | ad_types: this.ad_type, 795 | div_ids: div_id, 796 | callback: callback, 797 | keywords: this.keywords.join("|"), 798 | campaign_types: this.campaign_types.join("|"), 799 | format: "jsonp", 800 | client_version: AD_CLIENT_VERSION, 801 | placement_index: this.index, 802 | // location.href includes query params (possibly sensitive) and fragments (unnecessary) 803 | url: (window.location.origin + window.location.pathname).slice(0, 256), 804 | }; 805 | if (this.force_ad) { 806 | params["force_ad"] = this.force_ad; 807 | } 808 | if (this.force_campaign) { 809 | params["force_campaign"] = this.force_campaign; 810 | } 811 | if (this.rotations > 1) { 812 | params["rotations"] = this.rotations; 813 | } 814 | const url_params = new URLSearchParams(params); 815 | const url = new URL(AD_DECISION_URL + "?" + url_params.toString()); 816 | 817 | return new Promise((resolve, reject) => { 818 | window[callback] = (response) => { 819 | if (response && response.html && response.view_url) { 820 | this.response = response; 821 | const node_convert = document.createElement("div"); 822 | node_convert.innerHTML = response.html; 823 | return resolve(node_convert.firstChild); 824 | } else { 825 | // No ad to show for this targeting/publisher 826 | return resolve(null); 827 | } 828 | }; 829 | 830 | var script = document.createElement("script"); 831 | script.src = url; 832 | script.type = "text/javascript"; 833 | script.async = true; 834 | script.addEventListener("error", (err) => { 835 | // There was a problem loading this request, likely this was blocked by 836 | // an ad blocker. We'll resolve with an empty response instead of 837 | // throwing an error. 838 | return resolve(); 839 | }); 840 | document.getElementsByTagName("head")[0].appendChild(script); 841 | }); 842 | } 843 | 844 | /* Sends the view time of the ad to the server 845 | */ 846 | sendViewTime() { 847 | if ( 848 | this.view_time <= 0 || 849 | this.view_time_sent || 850 | !this.response || 851 | !this.response.view_time_url 852 | ) 853 | return; 854 | 855 | let pixel = document.createElement("img"); 856 | pixel.src = this.response.view_time_url + "?view_time=" + this.view_time; 857 | pixel.className = "ea-pixel"; 858 | this.target.appendChild(pixel); 859 | 860 | this.view_time_sent = true; 861 | } 862 | 863 | /* Detect whether this ad is "uplifted" meaning allowed by ABP's Acceptable Ads list 864 | * 865 | * Calls the provided callback passing a boolean whether this ad is uplifted. 866 | * We need this data to provide back to the AcceptableAds folks. 867 | * 868 | * This code comes directly from Eyeo/AdblockPlus team to measure Acceptable Ads. 869 | * 870 | * @static 871 | * @param {string} px - A URL of a pixel to test 872 | * @param {function) callback - A callback to call when finished 873 | */ 874 | detectABP(px, callback) { 875 | var detected = false; 876 | var checksRemain = 2; 877 | var error1 = false; 878 | var error2 = false; 879 | if (typeof callback != "function") return; 880 | px += "?ch=*&rn=*"; 881 | 882 | function beforeCheck(callback, timeout) { 883 | if (checksRemain == 0 || timeout > 1e3) 884 | callback(checksRemain == 0 && detected); 885 | else 886 | setTimeout(function () { 887 | beforeCheck(callback, timeout * 2); 888 | }, timeout * 2); 889 | } 890 | 891 | function checkImages() { 892 | if (--checksRemain) return; 893 | detected = !error1 && error2; 894 | } 895 | var random = Math.random() * 11; 896 | var img1 = new Image(); 897 | img1.onload = checkImages; 898 | img1.onerror = function () { 899 | error1 = true; 900 | checkImages(); 901 | }; 902 | img1.src = px.replace(/\*/, 1).replace(/\*/, random); 903 | var img2 = new Image(); 904 | img2.onload = checkImages; 905 | img2.onerror = function () { 906 | error2 = true; 907 | checkImages(); 908 | }; 909 | img2.src = px.replace(/\*/, 2).replace(/\*/, random); 910 | beforeCheck(callback, 250); 911 | } 912 | 913 | /* Returns an array of keywords (strings) found on the page 914 | * 915 | * @returns {Array[string]} Advertising keywords found on the page 916 | */ 917 | detectKeywords() { 918 | // Return previously detected keywords 919 | // If this code has already run. 920 | // Note: if there are "no" keywords (an empty list) this is still true 921 | if (detectedKeywords) return detectedKeywords; 922 | 923 | var keywordHist = {}; // Keywords found => count of keyword 924 | const mainContent = 925 | document.querySelector("[role='main']") || 926 | document.querySelector("main") || 927 | document.querySelector("body"); 928 | 929 | const words = mainContent.textContent.split(/\s+/); 930 | const wordTrimmer = /^[\('"]?(.*?)[,\.\?\!:;\)'"]?$/g; 931 | for (let x = 0; x < words.length && x < MAX_WORDS_ANALYZED; x++) { 932 | // Remove certain punctuation from beginning and end of the word 933 | let word = words[x].replace(wordTrimmer, "$1").toLowerCase(); 934 | if (KEYWORDS.has(word)) { 935 | keywordHist[word] = (keywordHist[word] || 0) + 1; 936 | } 937 | } 938 | 939 | // Sort the hist with the most common items first 940 | // Grab only the MAX_KEYWORDS most common 941 | const keywords = Object.entries(keywordHist) 942 | .filter( 943 | // Only consider a keyword with at least this many occurrences 944 | (a) => a[1] >= MIN_KEYWORD_OCCURRENCES 945 | ) 946 | .sort((a, b) => { 947 | if (a[1] > b[1]) return -1; 948 | if (a[1] < b[1]) return 1; 949 | return 0; 950 | }) 951 | .slice(0, MAX_KEYWORDS) 952 | .map((x) => x[0]); 953 | 954 | detectedKeywords = keywords; 955 | 956 | return keywords; 957 | } 958 | 959 | /* Apply custom styles based on data-ea-style 960 | * 961 | */ 962 | applyStyles(element) { 963 | // Stickybox: https://ethical-ad-client.readthedocs.io/en/latest/#stickybox 964 | if (this.style === "stickybox") { 965 | let hideButton = document.createElement("div"); 966 | hideButton.setAttribute("class", "ea-stickybox-hide"); 967 | hideButton.innerHTML = "×"; 968 | hideButton.addEventListener("click", function () { 969 | document.querySelector("[data-ea-publisher]").remove(); 970 | }); 971 | element.appendChild(hideButton); 972 | } 973 | 974 | // FixedFooter: https://ethical-ad-client.readthedocs.io/en/latest/#fixedfooter 975 | if (this.style === "fixedfooter") { 976 | //element.querySelector('.ea-callout a').remove(); 977 | 978 | let container = document.createElement("div"); 979 | container.setAttribute("class", "ea-fixedfooter-hide"); 980 | element.appendChild(container); 981 | 982 | let hideButton = document.createElement("span"); 983 | hideButton.append("Close Ad"); 984 | hideButton.addEventListener("click", function () { 985 | document.querySelector("[data-ea-publisher]").remove(); 986 | }); 987 | container.appendChild(hideButton); 988 | } 989 | 990 | // FixedHeader: https://ethical-ad-client.readthedocs.io/en/latest/#fixedheader 991 | // No special elements required 992 | } 993 | } 994 | 995 | /* Detects whether the browser supports the necessary JS APIs to support the ad client 996 | * 997 | * Generally we support recent versions of evergreen browsers (Chrome, Firefox, Safari, Edge) 998 | * but we no longer support IE11. 999 | * 1000 | * @returns {boolean} true if all dependencies met and false otherwise 1001 | */ 1002 | export function check_dependencies() { 1003 | if ( 1004 | !Object.entries || 1005 | !window.URL || 1006 | !window.URLSearchParams || 1007 | !window.Promise 1008 | ) { 1009 | logger.error( 1010 | "Browser does not meet ethical ad client dependencies. Not showing ads" 1011 | ); 1012 | return false; 1013 | } 1014 | 1015 | return true; 1016 | } 1017 | 1018 | /* Find all placement DOM elements and hot load HTML as child nodes 1019 | * 1020 | * @param {boolean} force_load - load placements even if they are set to load manually 1021 | * @returns {Promise<[Placement]>} Resolves to a list of Placement instances 1022 | */ 1023 | export function load_placements(force_load = false) { 1024 | // Find all elements matching required data binding attribute. 1025 | const node_list = document.querySelectorAll("[" + ATTR_PREFIX + "publisher]"); 1026 | let elements = Array.prototype.slice.call(node_list); 1027 | 1028 | if (elements.length === 0) { 1029 | logger.warn("No ad placements found."); 1030 | } 1031 | 1032 | // Create main promise. Iterator `all()` Promise will surround array of found 1033 | // elements. If any of these elements have issues, this main promise will 1034 | // reject. 1035 | return Promise.all( 1036 | elements.map((element, index) => { 1037 | const placement = Placement.from_element(element); 1038 | 1039 | if (!placement) { 1040 | // Placement has already been loaded 1041 | return null; 1042 | } 1043 | 1044 | placement.index = index; 1045 | 1046 | // Run AcceptableAds detection code 1047 | // This lets us know how many impressions are attributed to AceeptableAds 1048 | // Only run this once even for multiple placements 1049 | // All impressions will be correctly attributed 1050 | if (index === 0 && placement && !force_load) { 1051 | placement.detectABP(ABP_DETECTION_PX, function (usesABP) { 1052 | uplifted = usesABP; 1053 | if (usesABP) { 1054 | logger.debug( 1055 | "Acceptable Ads enabled. Thanks for allowing our non-tracking ads :)" 1056 | ); 1057 | } 1058 | }); 1059 | } 1060 | 1061 | if (placement && (force_load || !placement.load_manually)) { 1062 | return placement.load(); 1063 | } else { 1064 | // This will be manually loaded later or has already been loaded 1065 | return null; 1066 | } 1067 | }) 1068 | ); 1069 | } 1070 | 1071 | export function unload_placements() { 1072 | const node_list = document.querySelectorAll("[" + ATTR_PREFIX + "publisher]"); 1073 | let elements = Array.prototype.slice.call(node_list); 1074 | 1075 | elements.forEach((div) => { 1076 | div.innerHTML = ""; 1077 | div.classList.remove("loaded"); 1078 | }); 1079 | } 1080 | 1081 | export function set_verbosity() { 1082 | let element = document.querySelector("[" + ATTR_PREFIX + "publisher]"); 1083 | 1084 | if (element) { 1085 | let user_verbosity = element.getAttribute(ATTR_PREFIX + "verbosity"); 1086 | if (VERBOSITY.hasOwnProperty(user_verbosity)) { 1087 | logger.verbosity = VERBOSITY[user_verbosity]; 1088 | } 1089 | } 1090 | } 1091 | 1092 | // An error class that we will not surface to clients normally. 1093 | class EthicalAdsWarning extends Error {} 1094 | 1095 | /* Wrapping Promise to allow for handling of errors by user 1096 | * 1097 | * This promise currently does not reject on error as this will emit a console 1098 | * warning if the user hasn't added a promise rejection handler (which is most 1099 | * cases). 1100 | * 1101 | * This promise resolves to an aray of Placement instances, or an empty list if 1102 | * there was any error configuring the placements. 1103 | * 1104 | * For example, to perform an action when no placements are loaded: 1105 | * 1106 | * 1113 | * 1114 | * @type {Promise<[Placement]>} 1115 | */ 1116 | export var wait; 1117 | 1118 | /* Loading placements manually rather than the normal way 1119 | * 1120 | *
1121 | * 1124 | * 1125 | * @type function 1126 | */ 1127 | export var load; 1128 | 1129 | /* Reloading placements. Used by SPAs. 1130 | * @type function 1131 | */ 1132 | export var reload; 1133 | 1134 | /* Whether this ad impression is attributed to being on the Acceptable Ads list. 1135 | * @type boolean 1136 | */ 1137 | export var uplifted = false; 1138 | 1139 | /* Keywords detected on the page 1140 | * @type {Array[string]} 1141 | */ 1142 | export var detectedKeywords = null; 1143 | 1144 | /* If importing this as a module, do not automatically process DOM and fetch the 1145 | * ad placement. Only do this if using the module directly, from a `script` 1146 | * element. This will allow for future extension and packaging as a module. 1147 | * 1148 | * This also replicates JQuery `$(document).ready()`, with added protection for 1149 | * usage of `async` -- the DOM ready event can fire before the script is loaded.. 1150 | */ 1151 | if (window.ethicalads) { 1152 | // Always display this warning regardless of log level 1153 | // This is a code mistake by publishers and should be caught right away. 1154 | console.warn( 1155 | "Double-loading the EthicalAds client. Use reload() instead. https://ethical-ad-client.readthedocs.io/en/latest/#single-page-apps" 1156 | ); 1157 | } 1158 | if (check_dependencies()) { 1159 | // Set the client verbosity 1160 | set_verbosity(); 1161 | 1162 | const wait_dom = new Promise((resolve) => { 1163 | if ( 1164 | document.readyState === "interactive" || 1165 | document.readyState === "complete" 1166 | ) { 1167 | return resolve(); 1168 | } else { 1169 | document.addEventListener( 1170 | "DOMContentLoaded", 1171 | () => { 1172 | resolve(); 1173 | }, 1174 | { 1175 | capture: true, 1176 | once: true, 1177 | passive: true, 1178 | } 1179 | ); 1180 | } 1181 | }); 1182 | 1183 | wait = new Promise((resolve) => { 1184 | wait_dom.then(() => { 1185 | load_placements() 1186 | .then((placements) => { 1187 | resolve(placements); 1188 | }) 1189 | .catch((err) => { 1190 | resolve([]); 1191 | 1192 | if (err instanceof EthicalAdsWarning) { 1193 | logger.warn(err.message); 1194 | } else { 1195 | logger.error(err.message); 1196 | } 1197 | }); 1198 | }); 1199 | }); 1200 | 1201 | load = () => { 1202 | logger.debug("Loading placements manually"); 1203 | load_placements(true).catch((err) => { 1204 | if (err instanceof EthicalAdsWarning) { 1205 | logger.warn(err.message); 1206 | } else { 1207 | logger.error(err.message); 1208 | } 1209 | }); 1210 | }; 1211 | 1212 | reload = () => { 1213 | logger.debug("Reloading ad placement"); 1214 | 1215 | detectedKeywords = null; 1216 | unload_placements(); 1217 | load_placements().catch((err) => { 1218 | if (err instanceof EthicalAdsWarning) { 1219 | logger.warn(err.message); 1220 | } else { 1221 | logger.error(err.message); 1222 | } 1223 | }); 1224 | }; 1225 | } 1226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethical-ad-client", 3 | "version": "1.22.0", 4 | "description": "EthicalAds client", 5 | "main": "dist/client.js", 6 | "engines": { 7 | "npm": ">=10.0.0 <11.0.0", 8 | "node": ">=20.0.0 <21.0.0" 9 | }, 10 | "scripts": { 11 | "build": "npm ci && npm run build-min && npm run build-unmin", 12 | "build-min": "webpack --mode=production --progress --colors", 13 | "build-unmin": "webpack --mode=development --progress --colors", 14 | "dev": "webpack-dev-server --mode=development", 15 | "lint": "prettier -c .", 16 | "format": "prettier --write .", 17 | "changelog": "gh-changelog -o readthedocs -r ethical-ad-client -e '' -f CHANGELOG.rst", 18 | "test": "web-test-runner", 19 | "test:dev": "web-test-runner --watch", 20 | "test:debug": "web-test-runner --manual --open" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/readthedocs/ethical-ad-client.git" 25 | }, 26 | "author": "Read the Docs, Inc", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/readthedocs/ethical-ad-client/issues" 30 | }, 31 | "homepage": "https://github.com/readthedocs/ethical-ad-client#readme", 32 | "dependencies": { 33 | "verge": "^1.10.2" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.21.3", 37 | "@babel/plugin-proposal-class-properties": "^7.18.6", 38 | "@babel/preset-env": "^7.20.2", 39 | "@open-wc/testing": "^3.2.0", 40 | "@rollup/plugin-commonjs": "^25.0.4", 41 | "@rollup/plugin-image": "^3.0.2", 42 | "@rollup/plugin-json": "^6.0.0", 43 | "@web/dev-server-import-maps": "^0.1.1", 44 | "@web/test-runner": "^0.17.2", 45 | "babel-loader": "^8.3.0", 46 | "css-loader": "^3.6.0", 47 | "file-loader": "^2.0.0", 48 | "github-changelog": "git+https://github.com/davidfischer/github-changelog.git#davidfischer/update-deps", 49 | "optimize-css-assets-webpack-plugin": "^5.0.8", 50 | "prettier": "^2.8.4", 51 | "rollup-plugin-lit-css": "^4.0.1", 52 | "rollup-plugin-scss": "^4.0.0", 53 | "sass": "^1.59.3", 54 | "sass-loader": "^10.4.1", 55 | "sinon": "^17.0.0", 56 | "style-loader": "^1.3.0", 57 | "to-string-loader": "^1.2.0", 58 | "url-loader": "^0.6.2", 59 | "webpack": "^4.46.0", 60 | "webpack-cli": "^3.3.12", 61 | "webpack-dev-server": "^3.11.3", 62 | "webpack-merge": "^4.2.2" 63 | }, 64 | "babel": { 65 | "plugins": [ 66 | "@babel/plugin-proposal-class-properties" 67 | ], 68 | "env": { 69 | "test": { 70 | "plugins": [ 71 | "@babel/plugin-transform-modules-commonjs", 72 | "@babel/plugin-proposal-class-properties" 73 | ] 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /styles.scss: -------------------------------------------------------------------------------- 1 | /* Ad styles */ 2 | 3 | // Sizes 4 | $width-image-horizontal: 320px; 5 | $width-image-vertical: 180px; 6 | 7 | // Z-index used to ensure these custom ad styles are above the main content. 8 | // but below most modals 9 | $zindex-styles-stickybox: 100; 10 | // "fixedfooter" requires 200 minimum to be show over the Read the Docs Sphinx theme navbar. 11 | $zindex-styles-fixedfooter: 200; 12 | 13 | // Breakpoints (used for stickybox/fixedfooter formats) 14 | $breakpoint-ultrawide: 1300px; 15 | $breakpoint-xl: 1200px; 16 | $breakpoint-lg: 992px; 17 | $breakpoint-md: 768px; 18 | $breakpoint-mobile: 576px; 19 | 20 | // Colors 21 | $color-background: rgba(0, 0, 0, 0.03); 22 | $color-background-dark: rgba(255, 255, 255, 0.05); 23 | $color-link: rgb(80, 80, 80); 24 | $color-link-callout: lighten($color-link, 10%); 25 | $color-link-bold: #088cdb; 26 | $color-link-dark: rgb(220, 220, 220); // gainsboro 27 | $color-link-callout-dark: darken($color-link-dark, 10%); 28 | $color-link-bold-dark: lighten($color-link-bold, 20%); 29 | $color-bg-stickybox: $color-link-dark; 30 | $color-bg-dark-stickybox: $color-link; 31 | 32 | // Utilities, mostly to reduce file size 33 | @mixin links($color-primary, $color-secondary, $color-highlight) { 34 | a { 35 | &:link { 36 | color: $color-primary; 37 | } 38 | &:visited { 39 | color: $color-primary; 40 | } 41 | &:hover { 42 | color: $color-secondary; 43 | } 44 | &:active { 45 | color: $color-secondary; 46 | } 47 | strong, 48 | b { 49 | color: $color-highlight; 50 | } 51 | } 52 | } 53 | 54 | // Common styles default to most themes 55 | [data-ea-publisher].loaded, 56 | [data-ea-type].loaded { 57 | font-size: 14px; 58 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 59 | Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, 60 | Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 61 | font-weight: normal; 62 | font-style: normal; 63 | letter-spacing: 0px; 64 | vertical-align: baseline; 65 | line-height: 1.3em; 66 | 67 | a { 68 | text-decoration: none; 69 | } 70 | 71 | .ea-pixel { 72 | display: none; 73 | } 74 | 75 | .ea-content { 76 | margin: 1em 1em 0.5em 1em; 77 | padding: 1em; 78 | 79 | background: $color-background; 80 | color: $color-link; 81 | 82 | @include links($color-link, darken($color-link, 10%), $color-link-bold); 83 | } 84 | .ea-callout { 85 | @include links( 86 | $color-link-callout, 87 | darken($color-link-callout, 10%), 88 | $color-link-bold 89 | ); 90 | a { 91 | font-size: 0.8em; 92 | } 93 | } 94 | 95 | .ea-domain { 96 | margin-top: 0.75em; 97 | font-size: 0.8em; 98 | text-align: center; 99 | color: lighten($color-link-callout, 20%); 100 | } 101 | 102 | &.dark { 103 | .ea-content { 104 | background: $color-background-dark; 105 | color: $color-link-dark; 106 | @include links( 107 | $color-link-dark, 108 | lighten($color-link-dark, 10%), 109 | $color-link-bold-dark 110 | ); 111 | } 112 | 113 | .ea-callout { 114 | @include links( 115 | $color-link-callout-dark, 116 | lighten($color-link-callout-dark, 10%), 117 | $color-link-bold-dark 118 | ); 119 | } 120 | 121 | .ea-domain { 122 | color: darken($color-link-callout-dark, 20%); 123 | } 124 | } 125 | 126 | &.adaptive { 127 | // For adaptive color schemes, the default is light 128 | // TODO: Find a way to reuse these definitions from dark class above 129 | @media (prefers-color-scheme: dark) { 130 | .ea-content { 131 | background: $color-background-dark; 132 | color: $color-link-dark; 133 | @include links( 134 | $color-link-dark, 135 | lighten($color-link-dark, 10%), 136 | $color-link-bold-dark 137 | ); 138 | } 139 | 140 | .ea-callout { 141 | @include links( 142 | $color-link-callout-dark, 143 | lighten($color-link-callout-dark, 10%), 144 | $color-link-bold-dark 145 | ); 146 | } 147 | 148 | .ea-domain { 149 | color: darken($color-link-callout-dark, 20%); 150 | } 151 | } 152 | } 153 | } 154 | 155 | // Themes 156 | @mixin theme-flat { 157 | .ea-content { 158 | border: 0px; 159 | border-radius: 3px; 160 | box-shadow: none; 161 | } 162 | } 163 | 164 | @mixin theme-raised { 165 | .ea-content { 166 | border: 0px; 167 | border-radius: 3px; 168 | box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15); 169 | } 170 | } 171 | 172 | @mixin theme-bordered { 173 | .ea-content { 174 | border: 1px solid opacify($color-background, 0.01); 175 | border-radius: 3px; 176 | box-shadow: none; 177 | } 178 | &.dark .ea-content { 179 | border: 1px solid opacify($color-background-dark, 0.02); 180 | } 181 | 182 | &.adaptive { 183 | @media (prefers-color-scheme: dark) { 184 | .ea-content { 185 | border: 1px solid opacify($color-background-dark, 0.02); 186 | } 187 | } 188 | } 189 | } 190 | 191 | [data-ea-publisher] { 192 | --ea-container-md: 720px; 193 | --ea-container-lg: 960px; 194 | --ea-container-xl: 1040px; 195 | 196 | --ea-image-width: 120px; 197 | --ea-image-width-xs: 44px; 198 | 199 | --ea-fixedheader-height: 50px; 200 | 201 | // Necessary to reference SCSS colors with CSS properties 202 | // https://sass-lang.com/documentation/breaking-changes/css-vars/ 203 | --ea-stylefixed-bgcolor: #{$color-link-dark}; 204 | --ea-stylefixed-bgcolor-dark: #{$color-link}; 205 | 206 | --ea-link-color: #{$color-link}; 207 | --ea-link-color-active: #{darken($color-link, 10%)}; 208 | --ea-link-color-callout: #{$color-link-callout}; 209 | --ea-link-color-callout-active: #{darken($color-link-callout, 10%)}; 210 | --ea-link-color-bold: #{$color-link-bold}; 211 | --ea-link-color-dark: #{$color-link-dark}; 212 | --ea-link-color-dark-active: #{lighten($color-link-dark, 10%)}; 213 | --ea-link-color-callout-dark: #{$color-link-callout-dark}; 214 | --ea-link-color-callout-dark-active: #{lighten($color-link-callout-dark, 10%)}; 215 | --ea-link-color-bold-dark: #{$color-link-bold-dark}; 216 | } 217 | 218 | [data-ea-publisher].loaded, 219 | [data-ea-type].loaded { 220 | @include theme-raised; 221 | 222 | &.raised { 223 | @include theme-raised; 224 | } 225 | &.bordered { 226 | @include theme-bordered; 227 | } 228 | &.flat { 229 | @include theme-flat; 230 | } 231 | } 232 | 233 | // Image placement 234 | // ------------------------------------------------------------------------- 235 | // Explicit image ad type, or default image ad type not specified 236 | // https://ethical-ad-client.readthedocs.io/en/latest/#image-placement 237 | [data-ea-type="image"].loaded, 238 | [data-ea-publisher]:not([data-ea-type]).loaded, 239 | .ea-type-image { 240 | display: inline-block; 241 | 242 | .ea-content { 243 | max-width: $width-image-vertical; 244 | overflow: auto; 245 | 246 | text-align: center; 247 | 248 | > a > img { 249 | width: 120px; 250 | height: 90px; 251 | display: inline-block; 252 | } 253 | 254 | > .ea-text { 255 | margin-top: 1em; 256 | font-size: 1em; 257 | text-align: center; 258 | } 259 | } 260 | 261 | .ea-callout { 262 | max-width: $width-image-vertical; 263 | margin: 0em 1em 1em 1em; 264 | padding-left: 1em; 265 | padding-right: 1em; 266 | font-style: italic; 267 | text-align: right; 268 | } 269 | 270 | &.horizontal { 271 | .ea-content { 272 | max-width: $width-image-horizontal; 273 | 274 | > a > img { 275 | float: left; 276 | margin-right: 1em; 277 | } 278 | 279 | .ea-text { 280 | margin-top: 0em; 281 | text-align: left; 282 | overflow: auto; 283 | } 284 | } 285 | 286 | .ea-callout { 287 | max-width: $width-image-horizontal; 288 | text-align: right; 289 | } 290 | } 291 | } 292 | 293 | // Text placement (text-only ad type) 294 | // ------------------------------------------------------------------------- 295 | // https://ethical-ad-client.readthedocs.io/en/latest/#text-placement 296 | [data-ea-type="text"].loaded, 297 | .ea-type-text { 298 | font-size: 14px; 299 | 300 | .ea-content { 301 | text-align: left; 302 | } 303 | 304 | .ea-callout { 305 | margin: 0.5em 1em 1em 1em; 306 | padding-left: 1em; 307 | padding-right: 1em; 308 | text-align: right; 309 | font-style: italic; 310 | } 311 | } 312 | 313 | // Stickybox ad 314 | // ------------------------------------------------------------------------- 315 | // The stickybox is an ad that is shown in a floating box in the lower right 316 | // on very wide screens or a standard image ad 317 | // (text-only not supported) on screens less wide. 318 | // https://ethical-ad-client.readthedocs.io/en/latest/#stickybox 319 | [data-ea-style="stickybox"].loaded { 320 | // The outer div containing data-ea-publisher and data-ea-type 321 | // Needs to be positioned when using fixed positioning 322 | // in order for viewport detection to function correctly. 323 | position: fixed; 324 | bottom: 20px; 325 | right: 20px; 326 | z-index: $zindex-styles-stickybox; 327 | 328 | .ea-type-image { 329 | .ea-stickybox-hide { 330 | cursor: pointer; 331 | position: absolute; 332 | top: 0.75em; 333 | right: 0.75em; 334 | 335 | background-color: #fefefe; 336 | border: 1px solid #088cdb; 337 | border-radius: 50%; 338 | color: #088cdb; 339 | font-size: 1em; 340 | text-align: center; 341 | height: 1.5em; 342 | width: 1.5em; 343 | /* MDN says prefer unitless line-heights */ 344 | /* and the times symbol vertically centers best with this value */ 345 | line-height: 1.4; 346 | } 347 | } 348 | 349 | @media (max-width: $breakpoint-ultrawide) { 350 | position: static; 351 | bottom: 0; 352 | right: 0; 353 | margin: auto; 354 | text-align: center; 355 | 356 | .ea-stickybox-hide { 357 | display: none; 358 | } 359 | } 360 | 361 | // Our ads are normally partially transparent 362 | // This doesn't work with floating ads 363 | @media (min-width: $breakpoint-ultrawide + 1) { 364 | .ea-type-image .ea-content { 365 | background: $color-bg-stickybox; 366 | } 367 | &.dark .ea-type-image .ea-content { 368 | background: $color-bg-dark-stickybox; 369 | } 370 | &.adaptive { 371 | @media (prefers-color-scheme: dark) { 372 | .ea-type-image .ea-content { 373 | background: $color-bg-dark-stickybox; 374 | } 375 | } 376 | } 377 | } 378 | } 379 | 380 | // FixedFooter ad 381 | // ------------------------------------------------------------------------- 382 | // https://ethical-ad-client.readthedocs.io/en/latest/#fixedfooter 383 | [data-ea-style="fixedfooter"].loaded { 384 | // The outer div containing data-ea-publisher and data-ea-type 385 | // Needs to be positioned when using fixed positioning 386 | // in order for viewport detection to function correctly. 387 | position: fixed; 388 | bottom: 0; 389 | left: 0; 390 | z-index: $zindex-styles-fixedfooter; 391 | 392 | width: 100%; 393 | max-width: 100%; 394 | 395 | .ea-type-text { 396 | width: 100%; 397 | max-width: 100%; 398 | display: flex; 399 | 400 | z-index: $zindex-styles-fixedfooter; 401 | background: $color-bg-stickybox; 402 | 403 | @include theme-flat; 404 | 405 | .ea-content { 406 | background-color: inherit; 407 | max-width: 100%; 408 | margin: 0; 409 | padding: 1em; 410 | flex: auto; // Expand to fill remaining space 411 | } 412 | 413 | .ea-callout { 414 | max-width: 100%; 415 | margin: 0; 416 | padding: 1em; 417 | flex: initial; // Use the initial width of this item - does not expend 418 | 419 | // Hide Ads by EthicalAds on mobile 420 | @media (max-width: $breakpoint-mobile) { 421 | display: none; 422 | } 423 | } 424 | 425 | .ea-fixedfooter-hide { 426 | cursor: pointer; 427 | color: $color-link; 428 | padding: 1em; 429 | flex: initial; // Use the initial width of this item - does not expend 430 | margin: auto 0; 431 | 432 | // Make the close button - button-like 433 | span { 434 | padding: 0.25em; 435 | font-size: 0.8em; 436 | font-weight: bold; 437 | border: 0.15em solid $color-link; 438 | border-radius: 0.5em; 439 | white-space: nowrap; 440 | } 441 | } 442 | } 443 | 444 | .ea-type-image { 445 | // Fixed footer is for text-only ads (for now) 446 | display: none !important; 447 | } 448 | 449 | &.dark { 450 | .ea-type-text { 451 | background: $color-bg-dark-stickybox; 452 | 453 | .ea-fixedfooter-hide span { 454 | color: $color-link-dark; 455 | border-color: $color-link-dark; 456 | } 457 | } 458 | } 459 | 460 | &.adaptive { 461 | @media (prefers-color-scheme: dark) { 462 | .ea-type-text { 463 | background: $color-bg-dark-stickybox; 464 | 465 | .ea-fixedfooter-hide span { 466 | color: $color-link-dark; 467 | border-color: $color-link-dark; 468 | } 469 | } 470 | } 471 | } 472 | } 473 | 474 | // Fixed Header ad 475 | // ------------------------------------------------------------------------- 476 | // https://ethical-ad-client.readthedocs.io/en/latest/#fixedheader 477 | [data-ea-style="fixedheader"] { 478 | height: var(--ea-fixedheader-height); 479 | width: 100%; 480 | max-width: 100%; 481 | 482 | background: var(--ea-stylefixed-bgcolor); 483 | border-bottom: 1px solid var(--ea-background-color); 484 | 485 | // Hide the fixedheader ad completely below tablet width 486 | @media (max-width: $breakpoint-md) { 487 | display: none !important; 488 | } 489 | } 490 | 491 | [data-ea-style="fixedheader"].loaded { 492 | // The outer div containing data-ea-publisher and data-ea-type 493 | 494 | // Text-only or text+image 495 | .ea-type-image, 496 | .ea-type-text { 497 | width: var(--ea-container-xl); 498 | margin: 0 auto; 499 | display: flex; 500 | 501 | @media (max-width: $breakpoint-lg) { 502 | width: var(--ea-container-md); 503 | } 504 | @media (max-width: $breakpoint-xl) { 505 | width: var(--ea-container-lg); 506 | } 507 | 508 | @include theme-flat; 509 | 510 | .ea-content { 511 | background-color: inherit; 512 | max-width: 100%; 513 | margin: 0; 514 | padding: 0; 515 | flex: auto; // Expand to fill remaining space 516 | display: flex; 517 | 518 | color: var(--ea-link-color); 519 | a { 520 | &:link { 521 | color: var(--ea-link-color); 522 | } 523 | &:visited { 524 | color: var(--ea-link-color); 525 | } 526 | &:hover { 527 | color: var(--ea-link-color-active); 528 | } 529 | &:active { 530 | color: var(--ea-link-color-active); 531 | } 532 | strong, 533 | b { 534 | color: var(--ea-link-color-bold); 535 | } 536 | } 537 | } 538 | 539 | .ea-content .ea-text { 540 | margin-top: 0; 541 | padding: 1em; 542 | flex: auto; 543 | text-align: left; 544 | } 545 | 546 | .ea-callout { 547 | max-width: 100%; 548 | margin: 0; 549 | padding: 1em; 550 | flex: initial; // Use the initial width of this item - does not expend 551 | 552 | color: var(--ea-link-color-callout); 553 | a { 554 | &:link { 555 | color: var(--ea-link-color-callout); 556 | } 557 | &:visited { 558 | color: var(--ea-link-color-callout); 559 | } 560 | &:hover { 561 | color: var(--ea-link-color-callout-active); 562 | } 563 | &:active { 564 | color: var(--ea-link-color-callout-active); 565 | } 566 | strong, 567 | b { 568 | color: var(--ea-link-color-callout); 569 | } 570 | } 571 | 572 | // Hide Ads by EthicalAds on mobile 573 | @media (max-width: $breakpoint-mobile) { 574 | display: none; 575 | } 576 | } 577 | } 578 | 579 | // Text+image (displayed horizontally) 580 | .ea-type-image { 581 | img { 582 | width: var(--ea-image-width-xs) !important; 583 | height: auto !important; 584 | margin: 0.6em; 585 | } 586 | 587 | .ea-domain { 588 | // Doesn't fit with the image 589 | display: none; 590 | } 591 | } 592 | 593 | // Text-only 594 | .ea-type-text { 595 | } 596 | 597 | &.dark { 598 | background-color: var(--ea-stylefixed-bgcolor-dark); 599 | 600 | .ea-content { 601 | color: var(--ea-link-color-dark); 602 | a { 603 | &:link { 604 | color: var(--ea-link-color-dark); 605 | } 606 | &:visited { 607 | color: var(--ea-link-color-dark); 608 | } 609 | &:hover { 610 | color: var(--ea-link-color-dark-active); 611 | } 612 | &:active { 613 | color: var(--ea-link-color-dark-active); 614 | } 615 | strong, 616 | b { 617 | color: var(--ea-link-color-bold-dark); 618 | } 619 | } 620 | } 621 | 622 | .ea-callout { 623 | color: var(--ea-link-color-callout-dark); 624 | a { 625 | &:link { 626 | color: var(--ea-link-color-callout-dark); 627 | } 628 | &:visited { 629 | color: var(--ea-link-color-callout-dark); 630 | } 631 | &:hover { 632 | color: var(--ea-link-color-callout-dark-active); 633 | } 634 | &:active { 635 | color: var(--ea-link-color-callout-dark-active); 636 | } 637 | strong, 638 | b { 639 | color: var(--ea-link-color-callout-dark); 640 | } 641 | } 642 | } 643 | } 644 | 645 | &.adaptive { 646 | @media (prefers-color-scheme: dark) { 647 | background-color: var(--ea-stylefixed-bgcolor-dark); 648 | 649 | .ea-content { 650 | color: var(--ea-link-color-dark); 651 | a { 652 | &:link { 653 | color: var(--ea-link-color-dark); 654 | } 655 | &:visited { 656 | color: var(--ea-link-color-dark); 657 | } 658 | &:hover { 659 | color: var(--ea-link-color-dark-active); 660 | } 661 | &:active { 662 | color: var(--ea-link-color-dark-active); 663 | } 664 | strong, 665 | b { 666 | color: var(--ea-link-color-bold-dark); 667 | } 668 | } 669 | } 670 | 671 | .ea-callout { 672 | color: var(--ea-link-color-callout-dark); 673 | a { 674 | &:link { 675 | color: var(--ea-link-color-callout-dark); 676 | } 677 | &:visited { 678 | color: var(--ea-link-color-callout-dark); 679 | } 680 | &:hover { 681 | color: var(--ea-link-color-callout-dark-active); 682 | } 683 | &:active { 684 | color: var(--ea-link-color-callout-dark-active); 685 | } 686 | strong, 687 | b { 688 | color: var(--ea-link-color-callout-dark); 689 | } 690 | } 691 | } 692 | } 693 | } 694 | } 695 | -------------------------------------------------------------------------------- /tests/auto-placement.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/common.inc.js: -------------------------------------------------------------------------------- 1 | import { default as sinon } from "sinon"; 2 | 3 | import { Placement } from "../index"; 4 | 5 | export function mockAdDecision() { 6 | // Don't actually call the server 7 | // https://sinonjs.org/releases/v17/stubs/ 8 | let stub = sinon.stub(Placement.prototype, "fetch"); 9 | const response_html = "
"; 10 | const elem_placement = document.createElement("div"); 11 | elem_placement.innerHTML = response_html; 12 | stub.resolves(elem_placement.firstChild); 13 | 14 | return stub; 15 | } 16 | -------------------------------------------------------------------------------- /tests/keyword-detection.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

6 | PyTorch is an important module for machine learning. PyTorch can use 7 | your GPU to crunch numbers faster than a CPU. 8 |

9 |
10 | 11 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/missing-placement.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/placement-rotation.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /web-test-runner.config.mjs: -------------------------------------------------------------------------------- 1 | import { importMapsPlugin } from "@web/dev-server-import-maps"; 2 | import { fromRollup, rollupAdapter } from "@web/dev-server-rollup"; 3 | import { chromeLauncher } from "@web/test-runner-chrome"; 4 | import rollupCommonjs from "@rollup/plugin-commonjs"; 5 | import rollupImage from "@rollup/plugin-image"; 6 | import rollupJson from "@rollup/plugin-json"; 7 | import rollupLitCss from "rollup-plugin-lit-css"; 8 | import rollupScss from "rollup-plugin-scss"; 9 | 10 | const pluginCommonjs = fromRollup(rollupCommonjs); 11 | 12 | export default { 13 | rootDir: ".", 14 | files: ["./tests/**/*.html"], 15 | preserveSymlinks: true, 16 | nodeResolve: {}, 17 | mimeTypes: { 18 | "**/*.scss": "js", 19 | "**/*.css": "js", 20 | "**/*.svg": "js", 21 | "**/*.json": "js", 22 | }, 23 | plugins: [ 24 | rollupAdapter(rollupScss({ output: false, verbose: true })), 25 | rollupAdapter(rollupJson()), 26 | rollupAdapter(rollupImage()), 27 | rollupAdapter(rollupLitCss()), 28 | pluginCommonjs({ 29 | include: ["node_modules/**"], 30 | requireReturnsDefault: "preferred", 31 | }), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | 6 | // Use export as a function to inspect `--mode` 7 | module.exports = (env, argv) => { 8 | const production = argv.mode == "production"; 9 | 10 | return { 11 | entry: "./index.js", 12 | output: { 13 | path: path.resolve(__dirname, "dist"), 14 | filename: production ? "ethicalads.min.js" : "ethicalads.js", 15 | library: ["ethicalads"], 16 | globalObject: "this", 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.scss$/, 22 | use: ["style-loader", "css-loader", "sass-loader"], 23 | }, 24 | { 25 | test: /\.js$/, 26 | exclude: /(node_modules)/, 27 | use: { 28 | loader: "babel-loader", 29 | options: { 30 | presets: ["@babel/preset-env"], 31 | }, 32 | }, 33 | }, 34 | ], 35 | }, 36 | optimization: { 37 | minimize: production, 38 | minimizer: [new TerserPlugin(), new OptimizeCssAssetsPlugin({})], 39 | }, 40 | watchOptions: { 41 | aggregateTimeout: 300, 42 | poll: 1000, 43 | ignored: ["./node_modules/"], 44 | }, 45 | devServer: { 46 | open: false, 47 | hot: false, 48 | liveReload: true, 49 | publicPath: "/dist/", 50 | disableHostCheck: true, 51 | headers: { 52 | "Access-Control-Allow-Origin": "*", 53 | }, 54 | }, 55 | }; 56 | }; 57 | --------------------------------------------------------------------------------