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 Lit Context to share config across all addons #491

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 11 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
}
},
"dependencies": {
"@floating-ui/dom": "^1.6.13"
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.3"
}
}
19 changes: 13 additions & 6 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ <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-context>
<readthedocs-flyout></readthedocs-flyout>
</readthedocs-context>

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

Expand Down Expand Up @@ -57,11 +62,13 @@ <h2>Link Previews</h2>
</div>

<h2 id="notification">Notification</h2>
<readthedocs-notification class="raised toast"></readthedocs-notification>
<readthedocs-notification class="inverted"></readthedocs-notification>
<readthedocs-notification class="inverted raised"></readthedocs-notification>
<readthedocs-notification class="inverted" style="--readthedocs-notification-background-color: purple;"></readthedocs-notification>
<readthedocs-notification class="titled raised"></readthedocs-notification>
<readthedocs-notification class="titled inverted raised"></readthedocs-notification>
<readthedocs-context>
<readthedocs-notification class="raised toast"></readthedocs-notification>
<readthedocs-notification class="inverted"></readthedocs-notification>
<readthedocs-notification class="inverted raised"></readthedocs-notification>
<readthedocs-notification class="inverted" style="--readthedocs-notification-background-color: purple;"></readthedocs-notification>
<readthedocs-notification class="titled raised"></readthedocs-notification>
<readthedocs-notification class="titled inverted raised"></readthedocs-notification>
</readthedocs-context>
</body>
</html>
30 changes: 30 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { html, nothing, render, LitElement } from "lit";
import { ContextProvider } from "@lit/context";
import { createContext } from "@lit/context";
import { getReadTheDocsConfig } from "./readthedocs-config.js";
import { EVENT_READTHEDOCS_ADDONS_DATA_READY } from "./events";

export const configContext = createContext(Symbol("readthedocs-config"));

export class AddonsApp extends LitElement {
config = new ContextProvider(this, { context: configContext });

connectedCallback() {
super.connectedCallback();
document.addEventListener(
EVENT_READTHEDOCS_ADDONS_DATA_READY,
this._handleAddonsDataReady,
);
}

createRenderRoot() {
return this;
}

_handleAddonsDataReady = (event) => {
console.log("_handleAddonsDataReady");
this.config.setValue(event.detail.data());
};
}

customElements.define("readthedocs-context", AddonsApp);
44 changes: 21 additions & 23 deletions src/flyout.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ajv } from "./data-validation";
import READTHEDOCS_LOGO_WORDMARK from "./images/logo-wordmark-light.svg";
import READTHEDOCS_LOGO from "./images/logo-light.svg";
import { ContextConsumer } from "@lit/context";
import { library, icon } from "@fortawesome/fontawesome-svg-core";
import { faCodeBranch, faLanguage } from "@fortawesome/free-solid-svg-icons";
import { html, nothing, render, LitElement } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { default as objectPath } from "object-path";
import { configContext } from "./context.js";

import styleSheet from "./flyout.css";
import { AddonBase, addUtmParameters, getLinkWithFilename } from "./utils";
Expand All @@ -19,32 +21,31 @@ export class FlyoutElement extends LitElement {
static elementName = "readthedocs-flyout";

static properties = {
config: { state: true },
opened: { type: Boolean },
floating: { type: Boolean },
position: { type: String },
};

static styles = styleSheet;

// `_config` is the context we are going to consume when it's updated.
_config = new ContextConsumer(this, {
context: configContext,
subscribe: true,
});

constructor() {
super();

// `config` is the internal config for this addon that's updated based on `_config`.
this.config = null;

this.opened = false;
this.floating = true;
this.position = "bottom-right";
this.readthedocsLogo = READTHEDOCS_LOGO;
}

loadConfig(config) {
// Validate the config object before assigning it to the Addon.
// Later, ``render()`` method will check whether this object exists and (not) render
// accordingly
if (!FlyoutAddon.isEnabled(config)) {
return;
}
this.config = config;
console.log("Flyout _config (from constructor() method)");
console.log(this._config.value);
}

_close() {
Expand Down Expand Up @@ -305,11 +306,17 @@ export class FlyoutElement extends LitElement {

render() {
// The element doesn't yet have our config, don't render it.
if (this.config === null) {
// nothing is a special Lit response type
console.log("Flyout config (from render() method )");
console.log(this._config.value);

// Validate the context (`this._config.value`) on each update and return
// nothing if it's invalid. ``nothing`` is a special Lit response type.
if (!FlyoutAddon.isEnabled(this._config.value)) {
return nothing;
}

this.config = this._config.value;

const classes = { floating: this.floating, container: true };
classes[this.position] = true;

Expand Down Expand Up @@ -376,16 +383,7 @@ export class FlyoutAddon extends AddonBase {
// If there are no elements found, inject one
let elems = document.querySelectorAll("readthedocs-flyout");
if (!elems.length) {
elems = [new FlyoutElement()];

// We cannot use `render(elems[0], document.body)` because there is a race conditions between all the addons.
// So, we append the web-component first and then request an update of it.
document.body.append(elems[0]);
elems[0].requestUpdate();
}

for (const elem of elems) {
elem.loadConfig(config);
render(new FlyoutElement(), document.body);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function setup() {
if (addon.isEnabled(config, httpStatus)) {
promises.push(
new Promise((resolve) => {
// TODO: remove attribute `config` from here since it's not required anymore
return resolve(new addon(config));
}),
);
Expand Down
89 changes: 39 additions & 50 deletions src/notification.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ajv } from "./data-validation";
import { library, icon } from "@fortawesome/fontawesome-svg-core";
import { ContextConsumer } from "@lit/context";
import {
faCircleXmark,
faFlask,
Expand All @@ -8,6 +9,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { html, nothing, render, LitElement } from "lit";
import { default as objectPath } from "object-path";
import { configContext } from "./context.js";

import styleSheet from "./notification.css";
import { AddonBase, addUtmParameters, getLinkWithFilename } from "./utils";
Expand All @@ -18,7 +20,6 @@ export class NotificationElement extends LitElement {

/** @static @property {Object} - Lit reactive properties */
static properties = {
config: { state: true },
urls: { state: true },
highest_version: { state: true },
dismissedTimestamp: { state: true },
Expand All @@ -29,6 +30,12 @@ export class NotificationElement extends LitElement {
/** @static @property {Object} - Lit stylesheets to apply to elements */
static styles = styleSheet;

// `_config` is the context we are going to consume when it's updated.
_config = new ContextConsumer(this, {
context: configContext,
subscribe: true,
});

constructor() {
super();

Expand Down Expand Up @@ -125,15 +132,32 @@ export class NotificationElement extends LitElement {
this.timerID = null;
}

loadConfig(config) {
// Validate the config object before assigning it to the Addon.
// Later, ``render()`` method will check whether this object exists and (not) render
// accordingly
if (!NotificationAddon.isEnabled(config)) {
return;
getLocalStorageKeyFromConfig(config) {
const projectSlug = config.projects.current.slug;
const languageCode = config.projects.current.language.code;
const versionSlug = config.versions.current.slug;
return `${projectSlug}-${languageCode}-${versionSlug}-notification`;
}

firstUpdated() {
// Add CSS classes to the element on ``firstUpdated`` because we need the
// HTML element to exist in the DOM before being able to add tag attributes.
this.className = this.className || "raised toast";
}

render() {
if (this.autoDismissed) {
return nothing;
}

this.config = config;
if (!NotificationAddon.isEnabled(this._config.value)) {
return nothing;
}
this.config = this._config.value;

if (!this.config.addons.notifications.enabled) {
return nothing;
}

if (
this.config.addons.notifications.enabled &&
Expand All @@ -142,11 +166,11 @@ export class NotificationElement extends LitElement {
this.urls = {
// NOTE: point users to the new beta dashboard for now so we promote it more.
// We will revert this once we are fully migrated to the new dashboard.
build: config.builds.current.urls.build
build: this.config.builds.current.urls.build
.replace("readthedocs.org", "app.readthedocs.org")
.replace("readthedocs.com", "app.readthedocs.com")
.replace("app.app.", "app."),
external: config.versions.current.urls.vcs,
external: this.config.versions.current.urls.vcs,
};
}

Expand All @@ -156,48 +180,19 @@ export class NotificationElement extends LitElement {
"addons.notifications.show_on_latest",
false,
) &&
config.projects.current.versioning_scheme !==
this.config.projects.current.versioning_scheme !==
"single_version_without_translations" &&
config.versions.current.type !== "external"
this.config.versions.current.type !== "external"
) {
this.calculateStableLatestVersionWarning();
}
this.loadDismissedTimestamp(this.config);
}

getLocalStorageKeyFromConfig(config) {
const projectSlug = config.projects.current.slug;
const languageCode = config.projects.current.language.code;
const versionSlug = config.versions.current.slug;
return `${projectSlug}-${languageCode}-${versionSlug}-notification`;
}

firstUpdated() {
// Add CSS classes to the element on ``firstUpdated`` because we need the
// HTML element to exist in the DOM before being able to add tag attributes.
this.className = this.className || "raised toast";
}

render() {
if (this.autoDismissed) {
return nothing;
}

// The element doesn't yet have our config, don't render it.
if (this.config === null) {
// nothing is a special Lit response type
return nothing;
}

this.loadDismissedTimestamp(this.config);
// This notification has been dimissed, so don't render it
if (this.dismissedTimestamp) {
return nothing;
}

if (!this.config.addons.notifications.enabled) {
return nothing;
}

if (this.config.versions.current.type === "external") {
if (
objectPath.get(
Expand Down Expand Up @@ -411,19 +406,13 @@ export class NotificationAddon extends AddonBase {
static addonEnabledPath = "addons.notifications.enabled";
static addonName = "Notification";

constructor(config) {
constructor() {
super();

// If there are no elements found, inject one
let elems = document.querySelectorAll("readthedocs-notification");
if (!elems.length) {
elems = [new NotificationElement()];
document.body.append(elems[0]);
elems[0].requestUpdate();
}

for (const elem of elems) {
elem.loadConfig(config);
render(new NotificationElement(), document.body);
}
}
}
Expand Down