-
Notifications
You must be signed in to change notification settings - Fork 7
/
ethicalads.js
280 lines (244 loc) · 9.69 KB
/
ethicalads.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import { ajv } from "./data-validation";
import { AddonBase } from "./utils";
import { default as objectPath } from "object-path";
import styleSheet from "./ethicalads.css";
import { IS_TESTING, docTool } from "./utils.js";
// https://docs.readthedocs.io/en/stable/advertising/ad-customization.html#controlling-the-placement-of-an-ad
const EXPLICIT_PLACEMENT_SELECTORS = [
"#ethical-ad-placement",
"[data-ea-publisher]",
];
// https://ethical-ad-client.readthedocs.io/en/latest/
const AD_PLACEMENT_BOTTOM = "90px";
const AD_SIZE = 300; // pixels
const AD_SCRIPT_ID = "ethicaladsjs";
/**
* EthicalAds addon
*
* Show an ad in the documentation page.
*
* Read more at:
* - https://docs.readthedocs.io/en/stable/advertising/ethical-advertising.html
*
* @param {Object} config - Addon configuration object
*/
export class EthicalAdsAddon extends AddonBase {
static jsonValidationURI =
"http://v1.schemas.readthedocs.org/addons.ethicalads.json";
static addonEnabledPath = "addons.ethicalads.enabled";
static addonName = "EthicalAds";
constructor(config) {
super();
this.config = config;
this.injectEthicalAds();
}
createAdPlacement() {
let placement;
// TODO: fix this on testing. It works fine on production/regular browser.
// TypeError: Failed to execute 'indexed value' on 'ObservableArray<CSSStyleSheet>': Failed to convert value to 'CSSStyleSheet'.
if (!IS_TESTING) {
// Include CSS into the DOM so they can be read.
document.adoptedStyleSheets.push(styleSheet);
}
for (const explicitSelector of EXPLICIT_PLACEMENT_SELECTORS) {
placement = document.querySelector(explicitSelector);
if (placement) break;
}
if (placement) {
if (
placement.getAttribute("data-ea-type") !== "image" &&
placement.getAttribute("data-ea-type") !== "text"
) {
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
}
} else {
// Inject our own floating element
placement = document.createElement("div");
placement.setAttribute("id", "readthedocs-ea");
placement.classList.add("raised");
// Define where to inject the Ad based on the theme and if it's above the fold or not.
let selector;
let element;
let knownPlacementFound = false;
if (docTool.isSphinxReadTheDocsLikeTheme()) {
selector = "nav.wy-nav-side > div.wy-side-scroll";
element = document.querySelector(selector);
if (this.elementAboveTheFold(element)) {
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
placement.classList.add("ethical-rtd");
placement.classList.add("ethical-dark-theme");
knownPlacementFound = true;
}
} else if (docTool.isSphinxFuroLikeTheme()) {
// NOTE: The code to handle furo theme shouldn't be required,
// since furo uses explicit placement.
// However, the Jinja context variable READTHEDOCS is not injected anymore,
// and furo does not includes the explicit placement due to this.
// This is a temporal solution while they fix this.
selector = ".sidebar-tree";
element = document.querySelector(selector);
if (this.elementAboveTheFold(element)) {
placement.classList.add("ethical-alabaster");
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
placement.setAttribute("id", "furo-sidebar-ad-placement");
knownPlacementFound = true;
}
} else if (docTool.isSphinxBookThemeLikeTheme()) {
selector = ".sidebar-primary-items__start.sidebar-primary__section";
element = document.querySelector(selector);
if (this.elementAboveTheFold(element)) {
placement.classList.add("ethical-alabaster");
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
knownPlacementFound = true;
}
} else if (docTool.isSphinxAlabasterLikeTheme()) {
selector = "div.sphinxsidebar > div.sphinxsidebarwrapper";
element = document.querySelector(selector);
if (this.elementAboveTheFold(element)) {
placement.classList.add("ethical-alabaster");
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
knownPlacementFound = true;
}
} else if (docTool.isMaterialMkDocsTheme()) {
selector = ".md-sidebar__scrollwrap";
element = document.querySelector(selector);
if (this.elementAboveTheFold(element)) {
// TODO: use a more styled CSS class.
// See https://github.com/readthedocs/ethical-ad-client/issues/193
placement.classList.add("ethical-alabaster");
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
knownPlacementFound = true;
}
} else if (docTool.isDocusaurusTheme()) {
selector = ".menu.thin-scrollbar.menu_SIkG";
element = document.querySelector(selector);
if (this.elementAboveTheFold(element)) {
// TODO: use a more styled CSS class.
// See https://github.com/readthedocs/ethical-ad-client/issues/193
placement.classList.add("ethical-alabaster");
placement.classList.add("ethical-docusaurus");
placement.setAttribute("data-ea-type", "readthedocs-sidebar");
placement.setAttribute("data-ea-style", "image");
knownPlacementFound = true;
}
}
if (selector && knownPlacementFound) {
const elementToAppend = document.querySelector(selector);
if (elementToAppend) {
elementToAppend.append(placement);
}
} else if (window.innerWidth > 1300) {
// https://ethical-ad-client.readthedocs.io/en/latest/#stickybox
placement.setAttribute("data-ea-type", "image");
placement.setAttribute("data-ea-style", "stickybox");
this.addEaPlacementToElement(placement);
// `document.body` here is not too much relevant, since we are going to
// use this selector only for a floating stickybox ad
const elementInsertBefore = document.body;
elementInsertBefore.insertBefore(
placement,
elementInsertBefore.lastChild,
);
}
}
// Optional attributes coming from the API JSON response.
// We get them if exists or return default values otherwise.
const data = this.config.addons.ethicalads;
const keywords = objectPath.get(data, "keywords", []);
const campaign_types = objectPath.get(data, "campaign_types", []);
if (placement !== null) {
// This ensure us that all the `data-ea-*` attributes are already set in the HTML tag.
placement.setAttribute("data-ea-manual", "true");
// Set the keyword, campaign data, and publisher
placement.setAttribute("data-ea-publisher", data.publisher);
if (keywords.length) {
placement.setAttribute("data-ea-keywords", keywords.join("|"));
}
if (campaign_types.length) {
placement.setAttribute(
"data-ea-campaign-types",
campaign_types.join("|"),
);
}
}
return placement;
}
elementAboveTheFold(element) {
// Determine if this element would be above the fold.
// If this is off screen, instead create an ad in the footer.
// Assumes the ad would be AD_SIZE pixels high.
const div = document.createElement("div");
// Append a temporary element to check if it's visible on the screen.
element.append(div);
const offsetTop = div.offsetTop;
div.remove();
if (
!offsetTop ||
offsetTop - window.scrollY + AD_SIZE > window.innerHeight
) {
return false;
}
return true;
}
addEaPlacementToElement(element) {
// Add `ea-placement-bottom` to the element only if the flyout is enabled.
const flyoutEnabled = objectPath.get(
this.config,
"addons.flyout.enabled",
false,
);
if (flyoutEnabled) {
element.setAttribute("data-ea-placement-bottom", AD_PLACEMENT_BOTTOM);
}
}
loadEthicalAdLibrary() {
const library = document.createElement("script");
library.setAttribute("id", AD_SCRIPT_ID);
library.setAttribute("type", "text/javascript");
library.setAttribute("async", true);
// TODO: inject the stable version after we have tested this.
// Inject the Ethical Ad client (beta) only for our own documentation.
let src;
if (
window.location.hostname === "docs.readthedocs.io" ||
window.location.hostname.endsWith(".devthedocs.org")
) {
src = "https://media.ethicalads.io/media/client/beta/ethicalads.min.js";
} else {
src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
}
library.setAttribute("src", src);
document.head.appendChild(library);
document.getElementById(AD_SCRIPT_ID).addEventListener("load", function () {
if (typeof ethicalads !== "undefined") {
ethicalads.load();
}
});
}
addWidthListener() {
// Hide ad when the window is smaller than the stickybox width
const placement = document.querySelector("#readthedocs-ea");
if (placement && placement.dataset.eaStyle === "stickybox") {
window.addEventListener("resize", function () {
if (window.innerWidth <= 1300) {
placement.style.display = "none";
} else {
placement.style.display = "";
}
});
}
}
injectEthicalAds() {
// Create the placement first and after that load the EthicalAd library.
// This will automatically "load_placement" and render the ad properly.
this.createAdPlacement();
this.addWidthListener();
this.loadEthicalAdLibrary();
}
static isEnabled(config, httpStatus) {
return (
super.isEnabled(config, httpStatus) &&
config.addons.ethicalads.ad_free === false
);
}
}