Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use an AddonsApplication to manage load/reload of all addons #504

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c305707
Create an `AddonsApplication` to manage the `ContextRoot`
humitos Jan 22, 2025
86d0952
Use ContextRoot at document.html instead (#499)
agjohnson Jan 16, 2025
6887d85
Delete `index.js` file
humitos Jan 22, 2025
6a3946a
Merge branch 'main' of github.com:readthedocs/addons into humitos/lit…
humitos Jan 22, 2025
19c5879
Add `internal` attribute to the data event
humitos Jan 22, 2025
e3e70fb
Hit the API addons endpoint to get fresh data when URL changes
humitos Jan 22, 2025
9c7b711
Update docdiff to not define the custom element on constructor()
humitos Jan 22, 2025
892a26a
Read the `readthedocs.resolver.filename` attribute to generate links
humitos Jan 22, 2025
fb64f99
Update tests to pass with the new changes
humitos Jan 22, 2025
532cbb0
Install Lit Context as depedency
humitos Jan 22, 2025
237aa0d
Use just regular JavaScript events instead of `ContextRoot`
humitos Jan 23, 2025
8735a84
Merge branch 'main' of github.com:readthedocs/addons into humitos/lit…
humitos Jan 28, 2025
cf6e359
Require URL param for the flyout
humitos Jan 29, 2025
0ca5686
Minor updates
humitos Jan 29, 2025
ac58d84
Do not add the ethicalads js if it's already on the page
humitos Jan 29, 2025
3541046
Do not add custom script if it already exists in the page
humitos Jan 29, 2025
8720db6
Cleanup
humitos Jan 29, 2025
b3e3bc5
Merge branch 'main' of github.com:readthedocs/addons into humitos/lit…
humitos Jan 29, 2025
6df4c90
Keep a copy of addons instances and re-load them
humitos Jan 30, 2025
40cf0d6
Do not dispatch the URL changed event twice
humitos Jan 30, 2025
e5b9e5a
Update filetreediff tests
humitos Jan 30, 2025
eae8f6b
Detect if a doctool is SPA or not
humitos Jan 30, 2025
e3f3068
Docstring update
humitos Jan 30, 2025
24e8240
No need to `requestUpdate` here
humitos Jan 30, 2025
c62952d
Clean initialization code (don't use `Promise`)
humitos Feb 4, 2025
d1c96fe
Trigger the URL_CHANGED event only if from/to URL are different (#516)
humitos Feb 4, 2025
42081c0
Merge branch 'main' into humitos/lit-context-app
humitos Feb 4, 2025
e1e0eec
Merge branch 'humitos/lit-context-app' of github.com:readthedocs/addo…
humitos Feb 6, 2025
afefdf8
Merge branch 'main' of github.com:readthedocs/addons into humitos/lit…
humitos Feb 6, 2025
cc9095c
Merge branch 'main' into humitos/lit-context-app
humitos Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/_/readthedocs-addons.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
"readthedocs": {
"analytics": {
"code": "UA-12345"
},
"resolver": {
"filename": "/index.html"
}
},
"addons": {
Expand Down
3 changes: 3 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ <h1 id="documentation-addons">Documentation Addons</h1>
<h2 id="docdiff">CustomEvent</h2>
<p>Project slug using <em>CustomEvent</em>: <span id="custom-event-project-slug"></span></p>

<h2 id="flyout">Flyout</h2>
<readthedocs-flyout></readthedocs-flyout>

<h2 id="docdiff">DocDiff</h2>
<p>Visit <a href="docdiff.html">this page</a> to take a look at it.</p>

Expand Down
6 changes: 2 additions & 4 deletions src/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import { default as fetch } from "unfetch";

import { ajv } from "./data-validation";
import { AddonBase } from "./utils";
import { CLIENT_VERSION } from "./utils";
import { AddonBase, CLIENT_VERSION } from "./utils";

export const API_ENDPOINT = "/_/api/v2/analytics/";

Expand All @@ -27,8 +26,7 @@ export class AnalyticsAddon extends AddonBase {
static addonName = "Analytics";
static enabledOnHttpStatus = [200, 404];

constructor(config) {
super();
loadConfig(config) {
this.config = config;

// Only register pageviews on non-external versions
Expand Down
174 changes: 174 additions & 0 deletions src/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { CSSResult } from "lit";

import {
docTool,
domReady,
isEmbedded,
IS_PRODUCTION,
setupLogging,
setupHistoryEvents,
getMetadataValue,
} from "./utils";
import { getReadTheDocsConfig } from "./readthedocs-config";
import {
EVENT_READTHEDOCS_ADDONS_INTERNAL_DATA_READY,
EVENT_READTHEDOCS_URL_CHANGED,
} from "./events";

import * as notification from "./notification";
import * as analytics from "./analytics";
import * as search from "./search";
import * as docdiff from "./docdiff";
import * as flyout from "./flyout";
import * as ethicalads from "./ethicalads";
import * as hotkeys from "./hotkeys";
import * as linkpreviews from "./linkpreviews";
import * as filetreediff from "./filetreediff";
import * as customscript from "./customscript";
import * as application from "./application";
import { default as objectPath } from "object-path";

import doctoolsStyleSheet from "./doctools.css";

export class AddonsApplication {
constructor() {
setupLogging();
setupHistoryEvents();

this.addonsInstances = [];
this.config = null;

console.debug(
"Addons Application config (from constructor() method)",
this.config,
);

this.addons = [
flyout.FlyoutAddon,
notification.NotificationAddon,
analytics.AnalyticsAddon,
ethicalads.EthicalAdsAddon,
search.SearchAddon,

// HotKeys & FileTreeDiff have to be initialized before DocDiff because when
// `?readthedocs-diff=true` DocDiff triggers an event that HotKeys has to
// listen to to update its internal state.
// https://github.com/readthedocs/addons/blob/47645b013724cdf244716b549a5baa28409fafcb/src/docdiff.js#L105-L111
hotkeys.HotKeysAddon,
filetreediff.FileTreeDiffAddon,
docdiff.DocDiffAddon,

linkpreviews.LinkPreviewsAddon,
customscript.CustomScriptAddon,
];

this.httpStatus = getMetadataValue("readthedocs-http-status");

this.addDoctoolData();
getReadTheDocsConfig(this.sendUrlParam());
}

reload(config) {
console.debug("Addons Application config (from reload() method)", config);

if (!config) {
return;
}
this.config = config;
if (this.config && !this.loadWhenEmbedded()) {
return;
}

if (!this.addonsInstances.length) {
// Addons instances were not created yet
try {
this.addonsInstances = this.addons
.filter((addon) => addon.isEnabled(this.config, this.httpStatus))
.map((addon) => new addon(this.config));
} catch (err) {
console.error(err);
}
} else {
// Addons instances were already created. We just need to reload them with
// the new config object.
for (const addon of this.addonsInstances) {
addon.loadConfig(config);
}
}
return;
}

loadWhenEmbedded() {
const loadWhenEmbedded = objectPath.get(
this.config,
"addons.options.load_when_embedded",
false,
);
if (isEmbedded() && !loadWhenEmbedded) {
return false;
}
return true;
}

sendUrlParam() {
for (const addon of this.addons) {
if (addon.requiresUrlParam()) {
console.debug(`${addon.addonName} requires "url=" parameter.`);
return true;
}
}
return false;
}

addDoctoolData() {
// Apply fixes to variables for individual documentation tools
const elementHtml = document.querySelector("html");
if (elementHtml) {
// Inject styles at the parent DOM to set variables at :root
let styleSheet = doctoolsStyleSheet;
if (doctoolsStyleSheet instanceof CSSResult) {
styleSheet = doctoolsStyleSheet.styleSheet;
}
document.adoptedStyleSheets = [styleSheet];

// If we detect a documentation tool, set attributes on :root to allow
// for CSS selectors to utilize these values.
if (docTool.documentationTool) {
elementHtml.setAttribute(
"data-readthedocs-tool",
docTool.documentationTool,
);
}
if (docTool.documentationTheme) {
elementHtml.setAttribute(
"data-readthedocs-tool-theme",
docTool.documentationTheme,
);
}
}
}
}

export const addonsApplication = new AddonsApplication();

/**
* Subscribe to `EVENT_READTHEDOCS_ADDONS_INTERNAL_DATA_READY` to reload all the
* addons with fresh Addons API data once it's ready.
*
*/
document.addEventListener(
EVENT_READTHEDOCS_ADDONS_INTERNAL_DATA_READY,
(event) => {
addonsApplication.reload(event.detail.data(true));
},
);

/**
* Subscribe to `EVENT_READTHEDOCS_URL_CHANGED` to trigger a new request to
* Addons API to fetch fresh data.
*
*/
window.addEventListener(EVENT_READTHEDOCS_URL_CHANGED, (event) => {
console.debug("URL Change detected. Triggering a new API call", event);
getReadTheDocsConfig(addonsApplication.sendUrlParam());
});
8 changes: 6 additions & 2 deletions src/customscript.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export class CustomScriptAddon extends AddonBase {
static addonName = "CustomScript";
static enabledOnHttpStatus = [200, 403, 404, 500];

constructor(config) {
super();
loadConfig(config) {
this.config = config;

if (objectPath.get(this.config, "addons.customscript.src")) {
Expand All @@ -25,6 +24,11 @@ export class CustomScriptAddon extends AddonBase {
}

injectJavaScriptFile() {
// Do not add the script if it already exists in the page.
if (document.querySelector(`#${SCRIPT_ID}`) !== null) {
return;
}

const script = document.createElement("script");
script.id = SCRIPT_ID;
script.src = objectPath.get(this.config, "addons.customscript.src");
Expand Down
45 changes: 42 additions & 3 deletions src/data-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const addons_ethicalads = {
const addons_flyout = {
$id: "http://v1.schemas.readthedocs.org/addons.flyout.json",
type: "object",
required: ["addons", "projects", "versions"],
required: ["addons", "projects", "versions", "readthedocs"],
properties: {
addons: {
type: "object",
Expand Down Expand Up @@ -242,14 +242,27 @@ const addons_flyout = {
},
},
},
readthedocs: {
type: "object",
required: ["resolver"],
properties: {
resolver: {
type: "object",
required: ["filename"],
properties: {
filename: { type: "string" },
},
},
},
},
},
};

// Validator for File Tree Diff Addon
const addons_filetreediff = {
$id: "http://v1.schemas.readthedocs.org/addons.filetreediff.json",
type: "object",
required: ["addons"],
required: ["addons", "versions"],
properties: {
addons: {
type: "object",
Expand All @@ -272,6 +285,19 @@ const addons_filetreediff = {
},
},
},
versions: {
type: "object",
required: ["current"],
properties: {
current: {
type: "object",
required: ["type"],
properties: {
type: { type: "string" },
},
},
},
},
},
};

Expand Down Expand Up @@ -317,7 +343,7 @@ const addons_hotkeys = {
const addons_notifications = {
$id: "http://v1.schemas.readthedocs.org/addons.notifications.json",
type: "object",
required: ["addons"],
required: ["addons", "readthedocs"],
properties: {
addons: {
type: "object",
Expand Down Expand Up @@ -429,6 +455,19 @@ const addons_notifications = {
},
},
},
readthedocs: {
type: "object",
required: ["resolver"],
properties: {
resolver: {
type: "object",
required: ["filename"],
properties: {
filename: { type: "string" },
},
},
},
},
},
};

Expand Down
23 changes: 4 additions & 19 deletions src/docdiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import docdiffGeneralStyleSheet from "./docdiff.document.css";
// See https://github.com/readthedocs/addons/pull/234
import * as visualDomDiff from "visual-dom-diff";

import { AddonBase } from "./utils";
import {
EVENT_READTHEDOCS_DOCDIFF_ADDED_REMOVED_SHOW,
EVENT_READTHEDOCS_DOCDIFF_HIDE,
EVENT_READTHEDOCS_ROOT_DOM_CHANGED,
} from "./events";
import { nothing, LitElement } from "lit";
import { default as objectPath } from "object-path";
import { getQueryParam, docTool } from "./utils";
import { AddonBase, getQueryParam, docTool } from "./utils";
import { EMBED_API_ENDPOINT } from "./constants";

export const DOCDIFF_URL_PARAM = "readthedocs-diff";
Expand Down Expand Up @@ -266,23 +265,7 @@ export class DocDiffAddon extends AddonBase {
"http://v1.schemas.readthedocs.org/addons.docdiff.json";
static addonEnabledPath = "addons.doc_diff.enabled";
static addonName = "DocDiff";

constructor(config) {
super();

// TODO: is it possible to move this `constructor` to the `AddonBase` class?
customElements.define("readthedocs-docdiff", DocDiffElement);
let elems = document.querySelectorAll("readthedocs-docdiff");
if (!elems.length) {
elems = [new DocDiffElement()];
document.body.append(elems[0]);
elems[0].requestUpdate();
}

for (const elem of elems) {
elem.loadConfig(config);
}
}
static elementClass = DocDiffElement;

static requiresUrlParam() {
return (
Expand All @@ -294,3 +277,5 @@ export class DocDiffAddon extends AddonBase {
);
}
}

customElements.define(DocDiffElement.elementName, DocDiffElement);
9 changes: 7 additions & 2 deletions src/ethicalads.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ export class EthicalAdsAddon extends AddonBase {
static addonEnabledPath = "addons.ethicalads.enabled";
static addonName = "EthicalAds";

constructor(config) {
super();
loadConfig(config) {
this.config = config;

// Do not add another ad if we already added one
if (document.querySelector(`#${AD_SCRIPT_ID}`) !== null) {
return;
}

this.injectEthicalAds();
}

Expand Down
Loading