Instantly share code, notes, and snippets.
Last active
July 7, 2023 17:23
-
Star
(2)
2
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save isocroft/1d563ebe8f00c68e537e2e5083f44cca to your computer and use it in GitHub Desktop.
Monkey-patch on the History interface
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Algorithm {getNavDirection} created by @isocroft | |
* | |
* Copyright (c) 2021-2023 | Ifeora Okechukwu | |
* | |
* Works in ReactJS / VueJS / jQuery / Angular / Vanilla | |
* | |
* Algo implmented in Javascript: used to determine the direction | |
* of a single-page app navigation to track "Rage Refreshes" and to | |
* determine when the user visits a page from the back/forward button | |
* from a fresh page load. | |
* | |
* Using the code in the web link below: | |
* > https://www.codegrepper.com/code-examples/javascript/react+browser+back+button+event | |
* | |
* you can detect back/forward button navigations in ONLY ReactJS. How about if you could | |
* detect this in every single-page app library/framework with one code snippet ? | |
*/ | |
// basic stack data-structure definition | |
class Stack { | |
constructor (data = []) { | |
this.length = 0 | |
if (Array.isArray(data)) { | |
this.push.apply(this, data); | |
} | |
} | |
isEmpty () { | |
return this.length === 0; | |
} | |
size () { | |
return this.length; | |
} | |
peek () { | |
return this[this.size() - 1]; | |
} | |
push (...args) { | |
return Array.prototype.push.apply(this, args) | |
} | |
pop () { | |
return Array.prototype.pop.call(this); | |
} | |
replaceTop (...args) { | |
this.pop(); | |
this.push(...args); | |
} | |
toJSON () { | |
return '[ ' + Array.prototype.slice.call(this, 0).join(', ') + ' ]'; | |
} | |
toObject () { | |
try { | |
return JSON.parse(this.toJSON()) | |
} catch (e) { | |
if (e.name === 'SyntaxError') { | |
return Array.prototype.slice.call(this, 0, this.size()) | |
} | |
return [] | |
} | |
} | |
} | |
/* @NOTE: algorithm implementation of {getNavDirection} */ | |
const getNavDirection = (navStack, lastLoadedURL) => { | |
/* @NOTE: Direction: back (-1), reload (0), fresh load (-9) and forward (1) */ | |
let direction = -9; | |
/* @HINT: The current URL on browser page */ | |
const docURL = document.location.href; | |
/* @HINT: The temporary "auxillary" stack object to aid page nav logic */ | |
let auxStack = new Stack(); | |
/* @HINT: Take note of the intial state of the navigation stack */ | |
const wasNavStackEmpty = navStack.isEmpty(); | |
// Firstly, we need to check that if the navStack isn't empty, then | |
// we need to remove the last-loaded URL to a temporary stack so we | |
// can compare the second-to-last URL in the stack with the current | |
// document URL to determine the direction | |
if(!wasNavStackEmpty) { | |
auxStack.push( | |
navStack.pop() | |
); | |
} else { | |
auxStack.push(docURL); | |
} | |
// Check top of the navigation stack (which is the second-to-last URL loaded) | |
// if it's equal to the currentg document URL. If it is, then the navigation | |
// direction is 'Back' (-1) | |
if (docURL === navStack.peek()) { | |
// Back (back button was clicked) | |
direction = -1; | |
} else { | |
// Check top of the temporary "auxillary" stack | |
if (lastLoadedURL === auxStack.peek()) { | |
// if the last-loaded URL is the | |
// current one and then determine | |
// the correct direction | |
if (lastLoadedURL === docURL) { | |
if (wasNavStackEmpty) { | |
direction = -9; // Fresh Load | |
} else { | |
direction = 0; // Reload (refresh button was clicked) | |
} | |
} else { | |
direction = 1; // Forward (forward button was clicked) | |
} | |
} | |
} | |
// If the direction is not 'Back' (i.e. back button clicked), | |
// then replace the URL that was poped earlier and optionally | |
// record the current document URL | |
if (direction !== -1) { | |
// if the temporary stack isn't empty | |
// then empty it's content into the | |
// top of the navigation stack | |
if(!auxStack.isEmpty()){ | |
navStack.push( | |
auxStack.pop() | |
); | |
} | |
// push back the current document URL if and only if it's | |
// not already at the top of the navigation stack | |
if (docURL !== navStack.peek()) { | |
navStack.push(docURL); | |
} | |
} | |
// do away with the temporary stack (clean up action) | |
// as it's now empty | |
auxStack = null; | |
// return the direction of single-page app navigation | |
return direction; // Direction: back (-1), reload (0), fresh load (-9) and forward (1) | |
} | |
if (typeof window.History === 'function') { | |
// retrieve from storage (mostly for the refresh action of the browser) | |
const serializedStackFromStorage = window.sessionStorage.getItem('__nav_stack') || '[]' | |
const lastURLFromStorage = window.sessionStorage.getItem('__lastLoadedURL') || '' | |
// de-serialize the navigation stack from session storage | |
const deserializedStackFromStorage = JSON.parse(serializedStackFromStorage) | |
// monkey-patch the prototype object to include a navigation stack | |
window.History.prototype.spaNavigationStack = new Stack(deserializedStackFromStorage); | |
// monkey-patch the prototype object to include the last-loaded URL | |
window.History.prototype.lastLoadedURL = lastURLFromStorage || document.URL; | |
// Copy out the native browser `pushState` function for later use | |
var __pushState = window.History.prototype.pushState; | |
// Also, monkey-patch pushState (which is used by React / Vue / jQuery / Vanilla for their routing) | |
window.History.prototype.pushState = function (...args) { | |
const [, , url] = args | |
const origin = window.location.origin | |
const newURL = ((url.indexOf('http') === 0 ? url : origin + url) || '').toString(); | |
const oldURL = document.URL; | |
const isProperNav = oldURL !== newURL; | |
if(isProperNav){ | |
window.history.lastLoadedURL = newURL | |
window.sessionStorage.setItem('__lastLoadedURL', newURL) | |
} | |
return __pushState.apply(this, args); | |
}; | |
} | |
const $onPopState = window.onpopstate; | |
// Setup the popstate event which spys on when only the | |
// browser back and forward button(s) are clicked | |
window.onpopstate = function(e) { | |
// Get the direction of the navigation | |
const navDirection = getNavDirection( | |
window.history.spaNavigationStack, | |
window.history.lastLoadedURL | |
); | |
window.sessionStorage.setItem( | |
'__nav_stack', | |
window.history.spaNavigationStack.toJSON() | |
) | |
// Save the direction to storage (optional) | |
window.sessionStorage.setItem( | |
'curr_page_nav_direction', | |
String(navDirection) | |
) | |
// Save refresh count in storage | |
if (navDirection === 0) { | |
const lastRefreshCount = window.sessionStorage.getItem('refresh_count') || '0' | |
window.sessionStorage.setItem( | |
'refresh_count', | |
String(parseInt(lastRefreshCount + 1)) | |
) | |
} | |
// Trigger a custom event to fire | |
window.dispatchEvent(new CustomEvent(navDirection === -1 ? 'backwardNav' : 'navOccured')); | |
return typeof $onPopState === 'function' && typeof(e.isArtificial) === 'undefined' ? $onPopState(e) : undefined; | |
}; | |
// detecting a refresh | |
if (window.history.lastLoadedURL === document.location.href) { | |
document.addEventListener('readystatechange', function (event){ | |
if (event.readyState === 'complete') { | |
const artificialPopStateEvent = typeof(PopStateEvent) !== 'undefined' ? new PopStateEvent('popstate') : new Event('popstate') | |
artificialPopStateEvent.isArtificial = true | |
if (artificialPopStateEvent.constructor.name === 'Event') { | |
artificialPopStateEvent.state = null | |
} | |
/* @CHECK: https://docs.w3cub.com/dom/windoweventhandlers/onpopstate */ | |
window.onpopsate(artificialPopStateEvent) | |
} | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You can now listen for the custom event thus: