diff --git a/src/events.js b/src/events.js index 740bed97..02cba032 100644 --- a/src/events.js +++ b/src/events.js @@ -8,6 +8,11 @@ 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 URL has changed dynamically (modifying the history object) + */ +export const EVENT_READTHEDOCS_URL_CHANGED = "readthedocs-url-changed"; + /** * Event triggered when the Read the Docs data is ready to be consumed. * diff --git a/src/index.js b/src/index.js index 78781d15..f4aa0daf 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ import { IS_PRODUCTION, setupLogging, getMetadataValue, + setupHistoryEvents, } from "./utils"; import doctoolsStyleSheet from "./doctools.css"; @@ -46,6 +47,7 @@ export function setup() { domReady .then(() => { setupLogging(); + setupHistoryEvents(); let sendUrlParam = false; for (const addon of addons) { diff --git a/src/utils.js b/src/utils.js index 05fa39f1..ff7b2962 100644 --- a/src/utils.js +++ b/src/utils.js @@ -17,6 +17,7 @@ import { ANTORA, DOCSIFY, } from "./constants"; +import { EVENT_READTHEDOCS_URL_CHANGED } from "./events"; export const ADDONS_API_VERSION = "1"; export const ADDONS_API_ENDPOINT = "/_/addons/"; @@ -160,6 +161,44 @@ export class AddonBase { } } +/** + * Setup events firing on history `pushState` and `replaceState` + * + * This is needed when addons are used in SPA. A lot of addons rely + * on the current URL. However in the SPA, the pages are not reloaded, so + * the addons never get notified of the changes in the URL. + * + * While History API does have `popstate` event, the only way to listen to + * changes via `pushState` and `replaceState` is using monkey-patching, which is + * what this function does. (See https://stackoverflow.com/a/4585031) + * It will fire a `READTHEDOCS_URL_CHANGED` event, on `pushState` and `replaceState`. + * + */ +export function setupHistoryEvents() { + // Let's ensure that the history will be patched only once, so we create a Symbol to check by + const patchKey = Symbol.for("addons_history"); + + if ( + typeof history !== "undefined" && + typeof window[patchKey] === "undefined" + ) { + for (const methodName of ["pushState", "replaceState"]) { + const originalMethod = history[methodName]; + history[methodName] = function () { + const result = originalMethod.apply(this, arguments); + const event = new Event(EVENT_READTHEDOCS_URL_CHANGED); + event.arguments = arguments; + + dispatchEvent(event); + return result; + }; + } + + // Let's leave a flag, so we know that history has been patched + Object.defineProperty(window, patchKey, { value: true }); + } +} + /** * Debounce a function. *