From a89136ba021d9c959b9c2f2f9535c92a6e30aa5d Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 10 Dec 2024 12:03:53 +0100 Subject: [PATCH] Visual diff: trigger `DOM_CHANGED` event after enable/disable (#465) 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 Closes #460 --- public/_/readthedocs-addons.json | 3 ++ src/docdiff.js | 12 ++++- src/events.js | 17 ++++++ src/linkpreviews.js | 93 +++++++++++++++++++++++++++----- 4 files changed, 111 insertions(+), 14 deletions(-) diff --git a/public/_/readthedocs-addons.json b/public/_/readthedocs-addons.json index 5a75fa8a..81e3ec3f 100644 --- a/public/_/readthedocs-addons.json +++ b/public/_/readthedocs-addons.json @@ -116,6 +116,9 @@ "enabled": true, "code": "UA-12345" }, + "linkpreviews": { + "enabled": true + }, "notifications": { "enabled": true, "show_on_latest": true, diff --git a/src/docdiff.js b/src/docdiff.js index 21e10838..ac2beef4 100644 --- a/src/docdiff.js +++ b/src/docdiff.js @@ -15,6 +15,7 @@ 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"; @@ -103,7 +104,9 @@ export class DocDiffElement extends LitElement { // Enable DocDiff if the URL parameter is present if (hasQueryParam(DOCDIFF_URL_PARAM)) { - event = new CustomEvent(EVENT_READTHEDOCS_DOCDIFF_ADDED_REMOVED_SHOW); + const event = new CustomEvent( + EVENT_READTHEDOCS_DOCDIFF_ADDED_REMOVED_SHOW, + ); document.dispatchEvent(event); } } @@ -152,6 +155,10 @@ export class DocDiffElement extends LitElement { this.cachedRemoteContent = text; this.performDiff(text); }) + .finally(() => { + const event = new CustomEvent(EVENT_READTHEDOCS_ROOT_DOM_CHANGED); + document.dispatchEvent(event); + }) .catch((error) => { console.error(error); }); @@ -204,6 +211,9 @@ export class DocDiffElement extends LitElement { 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..740bed97 100644 --- a/src/events.js +++ b/src/events.js @@ -7,9 +7,26 @@ 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"; + /** * Object to pass to user subscribing to `EVENT_READTHEDOCS_ADDONS_DATA_READY`. * diff --git a/src/linkpreviews.js b/src/linkpreviews.js index ea1a4a0e..4efebd5e 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-linkpreviews"; - 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);