When to disconnect MutationObserver in Chrome / Web Extension? - javascript

I want to know when or if I need to disconnect a MutationObserver in my content script to avoid memory leaks.
So my content script checks all new additions to the DOM and updates the HTML accordingly. I use a MutationObserver for this which is created and started at in the content script.
My question is, does the MutationObserver destroy itself when a new page is loaded or must I listen to page changes to disconnect it and destroy it myself each time.
Here is the relevant code:
function startObserver(textSize: number, lineHeight: number, font: string = "Droid Arabic Naskh") {
let config: MutationObserverInit = {
attributes: false,
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: false,
};
let callback: MutationCallback = (mutationsList: MutationRecord[]) => {
mutationsList.forEach((record: MutationRecord) => {
// If something has been added
if (record.addedNodes.length > 0) {
// For each added node
record.addedNodes.forEach((addedNode: Node) => {
// For each node with Arabic script in addedNode
getArabicTextNodesIn(addedNode).forEach((arabicNode: Node) => {
// Update arabicNode only if it hasn't been updated
if (arabicNode.parentElement && arabicNode.parentElement.getAttribute("class") != "ar") {
updateNode(arabicNode, textSize, lineHeight, font);
}
});
});
}
});
};
if (!observer) {
observer = new MutationObserver(callback);
observer.observe(document.body, config);
}
}

Found it out a long time ago thanks to #xOxxm and some personal testing and playing around but answering myself in case someone else needs this in the future
Does the MutationObserver destroy itself when a new page is loaded or must I listen to page changes to disconnect it and destroy it myself each time?
Yes the MutationObserver destroys itself upon leaving the page (the document has changed) so the above code was in fact safe and devoid of any possible memory leaks.

Related

Chrome Extension And Dom Capture

I created a chrome extension for one of my pages. I'm getting the data in a dom element on the page and querying the api and calling it back. On the site where I get data, the page does not reload when switching between pages. So I can't catch Dom loading or locationchange in foreground.js because the page is not refreshed and i solve my problem by timeout.The dom in foreground.js is only triggered when the page is refreshed. The codes I wrote in settimeout are triggered when switching between pages. How can i solve this problem without timeout?
background.js CODES
chrome.tabs.onUpdated.addListener((tabId, changeInfo,tab) => {
if(changeInfo.status == "complete" && tab.status == "complete") {
chrome.scripting.executeScript({
target: {tabId: tabId},
files: ["./foreground.js"]
})
}
});
foreground.js CODES
document.addEventListener("DOMContentLoaded", () => { alert('Dom'); }); //not working
window.addEventListener('locationchange', function(){
console.log('location changed!');
}) //not working
setTimeout(() => {
let find = document.querySelector(".contentsdetailscontainer").textContent;
},1000); //this is working
foreground.js executed when document.readyState is complete, which means DOMContentLoaded has already been fired.
If at that point the needed element is not present in the DOM, you can use the Mutation Observer and wait for it.
Here is a simple example (very inefficient because it observes entire body, if only a specific element changes, you should observe it instead, so use it only as a guide):
var nodeFound = false;
const observer = new MutationObserver(() =>
{
const node = document.querySelector(".contentsdetailscontainer");
if (node && node.textContent)
{
if (!nodeFound)
{
//only show alert once per new page.
alert("node found");
nodeFound = true;
}
}
else
{
nodeFound = false; //reset in case page updated again
}
});
observer.observe(document.body, {subtree: true, childList: true});

Using MutationObserver to wait for button to appear

I am trying to create something that automatically clicks the required buttons on stackexchange type sites so that I can stop doing it myself. I am using a content script. I am trying to:
Find the button to customize cookies
click accept (they're usually all deselected with stackexchange sites)
Here is my code so far. I think it finds the right node, but clicks it too soon? I am not entirely sure since I'm new to js and whatnot.
function AcceptCustomization() {
let myNode;
let observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (!mutation.addedNodes) return
for (let i = 0; i < mutation.addedNodes.length; i++) {
// do things to your newly added nodes here
let node = mutation.addedNodes[i]
if (node.nodeType === 1) {
node
var a = node.getElementsByClassName("flex--item s-btn s-btn__primary save-preference-btn-handler onetrust-close-btn-handler js-consent-banner-hide js-consent-save")[0]
if (a) {
myNode = a
console.log(myNode)
console.log(myNode.click())
}
}
}
})
})
observer.observe(document.body, {
childList: true
, subtree: true
, attributes: false
, characterData: false
})
}
function CustomizeCookies() {
var a = document.getElementsByClassName("flex--item s-btn s-btn__filled js-cookie-settings")[0];
if (a) {
a.click();
AcceptCustomization();
}
else {
console.log(a);
}
}
CustomizeCookies();
In the future, I want to create an extension that I can use to create a sort of database of websites with corresponding button clicks. For example, if I don't have the cookie customization for a website, I would click the extension and run a script that captures my button clicks and redoes them every future time I visit (if it is not the same session).
Thanks :)

detect when challenge window is closed for Google recaptcha

I am using Google invisible recaptcha. Is there a way to detect when the challenge window is closed? By challenge window I mean window where you have to pick some images for verification.
I currently put a spinner on the button that rendered the recaptcha challenge, once the button is clicked. There is no way for the user to be prompted with another challenge window.
I am calling render function programmatically:
grecaptcha.render(htmlElement, { callback: this.verified, expiredCallback: this.resetRecaptcha, sitekey: this.siteKey, theme: "light", size: "invisible" });
I have 2 callback functions wired up the verified and the resetRecaptcha functions which look like:
function resetRecaptcha() {
grecaptcha.reset();
}
function verified(recaptchaResponse)
{
/*
which calls the server to validate
*/
}
I would have expected that grecaptcha.render has another callback that is called when the challenge screen is closed without the user verifying himself by selecting the images.
As you mention, the API doesn't support this feature.
However, you can add this feature yourself. You may use the following code with caution, Google might change its reCaptcha and by doing so break this custom code. The solution relies on two characteristics of reCaptcha, so if the code doesn't work, look there first:
the window iframe src: contains "google.com/recaptcha/api2/bframe"
the CSS opacity property: changed to 0 when the window is closed
// to begin: we listen to the click on our submit button
// where the invisible reCaptcha has been attachtted to
// when clicked the first time, we setup the close listener
recaptchaButton.addEventListener('click', function(){
if(!window.recaptchaCloseListener) initListener()
})
function initListener() {
// set a global to tell that we are listening
window.recaptchaCloseListener = true
// find the open reCaptcha window
HTMLCollection.prototype.find = Array.prototype.find
var recaptchaWindow = document
.getElementsByTagName('iframe')
.find(x=>x.src.includes('google.com/recaptcha/api2/bframe'))
.parentNode.parentNode
// and now we are listening on CSS changes on it
// when the opacity has been changed to 0 we know that
// the window has been closed
new MutationObserver(x => recaptchaWindow.style.opacity == 0 && onClose())
.observe(recaptchaWindow, { attributes: true, attributeFilter: ['style'] })
}
// now do something with this information
function onClose() {
console.log('recaptcha window has been closed')
}
As I mentioned in the comments of the answer submitted by #arcs, it is a good solution which works but it also fires onClose() when the user successfully completes the challenge. My solution is to change the onClose() function like so:
// now do something with this information
function onClose() {
if(!grecaptcha.getResponse()) {
console.log('recaptcha window has been closed')
}
}
This way, it only executes the desired code if the challenge has been closed and it has not been completed by the user, thus the response cannot be returned with grecaptcha.getResponse()
The drawback of detecting when iframe was hidden is that it fires not only when user closes captcha by clicking in background, but also when he submits the answer.
What I needed is detect only the first situation (cancel captcha).
I created a dom observer to detect when captcha is attached to DOM, then I disconnect it (because it is no longer needed) and add click handler to its background element.
Keep in mind that this solution is sensitive to any changes in DOM structure, so if google decides to change it for whatever reason, it may break.
Also remember to cleanup the observers/listeners, in my case (react) I do it in cleanup function of useEffect.
const captchaBackgroundClickHandler = () => {
...do whatever you need on captcha cancel
};
const domObserver = new MutationObserver(() => {
const iframe = document.querySelector("iframe[src^=\"https://www.google.com/recaptcha\"][src*=\"bframe\"]");
if (iframe) {
domObserver.disconnect();
captchaBackground = iframe.parentNode?.parentNode?.firstChild;
captchaBackground?.addEventListener("click", captchaBackgroundClickHandler);
}
});
domObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
To work in IE this solution needs polyfills for .include() and Array.from(), found below:
Array.from on the Internet Explorer
ie does not support 'includes' method
And the updated code:
function initListener() {
// set a global to tell that we are listening
window.recaptchaCloseListener = true
// find the open reCaptcha window
var frames = Array.from(document.getElementsByTagName('iframe'));
var recaptchaWindow;
frames.forEach(function(x){
if (x.src.includes('google.com/recaptcha/api2/bframe') ){
recaptchaWindow = x.parentNode.parentNode;
};
});
// and now we are listening on CSS changes on it
// when the opacity has been changed to 0 we know that
// the window has been closed
new MutationObserver(function(){
recaptchaWindow.style.opacity == 0 && onClose();
})
.observe(recaptchaWindow, { attributes: true, attributeFilter: ['style'] })
}
My solution:
let removeRecaptchaOverlayEventListener = null
const reassignGRecatchaExecute = () => {
if (!window.grecaptcha || !window.grecaptcha.execute) {
return
}
/* save original grecaptcha.execute */
const originalExecute = window.grecaptcha.execute
window.grecaptcha.execute = (...params) => {
try {
/* find challenge iframe */
const recaptchaIframe = [...document.body.getElementsByTagName('iframe')].find(el => el.src.match('https://www.google.com/recaptcha/api2/bframe'))
const recaptchaOverlay = recaptchaIframe.parentElement.parentElement.firstElementChild
/* detect when the recaptcha challenge window is closed and reset captcha */
!removeRecaptchaOverlayEventListener && recaptchaOverlay.addEventListener('click', window.grecaptcha.reset)
/* save remove event listener for click event */
removeRecaptchaOverlayEventListener = () => recaptchaOverlay.removeEventListener('click', window.grecaptcha.reset)
} catch (error) {
console.error(error)
} finally {
originalExecute(...params)
}
}
}
Call this function after you run window.grecaptcha.render() and before window.grecaptcha.execute()
And don't forget to remove event listener: removeRecaptchaOverlayEventListener()
For everybody that didn't quite get how it all works, here is another example with explanations that you might find useful:
So we have 2 challenges here.
1) Detect when the challenge is shown and get the overlay div of the challenge
function detectWhenReCaptchaChallengeIsShown() {
return new Promise(function(resolve) {
const targetElement = document.body;
const observerConfig = {
childList: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false,
subtree: false
};
function DOMChangeCallbackFunction(mutationRecords) {
mutationRecords.forEach((mutationRecord) => {
if (mutationRecord.addedNodes.length) {
var reCaptchaParentContainer = mutationRecord.addedNodes[0];
var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');
if (reCaptchaIframe.length) {
var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
if (reCaptchaChallengeOverlayDiv.length) {
reCaptchaObserver.disconnect();
resolve(reCaptchaChallengeOverlayDiv);
}
}
}
});
}
const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);
});
}
First, we created a target element that we would observe for Google iframe appearance. We targeted document.body as an iframe will be appended to it:
const targetElement = document.body;
Then we created a config object for MutationObserver. Here we might specify what exactly we track in DOM changes. Please note that all values are 'false' by default so we could only leave 'childList' - which means that we would observe only the child node changes for the target element - document.body in our case:
const observerConfig = {
childList: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false,
subtree: false
};
Then we created a function that would be invoked when an observer detects a specific type of DOM change that we specified in config object. The first argument represents an array of Mutation Observer objects. We grabbed the overlay div and returned in with Promise.
function DOMChangeCallbackFunction(mutationRecords) {
mutationRecords.forEach((mutationRecord) => {
if (mutationRecord.addedNodes.length) { //check only when notes were added to DOM
var reCaptchaParentContainer = mutationRecord.addedNodes[0];
var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');
if (reCaptchaIframe.length) { // Google reCaptcha iframe was loaded
var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
if (reCaptchaChallengeOverlayDiv.length) {
reCaptchaObserver.disconnect(); // We don't want to observe more DOM changes for better performance
resolve(reCaptchaChallengeOverlayDiv); // Returning the overlay div to detect close events
}
}
}
});
}
Lastly we instantiated an observer itself and started observing DOM changes:
const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);
2) Second challenge is the main question of that post - how do we detect that the challenge is closed? Well, we need help of MutationObserver again.
detectReCaptchaChallengeAppearance().then(function (reCaptchaChallengeOverlayDiv) {
var reCaptchaChallengeClosureObserver = new MutationObserver(function () {
if ((reCaptchaChallengeOverlayDiv.style.visibility === 'hidden') && !grecaptcha.getResponse()) {
// TADA!! Do something here as the challenge was either closed by hitting outside of an overlay div OR by pressing ESC key
reCaptchaChallengeClosureObserver.disconnect();
}
});
reCaptchaChallengeClosureObserver.observe(reCaptchaChallengeOverlayDiv, {
attributes: true,
attributeFilter: ['style']
});
});
So what we did is we get the Google reCaptcha challenge overlay div with the Promise we created in Step1 and then we subscribed for "style" changes on overlay div. This is because when the challenge is closed - Google fade it out.
It's important to note that the visibility will be also hidden when a person solves the captcha successfully. That is why we added !grecaptcha.getResponse() check. It will return nothing unless the challenge is resolved.
This is pretty much it - I hope that helps :)

How to stop a new node being added to DOM using Chrome/Firefox extension?

I am writing an extension to detect some malicious behavior of some scripts. These scripts add some nodes into the DOM after page gets loaded. I am able to get these nodes using
document.addEventListener("DOMNodeInserted", checkContents, false);
But how can I stop them from being loaded? Can this be done in Chrome or Firefox?
Use MutationObserver with childList and subtree.
var grid = iframe.contentDocument.getElementById('newtab-grid');
var mobs = new iframe.contentWindow.MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type, mutation);
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
var link = mutation.target.querySelector('.newtab-link');
if (link) {
link.setAttribute('target', '_parent');
console.log('set target on this el')
} else {
console.log('this ell has no link BUT it was an added el')
}
}
});
});
mobs.observe(grid, {
childList: !0,
subtree: !0
});
now this will tell you when its addingNodes and it gives you what was added in mutation.addedNodes. you can iterate throuh that and remove what was added. I'm not sure if you can prevent it from adding, but you can definitely remove it after it was added.
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver?redirectlocale=en-US&redirectslug=DOM%2FMutationObserver#MutationObserverInit

Event when window.location.href changes

I'm writing a Greasemonkey script for a site which at some point modifies location.href.
How can I get an event (via window.addEventListener or something similar) when window.location.href changes on a page? I also need access to the DOM of the document pointing to the new/modified url.
I've seen other solutions which involve timeouts and polling, but I'd like to avoid that if possible.
I use this script in my extension "Grab Any Media" and work fine ( like youtube case )
var oldHref = document.location.href;
window.onload = function() {
var bodyList = document.querySelector("body")
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
/* Changed ! your code here */
}
});
});
var config = {
childList: true,
subtree: true
};
observer.observe(bodyList, config);
};
With the latest javascript specification
const observeUrlChange = () => {
const oldHref = document.location.href;
const body = document.querySelector("body");
const observer = new MutationObserver(mutations => {
mutations.forEach(() => {
if (oldHref !== document.location.href) {
oldHref = document.location.href;
/* Changed ! your code here */
}
});
});
observer.observe(body, { childList: true, subtree: true });
};
window.onload = observeUrlChange;
Compressed with OpenAI
window.onload = () => new MutationObserver(mutations => mutations.forEach(() => oldHref !== document.location.href && (oldHref = document.location.href, /* Changed ! your code here */))).observe(document.querySelector("body"), { childList: true, subtree: true });
popstate event:
The popstate event is fired when the active history entry changes. [...] The popstate event is only triggered by doing a browser action such as a click on the back button (or calling history.back() in JavaScript)
So, listening to popstate event and sending a popstate event when using history.pushState() should be enough to take action on href change:
window.addEventListener('popstate', listener);
const pushUrl = (href) => {
history.pushState({}, '', href);
window.dispatchEvent(new Event('popstate'));
};
You can't avoid polling, there isn't any event for href change.
Using intervals is quite light anyways if you don't go overboard. Checking the href every 50ms or so will not have any significant effect on performance if you're worried about that.
There is a default onhashchange event that you can use.
Documented HERE
And can be used like this:
function locationHashChanged( e ) {
console.log( location.hash );
console.log( e.oldURL, e.newURL );
if ( location.hash === "#pageX" ) {
pageX();
}
}
window.onhashchange = locationHashChanged;
If the browser doesn't support oldURL and newURL you can bind it like this:
//let this snippet run before your hashChange event binding code
if( !window.HashChangeEvent )( function() {
let lastURL = document.URL;
window.addEventListener( "hashchange", function( event ) {
Object.defineProperty( event, "oldURL", { enumerable: true, configurable: true, value: lastURL } );
Object.defineProperty( event, "newURL", { enumerable: true, configurable: true, value: document.URL } );
lastURL = document.URL;
} );
} () );
Through Jquery, just try
$(window).on('beforeunload', function () {
//your code goes here on location change
});
By using javascript:
window.addEventListener("beforeunload", function (event) {
//your code goes here on location change
});
Refer Document : https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
Have you tried beforeUnload?
This event fires immediately before the page responds to a navigation request, and this should include the modification of the href.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD>
<TITLE></TITLE>
<META NAME="Generator" CONTENT="TextPad 4.6">
<META NAME="Author" CONTENT="?">
<META NAME="Keywords" CONTENT="?">
<META NAME="Description" CONTENT="?">
</HEAD>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
$(window).unload(
function(event) {
alert("navigating");
}
);
$("#theButton").click(
function(event){
alert("Starting navigation");
window.location.href = "http://www.bbc.co.uk";
}
);
});
</script>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#FF0000" VLINK="#800000" ALINK="#FF00FF" BACKGROUND="?">
<button id="theButton">Click to navigate</button>
Google
</BODY>
</HTML>
Beware, however, that your event will fire whenever you navigate away from the page, whether this is because of the script, or somebody clicking on a link.
Your real challenge, is detecting the different reasons for the event being fired. (If this is important to your logic)
Try this script which will let you run code whenever the URL changes (without a pageload, like an Single Page Application):
var previousUrl = '';
var observer = new MutationObserver(function(mutations) {
if (location.href !== previousUrl) {
previousUrl = location.href;
console.log(`URL changed to ${location.href}`);
}
});
based on the answer from "Leonardo Ciaccio", modified code is here:
i.e. removed for loop and reassign the Body Element if it is removed
window.addEventListener("load", function () {
let oldHref = document.location.href,
bodyDOM = document.querySelector("body");
function checkModifiedBody() {
let tmp = document.querySelector("body");
if (tmp != bodyDOM) {
bodyDOM = tmp;
observer.observe(bodyDOM, config);
}
}
const observer = new MutationObserver(function (mutations) {
if (oldHref != document.location.href) {
oldHref = document.location.href;
console.log("the location href is changed!");
window.requestAnimationFrame(checkModifiedBody)
}
});
const config = {
childList: true,
subtree: true
};
observer.observe(bodyDOM, config);
}, false);
Well there is 2 ways to change the location.href. Either you can write location.href = "y.html", which reloads the page or can use the history API which does not reload the page. I experimented with the first a lot recently.
If you open a child window and capture the load of the child page from the parent window, then different browsers behave very differently. The only thing that is common, that they remove the old document and add a new one, so for example adding readystatechange or load event handlers to the old document does not have any effect. Most of the browsers remove the event handlers from the window object too, the only exception is Firefox. In Chrome with Karma runner and in Firefox you can capture the new document in the loading readyState if you use unload + next tick. So you can add for example a load event handler or a readystatechange event handler or just log that the browser is loading a page with a new URI. In Chrome with manual testing (probably GreaseMonkey too) and in Opera, PhantomJS, IE10, IE11 you cannot capture the new document in the loading state. In those browsers the unload + next tick calls the callback a few hundred msecs later than the load event of the page fires. The delay is typically 100 to 300 msecs, but opera simetime makes a 750 msec delay for next tick, which is scary. So if you want a consistent result in all browsers, then you do what you want to after the load event, but there is no guarantee the location won't be overridden before that.
var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");
var callBacks = [];
var uglyHax = function (){
var done = function (){
uglyHax();
callBacks.forEach(function (cb){
cb();
});
};
win.addEventListener("unload", function unloadListener(){
win.removeEventListener("unload", unloadListener); // Firefox remembers, other browsers don't
setTimeout(function (){
// IE10, IE11, Opera, PhantomJS, Chrome has a complete new document at this point
// Chrome on Karma, Firefox has a loading new document at this point
win.document.readyState; // IE10 and IE11 sometimes fails if I don't access it twice, idk. how or why
if (win.document.readyState === "complete")
done();
else
win.addEventListener("load", function (){
setTimeout(done, 0);
});
}, 0);
});
};
uglyHax();
callBacks.push(function (){
console.log("cb", win.location.href, win.document.readyState);
if (win.location.href !== "http://localhost:4444/y.html")
win.location.href = "http://localhost:4444/y.html";
else
console.log("done");
});
win.location.href = "http://localhost:4444/x.html";
If you run your script only in Firefox, then you can use a simplified version and capture the document in a loading state, so for example a script on the loaded page cannot navigate away before you log the URI change:
var uuid = "win." + Math.random();
var timeOrigin = new Date();
var win = window.open("about:blank", uuid, "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes");
var callBacks = [];
win.addEventListener("unload", function unloadListener(){
setTimeout(function (){
callBacks.forEach(function (cb){
cb();
});
}, 0);
});
callBacks.push(function (){
console.log("cb", win.location.href, win.document.readyState);
// be aware that the page is in loading readyState,
// so if you rewrite the location here, the actual page will be never loaded, just the new one
if (win.location.href !== "http://localhost:4444/y.html")
win.location.href = "http://localhost:4444/y.html";
else
console.log("done");
});
win.location.href = "http://localhost:4444/x.html";
If we are talking about single page applications which change the hash part of the URI, or use the history API, then you can use the hashchange and the popstate events of the window respectively. Those can capture even if you move in history back and forward until you stay on the same page. The document does not changes by those and the page is not really reloaded.
ReactJS and other SPA applications use the history object
You can listen to window.history updating with the following code:
function watchHistoryEvents() {
const { pushState, replaceState } = window.history;
window.history.pushState = function (...args) {
pushState.apply(window.history, args);
window.dispatchEvent(new Event('pushState'));
};
window.history.replaceState = function (...args) {
replaceState.apply(window.history, args);
window.dispatchEvent(new Event('replaceState'));
};
window.addEventListener('popstate', () => console.log('popstate event'));
window.addEventListener('replaceState', () => console.log('replaceState event'));
window.addEventListener('pushState', () => console.log('pushState event'));
}
watchHistoryEvents();

Categories