From 3aea703e1e8803c48e27eff3f47f9a316e295783 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 5 Dec 2024 16:35:31 +0100 Subject: [PATCH 1/3] Visual diff: trigger `DOM_CHANGED` event after enable/disable A new event `READTHEDOCS_ROOT_DOM_CHANGED` is triggered when the "Visual diff" is enabled/disabled after the DOM was modified. This allows other addons to subscribe to this event to re-initialize if required. I used this event from links preview to call `setupTooltips` again to re-install tooltips on these links. I migrated links preview to a `LitElement` to be able to make usage of this pattern and follow what we already had. We will be able to do something similar on #157 --- public/_/readthedocs-addons.json | 3 ++ src/docdiff.js | 23 +++++--- src/events.js | 2 + src/linkpreviews.js | 93 +++++++++++++++++++++++++++----- 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/public/_/readthedocs-addons.json b/public/_/readthedocs-addons.json index 5a75fa8a..a07e104f 100644 --- a/public/_/readthedocs-addons.json +++ b/public/_/readthedocs-addons.json @@ -116,6 +116,9 @@ "enabled": true, "code": "UA-12345" }, + "linkspreview": { + "enabled": true + }, "notifications": { "enabled": true, "show_on_latest": true, diff --git a/src/docdiff.js b/src/docdiff.js index f4944b5f..28164fa2 100644 --- a/src/docdiff.js +++ b/src/docdiff.js @@ -16,6 +16,7 @@ import { AddonBase } from "./utils"; import { EVENT_READTHEDOCS_DOCDIFF_ADDED_REMOVED_SHOW, EVENT_READTHEDOCS_DOCDIFF_HIDE, + EVENT_READTHEDOCS_ROOT_DOM_CHANGED, } from "./events"; import { html, nothing, LitElement } from "lit"; import { default as objectPath } from "object-path"; @@ -103,7 +104,10 @@ export class DocDiffElement extends LitElement { // Enable DocDiff if the URL parameter is present if (hasQueryParam(DOCDIFF_URL_PARAM)) { - this.enableDocDiff(); + const event = new CustomEvent( + EVENT_READTHEDOCS_DOCDIFF_ADDED_REMOVED_SHOW, + ); + document.dispatchEvent(event); } } @@ -158,24 +162,29 @@ export class DocDiffElement extends LitElement { ); new_body.replaceWith(diffNode.firstElementChild); }) + .finally(() => { + const event = new CustomEvent(EVENT_READTHEDOCS_ROOT_DOM_CHANGED); + document.dispatchEvent(event); + }) .catch((error) => { console.error(error); }); } enableDocDiff() { - if (this.config === null) { - return null; + if (this.config !== null) { + this.enabled = true; + this.originalBody = document.querySelector(this.rootSelector); + this.compare(); } - - this.enabled = true; - this.originalBody = document.querySelector(this.rootSelector); - return this.compare(); } disableDocDiff() { this.enabled = false; document.querySelector(this.rootSelector).replaceWith(this.originalBody); + + const event = new CustomEvent(EVENT_READTHEDOCS_ROOT_DOM_CHANGED); + document.dispatchEvent(event); } _handleShowDocDiff = (e) => { diff --git a/src/events.js b/src/events.js index e739e9c1..291450aa 100644 --- a/src/events.js +++ b/src/events.js @@ -9,6 +9,8 @@ export const EVENT_READTHEDOCS_FLYOUT_SHOW = "readthedocs-flyout-show"; export const EVENT_READTHEDOCS_FLYOUT_HIDE = "readthedocs-flyout-hide"; export const EVENT_READTHEDOCS_ADDONS_DATA_READY = "readthedocs-addons-data-ready"; +export const EVENT_READTHEDOCS_ROOT_DOM_CHANGED = + "readthedocs-root-dom-changed"; /** * Object to pass to user subscribing to `EVENT_READTHEDOCS_ADDONS_DATA_READY`. diff --git a/src/linkpreviews.js b/src/linkpreviews.js index ea1a4a0e..db049aff 100644 --- a/src/linkpreviews.js +++ b/src/linkpreviews.js @@ -8,6 +8,7 @@ import { IS_TESTING, docTool, } from "./utils"; +import { EVENT_READTHEDOCS_ROOT_DOM_CHANGED } from "./events"; import { computePosition, autoPlacement, @@ -200,26 +201,28 @@ function setupTooltip(el, doctoolname, doctoolversion, selector) { } } -/** - * LinkPreviews addon - * - * @param {Object} config - Addon configuration object - */ -export class LinkPreviewsAddon extends AddonBase { - static jsonValidationURI = - "http://v1.schemas.readthedocs.org/addons.linkpreviews.json"; - static addonEnabledPath = "addons.linkpreviews.enabled"; - static addonName = "LinkPreviews"; +export class LinkPreviewsElement extends LitElement { + static elementName = "readthedocs-linkspreview"; - constructor(config) { + static properties = { + config: { + state: true, + }, + }; + + constructor() { super(); - this.config = config; if (!IS_TESTING) { // Include CSS into the DOM so they can be read. + // We can't include these CSS in the LitElement, because we need them to be globally available. document.adoptedStyleSheets.push(styleSheet); } + this.config = null; + } + + setupTooltips() { // Autodetect if the page is built with Sphinx and send the `doctool=` attribute in that case. const doctoolName = docTool.getDocumentationTool(); const rootSelector = @@ -242,7 +245,7 @@ export class LinkPreviewsAddon extends AddonBase { let elementHostname = elementUrl.hostname; const pointToSamePage = window.location.pathname.replace("/index.html", "") == - elementUrl.pathname.replace("/index.html"); + elementUrl.pathname.replace("/index.html", ""); if (elementHostname === window.location.hostname && !pointToSamePage) { element.classList.add("link-preview"); setupTooltip(element, doctoolName, null, rootSelector); @@ -254,4 +257,68 @@ export class LinkPreviewsAddon extends AddonBase { } } } + + render() { + return nothing; + } + + loadConfig(config) { + if (!LinkPreviewsAddon.isEnabled(config)) { + return; + } + + this.config = config; + this.setupTooltips(); + } + + _handleRootDOMChanged = (e) => { + // Trigger the setup again since the DOM has changed + this.setupTooltips(); + }; + + connectedCallback() { + super.connectedCallback(); + document.addEventListener( + EVENT_READTHEDOCS_ROOT_DOM_CHANGED, + this._handleRootDOMChanged, + ); + } + + disconnectedCallback() { + document.removeEventListener( + EVENT_READTHEDOCS_ROOT_DOM_CHANGED, + this._handleRootDOMChanged, + ); + super.disconnectedCallback(); + } +} + +/** + * LinkPreviews addon + * + * @param {Object} config - Addon configuration object + */ +export class LinkPreviewsAddon extends AddonBase { + static jsonValidationURI = + "http://v1.schemas.readthedocs.org/addons.linkpreviews.json"; + static addonEnabledPath = "addons.linkpreviews.enabled"; + static addonName = "LinkPreviews"; + + constructor(config) { + super(); + + // If there are no elements found, inject one + let elems = document.querySelectorAll("readthedocs-linkpreviews"); + if (!elems.length) { + elems = [new LinkPreviewsElement()]; + document.body.append(elems[0]); + elems[0].requestUpdate(); + } + + for (const elem of elems) { + elem.loadConfig(config); + } + } } + +customElements.define("readthedocs-linkpreviews", LinkPreviewsElement); From d0467b9e44b73f134888d3e477fe9971b4dbf5de Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 9 Dec 2024 11:26:25 +0100 Subject: [PATCH 2/3] Typo --- public/_/readthedocs-addons.json | 2 +- src/linkpreviews.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/_/readthedocs-addons.json b/public/_/readthedocs-addons.json index a07e104f..81e3ec3f 100644 --- a/public/_/readthedocs-addons.json +++ b/public/_/readthedocs-addons.json @@ -116,7 +116,7 @@ "enabled": true, "code": "UA-12345" }, - "linkspreview": { + "linkpreviews": { "enabled": true }, "notifications": { diff --git a/src/linkpreviews.js b/src/linkpreviews.js index db049aff..4efebd5e 100644 --- a/src/linkpreviews.js +++ b/src/linkpreviews.js @@ -202,7 +202,7 @@ function setupTooltip(el, doctoolname, doctoolversion, selector) { } export class LinkPreviewsElement extends LitElement { - static elementName = "readthedocs-linkspreview"; + static elementName = "readthedocs-linkpreviews"; static properties = { config: { From 97e3bc5928c0f224b45796c6900256b3c581f9c6 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 10 Dec 2024 11:09:56 +0100 Subject: [PATCH 3/3] Add docstring to events --- src/events.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/events.js b/src/events.js index 291450aa..740bed97 100644 --- a/src/events.js +++ b/src/events.js @@ -7,8 +7,23 @@ export const EVENT_READTHEDOCS_DOCDIFF_ADDED_REMOVED_SHOW = export const EVENT_READTHEDOCS_DOCDIFF_HIDE = "readthedocs-docdiff-hide"; export const EVENT_READTHEDOCS_FLYOUT_SHOW = "readthedocs-flyout-show"; export const EVENT_READTHEDOCS_FLYOUT_HIDE = "readthedocs-flyout-hide"; + +/** + * Event triggered when the Read the Docs data is ready to be consumed. + * + * This is the event users subscribe to to make usage of Read the Docs data. + * The object received is `ReadTheDocsEventData`. + */ export const EVENT_READTHEDOCS_ADDONS_DATA_READY = "readthedocs-addons-data-ready"; + +/** + * Event triggered when any addons modifies the root DOM. + * + * As an example, DocDiff triggers it when injecting the visual diferences. + * Addons subscribe to this event to re-initialize them in case they perform + * something specific on DOM elements from inside the root. + */ export const EVENT_READTHEDOCS_ROOT_DOM_CHANGED = "readthedocs-root-dom-changed";