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

POC Monkey-patch history pushState and replaceState to listen to location changes #455

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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: 15 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ <h2 id="search">Search</h2>
<p>Hit <code>/</code> to trigger the search modal, or click on the input from the flyout.</p>
<readthedocs-search></readthedocs-search>

<h2>Test URL changes</h2>

<p>Quick check to see if the SPA url changes using "pushState" are correctly picked up using our utils (check console).</p>
<button id="change-url-button" type="button">Change URL</button>

<script>
const button = document.getElementById("change-url-button");
button.addEventListener("click", () => {
const randomHash = (Math.random() + 1).toString(36).substring(7);
history.pushState({}, "", `#url-${randomHash}`);
});

window.addEventListener("readthedocsUrlChanged", (ev) => {console.log("URL Change detected!", ev)});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

</script>

<h2>Link Previews</h2>
<div role="main">
<ul>
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
IS_PRODUCTION,
setupLogging,
getMetadataValue,
setupHistoryEvents,
} from "./utils";

export function setup() {
Expand All @@ -36,6 +37,7 @@ export function setup() {
domReady
.then(() => {
setupLogging();
setupHistoryEvents();

let sendUrlParam = false;
for (const addon of addons) {
Expand Down
40 changes: 40 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const IS_TESTING =
export const IS_PRODUCTION =
typeof WEBPACK_IS_PRODUCTION === "undefined" ? false : WEBPACK_IS_PRODUCTION;

export const READTHEDOCS_URL_CHANGED_EVENT = "readthedocsUrlChanged";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const READTHEDOCS_URL_CHANGED_EVENT = "readthedocsUrlChanged";
export const READTHEDOCS_URL_CHANGED_EVENT = "readthedocs-url-changed";

We are using readthedocs-addons-data-ready or similar for another event we are triggering.


export const domReady = new Promise((resolve) => {
if (
document.readyState === "interactive" ||
Expand Down Expand Up @@ -155,6 +157,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);
humitos marked this conversation as resolved.
Show resolved Hide resolved
const event = new Event(READTHEDOCS_URL_CHANGED_EVENT);
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.
*
Expand Down